Archive for the 'php' Category

Reducing the payload: compression, minification, 204s

Friday, December 11th, 2009

Dec 11 This post is part of the 2009 performance advent calendar experiment. Stay tuned for the next articles.

After removing all the extra HTTP requests you possibly can from your waterfall, it's time to make sure that those that are left are as small as they can be. Not only this makes your pages load faster, but it also helps you save on the bandwidth bill. Your weapons for fighting overweight component include: compression and minification of text-based files such as scripts and styles, recompression of some downloadable files, and zero-body components. (A follow-up post will talk about optimizing images.)

Gzipping plain text components

Hands down the easiest and at the same time quite effective optimization - turning on gzipping for all plain text components. It's almost a crime if you don't do it. Doesn't "cost" any development time, just a simple flip of a switch in Apache configuration. And the results could be surprisingly pleasant.

When Bill Scott joined Netflix, he noticed that gzip is not on. So they turned it on. And here's the result - the day they enabled it, the outbound traffic pretty much dropped in half (slides)

Netflix traffic after turning on gzipping

Gzip FAQ

How much improvement can you expect from gzip?
On average - 70% reduction of the file size!
Any drawbacks?
Well, there's a certain cost associated with the server compressing the response and the browser uncompressing it, but it's negligible compared to the benefits you get
Any browser quirks?
Sure, IE6, of course. But only in IE6 service pack 1 and fixed for after that. You can boldly ignore this edge case, but if you're extra paranoid you can disable gzip for this user agent
How to tell if it's on?
Run YSlow/PageSpeed and they'll will warn you if it's not on. If you don't have any of those tools just look at the HTTP headers with any other tool, e.g. Firebug, webpagetest.org. You should see the header:

Content-Encoding: gzip

provided, of course, that your browser claimed it supports compression by sending the header:

Accept-Encoding: gzip, deflate
What types of components should you gzip?
All text components:

  • javascripts
  • css
  • plain text
  • html, xml, including any other XML-based format such as SVG, also IE's .htc
  • JSON responses from web service calls
  • anything that's not a binary file...

You should also gzip @font-files like EOT, TTF, OTF, with the exception of WOFF. Average about 40% to be won there with font files.

How-to turn on gzipping

Ideally you need control over the Apache configuration. If not full control, at least most hosting providers will offer you ability to tweak configuration via .htaccess. If your host doesn't, well, change the host.

So just add this to .htaccess:

AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/javascript application/json

If you're on Apache before version 2 or your unfriendly host don't allow any access to configuration, not all is lost. You can make PHP do the gzipping for you. It's not ideal but the gzip benefits are so pronounced that it's worth the try. This article describes a number of different options for gzipping when dealing with uncooperative hosts.

Rezipping

As Billy Hoffman discovered, there's potential for file size reduction with common downloadable files, which are actually zip files in disguise. Such files include:

  • Newer MS Office documents - DOCX, XLSX, PPTX
  • Open Office documents - ODT, ODP, ODS
  • JARs (Java Applets, anyone?)
  • XPI Firefox extensions
  • XAP - Silverlight applications

These ZIP files in disguise are usually not compressed with the maximum compression. If you allow such downloads from your website, consider recompressing them beforehand with maximum compression.

There could be anywhere from 1 to 30% size reduction to be won, definitely worth the try, especially since you can do it all on the command line, as part of the build process, etc. (re)Compress once, save bandwidth and offer faster downloads every time ;)

15% uncompressed traffic

Tony Gentilcore of Google reported his findings that a significant chunk of their traffic is still sent uncompressed. Digging into it he realized there's a number of anti-virus software and firewalls that will mingle with the browser's Accept-Encoding header changing into the likes of:

Accept-Encoding: xxxx, deflxxx
Accept-Enxoding: gzip, deflate

Since this is an invalid header, the server will decide that the browser doesn't support gzip and send uncompressed response. And why would the retarded anti-virus program do it? Because it doesn't want to deal with decompression in order to examine the content. Probably not to slow down the experience? In doing so it actually hurts the user to a greater extend.

So compression is important, but unfortunately it's not always present. That's why minification helps - not only because compressing minified responses is even smaller, but because sometimes there is no compression despite your best efforts.

Minification

Minification means striping extra code from your programs that is not essential for execution. The code in question is comments, whitespace, etc from styles and scripts, but also renaming variables with shorter names, and various other optimizations.

This is best done with a tool, of course, and luckily there a number of tools to help.

Minifying JavaScript

Some of the tools to minify JavaScript include:

How much size reduction can you expect from minification? To answer that I ran jQuery 1.3.2. through all the tools mentioned above (using hosted versions) and compared the sizes before/after and with/without gzipping the result of minification.

The table below lists the results. All the % figures are % of the original, so smaller is better. 29% means the file was reduced to 29% of its original version, or a saving of 71%

File original size size, gzipped % of original gzip, % of original
original 120619 35088 100.00% 29.09%
closure-advanced 49638 17583 41.15% 14.58%
closure 55320 18657 45.86% 15.47%
jsmin 73690 21198 61.09% 17.57%
packer 39246 18659 32.54% 15.47%
shrinksafe 69516 22105 57.63% 18.33%
yui 57256 19677 47.47% 16.31%

As you can see gzipping alone gives you about 70% savings, minification alone cuts script sizes with more than half and both combined (minifying then gzipping) can make your scripts 85% leaner. Verdict: do it. The concrete tool you use probably doesn't really matter all that much, pick anything you're comfortable with to run before deployment (or best, automatically during a build process)

Minifying CSS

In addition to the usual stripping of comments and whitespaces, more advanced CSS minification could include for example:

// before
#mhm {padding: 0px 0px 0px 0px;}
// after
#mhm{padding:0}

// before
#ha{background: #ff00ff;}
// after
#ha{background:#f0f}
//...

A CSS minifier is much less powerful than a JS minifier, it cannot rename properties or reorganize them, because the order matters and for example text-decoration:underline cannot get any shorter than that.

There's not a lot of CSS minifiers, but here's a few I tested:

  • YUI compressor - yes, the same YUI compressor that does JavaScript minification. I've actually ported the CSS minification part of it to JavaScript (it's in Java otherwise) some time ago. There's even an online form you can paste into to test. The CSS minifier is regular expression based
  • Minify is a PHP based JS/CSS minification utility started by Ryan Grove. The CSS minifier part is also with regular expressions, I have the feeling it's also based on YUICompressor, at least initially
  • CSSTidy - a parser and an optimizer written in PHP, but also with C version for desktop executable. There's also a hosted version. It's probably the most advanced optimizer in the list, being a parser it has a deeper understanding of the structure of the styleshets
  • HTML_CSS from PEAR - not exactly an optimizer but more of a general purpose library for creating and updating stylesheets server-side in PHP. It can be used as a minifier, by simply reading, then printing the parsed structure, which strips spaces and comments as a side effect.

Trying to get an average figure of the potential benefits, I ran these tools on all stylesheets from csszengarden.com, collected simply like:

<?php
$urlt = "http://csszengarden.com/%s/%s.css";
for ($i = 1; $i < 214; $i++) {
  $id = str_pad($i, 3, "0", STR_PAD_LEFT);
  $url = sprintf($urlt, $id, $id);
  file_put_contents("$id.css", file_get_contents($url));
}
?>

3 files gave a 404, so I ran the tools above on the rest 210 files. CSSTidy ran twice - once with its safest settings (which even keep comments in) and then with the most aggressive. The "safe" way to use CSSTidy is like so:

<?php
// dependencies, instance
include 'class.csstidy.php';
$css = new csstidy();

// options
$css->set_cfg('preserve_css',true);
$css->load_template('high_compression');

// parse
$css->parse($source_css_code);

// result
$min = $css->print->plain();
?>

The aggressive minification is the same only without setting the preserve_css option.

Running Minify is simple:

<?php
// dependencies, instance
require 'CSS.php';
$minifier = new Minify_CSS();

// minify in one shot
$min = $minifier->minify($source_css_string_or_url);

As for PEAR::HTML_CSS, since it's not a minifier, you only need to parse the input and print the output.

<?php
require 'HTML/CSS.php';

$options = array(
    'xhtml' => false,
    'tab' => 0,
    'oneline' => true,
    'groupsfirst' => false,
    'allowduplicates' => true,
);

$css = new HTML_CSS($options);
$css->parseFile($input_filename);
$css->toFile($output_filename);
// ... or alternatively if you want the result as a string
// $minified = $css->toString();

So I ran those tools on the CSSZenGarden 200+ files and the full table of results is here, below are just the averages:

  Original YUI Minify CSSTidy-safe CSSTidy-small PEAR
raw 100% 68.18% 68.66% 84.44% 63.29% 74.60%
gzipped 30.36% 19.89% 20.74% 28.36% 19.44% 20.20%

Again, the numbers are percentage of the original, so smaller is better. As you can see, on average gzip alone gives you 70% size reduction. The minification is not so successful as with JavaScript. Here even the best tool cannot reach 40% reduction (for JS it was usually over 50%). But nevertheless, gzip+minification on average gives you a reduction of 80% or more. Verdict: do it!

An important note here is that in CSS we deal with a lot of hacks. Since the browsers have parsing issues (which is what hacks often exploit), what about a poor minifier? How safe are the minifiers? Well, that's a subject for a separate study, but I know I can at least trust the YUICompressor, after all it's used by hundreds of Yahoo! developers daily and probably thousands non-Yahoos around the world. PEAR's HTML_CSS library also looks pretty safe because it has a simple parser that seems to tolerate all kinds of hacks. CSSTidy also claims to tolerate a lot of hacks, but given that the last version is two years old (maybe new hacks have surfaced meanwhile) and the fact that it's the most intelligent optimizer (knows about values, colors and so on) it should be approached with care.

204

Let's wrap up this lengthy posting with an honorable mention of the 204 No Content response (blogged before). It's the world's smallest componet, the one that has no body and a Content-Length of 0.

Often people use 1x1 GIFs for logging and tracking purposes and other types of requests that don't need a response. If you do this, you can return a 204 status code and no response body, only headers. Look no further that Google search results with your HTTP sniffer ON to see examples of 204 responses.

The way to send a 204 response from PHP is simply:

header("HTTP/1.0 204 No Content");

A 204 response saves just a little bit but, hey, every little bit helps.

And remember the mantra: every extra bit is a disservice to the user :)

Thank you for reading!

Stay tuned for the next article continuing the topic of reducing the component sizes as much as possible.

 

Collecting web data with a faster, free server

Tuesday, December 8th, 2009

Dec 8 This article is part of the 2009 performance advent calendar experiment. This is also the first ever guest post to this blog.
Please welcome the world-famous Christian Heilmann! And stay tuned for the next articles.

