cakePHP testing tip : Creating or edit a datasource on the fly

For one of the tests I'm currently writing, I needed to assert that my method was correctly returning an error if the connection to the database was impossible.

I couldn't find an easy way to change datasource credentials once the app was initiated, so I decided to manually update the ConnectionManager inner values.

Here's how I did it :

// Getting the datasource cache in the ConnectionManager object
$connectionManagerInstance = ConnectionManager::getInstance();
$databaseConfig = &$connectionManagerInstance->_dataSources;

// Saving the initial setting for reverting it later
$_defaultConfig = $databaseConfig['default'];

// Changing the password so the credentials will fail
$databaseConfig['default']->config['password'].= 'pass';

// Getting the updated datasource
$connect = ConnectionManager::getDataSource('default');

// Error handling when connection unavailable
[...]

// And reverting the settings back
$databaseConfig['default'] = $_defaultConfig;

This proved really useful when testing to simulate a database server error.

Displaying a date in the correct locale with cakePHP

The easiest way to display a date in a given format is to use a combination of strftime() and strtotime().

Sometimes, you also need your date to be displayed in a specific language. For example, for a french website, I needed a date to be displayed as "mardi 03 août 2010" instead of "Wednesday, August 3rd".

Setting the locale

To achieve that, you just have to tell PHP which locale to use when displaying date with setlocale(LC_TIME, $locale).

The value of $locale is OS dependent, though. For example, on a linux server, you have to set fr_FR while it is fr or even frenchon Windows.

