Symfony 1.4 over PHP 5.5

As you might know Symfony 1.4.x has ended its LTS in last November 2012, at that time PHP 5.5 was not released yet. That 5.5 version came in June 2013 and if you want to upgrade to that version you need to know about Symfony 1.4.x is not 100% compatible with PHP 5.5.

If you upgrade to PHP 5.5 you will start seeing a couple of warnings in your Symfony 1.4 application:

Deprecated: preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead in lib/vendor/symfony/…

In this post I will show you a patch to fix these warnings and make Symfony works over PHP 5.5.

In lib/vendor/symfony/lib/command/sfCommandManager.class.php:

1
2
3
4
5
6
7
8
9
10
11
@@ -108,7 +108,9 @@ class sfCommandManager
     else if (!is_array($arguments))
     {
       // hack to split arguments with spaces : --test="with some spaces"
-      $arguments = preg_replace('/(\'|")(.+?)\\1/e', "str_replace(' ', '=PLACEHOLDER=', '\\2')", $arguments);
+      $arguments = preg_replace_callback('/(\'|")(.+?)\\1/', function($matches) {
+         return str_replace(' ', '=PLACEHOLDER=', $matches[2]);
+     }, $arguments);
       $arguments = preg_split('/\s+/', $arguments);
       $arguments = str_replace('=PLACEHOLDER=', ' ', $arguments);
     }

In lib/vendor/symfony/lib/form/addon/sfFormObject.class.php:

1
2
3
4
5
6
7
8
@@ -278,6 +278,6 @@ abstract class sfFormObject extends BaseForm

   protected function camelize($text)
   {
-    return preg_replace(array('#/(.?)#e', '/(^|_|-)+(.)/e'), array("'::'.strtoupper('\\1')", "strtoupper('\\2')"), $text);
+    return sfToolkit::camelize($text);
   }
 }

In lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/form/sfFormFilterDoctrine.class.php:

1
2
3
4
5
6
7
@@ -323,7 +323,7 @@ abstract class sfFormFilterDoctrine extends sfFormFilter

   protected function camelize($text)
   {
-    return sfToolkit::pregtr($text, array('#/(.?)#e' => "'::'.strtoupper('\\1')", '/(^|_|-)+(.)/e' => "strtoupper('\\2')"));
+    return sfToolkit::camelize($text);
   }

In lib/vendor/symfony/lib/plugins/sfPropelPlugin/lib/form/sfFormFilterPropel.class.php:

1
2
3
4
5
6
7
8
@@ -263,6 +263,6 @@ abstract class sfFormFilterPropel extends sfFormFilter

   protected function camelize($text)
   {
-    return sfToolkit::pregtr($text, array('#/(.?)#e' => "'::'.strtoupper('\\1')", '/(^|_|-)+(.)/e' => "strtoupper('\\2')"));
+       return sfToolkit::camelize($text);
   }
 }

In lib/vendor/symfony/lib/response/sfWebResponse.class.php:

1
2
3
4
5
6
7
@@ -406,7 +406,7 @@ class sfWebResponse extends sfResponse
    */
   protected function normalizeHeaderName($name)
   {
-    return preg_replace('/\-(.)/e', "'-'.strtoupper('\\1')", strtr(ucfirst(strtolower($name)), '_', '-'));
+    return preg_replace_callback('/\-(.)/', function ($matches) { return '-'.strtoupper($matches[1]); }, strtr(ucfirst(strtolower($name)), '_', '-'));
   }

In lib/vendor/symfony/lib/util/sfInflector.class.php:

1
2
3
4
5
6
7
8
9
10
@@ -28,10 +28,7 @@ class sfInflector
   public static function camelize($lower_case_and_underscored_word)
   {
     $tmp = $lower_case_and_underscored_word;
-    $tmp = sfToolkit::pregtr($tmp, array('#/(.?)#e'    => "'::'.strtoupper('\\1')",
-                                         '/(^|_|-)+(.)/e' => "strtoupper('\\2')"));
-
-    return $tmp;
+    return sfToolkit::camelize($tmp);;
   }