Christian HeilmannChris Heilmann is a self confessed data junkie and worked for over 12 years as a professional web developer. Having published several books on JavaScript, Accessibility and web development using web services he right now works as a developer evangelist for the Yahoo Developer Network. He blogs at http://wait-till-i.com/, has all his talks and videos at http://icant.co.uk/ and can be found on Twitter as @codepo8.

 

RSS is a wonderful format to get information from all kind of different sources. It is dead easy to provide, has a predictable (albeit limited) format and is very easy to use. The problem of course is that with the amount of different feeds used in one page its performance goes down.

The reason is the classic HTTP request issue - the more you negotiate, find and pull the slower your page renders. Therefore you need to try to shorten the time the calls happen.

Say you want to pull the following five RSS feeds and display them:

  • http://code.flickr.com/blog/feed/rss/
  • http://feeds.delicious.com/v2/rss/codepo8?count=15
  • http://www.stevesouders.com/blog/feed/rss
  • http://www.yqlblog.net/blog/feed/
  • http://www.quirksmode.org/blog/index.xml

The least effective way of doing that is pulling and displaying them one after the other:

Retrieving five RSS feeds using curl takes about 11 seconds.

$oldtime = microtime(true);
$url = 'http://code.flickr.com/blog/feed/rss/';
$content[] = get($url);
$url = 'http://feeds.delicious.com/v2/rss/codepo8?count=15';
$content[] = get($url);
$url = 'http://www.stevesouders.com/blog/feed/rss';
$content[] = get($url);
$url = 'http://www.yqlblog.net/blog/feed/';
$content[] = get($url);
$url = 'http://www.quirksmode.org/blog/index.xml';
$content[] = get($url);
display($content);
echo '<p>Time spent: <strong>' . (microtime(true)-$oldtime) .'</strong></p>';
function get($url){
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  $output = curl_exec($ch);
  curl_close($ch);
  return $output;
}
function display($data){
  foreach($data as $d){
    $obj = simplexml_load_string($d);
    echo '<div><h2><a href="'.$obj->channel->link.'">'.
          $obj->channel->title.'</a></h2>';
    echo '<ul>';
    foreach($obj->channel->item as $i){
      echo '<li><a href="'.$i->link.'">'.$i->title.'</a></li>';
    }
    echo '</ul></div>';
  }
}

Using Stoyan's Multi Curl function you already realise quite an increase in speed.

Retrieving five RSS feeds with multicurl takes about 2.8 seconds.

$data = array(
  'http://code.flickr.com/blog/feed/rss/',
  'http://feeds.delicious.com/v2/rss/codepo8?count=15',
  'http://www.stevesouders.com/blog/feed/rss',
  'http://www.yqlblog.net/blog/feed/',
  'http://www.quirksmode.org/blog/index.xml'

);
$r = multiRequest($data);
display($r);

function multiRequest($data, $options = array()) {
  // array of curl handles
  $curly = array();
  // data to be returned
  $result = array();
  // multi handle
  $mh = curl_multi_init();
  // loop through $data and create curl handles
  // then add them to the multi-handle
  foreach ($data as $id => $d) {
    $curly[$id] = curl_init();
    $url = (is_array($d) && !empty($d['url'])) ? $d['url'] : $d;
    curl_setopt($curly[$id], CURLOPT_URL,            $url);
    curl_setopt($curly[$id], CURLOPT_HEADER,         0);
    curl_setopt($curly[$id], CURLOPT_RETURNTRANSFER, 1);
    // post?
    if (is_array($d)) {
      if (!empty($d['post'])) {
        curl_setopt($curly[$id], CURLOPT_POST,       1);
        curl_setopt($curly[$id], CURLOPT_POSTFIELDS, $d['post']);
      }
    }
    // extra options?
    if (!empty($options)) {
      curl_setopt_array($curly[$id], $options);
    }
    curl_multi_add_handle($mh, $curly[$id]);
  }
  // execute the handles
  $running = null;
  do {
    curl_multi_exec($mh, $running);
  } while($running > 0);
  // get content and remove handles
  foreach($curly as $id => $c) {
    $result[$id] = curl_multi_getcontent($c);
    curl_multi_remove_handle($mh, $c);
  }
  // all done
  curl_multi_close($mh);
  return $result;
}
function display($data){
  foreach($data as $d){
    $obj = simplexml_load_string($d);
    echo '<div><h2><a href="'.$obj->channel->link.'">'.
          $obj->channel->title.'</a></h2>';
    echo '<ul>';
    foreach($obj->channel->item as $i){
      echo '<li><a href="'.$i->link.'">'.$i->title.'</a></li>';
    }
    echo '</ul></div>';
  }
}

However, there are still two things that are annoying:

  • You pull far more data than you really need
  • You do all the request from your server

Yahoo Pipes has been used for that kind of task for quite a while, but the issue was that it is a visual interface and therefore hard to maintain. The server was also not the best performing out there.

The good news is that there is a new(er) kid on the block in Yahoo Land called YQL running on a massively fast server farm and with one purpose: making it easier to use web services, mix them and get only the data back that you want.

YQL in itself is a web service and you post queries to it that access other web services in a SQL style syntax. Normally you'd get RSS feeds using the RSS table, like so:

select * from rss where url="http://feeds.delicious.com/v2/rss/codepo8?count=15"

See the single RSS in the YQL console (you need a Yahoo account to log in). You can also see the single RSS retrieval output.

The issue with this is that it only retrieves the items of the RSS feed and not the title, which we need for the headings. Therefore we need to use the XML table:

select * from xml where url="http://feeds.delicious.com/v2/rss/codepo8?count=15"

See the single XML in the YQL console (you need a Yahoo account to log in). You can also see the single XML retrieval output.

This gives us the same data the normal cURL calls give us. The cool thing about YQL is though that you can filter the data you get back to the bare minimum. In our case, this means replacing the * with the title and the link of the feed and of the items:

select channel.title,channel.link,channel.item.title,channel.item.link 
  from xml 
  where url="http://feeds.delicious.com/v2/rss/codepo8?count=15"

See the filtered RSS in the YQL console (you need a Yahoo account to log in). You can also see the filtered RSS retrieval output.

In order to use this with all of our RSS feeds, we can use the in() command:

select channel.title,channel.link,channel.item.title,channel.item.link
    from xml where url in(
      'http://code.flickr.com/blog/feed/rss/',
      'http://feeds.delicious.com/v2/rss/codepo8?count=15',
      'http://www.stevesouders.com/blog/feed/rss',
      'http://www.yqlblog.net/blog/feed/',
      'http://www.quirksmode.org/blog/index.xml'
    )

Check the aggregation in the console or the aggregation output.

This leaves all the hard work to the Yahoo Server farm. YQL pulls all the RSS feeds, adds one after the other and then gives it back to us as XML. We could simply use the generated URL from the console, but it is much more versatile to assemble the query in PHP:

$data = array(
  'http://code.flickr.com/blog/feed/rss/',
  'http://feeds.delicious.com/v2/rss/codepo8?count=15',
  'http://www.stevesouders.com/blog/feed/rss',
  'http://www.yqlblog.net/blog/feed/',
  'http://www.quirksmode.org/blog/index.xml'
);
$url ='http://query.yahooapis.com/v1/public/yql?q=';
$query = "select channel.title,channel.link,channel.item.title,channel.item.link from xml where url in('".implode("','",$data)."')";
$url.=urlencode($query).'&format=xml';
$content = get($url);
display($content);
function get($url){
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  $output = curl_exec($ch);
  curl_close($ch);
  return $output;
}
function display($data){
  $data = simplexml_load_string($data);
  $sets = $data->results->rss;
  $all = sizeof($sets);
  for($i=0;$i<$all;$i++){
    $r = $sets[$i];
    $title = $r->channel->title.'';
    if($title != $oldtitle){
      echo '<div><h2><a href="'.($r->channel->link.'').'">'.
           ($r->channel->title.'').'</a></h2><ul>';
    }
      echo '<li><a href="'.($r->channel->item->link.'').'">'.
           ($r->channel->item->title.'').'</a></li>';
    if($title != $sets[$i+1]->channel->title.''){
      echo '</ul></div>';
    }
    $oldtitle = $r->channel->title.'';
  };
}

As you can see the loop to display the different RSS feeds a bit clunky and we could use an open YQL table to move the whole conversion to a server-side JavaScript. However, as it is the performance of this way of retrieving the RSS feeds beats all the others hands-down already:

Retrieving five RSS feeds using YQL takes about half a second

You can try it yourself, get the demo code from GitHub and run it on your own server to see the magic of YQL.

 

Data URIs, MHTML and IE7/Win7/Vista blues

Monday, December 7th, 2009

Dec 7 This is the seventh in the series of performance articles as part of my 2009 performance advent calendar experiment. Stay tuned for the next articles.

Let's start this post as a dialog:

- Data URIs are a way to embed the base64-encoded content of images inside HTML and CSS. Inlining images in markup or stylesheets helps you save precious HTTP requests. It's an alternative to CSS sprites.
- BUT! IE6 and IE7 don't support data URIs
- Yes, for them you can inline the images using MHTML
- BUT! Looks like IE7 on Vista and IE7 on Win7 have problems with MHTML
- [Sigh...] For those browser/OS combos there's a solution too.

I'll quickly go over what are data URIs and how MHTML addresses IE6 and IE7. If you're familiar with these, feel free to skip down to the Vista blues part.

Data URIs

Here's how to use data URIs in your pages:

  1. take an image:
    horoscopes icon image
  2. Use PHP to read this image's binary content and encode it using base64 encoding:
    $ php -r "echo base64_encode(file_get_contents('horoscopes.png'));"
    iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAADAFBMVEX///8mPnru9...
    ...more scary stuff goes here...
    dajNGGzlDAAAAABJRU5ErkJggg==
  3. To use this image in CSS, paste the encoded image content in the stylesheet following this syntax:
    .horoscopes {
        background-image: url("data:image/png;base64,iVBOR...rkJggg==");
    }

    Note: the trailing == is part of the image content it's not part of the syntax

  4. Alternatively if you want to use this image in the HTML, you can go like:
    <img src="data:image/png;base64,iVBOR...rkJggg==" />

And that's about it for data URIs, it's quite simple actually.

Data URIs in the wild

To see real-life, high-traffic sites using data URIs look no further than your favorite search engines.

Yahoo! Search using data URI in CSS for a button background:

yahoo search screenshot

Google Search using data URI in an IMG tag for a video thumb:

Google search screenshot

Base64-encoded filesizes

The base64 encoding adds about 33% to the filesize. But, then you gzip the result, the compression brings the size back to the original. Plus or minus. Interestingly enough, sometimes after base64 and gzip, the result is smaller than the original. But don't count on that.