Fortunatly, you can pass an array of locales to setlocale(, and the system will use the first one it can find. You just have to pass an array containing all possible values and you'll be good to go.

I wrote a little method that use the L10nobject that comes with cakePHP to automate the creation of such an array. Just feed him a 3-letter language code and it will return an array of the most common locale names.

function getLocales($lang) {
// Loading the L10n object
App::import('L10n');
$l10n = new L10n();

// Iso2 lang code
$iso2 = $l10n->map($lang);
$catalog = $l10n->catalog($lang);

$locales = array(
  $iso2.'_'.strtoupper($iso2).'.'.strtoupper(str_replace('-', '', $catalog['charset'])),    // fr_FR.UTF8
  $iso2.'_'.strtoupper($iso2),    // fr_FR
  $catalog['locale'],             // fre
  $catalog['localeFallback'],     // fre
  $iso2                           // fr
  );
return $locales;
}

You may note that I set in first position a locale with the mention of the encoding. This is only used on Linux machines, Windows does not handle that. That's a pity, but I'll show you how to correctly make it work underWwindows.

As a side note, setlocale will not return false if the locale is not found, it will just fail to load it.

Displaying date in UTF8

If your app is in UTF8 (and it should be !) you may run into problem when trying to display a simple strftime("%B", strtotime($date)) on Windows.

%B translate to the current month name. For a month like Août (August) the funny û will not get correctly displayed, because Windows does its locale translation in its native encoding.

You'll need to manually encode it in utf8, but if you do so on a linux machine, as the result is already encoded in utf8 you may end in double encoding the same string, resulting in others display errors.

Note also that if your format string itself contains utf8 encoded characters (like%A %d %B %Y à %Hh%M), encoding it in utf8 again will also result in wrong characters displayed.

What I've done is creating a simple method in an helper that will take care of encoding the result if needed.

function time($format, $date = null) {
  // On Windows, we will force the utf8 encoding of the date
  if (DIRECTORY_SEPARATOR == '\\') {
    return utf8_encode(strftime(utf8_decode($format), strtotime($date)));
  }
  // On linux, this is already taken care of by setlocale()
  return strftime($format, strtotime($date));
}

This way, we make sure that the date is correctly displayed in utf8, no matter the OS, even if you already supply utf8 characters in the format string.

Breaking out of an if statement in PHP

One pattern I use when writing new method is to put conditions that may end the script early on top. Like "stop the method if the arguments are not right" or "stop the action if the user is not loggued in".

It allow me to avoid having nested if/elsestatement that became unreadable.

Felix from Debuggable wrote something about that a while ago.

This is a pretty easy pattern to follow when writing methods, but can be quite harder to achieve if you need to stick inside a main method. You just want to go "out" of the if statement, to continue the script, but not end the method.

Break to the rescue

You can't use the breakkeyword in an if statement like you would in a loop. It just throws an error.

But, you can define a simple do {} while (false) loop and use the break goodness inside it.

do {
  if (empty($data)) break;
                 
  $this->create($data);
                   
  if (!$this->validates()) break;
                   
  $this->save();
} while (false);

This helped me some times, hope it can help someone else.

CSS3 gradients with CSSTidy

Gradients are one of the new cool stuff CSS3 brought with it. Like the others cool things, it still suffer from a partial implementation and vendor-specific properties.

It also isn't correctly parsed by CSSTidy. Here I'll show you how to patch your CSSTidy to make it eat gradients correctly.

Quick and dirty patch

First, you'll need to edit the huge parse() method in csstidy.php. You'll have to add a condition to explictly tell CSSTidy not to discard -webkit- gradient and -moz-linear-gradient.

Just open your csstidy.php file, find the parse() method and locate the case 'instr' in the huge switchstatement.

if (!($this->str_char === ')' && in_array($string{$i}, $GLOBALS['csstidy']['whitespace']) && !$this->str_in_str)) {
  $this->cur_string .= $temp_add;
} **else {**
**  if ($this->sub_value=="-webkit-gradient" || $this->sub_value=="-moz-linear-gradient") {**
**      $this->cur_string.=' ';**
**  }**
**}**

In bold, the elsepart to add. This will make sure your webkit and firefox gradient rules will get processed correctly.

I don't really understand WHY it work, but it does. The parse()method is a huge uncommented mess, it is quite difficult to understand it. There must be a better way, a more generic one than specifying some properties, but I didn't manage to come with anything better than that.

Fortunatly, the next part is cleaner.

Telling CSSTidy which properties not to merge

If you write a css like the following, only the latest (color:white) rule will get through CSSTidy.

body {
  color:red;
  color:white;
}

That's logical, because CSSTidy will remove any unused CSS declaration. Unfortunatly, this is not what we want, because we need to declare several background: rules, one for Webkit, and one for Firefox.

By looking at CSSTidy source code, we can find that it contain a quick fix to allow the cursor: property to be defined several time (to cope with the old cursor:pointer / cursor:hand issue).

I just extended this quick fix to work for other properties as well, and even managed to allow them to be passed as a config value.

Defining the config value

First, open the csstidy.php file, and around line 310 you should find a list of default config values. Just add the following :

$this->settings['multiple_properties'] = array('cursor', 'background');

This will define the default list of properties that are allowed to be defined several times in a css rule.

Next, we'll edit the set_cfg() method to allow the passing of array values. Just replace the else statement with this one :

else if(isset($this->settings[$setting]) && $value !== '') {
  // Merging array settings
  if (is_array($value) && is_array($this->settings[$setting])) {
    $this->settings[$setting] = array_merge($this->settings[$setting], $value);
  } else {
    // Setting classic setting
  $this->settings[$setting] = $value;
  }

  if ($setting === 'template') {
    $this->_load_template($this->settings['template']);
  }
  return true;
}

You can now pass a list of properties to be added to the existing list by calling ->set_cfg('multiple_properties', array('property1', 'property2'));

Now, find the css_add_property() method, and around line 1066, change theif (strtolower($property) == 'cursor')if statement to this more generic one :

if (in_array($property, $this->get_cfg('multiple_properties')))

And now, in csstidy_print.php, find the_print() method, and replace the case PROPERTY block with this (more concise) one :

case PROPERTY:
  // Converting back multiple properties
  $multipleProperties = $this->parser->get_cfg('multiple_properties');
  foreach($multipleProperties as $property) {
    $propertyLength = strlen($property);
    if (substr($token[1], 0, $propertyLength)==$property) $token[1] = $property;
  }


  // Applying correct casing
  $caseProperties = $this->parser->get_cfg('case_properties');
  if ($caseProperties==2) $token[1] = strtoupper($token[1]);
  if ($caseProperties==1) $token[1] = strtolower($token[1]);

  $out .= $template[4] . $this->_htmlsp($token[1], $plain) . ':' . $template[5];
break;

And that's it

You now can have gradients compressed with CSSTidy. Well sort of, because this is just a quick and dirty patch, as I'm not the creator of CSSTidy.

This could surely be improved in a less hacky way, for example by compressing the color code used in the gradients...

Meeting with Eric Daspet and Stoyan Stefanov in Paris

On July 21st will be held an informal meeting in Paris for all the web developers that have an interest in web performance.

Eric Daspet and Stoyan Stefanov will be there and host a talk about performance impact on sales and some more technical less known facts.

The first part will be in French while Stoyan's will be in English. If you have the chance to be in Paris in July, do not miss this event.

I will, of course, be there. Eric Daspet is the french "gourou" on web performances, and Stoyan Stefanov is one of the main author of the Smushit plugin/websit.

Web performance is a really interesting subject where there is so much to learn and so much to try.