EDIT: I’ve added a improved camelize function that the original.

In lib/vendor/symfony/lib/util/sfToolkit.class.php:

1
2
3
4
5
6
7
8
9
10
@@ -608,4 +608,15 @@ class sfToolkit

     return set_include_path(join(PATH_SEPARATOR, $paths));
   }
+
+   public static function camelize($text)
+   {
+       return strtr(ucwords(strtr($text, array('/' => ':: ', '_' => ' ', '-' => ' '))), array(' ' => ''));
+   }
 }

This patch was tested in Symfony 1.4.20 and it works properly.

EDIT: Do NOT use Symfony 1.4.x for new projects. This patch works for existing legacy symfony1 applications, but Symfony2 is the way to go today.

Unofficial Symfony 1.5

Since the official support for Symfony 1.x has been interrupted in November 2012, a community driven fork of symfony 1.4 has being developed to support DIC, form enhancements, latest Swiftmailer, better performance, composer compatible and PHP 5.5+ support.

This unofficial version can be found at https://github.com/LExpress/symfony1.

And you can read the README file there to know what has been added.

Keep on mind as they mention in the README file “Do not use it for new projects: this version is great to improve existing symfony1 applications, but Symfony2 is the way to go today“.

symfony-all command line

If you are a symfony developer you already should know about the great commands you can use to ease your development. To learn more about those commands I would recommend you this chapter in the symfony’s documentation.

The most common commands are those to generate our model, forms and filter; so to do that you can to run each command individually.

To speed up this common use, I would share with you a really simply bash script to do all this in just one command:

1
2
3
4
5
#!/bin/bash
php /path/to/symfony doctrine:build-model
php /path/to/symfony doctrine:build-forms
php /path/to/symfony doctrine:build-filters
php /path/to/symfony cc

The above script will build our model, forms and filters, and it will clear the cache. Of course, you also could run the database related commands, but we have to be careful about changing database structures in existing projects.

I hope this script will be useful for you guys.

Different environments in the same front controller

If you are familiar with symfony 1.x you should know that the framework comes with a different front controller for each environment, for instance: frontend.php for production, frontend_dev.php for development and frontend_stage.php for stage environment.

Well, I like much to have just one unique front controller which handle the different environments we have. That is what I do in all my projects, and here is the code snippet to do it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
$domainArrayReversed = array_reverse(explode('.', $_SERVER['HTTP_HOST']));
$subdomain = isset($domainArrayReversed[2]) ? $domainArrayReversed[2] : '';

//This is for switching the environment based on the subdomain
$debug = false;
$app = 'frontend';

switch ($subdomain) {
    case 'local':
    case 'dev':
        $env = 'dev';
        $debug = true;
        break;
    case 'stage':
        $env = 'stage';
        break;
    case 'www':
    default:
        $env = 'prod';
        break;
}

require_once(dirname(__FILE__) . '/../config/ProjectConfiguration.class.php');

$configuration = ProjectConfiguration::getApplicationConfiguration($app, $env, $debug);
$sfContext = sfContext::createInstance($configuration);
$sfContext->dispatch();

So with the above script we will be running the application on DEV mode when we use it local.hasheado.com or dev.hasheado.com.

I hope this script will be helpful for you.

Symfony 1.x: Maintenance mode

Symfony Maintenance mode, when enabled, it disallows access to our Symfony project’s appand replaces it with a placeholder (unavailable) page.

To put in maintenance (disable) our application we should run:

1
user@unix:~$ php symfony project:disable [app] [env]

Once this command finishes Symfony looks for an unavailable.php file and let you choose where you prefer to put it. If we have many apps and we want to have a customized unavailable page foe each, we should put it in

/path/to/my/sfProject/apps/[MyApplicationName]/config/unavailable.php.

But, if we want the same Unavailable page for all our apps we should put it in

/path/to/my/sfProject/config/unavailable.php.

To then enable the app again:

1
user@unix:~$ php symfony project:enable [app] [env]

NOTE: To make this command works we should make sure that the check_lock setting is set to “yes/true”.

