Relaying Doctrine events to symfony
If you need to relay your Doctrine events to symfony, you might get tempted to create a dependency to sfContext in your models. As we all know (right?) this is a path straight down to hell.
One might argue that triggering symfony events should be done in the controller only, but if you want to hook too many different record events for example, it's quite cumbersome to do this in every controller where you perform a certain (let's say "update") action on a model, and it is not very DRY.
A solution I used in a recent project was creating a small container for my symfony event dispatcher, think of it as a mini-DIC.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php class myEventDispatcherContainer { protected static $dispatcher; public static function setDispatcher(sfEventDispatcher $dispatcher) { self::$dispatcher = $dispatcher; } public static function getDispatcher() { if (is_null(self::$dispatcher)) { self::$dispatcher = new sfEventDispatcher(); } return self::$dispatcher; } } |
Now, we have to make sure that in our symfony context, we are setting the right dispatcher on the container. We can do that in our application configuration.
1 2 3 4 5 6 7 8 9 10 | <?php class frontendConfiguration extends sfApplicationConfiguration { public function initialize() { myEventDispatcherContainer::setDispatcher($this->dispatcher); // other stuff... } } |
Now in our models we can "safely" trigger symfony events, for example:
1 2 3 4 5 6 7 8 9 | <?php class Foo extends BaseFoo { public function preInsert($event) { myEventDispatcherContainer::getDispatcher()->notify(new sfEvent($this, 'pre_insert')); } } |
YeloPlayer
This post is in Dutch only.
Wil je de Yelo live stream in een apart venstertje bekijken terwijl je in andere applicaties verder werkt? Sleep dan onderstaandje link naar je favorieten / bookmarks en klik erop terwijl je een stream bekijkt.
YeloPlayer (sleep naar favorieten)
Voorlopig enkel getest op Safari, dus feedback welkom!
Organizing larger symfony projects with plugins
Update: if you are French speaking, check out this follow-up to my post with step-by-step instructions.
Maybe you have discovered the advantages of developing your symfony applications in plugins, like I have. This allows you to group modules, models and libraries together, helping you organize larger symfony projects.
One thing that bothered me however is that my project's plugins directory grew to a mix of real plugins, I checked out from other repositories, and my application specific plugins, which I prefer to think of as "packages".
It seems though there is a relative easy way to seperate the two, by implementing a similar method in your ProjectConfiguration class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php class ProjectConfiguration extends sfProjectConfiguration { public function enablePackages($packages) { if (!is_array($packages)) { if (func_num_args() > 1) { $packages = func_get_args(); } else { $packages = array($packages); } } foreach ($packages as $package) { $this->setPluginPath($package, sfConfig::get('sf_root_dir') . '/packages/' . $package); } $this->enablePlugins($packages); } } |
This way I can keep using my plugins directory for external plugins, and stick my "packages" in SF_ROOT_DIR/packages. Enabling these packages is a simple as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php class ProjectConfiguration extends sfProjectConfiguration { public function setup() { $this->enablePlugins( 'sfDoctrinePlugin', 'sfFormExtraPlugin', ... ); $this->enablePackages( 'fooPackage', 'barPackage', ... ); } ... } |
Returning your symfony form’s error schema as an array
While I wish there would exist a sfValidatorErrorSchema::toArray(), you can implement the following method on your BaseForm to easily retrieve your error schema as an array. This is especially handy when you want to return the errors in a JSON response.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php class BaseForm extends sfFormSymfony { public function getErrorSchemaAsArray() { $errorSchema = $this->getErrorSchema(); $array = array('global' => array(), 'named' => array()); foreach ($errorSchema->getGlobalErrors() as $error) { $array['global'][] = $error->getMessage(); } foreach ($errorSchema->getErrors() as $name => $error) { $array['named'][$name] = $error->getMessage(); } return $array; } } |
Checking symfony plugin dependencies
When developing symfony projects, I try to modularize as many components as possible in plugins. This is a very flexible and reusable approach that definitely pays off in the long term.
Occasionally however, a plugin might depend on another, and if you do not check these dependencies properly, debugging the errors that follow from a missing dependency is not always straight-forward.
That's why I try to build in checks that warn me when I forgot to enable a dependency as so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php class BarPluginConfiguration extends sfPluginConfiguration { public function getDependencies() { return array('FooPlugin'); } public function initialize() { // Check after factories are loaded, so the exception gets caught by symfony $this->dispatcher->connect('context.load_factories', array($this, 'checkDependencies')); } public function checkDependencies() { $plugins = $this->configuration->getPlugins(); foreach ($this->getDependencies() as $dependency) { if (!in_array($dependency, $plugins)) { throw new sfException(sprintf('The plugin "BarPlugin" requires "%s" to be enabled.', $dependency)); } } } } |
Making template vars available in the layout
By default in symfony, the only way to make variables available to the layout/decorator is using slots. This can be a bit cumbersome.
While I welcome your feedback on possible cons of doing such a thing, making all assigned template variables available to the layout as well is quite straightforward by extending sfPHPView.
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 36 37 38 39 40 41 | <?php class myPHPView extends sfPHPView { /** * Loop through all template slots and fill them in with the results of presentation data. * * @param string $content A chunk of decorator content * * @return string A decorated template */ protected function decorate($content) { if (sfConfig::get('sf_logging_enabled')) { $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Decorate content with "%s/%s"', $this->getDecoratorDirectory(), $this->getDecoratorTemplate())))); } // set the decorator content as an attribute $attributeHolder = $this->attributeHolder; $attributes = array_merge($attributeHolder->getAll(), array('sf_content' => new sfOutputEscaperSafe($content))); $this->attributeHolder = $this->initializeAttributeHolder($attributes); $this->attributeHolder->set('sf_type', 'layout'); // check to see if the decorator template exists if (!is_readable($this->getDecoratorDirectory().'/'.$this->getDecoratorTemplate())) { throw new sfRenderException(sprintf('The decorator template "%s" does not exist or is unreadable in "%s".', $this->decoratorTemplate, $this->decoratorDirectory)); } // render the decorator template and return the result $ret = $this->renderFile($this->getDecoratorDirectory().'/'.$this->getDecoratorTemplate()); $this->attributeHolder = $attributeHolder; return $ret; } } |
You can configure the view class to use per module in module.yml. However, if you put the following module.yml in your project dir's config it would apply the extended view class to all modules by default.
1 2 3 | default: enabled: true view_class: myPHP |
At first I was looking for a way to have my view parameters namespaced and to only assign global variables to the layout. However, both sfViewParameterHolder and sfNamespacedParameterHolder extend sfParameterHolder, so you'd probably have to do some merging of the two classes. With a little more time perhaps I'll look into a cleaner solution for this common problem.
My typical symfony frontend controller
You probably dislike URL's like http://yourdomain.com/backend.php/foo as much as I do.
For that reason I prefer to set up multiple aliases in my vhost for my symfony project, all pointing to the same webroot. For example:
1 2 3 4 5 6 7 8 9 10 | <VirtualHost *:80> ServerName www.mydomain.dev ServerAlias admin.mydomain.dev DocumentRoot "/my/symfony/project/web" <Directory "/my/symfony/project/web"> AllowOverride All Order allow,deny Allow from all </Directory> </VirtualHost> |
Once you set up your different vhosts, you can set up some rewriting rules in your .htaccess to point to the right PHP file depending on the requested host, but I prefer to do that bit of magic in my frontend controller (index.php).
This is what my typical frontend controller looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php require_once(dirname(__FILE__) . '/../config/ProjectConfiguration.class.php'); if (strpos($_SERVER['SERVER_NAME'], 'admin.') === 0) { $app = 'backend'; } else { $app = 'frontend'; } if (substr($_SERVER['SERVER_NAME'], -4) == '.dev') { $env = 'dev'; $debug = true; } else { $env = 'prod'; $debug = false; } $configuration = ProjectConfiguration::getApplicationConfiguration($app, $env, $debug); sfContext::createInstance($configuration)->dispatch(); |
This allows me to remove any additional PHP scripts in the /web directory (typically frontend_dev.php, backend.php and backend_dev.php) and even has the added benefit of saving you some worries about accidently deploying the development environments to production.
PHP on Mac OS X Snow Leopard
Good news for my fellow PHP developers on Mac OS X, not only does Mac OS X 10.6 come with PHP 5.3, it now is also compiled with MySQL support out of the box.
MySQL support was notoriously missing in the PHP binaries that came compiled with previous versions of Mac OS X, which led me and many other developers to installing alternative solutions like MAMP.
Other PHP extensions previously missing and now installed include the GD library, SOAP and CURL. I also included a copy of the phpinfo() print-out.
Tracking outgoing links with Google Analytics and jQuery
When looking for an easy way to track outgoing links in Google Analytics with jQuery I came across this plugin. However, we needed a more flexible way of link tracking, with more control on which links to track and in what format.
For those reasons I wrote a small jQuery plugin. Usage is simple:
1 | $('a[rel=external]').gaLinkTrack(); |
Ofcourse you can modify the selector to your needs, but (for now) you can only track links.
Inside Google Analytics, the tracked link takes the format "/{rel}/{domain}/{page}" by default. If no rel attribute is present, it defaults to "external". You can change the format of the tracked links by passing the path option:
1 | $('a[rel=download]').gaLinkTrack( { path: '/downloads/{page}' } ); |
When specifying the path format you can use any of the following placeholders: {rel}, {domain}, {page}. You can specify a default value for any of the placeholders (notably {rel}) as illustrated here:
1 | $('a.download').gaLinkTrack( { path: '/{rel:download}/{page}' } ); |
This is a first version so feedback and suggestions are more than welcome. Things still to come: proper handling of email links, more options for customizing the tracked path, and more...
Download the full source code (jquery-galinktrack-1.0.js) or the minified version (jquery-galinktrack-1.0.min.js).
Sharing layouts cross-application in symfony
Many of my projects share (some) layout files across different applications. Up till now I just copied the files to the different locations and maintained each copy. But, there is a cleaner solution to share your layout across applications:
1 2 3 4 5 6 7 | class appNameConfiguration extends sfApplicationConfiguration { public function configure() { sfConfig::set('sf_app_template_dir', sfConfig::get('sf_root_dir') . '/templates'); } } |
This would look for the layout files in a templates directory in the root of my project. Just edit each apps/appName/config/appNameConfigurations.class.php accordingly and you're all set!
Note that this will share all layout files across your applications, and might not be the best solution if you want to selectively share layouts.