Today, I'm really excited to announce a great new feature for the upcoming symfony 1.3 version: the ability to customize the project creation process. Let me explain why it is useful and how you can take advantage of this cool feature.

Customizing the generate:project Task

As you might know, symfony tasks are classes. As any other class, it is pretty easy to customize and extend the existing tasks; except for one of them: the generate:project task. That's because no project exists when you execute this task, and so there is no way to customize it... until now. The task now takes an --installer option, which is a PHP script that will be executed during the project creation process:

$ php /path/to/symfony generate:project --installer=/domewhere/fabien_installer.php

If you enable URL file-access for the include() function in your php.ini, you can even pass a URL as an installer (of course you need to be very careful when doing this with script you know nothing about):

 $ symfony generate:project
 --installer=http://example.com/sf_installer.php

This script is executed in the context of the sfGenerateProjectTask instance, so you have access to all its methods to do your job, and there is a bunch of them.

installDir()

The first useful method is installDir(). It allows you to copy a bunch of files in the newly created project. Let's say you want to add some files here and there in the default directory structure, create them under a skeleton directory and add the following code in your installer script:

$this->installDir(dirname(__FILE__).'/skeleton');

runTask()

You can also run another task with the runTask() method. It takes the task name, and a string representing the arguments and the options you want to pass to it:

$this->runTask('configure:author', "'Fabien Potencier'");

You can also pass the arguments and the options as arrays:

$this->runTask('configure:author', array('author' => 'Fabien Potencier'));

The task shortcut names also work as expected:

$this->runTask('cc');

You can of course install plugins:

$this->runTask('plugin:install', 'sfDoctrineGuardPlugin');

If you want to install a specific version of a plugin, just appends the needed options to the argument string:

$this->runTask('plugin:install', 'sfDoctrineGuardPlugin --release=10.0.0 --stability=beta');

If you need to execute a task from a freshly installed plugin, don't forget to reload the tasks:

$this->reloadTasks();

Loggers

As you are inside a task context, you can log things pretty easily:

// a simple log
$this->log('some installation message');
 
// log a block
$this->logBlock(array('', 'Fabien\'s Crazy Installer', ''), 'ERROR');
 
// log in a section
$this->logSection('install', 'install some crazy files');

You can ask a confirmation:

if (!$this->askConfirmation('Are you sure you want to run this crazy installer?'))
{
  $this->logSection('install', 'You made the right choice!');
 
  return;
}

You can also ask any question:

$secret = $this->ask('Give a unique string for the CSRF secret:');

Or ask a question and validate the answer:

$validator = new sfValidatorEmail(array(), array('invalid' => 'hmmm, it does not look like an email!'));
$email = $this->askAndValidate('Please, give me your email:', $validator);

Filesystem Operations

If you want to do filesystem changes, you can access the filesystem object like this:

$this->getFilesystem()->...();

It's just a PHP script

The installer script is just another PHP file. So, you can do pretty anything you want. Be creative!

Example Script

Here is an example that uses a lot of the possibilities described above:

<?php
 
$this->logBlock(array('', 'Fabien\'s Crazy Installer', ''), 'ERROR');
 
if (!$this->askConfirmation('Are you sure you want to run this crazy installer?'))
{
  $this->logSection('install', 'You made the right choice!');
 
  return;
}
 
$this->installDir(dirname(__FILE__).'/skeleton');
 
$this->runTask('plugin:publish-assets');
 
$validator = new sfValidatorEmail(array(), array('invalid' => 'hmmm, it does not look like an email!'));
$email = $this->askAndValidate('Please, give me your email:', $validator);
 
$this->runTask('configure:author', sprintf("'%s'", $email));
 
$secret = $this->ask('Give a unique string for the CSRF secret:');
 
$this->runTask('generate:app', 'frontend --escaping-strategy=true --csrf-secret='.$secret);
 
$this->runTask('plugin:install', 'sfDoctrineGuardPlugin');
$this->reloadTasks();
 
$this->runTask('guard:create-user', 'fabien SuperPassword');
 
$this->runTask('cache:clear');

The Sandbox Creation Process

You probably know the symfony sandbox. It's a pre-packaged symfony project with a ready-made application and a pre-configured SQLite database. It helps newcomers bypass the command-line altogether as they just have to download the archive and they are ready to go.

The sandbox is nothing more than a bunch a commands executed before the archive is created. Until now, this job was done by the data/bin/create_sandbox.sh script. As of symfony 1.3, it is just an installer script. So, you can create a project that is the same as the sandbox like this:

$ php symfony generate:project --installer=/path/to/symfony/data/bin/sandbox_installer.php

Is it useful to create a sandbox? Probably not. But you can have a look at the installer script as a good example of what can be done:

$this->installDir(dirname(__FILE__).'/sandbox_skeleton');
 
$this->logSection('install', 'add symfony CLI for Windows users');
$this->getFilesystem()->copy(dirname(__FILE__).'/symfony.bat', sfConfig::get('sf_root_dir').'/symfony.bat');
 
$this->logSection('install', 'add LICENSE');
$this->getFilesystem()->copy(dirname(__FILE__).'/../../LICENSE', sfConfig::get('sf_root_dir').'/LICENSE');
 
$this->logSection('install', 'default to sqlite');
$this->runTask('configure:database', sprintf("'sqlite:%s/sandbox.db'", sfConfig::get('sf_data_dir')));
 
$this->logSection('install', 'create an application');
$this->runTask('generate:app', 'frontend');
 
$this->logSection('install', 'publish assets');
$this->runTask('plugin:publish-assets');
 
$this->logSection('install', 'fix sqlite database permissions');
touch(sfConfig::get('sf_data_dir').'/sandbox.db');
chmod(sfConfig::get('sf_data_dir'), 0777);
chmod(sfConfig::get('sf_data_dir').'/sandbox.db', 0777);
 
$this->logSection('install', 'add an empty file in empty directories');
$seen = array();
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(sfConfig::get('sf_root_dir')), RecursiveIteratorIterator::CHILD_FIRST) as $path => $item)
{
  if ($item->isDir() && !$item->isLink() && !isset($seen[$path]))
  {
    touch($item->getRealPath().'/.sf');
  }
 
  $seen[$item->getPath()] = true;
}

That's pretty much it...

Instead of running the same tasks again and again each time you create a new symfony project, you can now create your own installer script and tweak your symfony project installations the way you want.

I hope you will find useful usages for this new feature. If you create great installer scripts, please share them with the community (copy the URL in the comments).

Published in #Living on the edge