Symfony and Doctrine behaviors

This post is about symfony and doctrine behaviors which are useful for daily uses.

These behaviors allow us to speed up the application development and in this post we will find short descriptions about most common behaviors.

Basically, a behavior will give us some relationships, algorithms and other features between our business entities. Those behaviors are deeply configurable (they should be at least), that configurations include: activate/deactivate behavior features, rename some additional schema attributes, etc.
We can use many Doctrine’s behaviors in our projects, such as core behaviors, extension behaviors, symfony’s plugins, or well, we can build our own behaviors.

Core Behaviors

Doctrine already comes with many behaviors (core behaviors), those are:

  • # Versionable – adds a new table to store our diferent versions of the entity to keep our entities versionable.
  • # Timestampable – this probably is the most popular behavior, adds two new columns (created_at and updated_at) to automatically store the dates when the entity is created or updated respectively.
  • # Sluggable – adds a new column (slug) which is unique and could be use by sfDoctrineRoute to refer the entity.
  • # I18n – adds a new table to provide Internationalization (I18n) to our model, this behavior is esential when we develop multi-language apps.
  • # NestedSet – adds a few columns (root_id, lft, rgt and level) to our entity to develop a hierarchy data structure (tree structure).
  • # Searchable – choses the columns of our entity which we want to index and adds a new table, speeding up the search engine development.
  • # Geographical – adds the longitude and latitude columns storing specific geographical coordinates, and provides us a getDistance() method to calculate the distance between 2 geographical entities.
  • # SoftDelete – adds a new deleted_at column which defines if a record has been marked as deleted (and when). It is an useful behavior when we need important consistency data.

Extension Behaviors

Also, we can use the Doctrine exntesions:

  • # Blameable – adds an additional level to audit our entities, it allows us to follow who has created or updated an entity.
  • # EventLoggable – saves a log of every Doctrine’s event (pre/post Delete/Insert/…) used by a record.
  • # Localizable – gives us the functionality to convert measure units (for instance, kilometers to miles).
  • # Locatable – gives us the functionality to use Google Maps to automatically fill our entity with longitude and latitude information from Google.
  • # Sortable – gives us the functionality to sort our entities.
  • # Taggable – adds abilities to tagging, it creates two tables using m:n relationships and provides easy tags administration.

Plugin Behaviors

Finally, there are some symfony’s plugins which gives us more behaviors:

I hope this post is useful.

Symfony: Executing queries after load fixtures

In this post we will see how to use symfony’s events to execute SQL queries after loading fixtures.

Sometimes we have the need to execute some SQL queries after we have loaded our fixtures, for instance, we need to do something specific for our RDBMS (Relational Database Management System) which is not supported by Doctrine nor Propel. Fortunately, symfony 1.4 has an excellent events system which allows us to connect the framework’s core and execute our own code when certain actions take place.

