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 yourphp.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).
Looks great :)
Thanks.
This is brilliant Fabien, thank you.
How would installDir work when the installer script is referenced via http://? It wouldn't presumably...?
@Tom Boutell: If the script is on the Internet, then your script will have to download the skeleton before doing anything else. Remember, the installer script is just plain PHP code, so you can do anything you want ;)
This is awesome. I often found myself doing the same things over and over again when creating new projects. This will certainly aid in creating truly custom builds:-)
Nice!
Fantastic work, Sensio team! We look forward to building an installer for a few different scenarios for centresource -- and probably release them to the public (like plugins).
Yeah! Its a feature, which I really need!
Great , can't wait for the release !
A really nice feature to have! Kudos to all of you from Sensio!
PS: I guess your 'domewhere' on the first snippet was a typo. :-)
Great!, I have some custom files that I use in every project, so this makes my life a lot easier! Thanks!
Ok, that's nice. But don't you guys get tired of copying rails? It'd be easier to just use it instead of rewriting it.
Marcelo, actually i think maven had it first :-)
Marcelo, don't you get tired of being an ignorant fool? It'd be easier to just not be one instead of being one.
THANK YOU!!!!
I was doing custom setups using a script I created myself before, but this looks a heckuva lot more customizable and expandable.
Another well done and thank you from me!
Super useful, dumb question, how to choose the ORM in the setup script? Doing plugin remove/install ?