Theoretically also when you base64 encode a bunch of images and inline them in the same CSS or HTML, you'll have a larger string to compress, so increasing the chance of repetitions and compressing better. (Todo: test this assumption)

In any event, the benefit of reducing HTTP requests will likely greatly outweigh any fluctuation in the file size.

MHTML

IE8 supports data URIs, but earlier IEs do not. For them you can use MHTML (Multipart HTML, blogged previously here)

Continuing with the previous example, in order to display the horoscopes image in IE < 8 you can use a stylesheet like this:

/*
Content-Type: multipart/related; boundary="_MY_BOUNDARY_SEPARATOR"

--_MY_BOUNDARY_SEPARATOR
Content-Location:horoscopes
Content-Transfer-Encoding:base64

iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAADAFBMVEX///8mPnru9....U5ErkJggg==
*/

.horoscopes{*background-image:url(mhtml:http://example.org/styles.css!horoscopes);}

A few notes on this example:

  • the image content is base64-encoded just like before
  • if you need more images, use your chosen separator prepended with --, in this case --_MY_BOUNDARY_SEPARATOR
  • Content-Location:horoscopes is how you give a name to the image in order to use it later
  • then later in your stylesheet you can reference the name in the stylesheet using http://url/styles.css!horoscopes
  • you need absolute URLs in the mhtml:http://.... part (that's retarded, hope I'm mistaken. Didn't work for me with relative URLs)
  • you can use a single CSS file to support both old IE as well as new browsers, but you'll have to repeat the image stream twice, probably a better approach is to use browser specific stylesheets

For more examples of MHTML (which is also used for multipart email messages) check this and for a brilliant example of using one MHTML to embed images in HTML (not CSS), check Hedger's test page here (appears 404 at the moment of writing). For a tool to automate the dataURI-zation/MHTML-ization, be sure to check Nicholas Zakas' CSSEmbed

The problem with IE7 on Vista (and Win7)

So far we have data URIs and MHTML fallback. Turns out we're not done yet, because IE7 on Vista and Windows 7 fails with the MHTML example. Two points:

  • IE8 is the default in Windows 7, but it comes with several IE7 modes (either by default or turned on by users when their favorite pages break). Compatibility view, ie7 mode, ie8 in ie7 mode, blah-blah, it's all pretty confusing, but all modes with the exception of IE8 in proper IE8 standards don't work with the MHTML
  • I tested Windows 7 evaluation copy, but I have the bad, bad feeling that Windows 7 normal copy will be as broken when it comes to MHTML. I believe it has something to do with security, if anyone finds an article on MSDN, or anywhere, please share.

The solution to the Vista problem is to split the CSS shown above (the one that uses MHTML) in two: one which contains the encoded images (the commented part) and a second one which only has the normal CSS selectors part.

In other words have something like:

  1. mhtml.txt:
    Content-Type: multipart/related; boundary="_MY_BOUNDARY_SEPARATOR"
    
    --_MY_BOUNDARY_SEPARATOR
    Content-Location:horoscopes
    Content-Transfer-Encoding:base64
    
    iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAADAFBMVEX///8mPnru9....U5ErkJggg==
    

    No need to use comments. Could be .txt, .css or anything you like. In my tests the content-type didn't matter - text/html, text/css, text/plain all worked

  2. styles.css:
    .horoscopes{*background-image:url(mhtml:http://example.org/mhtml.txt!horoscopes);}

    The normal CSS simply references the new MHTML file appending !identifier

  3. in your HTML you don't need to reference the MHTML document, you just point your link tag to styles.css as usual and styles.css contains the reference to the MHTML doc

Time to celebrate? Almost.

Another ugly bug surfaces - this procedure outlined above works only once. Once the MHTML.txt is cached, it stops working. That means repeating visits to the page or other pages using the same mhtml. The effect also means hovering on the same page. Say you have two images - one normal and one mouse over, both in the same MHTML. The normal works and the hover breaks, mouseout breaks too. Insane, isn't it?

There's a solution though - you should make the browser request the MHTML document every time. This is kind of backwards when it comes to performance optimization, where we want to cache everything. It's pretty unfortunate. The best you can do is avoid sending the MHTML every time, but only send a "not-modified" header. In order for this to work, the browser has to send If-Modified-Since or If-None-Match.

So you can send your MHTML file with an ETag (yes, ETags can help sometimes ;)) Then the browser will send an If-None-Match and you can happily reply "304 Not Modified".

Sounds complicated? It sure is. And, remember, all this is only for IE7 (or any IE7 mode) on Vista and Windows 7. All other IEs on all other platforms work with the easy MHTML and IE8 works with data URIs.

Everything is a trade-off (see last night's post) and I can understand how you may think "That is one big tradeoff". There's always the option of serving normal images to the affected browser/os and just don't implement this optimization for them.

But it's good to know there is a solution.

DataSprites class

Let me offer this DataSprites PHP class I coded that takes care of all the scenarios. It takes a bunch of image filenames as input and produces:

  • a CSS containing data URIs for normal browsers (example output)
  • an MTHML CSS fallback for IE6,7 (example output)
  • a CSS that refers to an additional MHTML for IE7 on Vista, Win7 (example output - css and mhtml). The MHTML in this case is sent out with an ETag (derived from the input image filenames) and consecutive requests with the same ETag return 304 Not Modified

You can see a test page here and view the source code here. The directory listing is also available if you want to grab the images for testing.

The perfect use case for such an approach is when you want to combine background images on the fly. When you would normally use CSS sprites, but you want to produce them dynamically at run time (maybe because you have too many combinations?).

I've tested this in Safari, Firefox, Opera, IE6, IE7 and IE8 (in all compat modes) on Vista and Windows 7 evaluation copy. If you find the test page is not working for you, please let me know.

In this DataSprites class, I'm checking the problem OS looking for the strings "Windows NT 6" and "Windows NT 7" in the user agent string. My Win7 evaluation copy had "Windows NT 6.1" in the UA, so I may be a little forwards-aggressive with the check, but somehow I thought Win7 proper should have "Windows NT 7" in the UA string. And also that Win7 will suffer from the same issue as Vista and Windows 7 evaluation.

UPDATE: Added a hover example.

UPDATE 2: Recorded a screencast to demo the IE7/Vista experience

Thanks!

Thanks for reading! Now you know all about data URIs, MHTML, and maybe a little too much about IE7/Vista's challenges :) Ready to use data URIs and save some HTTP requests?

 

Performance tools

Wednesday, December 2nd, 2009

Dec 2 This is the second in the series of performance articles as part of my 2009 performance advent calendar experiment. Stay tuned for the next articles.

While theoretically you can speed up your site by just blindly following advice from this blog and other sources, it is much better to understand what's going on on the page and what you're dealing with. That's where the tools come in. Some tools give you insight about the network activities going on between the server and the browser (packet sniffers), some help you benchmark code execution on the client (profilers), some even give recommendations specific to performance improvements. You should aim at mastering as many of the tools as possible, because there's no single one that is The Tool. And that's not a bad thing, it's normal, because performance optimization is a multi-discipline activity touching a lot of different aspects of the the development process.

YSlow

(Full disclaimer: I helped with YSlow development and I was the architect of YSlow 2.0 so this fact may or may not have something to do with why YSlow is the first in the list. Plus, this is an unordered list.)

YSlow is an extension to Firebug, that:

  • inspects the DOM
  • hooks into the Net panel to listen to network activity and discover components that are not part of the DOM

Then the tool looks at the page and the components and tries to figure out how closely they match with Yahoo!'s performance best practices. Then you're given a list of findings with some advice and links for more information on how to improve.

PageSpeed

page speed screenshot

PageSpeed inspects the page and the components and checks it against conformance with Google's performance best practices.

In addition to that, PageSpeed has some quite advanced features like the Activity Panel which shows more detailed information on the page's, well, activity - such as the browser paint events, javascript parsing, execution, DNS lookups and so on. PageSpeed also tells you how many (and which) JavaScript functions were never called before onload so you can take some hints to lazy-load some of the JavaScript payload (after analysing, of course that the code is not needed in other browsers or other page use cases). Same with CSS - PageSpeed gives you a list of unused selectors so you can check whether you have leftovers from previous versions of the page.

MSFast

MSFast (from MySpace) inspects the page and helps answer many questions left open by YSlow and PageSpeed, such as:

  • What's going on with IE?
  • What's the memory and CPU footprint of your code?
  • How does the page looks like (as in screenshots) while it's being loaded so you can see what the people with slower connections have to experience

PageTest

AOL's PageTest is an IE plugin but also a hosted service which is a great way to show your boss/client performance details without inconveniencing them with challenging download and installation activities. PageTest gives you a waterfall view of the page load and a checklist of things to improve, plus some screenshots of interesting moments during load and even a video - an excellent view of how the page looks like in slow speeds. The hosted service can show you the dial-up experience in 4 different places in the world.

DynaTrace's Ajax Edition

dynatrace screenshot

Dynatrace Ajax is a very detailed lower level tool that not only shows the waterfall of components downloads but also the rendering time, CPU consumption, JavaScript parsing and execution. The screenshot above is just the tip of the iceberg of the tool's plethora of views and insights. It's highly recommended. (free, registrationware)

Packet sniffers

A good packet sniffer is indispensable for inspecting the HTTP traffic and figuring out how the page loads and what the request/responses and their headers look like. Here's a list of recommended sniffers, each with something good on top of the others:

  • IBM PageDetailer - a mature tool, somewhat simple which makes it a good start, requires registration to download
  • Fiddler - very powerful, extensible
  • HTTPWatch - (paid, but with a free version) integrates into the browser (both IE and FF) as a panel - very convenient to use. Extensible.
  • Microsoft Visual Round Trip analyser (and an excellent writeup)- goes even lower into the packet level of the requests and draws a different view of the waterfall, one that visualizes the TCP packets and the TCP slow start. It also gives recommendations for performance improvements. Built on top of NetMon (Microsoft Network Monitor) to present the data in a more useful and friendly way.
  • Charles proxy - the only non-windows tool in the list is an excellent packet sniffer for Mac

Time for a little rant - a more detailed view into the HTTP chunks is something that I think is important (will blog about it as part of this series) and missing from the current tools. HTTPWatch is the only tool that at least tells you the number of chunks and Fiddler prompts you to de-chunk HTTP responses when inspecting the body, which gives you a hint that the response was chunked. I hope to see more in that area, hopefully soon.

Thanks for reading

That concludes day 2 of the performance advent calendar. Hope you'll have fun installing and playing with new toys!

Did I miss a tool that should've been in the list? Let me know.

 

Installing PHP and Apache on Mac OSX - that was (pretty) easy

Saturday, March 7th, 2009

This posts is one of those "note to self" kinda posts. I just finished installing PHP and Apache on my Mac OS 10.5.6 and though I should document the experience should I (or you) need to do it again.

It could already be there

The default OS install came with goodies like ruby and php already there. So I could use php on the command line already. But it wasn't "hooked" to Apache for proper web development.

Also turns out Mac comes with some version of Apache, looks like it's disabled by default, but if it isn't, disable it from System Preferences / Sharing / Web Sharing.

Now let's start fresh with PHP5 and Apache 2, ignoring the PHP that's already there.

Mac ports prerequisite

Mac ports makes installing many software packages a breeze on the Mac. If you don't have it already, do set up Mac ports first.

Ready? Set? Go

  1. Log into the mac ports prompt:
    $ sudo port
    Password:
    MacPorts 1.700
    Entering interactive mode... ("help" for help, "quit" to quit)
    [Users/stoyan] >
  2. inside the Mac ports prompt simply do:
    [Users/stoyan] > install php5
  3. This is it! Give it a bit of time to pull out all dependencies, including Apache2. The rest is just some configuration...
  4. Start Apache and make it start when you power on the computer next time:
    $ sudo launchctl load -w /Library/LaunchDaemons/org.macports.apache2.plist
  5. Test that Apache runs fine by pointing your browser to http://localhost/. You should see a page that says "It works!"
  6. Tell Apache that PHP exists:
    $ sudo /opt/local/apache2/bin/apxs -a -e -n "php5" libphp5.so
    [activating module `php5' in /opt/local/apache2/conf/httpd.conf]
    
  7. Create a php.ini file (PHP configuration) by copying the default .ini
    sudo cp /opt/local/etc/php.ini-dist /opt/local/etc/php.ini
  8. If you need Apache stuff, like config files, error/access logs, htdocs... look around /opt/local/apache2. The web root for example is /opt/local/apache2/htdocs. I found it kinda convoluted so decided to move the web root to my home directory. So next two steps are optional.
  9. I'll store all web apps and pages and scripts in a directory called /localhost in my home directory:
    mkdir ~/localhost
  10. Time to edit the Apache config to tell it about the new location of the web root. Open httpd.conf
    sudo vi /opt/local/apache2/conf/httpd.conf

    Search for "DocumentRoot" and replace the current value /opt/local... with /Users/[your username]/localhost.
    The result would be like:

    DocumentRoot "/Users/stoyan/localhost"

    Then do the same further down in the config where it says <Directory..., so it should read:

    #
    # This should be changed to whatever you set DocumentRoot to.
    #
    <Directory "/Users/stoyan/localhost">
    
  11. Apache also need to know that files that end with .php will be handled by the php module, so edit httpd.conf (the same one from the previous step). Search for "php" and you'll find:
    LoadModule php5_module        modules/libphp5.so

    Add one more line so it looks like:

    LoadModule php5_module        modules/libphp5.so
    AddHandler application/x-httpd-php .php
  12. restart Apache (see below) and you're all done

start/stop/restart Apache

Here's how you start/stop/restart Apache:

  • $ sudo /opt/local/etc/LaunchDaemons/org.macports.apache2/apache2.wrapper start
  • $ sudo /opt/local/etc/LaunchDaemons/org.macports.apache2/apache2.wrapper stop
  • $ sudo /opt/local/etc/LaunchDaemons/org.macports.apache2/apache2.wrapper restart

Or to stop and disable starting up every time you power on:

$ sudo launchctl unload -w /Library/LaunchDaemons/org.macports.apache2.plist

Verify that all is good

You can check that all is good by creating a PHP info script.

echo "<?php phpinfo(); ?>" > ~/localhost/test.php

Now point your browser to http://localhost/test.php. It should give you the php info page (a looong page of PHP-related information)

 

image diff

Saturday, November 15th, 2008

Was having fun today with idiff.php - a PHP shell script to tell you if two images are visually different by comparing them pixel by pixel. If there's a difference, the script creates a third image - black background with the different pixels in green. Only after writing the script I found that there's an imagemagick command compare that does the same :)

the script

The idea is that two GD images are created from the input files, also a third GD image with black background to store the diff. Loop through all the pixels and compare them one by one. If one is different, write a green pixel at the same location of the diff image.

<?php
/**
 * Shell script to tell if two images are identical.
 * If not, a third image is written - black background with the different pixels painted green
 * Code partially inspired by and borrowed from http://pear.php.net/Image_Text test cases
 */

// check if there's enough input
if (empty($argv[1]) || empty($argv[2])) {
    echo 'gimme at least two image filenames, please.', "\n";
    echo 'e.g. "php idiff.php img1.png img2.png"';
    echo "\n", 'third filename is the image diff, optional, default is "diffy.png"';
    exit(1);
}

// create images
$i1 = @imagecreatefromstring(file_get_contents($argv[1]));
$i2 = @imagecreatefromstring(file_get_contents($argv[2]));

// check if we were given garbage
if (!$i1) {
    echo $argv[1] . ' is not a valid image';
    exit(1);
}
if (!$i2) {
    echo $argv[2] . ' is not a valid image';
    exit(1);
}

// dimensions of the first image
$sx1 = imagesx($i1);
$sy1 = imagesy($i1);

// compare dimensions
if ($sx1 !== imagesx($i2) || $sy1 !== imagesy($i2)) {
    echo "The images are not even the same size";
    exit(1);
}

// create a diff image
$diffi = imagecreatetruecolor($sx1, $sy1);
$green = imagecolorallocate($diffi, 0, 255, 0);
imagefill($diffi, 0, 0, imagecolorallocate($diffi, 0, 0, 0));

// increment this counter when encountering a pixel diff
$different_pixels = 0;

// loop x and y
for ($x = 0; $x < $sx1; $x++) {
    for ($y = 0; $y < $sy1; $y++) {

        $rgb1 = imagecolorat($i1, $x, $y);
        $pix1 = imagecolorsforindex($i1, $rgb1);

        $rgb2 = imagecolorat($i2, $x, $y);
        $pix2 = imagecolorsforindex($i2, $rgb2);

        if ($pix1 !== $pix2) { // different pixel
            // increment and paint in the diff image
            $different_pixels++;
            imagesetpixel($diffi, $x, $y, $green);
        }

    }
}

if (!$different_pixels) {
    echo "Image is the same";
    exit(0);
} else {
    if (empty($argv[3])) {
        $argv[3] = 'diffy.png'; // default result filename
    }
    imagepng($diffi, $argv[3]);
    $total = $sx1 * $sy1;
    echo "$different_pixels/$total different pixels, or ", number_format(100 * $different_pixels / $total, 2), '%';
    exit(1);
}
?>

usage

Type in the command line something like:

php idiff.php img1.png img2.png result.png

This command will compare img1.png with img2.png. If they are not the same dimensions, will exit with error code. If their pixels exactly the same will exit with success code 0 and print a success message. If the images are not the same, will write the file result.png.

If you omit result.png, the generated file will be called diffy.png

example

img1.png:

img2.png

running the command:

>php idiff.php img1.png img2.png
70/24600 different pixels, or 0.28%

the result diffy.png:

imagemagick

yep, so after I did this I found that imagemagick has a command called compare that does the same, only with more features, like the -fuzz option for example that allows you to specify how different should the pixels be, in order to consider them important.

the command:

>compare img1.png img2.png diffy-im.png

the diffy-im.png result

As you can see the different pixels are red and the actual image is a very pale background. Nice.

 

Best open-source PHP CMS 2008

Sunday, October 26th, 2008

It's that time of the year again. Time to cast my vote as part of the jury in Packt's open-source awards, category "PHP CMS".

(for my last year's rant, check the internet archive copy)

How to judge a CMS?

How to judge a CMS? Tough one. How do you judge any piece of software anyway?

Features? Usually any system or product lists the features in way meant to wow you, I mean a list of features always looks nice, it's a sales tool. And the features don't mean that much, actually some say, you should underfeature the competition. Of course, you can't take that to the extreme and build software that does nothing. You need features, but the best CMS doesn't mean the one with the most features, neither the one with less. You just need the right mix and "right" may wildly vary depending on the task. Are you building an e-commerce site, a library of articles, a 5-page business card-like site, on online community? No way to judge a system without an idea of what you want it to do for you. And no way what you want to do is what everyone else wants to do.

Quality? This is much easier. Does the code validate, YSlow score, accessibility, etc. Easy, even automated, but again, the system can be written beautifully but doesn't do the right thing or work the right way.

Documentation? A must-have, usually a problem with any software. Can be too specific in areas that don't matter and completely lacking where you really need it. Or outdated. It's also nice to have some books, sign that the system is relatively popular, mature and at the end of it, it's cozier to share the couch with a book then with an online text. Also API documentation, at least it's easier to generate and keep updated.

Community? Very important, you'll inevitably have questions when you start working with the system. It would be nice to have a friendly place where you can ask/answer questions. But how do you judge a community? The only way is to get involved and spend time. No way to do this when you're judging 5 systems in a matter of days.

Verdict

It's virtually impossible to judge any piece of software until you start using it. It may have the right features, but they might be unfriendly or virtually unusable. It may have documentation, but not the one you need, etc, etc. So the only way is to spend time with the software and try to make it work for you.

And since I've been meaning to create my personal stoyanstefanov.com, I thought it would be a good idea to kill two birds with one stone - cast my vote and have a working site at the end, powered by my personal CMS favorite.

Content

So I sit down and listed the features I need, nothing overly complicated, and also decided on some rough structure. Here's my feature list:

  • Blog. In English and also in Bulgarian, nice test for i18n capabilities
  • Photos
  • Syndicated Twitter feed, probably the CMS will not have it out of the box, but syndicating content and consuming web services is so common these days that the software better be capable of doing it easily or let me write an extension easily
  • Slides, with the help of Slideshare's API, same as above
  • Books - a page for each book maybe, reviews, image, etc
  • Projects - list of stuff I'm involved in
  • Publications - articles I wrote for other sites and magazines
  • Conferences - list the conferences I've spoken to, encourage people to invite me, bio blurb, hi-res photo
  • About
  • Contact
  • Social - links to facebook profile, linked in, etc
  • Sites - other sites of mine
  • Syndicated content - again, syndicate friendfeed or a Yahoo pipe that contains pretty much all my online activity, such as other blogs and even tiny things like last.fm love/hate ratings
  • Subscribe - option to subscribe to RSS feeds or even monthly email
  • custom 404

All these features I would combine in 5 top-level menu items: blog, news, writing, speaking, info.

Checklist

So now that I know what I want to build, I can go ahead and (attempt to) do it with each CMS while looking out for some pre-defined checks.

Info

URL:
Maturity: (since when the CMS exists)
Frequency of new releases: (is the project active)

Tasks

The best way to get a feeling of the software is by performing some tasks, the same tasks for all CMS

  1. Download
  2. Install
  3. setup friendly urls
  4. setup sections and content
  5. customize presentation
  6. customize homepage
  7. create an extension
  8. customize 404

QA

Test both the front-end and the back-end admin sections.

  • works without js
  • works without css
  • validates html
  • validates css
  • jslint
  • yslow grade
  • smush.it

Misc

  • Is there a WYSIWYG, people like those (I avoid them)
  • available extensions
  • available themes
  • Documentation
  • API docs
  • Community
  • Books

Whew!

Lotsa stuff, wish me luck with aaall that :) And I would be interested to hear any comments on the subject. Thanks for reading!

 

php|works and pyWorks

Wednesday, September 10th, 2008

I'll be speaking at the php|works and pyWorks conferences in Atlanta, Georgia in November, they'll be held together and there is a central track that has topics of interest to both phpiers and pythonistas, this is where I come in.

The conference(s) schedule is here and this is me: "Image optimization for the web"

 

My online footprint lately

Wednesday, July 23rd, 2008

This is a sort of a catch-up post for listing what I've been up to lately.

  • YUI Blog just published my first article, I'm so proud. It's about loading JavaScript in non-blocking fashion, because JavaScripts, they, you know, like, block downloads. Luckily, there's an easy fix - DOM includes, which I've previously discussed, discussed and discussed.
  • SitePoint published an update to my older article that introduces AJAX, ok, Ajax, by creating a command-line-like interface with PHP on the server side. The updated article features improved code, jQuery example, YUI example, JSON discussion and example. Check it out, bookmark and recommend to your friends that keep asking you "What's this AJAX (they are new, don't know it's now spelled "Ajax") thing? Do you know of a good article?"
  • YDN (developer.yahoo.com) published a video presentation of me and my lovely teammate Nicole Sullivan where we talk about some new and cool front-end performance techniques. So if you wandered how I look and are eager to hear my fabulous Balkan peninsula accent, give it a shot. The talk is called "After YSlow 'A'" and is targeted at those of you who have reached performance nirvana, but are still hungry for more. We talk about preloading components, post-loading, javascript, images, using flush() in PHP to send first byte early on and other fun stuff.
  • Last, not least, I decided to try and find some time to update my JavaScript patterns site. Unfortunately I got sidetracked (yep, I'm easily distracted by shiny objects) and played with a not-so-javascript pattern. The post I published (includes a pretty lame screencast! and) demonstrates how you can use animated background position to indicate loading progress.

Whew, c'est tout pout ce moment, expect a lot more now that the JavaScript book is out of the way. Ah, yep, if you feel like it, join me on Facebook, I created a JS book page.

 

www vs no-www and cookies

Tuesday, May 13th, 2008

One of Yahoo's performance rules says: Use cookie-free domains for static components. This is good because the server has no use for cookie information when serving a JPEG or another static component, so all this cookie information creates network traffic for no reason.

One of the implications of following the rule is related to the whole www vs no-www question. Basically you should always use www if you're planning to use any other sub-domains and you want them cookie-free. This is because you have no way to set a cookie only to the top-level domain. So for example you cannot write a cookie only to phpied.com. If you load component from img.phpied.com, the cookie from phpied.com will be sent again. In Firefox this doesn't seem to be the case, but in IE it is.

Here are two test scripts that demonstrate the behavior:

Load both of these pages and then reload them to see what cookies are sent. They both try to set cookies in all possible ways:

  1. omitting the domain name
  2. .domain.com
  3. domain.com
  4. www.domain.com

Here's the source code of the files:

nowww.php [test]

<?php
setcookie('no0', 'no www, no domain');
setcookie('no1', 'no www, .phpied.com', 0, '/', '.phpied.com');
setcookie('no2', 'no www, phpied.com', 0, '/', 'phpied.com');
setcookie('no3', 'no www, www.phpied.com', 0, '/', 'www.phpied.com');

echo '<pre>';
print_r($_COOKIE);
?>

yeswww.php [test]

<?php
setcookie('yes0', 'yes www, no domain');
setcookie('yes1', 'yes www, .phpied.com',    0, '/', '.phpied.com');
setcookie('yes2', 'yes www, phpied.com',     0, '/', 'phpied.com');
setcookie('yes3', 'yes www, www.phpied.com', 0, '/', 'www.phpied.com');

echo '<pre>';
print_r($_COOKIE);
?>

Loading the two pages twice shows how in IE, no0, no1, and no2 are all visible when using www as well as when not using it. In Firefox it's almost the same, only that no0 is not visible when using www.

As a take-home:

  • use www
  • write cookies to the appropriate domain level (e.g. don't write to *.domain.com)
 

strftime() in JavaScript

Friday, April 25th, 2008

Philip "Bluesmoon" Tellis has posted a tiny (under 3K minified) JavaScript library to format dates, inspired by PHP's strftime()

Examples:

d.strftime('%Y/%m/%d')
» en: 2008/04/25
» fr: 2008/04/25
» de: 2008/04/25

d.strftime('%A, %d %B')
» en: Friday, 25 April
» fr: Vendredi, 25 Avril
» de: Freitag, 25 April

There's also a demo to fiddle with.

I've previously had fun with kinda like the opposite: translating human-readable times into JS Date objects. Also here and here you have strtotime() look-alikes that take human-readable dates and turn them into Date objects.

 

Simultaneuos HTTP requests in PHP with cURL

Tuesday, February 19th, 2008

The basic idea of a Web 2.0-style "mashup" is that you consume data from several services, often from different providers and combine them in interesting ways. This means you often need to do more than one HTTP request to a service or services. In PHP if you use something like file_get_contents() this means all the requests will be synchronous: a new one is fired only after the previous has completed. If you need to make three HTTP requests and each call takes a second, your app is delayed at least three seconds.

Solution

An improvement of course is to cache responses as much as possible, but at one point or another you still need to make those requests.

Using the curl_multi* family of cURL functions you can make those requests simultaneously. This way your app is as slow as the slowest request, as opposed to the sum of all requests. And that's something.

A function

Here's a little function I coded that will allow you do multi requests.

<?php

function multiRequest($data, $options = array()) {

  // array of curl handles
  $curly = array();
  // data to be returned
  $result = array();

  // multi handle
  $mh = curl_multi_init();

  // loop through $data and create curl handles
  // then add them to the multi-handle
  foreach ($data as $id => $d) {

    $curly[$id] = curl_init();

    $url = (is_array($d) && !empty($d['url'])) ? $d['url'] : $d;
    curl_setopt($curly[$id], CURLOPT_URL,            $url);
    curl_setopt($curly[$id], CURLOPT_HEADER,         0);
    curl_setopt($curly[$id], CURLOPT_RETURNTRANSFER, 1);

    // post?
    if (is_array($d)) {
      if (!empty($d['post'])) {
        curl_setopt($curly[$id], CURLOPT_POST,       1);
        curl_setopt($curly[$id], CURLOPT_POSTFIELDS, $d['post']);
      }
    }

    // extra options?
    if (!empty($options)) {
      curl_setopt_array($curly[$id], $options);
    }

    curl_multi_add_handle($mh, $curly[$id]);
  }

  // execute the handles
  $running = null;
  do {
    curl_multi_exec($mh, $running);
  } while($running > 0);

  // get content and remove handles
  foreach($curly as $id => $c) {
    $result[$id] = curl_multi_getcontent($c);
    curl_multi_remove_handle($mh, $c);
  }

  // all done
  curl_multi_close($mh);

  return $result;
}

?>

Consuming

The function accepts an array of URLs to hit and optionally an array of cURL options if you need to pass any. The first array can be a simple indexed array or URLs or it can be an array of arrays where the second has a key named "url". If you use the second way and you also have a key called "post", the function will do a post request.

The function returns an array of responses as strings. The keys in the result array match the keys in the input.

A GET example

Let's say you want to use some Yahoo search web services (consult YDN) to create a music artist band-o-pedia kind of mashup. Here's how you can search audio, video and images at the same time:

<?php

$data = array(
  'http://search.yahooapis.com/VideoSearchService/V1/videoSearch?appid=YahooDemo&query=Pearl+Jam&output=json',
  'http://search.yahooapis.com/ImageSearchService/V1/imageSearch?appid=YahooDemo&query=Pearl+Jam&output=json',
  'http://search.yahooapis.com/AudioSearchService/V1/artistSearch?appid=YahooDemo&artist=Pearl+Jam&output=json'
);
$r = multiRequest($data);

echo '<pre>';
print_r($r);

?>

This will print something like:

Array
(
    [0] => {"ResultSet":{"totalResultsAvailable":"633","totalResultsReturned":...
    [1] => {"ResultSet":{"totalResultsAvailable":"105342","totalResultsReturned":...
    [2] => {"ResultSet":{"totalResultsAvailable":10,"totalResultsReturned":...
)

A POST example

There's an interesting Yahoo search service called term extraction which analyses content. It accepts POST requests. Here's how to consume this service with the function above, making two simultaneous requests:

<?php
$data = array(array(),array());

$data[0]['url']  = 'http://search.yahooapis.com/ContentAnalysisService/V1/termExtraction';
$data[0]['post'] = array();
$data[0]['post']['appid']   = 'YahooDemo';
$data[0]['post']['output']  = 'php';
$data[0]['post']['context'] = 'Now I lay me down to sleep,
                               I pray the Lord my soul to keep;
                               And if I die before I wake,
                               I pray the Lord my soul to take.';

$data[1]['url']  = 'http://search.yahooapis.com/ContentAnalysisService/V1/termExtraction';
$data[1]['post'] = array();
$data[1]['post']['appid']   = 'YahooDemo';
$data[1]['post']['output']  = 'php';
$data[1]['post']['context'] = 'Now I lay me down to sleep,
                               I pray the funk will make me freak;
                               If I should die before I waked,
                               Allow me Lord to rock out naked.';

$r = multiRequest($data);

print_r($r);
?>

And the result:

Array
(
    [0] => a:1:{s:9:"ResultSet";a:1:{s:6:"Result";s:5:"sleep";}}
    [1] => a:1:{s:9:"ResultSet";a:1:{s:6:"Result";a:3:{i:0;s:5:"freak";i:1;s:5:"sleep";i:2;s:4:"funk";}}}
)
 

Fancy formatting

Friday, December 21st, 2007

Writing readable code means proper indentation. Usually you'd tab (or use 2 or 4 or 3 spaces) after every curly bracket. Something like this:

if (true) {
    // indent
    if (false) {
        // another indent
        // and some more
    }
}

Same goes when you have a bigger hash/object sort of thing:

var memememe = {
    name: 'Stoyan',
    family_name: 'Stefanov',
    blog: 'http://www.phpied.com',
    kids_count: 2,
    books_count: 3,
    occupation: 'programmer'
}

Sometimes I find myself going a little fancy and aligning all the values in the name/value pairs:

var memememe = {
    name:        'Stoyan',
    family_name: 'Stefanov',
    blog:        'http://www.phpied.com',
    kids_count:  2,
    books_count: 3,
    occupation:  'programmer'
}

But recently, inspired by Firebug's Net panel way of presenting header information, I tried aligning the keys to the right in addition to aligning the values to the left. So I ended up with something like this:

var memememe = {
          name: 'Stoyan',
   family_name: 'Stefanov',
          blog: 'http://www.phpied.com',
    kids_count: 2,
   books_count: 3,
    occupation: 'programmer'
}

Fancy, eh? I liked the way it looks. But then I thought that when writing maintainable code, anything fancy suggests uncommon, uncommon suggests that other team members won't be using it, so it means breaking the rule #1 of writing maintainable code: be predictable. (this also happens to be rule #1 of other common activities, such as driving on the highway and designing usable web sites)

This type of formatting is also not easy to type in an editor, so it will require a little more effort. Those two drawbacks are enough, I believe, to dismiss this idea. But I can't help myself liking the way the code looks. Here's a piece of PHP, which looks even better than javascript, because even more characters are centered.

<?php
$memememe = array(
          'name' => 'Stoyan',
   'family_name' => 'Stefanov',
          'blog' => 'http://www.phpied.com',
    'kids_count' => 2,
   'books_count' => 3,
    'occupation' => 'programmer'
);
?>

Ain't that cool?

 

Image fun with PHP - part 2

Tuesday, November 13th, 2007

This post is a demo of what the imagefilter() PHP function can do for you.

The Original

Nathalie

imagefilter() called with different filter constants

img_filter_brightness_5.png
Filter: IMG_FILTER_BRIGHTNESS
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_BRIGHTNESS5);
imagepng($image'img_filter_brightness_5.png');
imagedestroy($image);
?>

img_filter_brightness_50.png
Filter: IMG_FILTER_BRIGHTNESS
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_BRIGHTNESS50);
imagepng($image'img_filter_brightness_50.png');
imagedestroy($image);
?>

img_filter_brightness_100.png
Filter: IMG_FILTER_BRIGHTNESS
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_BRIGHTNESS100);
imagepng($image'img_filter_brightness_100.png');
imagedestroy($image);
?>

img_filter_grayscale.png
Filter: IMG_FILTER_GRAYSCALE
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GRAYSCALE);
imagepng($image'img_filter_grayscale.png');
imagedestroy($image);
?>

img_filter_contrast_5.png
Filter: IMG_FILTER_CONTRAST
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_CONTRAST5);
imagepng($image'img_filter_contrast_5.png');
imagedestroy($image);
?>

img_filter_contrast_-40.png
Filter: IMG_FILTER_CONTRAST
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_CONTRAST, -40);
imagepng($image'img_filter_contrast_-40.png');
imagedestroy($image);
?>

img_filter_contrast_50.png
Filter: IMG_FILTER_CONTRAST
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_CONTRAST50);
imagepng($image'img_filter_contrast_50.png');
imagedestroy($image);
?>

img_filter_colorize_100_0_0.png
Filter: IMG_FILTER_COLORIZE
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_COLORIZE10000);
imagepng($image'img_filter_colorize_100_0_0.png');
imagedestroy($image);
?>

img_filter_colorize_0_100_0.png
Filter: IMG_FILTER_COLORIZE
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_COLORIZE01000);
imagepng($image'img_filter_colorize_0_100_0.png');
imagedestroy($image);
?>

img_filter_colorize_0_0_100.png
Filter: IMG_FILTER_COLORIZE
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_COLORIZE00100);
imagepng($image'img_filter_colorize_0_0_100.png');
imagedestroy($image);
?>

img_filter_colorize_100_100_-100.png
Filter: IMG_FILTER_COLORIZE
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_COLORIZE100100, -100);
imagepng($image'img_filter_colorize_100_100_-100.png');
imagedestroy($image);
?>

img_filter_colorize_50_-50_50.png
Filter: IMG_FILTER_COLORIZE
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_COLORIZE50, -5050);
imagepng($image'img_filter_colorize_50_-50_50.png');
imagedestroy($image);
?>

img_filter_edgedetect.png
Filter: IMG_FILTER_EDGEDETECT
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_EDGEDETECT);
imagepng($image'img_filter_edgedetect.png');
imagedestroy($image);
?>

img_filter_emboss.png
Filter: IMG_FILTER_EMBOSS
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_EMBOSS);
imagepng($image'img_filter_emboss.png');
imagedestroy($image);
?>

img_filter_gaussian_blur.png
Filter: IMG_FILTER_GAUSSIAN_BLUR
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GAUSSIAN_BLUR);
imagepng($image'img_filter_gaussian_blur.png');
imagedestroy($image);
?>

img_filter_selective_blur.png
Filter: IMG_FILTER_SELECTIVE_BLUR
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_SELECTIVE_BLUR);
imagepng($image'img_filter_selective_blur.png');
imagedestroy($image);
?>

img_filter_mean_removal.png
Filter: IMG_FILTER_MEAN_REMOVAL
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_MEAN_REMOVAL);
imagepng($image'img_filter_mean_removal.png');
imagedestroy($image);
?>

img_filter_smooth_5.png
Filter: IMG_FILTER_SMOOTH
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_SMOOTH5);
imagepng($image'img_filter_smooth_5.png');
imagedestroy($image);
?>

img_filter_smooth_50.png
Filter: IMG_FILTER_SMOOTH
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_SMOOTH50);
imagepng($image'img_filter_smooth_50.png');
imagedestroy($image);
?>

img_filter_negate.png
Filter: IMG_FILTER_NEGATE
Code to reproduce:
<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_NEGATE);
imagepng($image'img_filter_negate.png');
imagedestroy($image);
?>

A lazy way to do sepia

In order to do sepia, first you do grayscale, then colorize. Here are some experiments:

sepia_100_50_0.png

<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GRAYSCALE);
imagefilter($imageIMG_FILTER_COLORIZE100500);
imagepng($image'sepia_100_50_0.png');
imagedestroy($image);
?>

sepia_100_70_50.png

<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GRAYSCALE);
imagefilter($imageIMG_FILTER_COLORIZE1007050);
imagepng($image'sepia_100_70_50.png');
imagedestroy($image);
?>

sepia_90_60_30.png

<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GRAYSCALE);
imagefilter($imageIMG_FILTER_COLORIZE906030);
imagepng($image'sepia_90_60_30.png');
imagedestroy($image);
?>

sepia_60_60_0.png

<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GRAYSCALE);
imagefilter($imageIMG_FILTER_COLORIZE60600);
imagepng($image'sepia_60_60_0.png');
imagedestroy($image);
?>

sepia_90_90_0.png

<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GRAYSCALE);
imagefilter($imageIMG_FILTER_COLORIZE90900);
imagepng($image'sepia_90_90_0.png');
imagedestroy($image);
?>

sepia_45_45_0.png

<?php
$image
imagecreatefrompng('nathalie.png');
imagefilter($imageIMG_FILTER_GRAYSCALE);
imagefilter($imageIMG_FILTER_COLORIZE45450);
imagepng($image'sepia_45_45_0.png');
imagedestroy($image);
?>

You can read about the right way to do sepia in Wikipedia, but I'd say that the fakes above look pretty good too.
You can try with different values for R, G and B, but hear this - the thing about sepia is that it's brownish-yellow.
Brown is something that has more red, little blue and the green exactly in between the red and the blue. So brown examples would be (200, 100, 0) or (150, 100, 50).
Yellow on the other hand is equal red and green and no blue, like (255, 255, 0). So if you want more brownish sepia, use the first pattern when calling the colorize filter.

About part one

Part one of the image fun is here, it contains code that more or less does the same things, but pixel by pixel, which is very slow, but also works in PHP4.
At the time of writing part one, imagefilter() funtion was probaly only in cvs, not part of the official PHP. imagefilter() is PHP5-only.

 

Maui, PHP Quebec, etc

Friday, November 9th, 2007

Aloha, I'm back from a family vacation in Maui, HI, feeling rejuvenated after having fun on some nice beaches with crystal water, seen an ex-volcano, sunsets, etc.

An email from PHP Quebec was in my inbox saying I'll be speaking at the 2008 PHP Conference in Montreal. Isn't that great?! I'll have a chance to (have some poutine) meet my friends in Montreal, enjoy (the poutine!) the snow I'm sure I'll be missing this year in LA. If you're in Montreal (do try poutine!), the conference is in March and features quite an impressive lineup of PHP speakers.

I had about 200 entries to go over in my RSS reader. I don't read a lot of blogs (not up-to-date list here) but those that I do are really interesting. That explains why after 2-3 hours of reading, I have only 150 out of 200 posts left to read. Anyway, here's a jewel I came across today - theonion.com (via)

 

JS/PHP string concatenation mistype

Thursday, October 25th, 2007

Another one from the "this is not a syntax error" department.

The front-end developer is a strange beast who has to jiggle to and fro and code in several languages literally at the same time - javascript, html, css, php or some other server side language, some SQL dialect... No wonder that sometimes we make silly mistakes like:

var $myarray;
var array = array();
$myarray = [];
foreach(var i in myarray)

Last night I just did a silly mistake like this. In JavaScript I used the PHP way of concatenating strings. Something like:

var user = 'Stoyan';
alert('hello ' . user);

This is obviously wrong, but the thing is that it's not a syntax error as one might expect. It alerts "undefined". Why is that?

Well, 'hello' is a string object. You can call methods and properties on it, like:

>>> 'hello'.toUpperCase()
"HELLO"
>>> 'hello'.length
5

And spaces don't matter...

>>> 'hello'     .   length
5
>>> 'hello'  . toUpperCase()
"HELLO"

So 'hello' . user is an attempt to access the "user" property of the string object 'hello'. This property doesn't exist, hence the "undefined" result.

Doing the opposite (using JavaScript-type concatenation in PHP) is also not an error:

$user = 'Stoyan';
echo 'Hello ' + $user; // prints 0
 

5 domain names - 5 CMS’s

Tuesday, October 2nd, 2007

As bloged before, my publisher organizes a "Best CMS" award thingie and I'm one of the judges in the "Best PHP CMS" category. It's time now to sit on my behind and judge, as scary as it sounds. I have to pick three from the five finalists in the PHP category in any order, so that makes it easier. It could also mean I just pick two I don't like and call it a day. But I want to be more responsible than that and actually experience the pain and the pleasure of building a web site with each CMS. Installing, customizing, add/edit/publishing content, upgrading ... the whole thing.

So I picked 5 domains of mine that already have something published and decided to convert them to one of the CMS's. I picked which CMS will be used for each domain by ordering the domains and the CMS's alphabetically, I think this is as random as any other way of doing it. Here's the list in the format: domain-cms-description.

  • bebetata.com - CMS Made Simple - will have photos of my kids, content in Bulgarian, "bebetata" means "the babies"
  • csssprites.com - Drupal - currently tool for creating CSS sprites, will expand to have articles, tips, etc.
  • hiliteme.com - e107 - tool for highlighting code, will add articles, different methods to highlight, etc.
  • jspatterns.com - Joomla! - best/worst practices when it comes to coding javascript
  • ragtimebg.com - PHP Fusion - this will be a site for my mother-in-law's business, in Bulgarian. "About us", "contact", that kind of stuff. Also articles on how to take care of your tires. The current version is at ragtime.hit.bg (don't laugh, this site is ooold, featuring client-side inclusion of header and footer, using document.write, fun stuff)

As you can see, different sites with different requirements, so hopefully I'll think of interesting scenarios to try and achieve with each CMS. Any requirement I implement in one CMS, I'll try in all others, so that things are as fair as possible.

I posted initial musings on the "best CMS" problem some time ago in this posting over at opensourcecommunity.org, where the charming Amy Stephen is a frequent contributor.

Wish me luck to have this initiative more than just a nice "wannado".

 

file_put_contents() for PHP4

Monday, September 10th, 2007

Simplified, but still...

<?php
if (!function_exists('file_put_contents')) {
    function file_put_contents($filename, $data) {
        $f = @fopen($filename, 'w');
        if (!$f) {
            return false;
        } else {
            $bytes = fwrite($f, $data);
            fclose($f);
            return $bytes;
        }
    }
}
?>
 

LA Web devs meetup at Yahoo

Monday, July 23rd, 2007

So there is this group of local LA web developers that meet every month or so to meet and discuss what's up. More about/join the group here.

This month Yahoo will be hosting the meetup in the Santa Monica office (my workplace), it's actually tomorrow, so if you're in LA, don't miss the opportunity for beer, pizza and meeting fellow web devs. RSVP here.

On Yahoo's side, Jim Bumgardner, a.k.a. krazydad will be demoing the Facebook app he did that allows you to find music videos and discover artists similar to the ones you like. The app, Jim talking about it, Yahoo Developers Network posting.

Sounds like it would be fun, and also a chance for a local web dev to see what Yahoo's office looks like, meet some of the people that work here, and in a way to try-before-you-apply :D

 

CMS award nominations open

Monday, July 16th, 2007

Nominate your favorite open-source CMS before August 31st. (Yours truly is one of the judges in the PHP category.)

 

PHP/Javascript dev tools for TextPad

Monday, July 16th, 2007

Here are some convenient tools I've added to my TextPad editor, hope you'll like 'em.

TextPad tools

Stuff can easily be added to TextPad's Tools menu, like I did, shown on the screenshot.
textpad-tools.png

In order to do so, you go Configure -> Preferences. Then select Tools on the tree to the left, then Add. You can add a DOS command, an application or a help file (.hlp or .chm)

The first three tools on the picture above come out-of-the-box, the other 4 I've added myself. Let me show you what I did.

Tool #1 - PHP lint (a.k.a. syntax check)

So I'm editing a PHP file and I want to syntax check it from the editor. Good. PHP on the command line comes with the -l option (this is lowercase L) which does just that. For example if you run this from your command prompt, it will check the file test.php for syntax errors:
C:\php> php -l test.php

So for tool #1 I just did - Configure-Preferences-Tools-Add-DOS command, then typed:
php –l $File

In Textpad apparently $File refers to the current file being edited. So now I can edit a file, press CTRL+4 and syntax check the file. Neat.

Tool #2 - PHP help

This is exactly the same idea as the previous tool. I use PHP command line option --rf which gives you help information. For example try getting help with the str_replace() function:

C:\php>php --rf str_replace

The result is

Function [ <internal> public function str_replace ] {

  - Parameters [4] {
    Parameter #0 [ <required> $search ]
    Parameter #1 [ <required> $replace ]
    Parameter #2 [ <required> $subject ]
    Parameter #3 [ <optional> &$replace_count ]
  }
}

Adding this functionality to textpad is very similar to tool #1, only this time the command is:

php --rf $SelWord

$SelWord is the currently selected word in textpad (just placing the cursor somewhere in the word is enough)

Tool #3 - PHP Manual

Sometimes the help above is not enough and you want to hit the manual on php.net. Here's the next tool. You go:
Configure-Preferences-Tools-Add-Program and you find your firefox.exe, like
C:\Program Files\Mozilla Firefox\firefox.exe

Now, in order to edit a tool you created in TextPad, you need to expand the Tools node of the Preferences tree and click the tool you need, as shown on the screenshot:

In this screen, you need to type this in the Parameters field:
http://php.net/$SelWord

Tool #4 - JS Lint

JSLint is a tool for checking JavaScript code, it also can be run on the command line in Windows, using cscript. So if your jslint.js is in C:\, you can have tool #4 another DOS command:

cscript C:\jslint.js <$File

Hope you like it

Or maybe add these simple tools to your text editor of choice.

Last thing

One little thing I didn't mention - it's a bit of a challenge to figure out how to rename a tool once created. Basically on the list of tools (next to the Add button), just click, right click, double-click or simultaneously click left and right mouse buttons. One of these will work eventually :)

 

phpBB front-end optimization - 1 hour workshop

Friday, July 13th, 2007

Let's go ahead and optimize our phpBB installation for front-end performance. I'll follow Yahoo's 14 optimization rules, but only implement the ones that apply for phpBB. During this short workshop there will be no changes to the phpBB code, we'll create a new template instead, so that in case something bad happens, your board will continue to work normally. As an example, I'll use the bgcanada.com/phpBB2, the board I volunteer to administrate.

The plan

1. creating two subdomains to host assets - images and css, maximizing parallel downloads (also following rule #9)
2. creating a new template (theme) based on the default subSilver template
3. moving CSS to an external file (rule #8), merging the two css files (rule #1)
4. copying images and css to the new subdomains
5. making sure css is served gzipped (rule #4) and also making sure php pages are served gzipped)
6. turning ETags off (rule #13)
7. setting the Expires header (rule #3)
There will be no explanations on the reasoning why these rules exists, but only how to implement the applicable ones in phpBB. It's a good idea though to go through the links above and read the detailed description of the rules. Maybe you'll find something that can be done in addition to the plan above. The suggested implementations were done and tested on a normal $60/year shared host, working around the limitations of such hosting. I'll speculate that at least 90% of the phpBB installations out there use shared host so I hope my implementation will be relevant to your phpBB install as well.

New subdomains

Typically, shared hosts allow subdomains and have their admin interface for doing so. In case your host is an exception, alternatively you can buy one or two extra domains to use for the same purpose - storing page assets.

So in my case, for the domain bgcanada.com I create two sub-domains - i1.bgcanada.com and i2.bgcanada.com (i as in image). Now the question would be how to divide assets between the two domains. I decided that more or less half of the images are multilingual (they don't contain text) so these go to i2. i1 will have all the rest - translatable images found in subSilver/images/lang_english, smilies and the stylesheet.

creating a new template

Let's go ahead and create a new template (theme), based on the default subSilver template. I'll call mine "sso" as in "subSilver optimized" (or the longer mirror-like version "ssoss" as in "stoyan stefanov, oh, so smart" ;) )

For this purpose, just take the contents of path/to/phpbb/templates/subSilver and copy it under the name "sso", so you'll have /templates/sso, an exact subSilver copy.

Now go ahead and modify templates/sso/subSilver.cfg, the theme configuration. First rename it to sso.cfg. Then at the top of the document, replace the line:

$current_template_images = $current_template_path . "/images";

with

//$current_template_images = $current_template_path . "/images";
$i1 = "http://i1.bgcanada.com/sso"; // language and smilies
$i2 = "http://i2.bgcanada.com/sso"; // the rest

use your domain, of course.

Then do a search/replace. All occurrences of:

$current_template_images/{LANG}/

should become

$i1/{LANG}/

And all occurrences of

$current_template_images/

become

$i2/

At the end of the file, just before ?> add a new line:

$board_config['smilies_path'] = 'http://i1.bgcanada.com/smilies';

This will overwrite the path to the smilies you set in the admin interface. This is optional, you can achieve the same by using the admin panel, but we said we wanted to have the board unaffected by the changes.

Now open templates/sso/theme_info.cfg and replace all occurrences of "subSilver" with "sso".

Now the bad news is that some of the template files (.tpl) still contain relative paths to images, we need to make these absolute and pointing to the subdomians. Since these paths are hardcoded in the templates we're sure that they are language independent, so they'll all go to the i2 subdomain. If you have a good text editor that can search/replace in multiple files, go ahead. Alternatively, use the php script I came up with. Download it, copy it to your /templates/sso folder, rename to replace.php and use it like:
C:\path\to\phpbb\templates\sso> php replace.php
It will report what it replaced.
It only searches for templates/subSilver/images and replaces with http://i2.bgcanada.com/sso

Yep, almost done with the template, one last step - the css.

Moving CSS to an external file, merging

In the default subSilver there are style definitions in overall_header.tpl. Remove them, the whole thing between the <style> tags and replace with:

<link rel="stylesheet" href="http://i1.bgcanada.com/sso.css" type="text/css">

Now rename subSilver.css to sso.css. Copy the contents of formIE.css and append it to the end of sso.css. Optionally, you can walk through the new file and strip all comments and white space. Or use my stripped version.

Note that as a side effect, the font of the board will look somewhat different, because the definitions in subSilver.css are not exactly the same as those in the overall_header.tpl. It's not a big difference, but if it's important to you, just ignore the original subSilver.css and create sso.css copying the styles from overall_header.tpl and formIE.css.

We're done with the files, the rest is sysadmin stuff.

copying files to the new subdomains

That's easy, just take everything from templates/sso/images (leave the lang_english or any other language folders) and copy to http://i2.bgcanada.com/sso

Than take all lang_* folders and copy to http://i1.bgcanada.com/sso, so you'll have http://i1.bgcanada.com/sso/lang_english/

Now copy all smilies from phpbbroot/images/smilies to http://i1.bgcanada.com/smilies/

Now take the sso.css you created and copy to http://i1.bgcanada.com/sso.css

Last, take the whole sso directory and copy to the main domain - http://www.bgcanada.com/phpBB2/templates/ so that it's in the same folder next to subSilver.

Login to the admin panel and activate the new theme as usual. Login to your account and change your theme preference to sso. You still want to test before making it a default theme for everyone.

At this point you should be able to browse your forum with the new theme and everything should look like as if you were using subSilver.

Serving gzipped content

The rule is that all served files should be gzipped, with exception for images, because gifs, jpgs, pngs are already compressed.

To serve the normal html (php) pages gzipped, just use the built-in phpBB feature, log on to the admin panel and enable gzip compression.

To serve sso.css gzipped, ideally you should only create an .htaccess file and have Apache do it for you (more info). Unfortunately my host won't allow it, so I took the alternative path - have PHP gzip and serve the css file. To do so I created an .htaccess file http://i1.bgcanada.com/.htaccess and put in it:

AddHandler application/x-httpd-php .css

This affects all CSS files (but we have only one) and makes them php scripts. If your host allows you to use php_value in .htaccess, you can do the rest of the job by only using .htaccess. Otherwise create a http://i1.bgcanada.com/php.ini file and put in it:

[PHP]
default_mimetype = "text/css"
zlib.output_compression = On
zlib.output_compression_level = 6
expose_php = Off
auto_prepend_file = "pre.php"

The first line makes php send text/css header instead of the default text/html. Some browsers won't like CSS files served with text/html header. The second line enables compression and the next line sets the compression level (could be up to 9). 4th line is absolutely optional, just removes an extra header that PHP sends. The last line sets that all served php files should include the file pre.php as if you used include "pre.php" inside every PHP script. This is actually used later for the Expires header.

turning ETags off

That's super easy. Just add .htaccess rules in both i1 and i2 to say:
FileETag none

Expires header

To set the expires header, add these lines to .htaccess on both i1 and i2

ExpiresActive On
ExpiresDefault "access plus 2 years"

This makes all files - images and CSS - on i1 and i2 expire in 2 years, if that's too much/not enough, feel free to change.

If it doesn't work for you (it didn't for me, because of the host), you can go the php auto_prepend route described above and add in the pre.php file
header('Expires: ' .gmdate("D, d M Y H:i:s",time() + (60 * 60)) . ' GMT');
Feel free to set 60 * 60 to whatever you think makes sense for you. Note that this will only affect the CSS file, images will be served "normally". You can also have PHP serve all the images, like we did for CSS, but I think it's probably too much.

Sysadmin summary

There was a lot of if-then above so let me summarize what worked for me, but try your other options first to see how "liberal" your host is in order to do a better job than me.

http://i1.bgcanada.com/.htaccess

AddHandler application/x-httpd-php .css
FileETag none

http://i1.bgcanada.com/php.ini

[PHP]
default_mimetype = "text/css"
zlib.output_compression = On
zlib.output_compression_level = 6
expose_php = Off
auto_prepend_file = "pre.php"

http://i1.bgcanada.com/pre.php

<?php
header('Expires: ' .gmdate("D, d M Y H:i:s",time() + (60 * 60)) . ' GMT');
?>

Done!

We're done! Did it take more than an hour? Hope not, although with these computers and stuff, everything takes more time than expected.

Do you see anything missing? Or something that can be improved? Or something didn't work for you? Please post a comment. The whole posting is a bit fast-paced and written in a rush (I really need to go to bed), so if there's something unclear please ask.

 

On a publishing diet

Thursday, July 12th, 2007

So I launched this little tool csssprites.com that allows you to upload images and create one CSS sprite image, plus it gives the background-position CSS definitions to use in order to show parts of the sprite. People have been trying it out, but unfortunately sometimes uploading 20 megs of images to create a sprite, which is not the point of the css sprites technique. Anyway as a result I exceeded the disk quota my host gives me and since the site is hosted on the same server as this blog, the blog stopped working. Hence the publishing diet.

Initially I blamed WordPress because it started acting strangely, asking me to update my database, saying that I don't have admin privileges, then not loading the CSS files and finally just stopped working even on the front end. I said oh well, I need to upgrade it anyway, so let's do it now. Just trying to FTP a single file got me the message that I can't copy so I finally figured out the real case - the exceeded disk space.

It's all good now, I just deleted all CSS sprite images that were generated, I was planning to do a cron job do delete the ones older than a day or two anyway, but never got around doing it. I should just check and warn the cssspritres.com users not to upload huge images, because this is not how CSS sprites were designed to work anyway.

Long story, short message. I'm off the publishing diet now.

Meanwhile I wrote an article for the International PHP Magazine, it's an intro to unit tetsing with PHPT, called "PHPT - Unit testing for the rest of us". Nice, eh? Just got an email today that the new IPM issue is out the door, you can check the TOC here. I wanted to further experiment with PHPT and was thinking of writing this test generation tool. Say you have a bunch of classes, you run the tool and it generates PHPT test stubs, based on the classes and methods in finds. Then you tweak the generated stubs here and there to implement the actual tests. PHPUnit has this feature, so why not PHPT as well. We'll see if I'll find the time.

On a different technology, I was playing around implementing the decorator pattern in Javascript, will post about it later (sneak peek).

On a different subject, just added a few very simple tools to my favorite Textpad, I found them helpful for PHP development, will post about them later.

On a yet another subject a few days ago I finished a draft outline for my new book-to-be and we started discussing with the editor.

On a totally unrelated subject, I did a new phpBB theme (copy of the default subSilver) following as many of the Yahoo front end performance rules as I found applicable. Naturally, I'll post about it later.

Otherwise life has been good. I moved with my family to LA to start working for this company called Yahoo!. Work is great, LA was bit of a surprise and not very welcoming, but hey, it's the experience. We had some initial rental issues (we lost quite a bunch of money double and some point triple renting), then there was the stress of the whole move, having to start everything over again, driving licenses, shocking 20% APR rates from Toyota, credit cards refusals and stuff (the only thing that I still use here from Canada is the Costco card!). Yahoo did help a lot during the moving process, can't imagine what would have been without all the little and not so little perks I got during the relocation. So anyway, after all that initial shock, the family is starting to settle. The kids just loooove Disneyland, we ended up getting anual passes for California residents, so we'll be seeing it a lot. Also the beach is quite nice, not the cleanest mind you, but it's probably because we're new don't know where to go, we just hit the closest to us, in Venice. By the way, Venice is amazingly similar to some little Bulgarian towns on the Black Sea. In general LA is quite expensive, especially the Santa Monica area, where the office is, but I wanted to be close to the family in those times of change, so we ended up renting a place only 5 miles from the office (still getting used to those miles and pounds). I proudly bike to work now, doing my share in saving the environment. Half an hour in each direction, it's a nice excersise. Talking about biking, here's some biking-to-work wisdom for you:

Tree branches are hanging lower than they appear.

also

Just when you thought you learned how to bike without using your hands and to light a cigarette meanwhile... you didn't.

 

CSS Sprites generation tool

Wednesday, June 27th, 2007

Here's my last weekend's project - a web-based tool to generate images for CSS sprites: http://www.csssprites.com. Cool domain name, eh? I couldn't believe it was not taken.

CSS Spr...what?

This is a simple technique used for page load performance purposes. Since HTTP requests are the most expensive thing you can do in regards to front-end performance, you strive for making as little requests as possible. So instead of having the browser download 4 rounded corner images for example, you create one image that has all four. Then you use CSS' background-position to only show the part of the image you want. More on the subject in this ALA article

How does the tool work

You upload as many images as you want and the tool generates a mosaic of all images, gives you the result as PNG and gives you the coordinates you need to use in the background-position declaration. The tool also gives you an html page as an example, so you can save both the PNG and the html page for reference.

Image size

If you properly optimize the big image, you might actually have smaller size than all the individual images combined. In my tool, the PNG image generated is not optimized at all, I leave this to you to use PNGOUT or any other tool you know. Also you can convert the PNG into GIF if that's better for your purposes.

Implementation - PHP

The PHP code is fairly simple. The actual spriting (is that a word?) class takes a list of images and calls getimagesize() on each one to get the dimensions. The image with the biggest height is used as distance between images. The rest is just composing the imagemagick command that will to the work. Here's the important method:

<?php
function combine() {
    if ($this->distance === false) {
        $distance = $this->_biggest;
    } else {
        $distance = (int)$this->distance;
    }

    if ($this->output_dir === false) {
        $output_dir = $this->_dir;
    } else {
        $output_dir = $this->output_dir;
    }

    $half = ceil($distance / 2);

    $coord = array();
    $y = 0;

    foreach ($this->images as $i=>$data) {
        $this->images[$i]['x'] = $half;
        $this->images[$i]['y'] = $half + $y;
        $coord[] = '-page +0+' . $y . ' ' . $i;
        $y += $data[1] + $distance;
    }

    $cmd = 'convert ' . implode(' ', $coord)
         . ' -background none -mosaic -bordercolor none -border '
         . $half . 'x' . $half
         . ' ' . $output_dir . '/result.' . $this->output_format;
    system($cmd, $ret);

    return $ret === 0;

}
?>

Implementation - JS

In the spirit of web2 I couldn't afford a complete page reload :lol: although it would've been much simpler. I just had to get fancy. YUI to the rescue. On page load I set up the form for async request, using YAHOO.util.Connection. In case of file uploads YUI generates an iframe behind the scenes and uploads to the iframe. Then it takes whatever is in the body on the iframe and gives it to you instead of the XMLHttpRequest's responseText property.

So the files are uploaded to upload.php which calls the class that has the method mentioned above then loops through the $images property of the said class and writes the example html file as well as prints out a JSON string with the same image information.

YUI's Connection calls my callback function and I get the invaluable responseText. Parsing the JSON with the json.js, I get a JS object. Looping through it and DOM-generating a table of results is the semi-last step. The last is (we're fancy, remember?) to yellow-fade the background color of the result, using YAHOO.util.Animation.

BTW, I got fancy once again and combined and minified json.js with my JS file, so that there is one less request and a side effect impossible to read. The unminified version of the JS that does all the work is here for reference.

Comments

I hope this tool cane be useful for quickly generating those sprites, if only for prototyping purposes. Any comments, requests, bug reports are all very welcome.

And how do you like the version of the tool? Anyone n00b can do "beta", it takes a true h@x0r (or something) to do a better job :D

Ah, yeah, and the page badly needs a stylesheet, do you want to help?

 

Good PHP CMS?

Thursday, June 21st, 2007

I've been selected as a judge in this year's Open Source CMS Award, organized by Packt Publishing. I'll be voting for three out of 5 PHP CMS projects where those 5 are selected by votes from the site visitors. Join me in a discussion I started at opensourcecommunity.org "What do you think makes a good CMS?"