The below code snippet is an example of how we can execute some SQL queries manually after of doctrine:data-load:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class ProjectConfiguration extends sfProjectConfiguration
{
    public function setup()
    {
        // ...
        $this->dispatcher->connect('command.post_command', array(
            $this, 'listenToCommandPostCommandEvent'
        ));
    }

    public function listenToCommandPostCommandEvent(sfEvent $event)
    {
        $invoker = $event->getSubject();
        if($invoker instanceof sfDoctrineDataLoadTask)
        {
            $conn = Doctrine_Manager::connection();
            $conn->exec(// ...);
        }
    }
}

Symfony 1.4 have a lot of events we can use to customize features. You can learn more about these events in the Symfony’s documentation.

Symfony: generate-admin filters customization

In this post we will see how to customize the generate-admin filters to get more customized filters adapted to our needs.

If some time we used the Symfony generate-admin we could see that we have the ability to filter what data to show. These filters allow us to filter by whatever existing field in the entity. Now, we will see how to add a customized field to the filters, so we can filter according to our needs.

Let’s suppose we have an User and Phonenumber objects and in the phonenumbers list we want to filter by user. To do that we can modify the generator.yml file in the phonenumber module. In phonenumber/config/generator.yml:

1
2
3
config:
  filter:
    display: [ another, field, user_by_name]

Also, we need to define the widget and validator to this new field in lib/filter/doctrine/PhonenumberFormFilter.class.php:

1
2
3
4
5
6
7
8
9
<?php
public function configure()
{
    //...
    $this->setWidget('user_by_name', new sfWidgetFormFilterInput(array('with_empty' => false));
 
    //...
    $this->setValidator('user_by_name', new sfValidatorPass(array('required' => false));
}

In the same form class we need to overwrite the getFields() method with the new added field:

1
2
3
4
5
6
7
8
<?php
public function getFields()
{
    $fields = parent::getFields();
    $fields['user_by_name'] = 'custom';
   
    return $fields;
}

And last, we need to add the method which does the filtering, this field follows the convention addColumnNameColumnCriteria:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
public function addUserByNameColumnQuery($query, $field, $value)
{
    $text = $value['text'];
    if ($text) {
        $query->leftJoin($query->getRootAlias().'.User u')->andWhere('(u.first_name LIKE ?
            OR u.last_name LIKE ?
            OR u.username LIKE ?)'
, array("%$text%", "%$text%", "%$text%"));
    }
   
    return $query;
}

Done. We have a customized filter.

Symfony: the new definitive guide

The new Symfony book is already available, updated for 1.3 and 1.4 versions.

A few days ago the last definitive guide for Symfony has go out, which covers the last 1.3 and 1.4 versions of this already popular PHP framework. In this opportunity the name of the book is “A gentle Introduction to Symfony”.

In this new edition the two first chapters talk a little more about the framework philosophy. Also, there are some new chapters, one dedicated for Doctrine, another one for Symfony Form framework, and another one for emails. In summary, we can learn about:

  • If we want to learn about Symfony, we should read the Practical Symfony book if you want to learn the framework with a sample project; or we should read A Gentle introduction to symfony book if you want more theory and further explanations about main features.
  • If you want to find about how to configure Symfony we should read the Reference guide book.
  • If we want to learn more about Symfony about advanced stuff we should read More with symfony book.

definitive_guide

Symfony: conditional validator

Here we will see how to implement a conditional validator for Symfony’s forms.

As you may know the form framework inside Symfony gives us the ability to use validators for each form field, therefore we can validate the required fields, the data format, etc.

When we need to validate certain logic for a field which could not be accomplish it with the normal Symfony validators, we have to use a post validator. A post validator is executed after all normal Symfony validators and it receives an array with all entered values through the form.

As a practical case, we might want to validate that an entered password in a login form is equal to the entered username. Obviously, this is a easy but practical example.

The login form could be as follow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
class loginForm extends sfForm
{
    public function configure()
    {
        $this->setWidgets(array(
            'username'  => new sfWidgetFormInput(),
            'password'  => new sfWidgetFormInputPassword(),
        ));
 
        $this->setValidators(array(
            'username' => new sfValidatorString(array('required' => true)),
            'password' => new sfValidatorString(array('required' => true)),
        ));
 
        $this->widgetSchema->setNameFormat('login[%s]');
 
        // add a post validator
        $this->validatorSchema->setPostValidator(
            new sfValidatorCallback(array('callback' => array($this, 'checkPassword')))
        );
    }
 
    public function checkPassword($validator, $values)
    {
        // before validating the password, check that the username is not empty
        if (!empty($values['username']) && $values['password'] != $values['username']) {
            // password is not correct, throw an error
            throw new sfValidatorError($validator, 'Invalid password');
        }
 
        // password is correct, return the clean values
        return $values;
    }
}

In this way, the validator callback will throw a “global” error when the entered password is not equal to the entered username. If we want to get a specific field error we should modify the checkPassword() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
public function checkPassword($validator, $values)
{
    if (!empty($values['username']) && $values['password'] != $values['username']) {

        $error = new sfValidatorError($validator, 'Invalid password');
 
        // throw an error bound to the password field
        throw new sfValidatorErrorSchema($validator, array('password' => $error));
    }
 
    return $values;
}

That is! I hope it’s helpful.