Thursday, August 23, 2012

How to access configuration values inside an entity in Symfony2


So you have an entity class (let's say, Post) and you need to access a configuration value (let's say, my_bundle.upload_dir). Well, you can't. Or, better, you can but you may end breaking several good practices in the process.

 The reason is that your configuration values live inside the DIC (Dependency Injection Container, or the container for short) and you not have access to it from inside an entity class. The question on how to solve this problem have been made a great number of times and the answers provided by the community range from "just inject the container to the entity object" (which only works if you inject it manually to each object, because Doctrine will not do it during hydration) to 'just define the parameter as a constant inside the entity class" (which lacks the flexibility of parameterization in config.yml). And a lot more, none of them truly satisfactory.

I will show my solution, in the hope that someone will find it useful.

Your entity class:
// src/My/AwesomeBundle/Entity/Post.php
namespace My\AwesomeBundle\Entity;

use My\AwesomeBundle\Lib;

class Post
{
    .... 
     public function getUploadedImageName()
     {
        return Globals::getUploadDir().'/'.$this->slug.'.jpg';
     }
} 

The Globals class:
// src/My/AwesomeBundle/Lib/Globals.php
namespace My\AwesomeBundle\Lib;

class Globals
{
    protected static $uploadDir;
   
    public static function setUploadDir($dir)
    {
        self::$uploadDir = $dir;
    }

    public static function getUploadDir()
    {
        return self::$uploadDir;
    }
}

So, the question that remains is: when (and where) this $uploadDir static variable gets populated?

In your bundle class:
// src/My/AswesomeBundle
namespace My\AwesomeBundle;

use My\AwesomeBundle\Lib\Globals;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class AwesomeBundle extends Bundle
{
    public function boot()
    {
        // Set some static globals 
        Globals::setUploadDir($this->container->getParameter('awesome.upload_dir'));
    }

}

For this to work you will need to define parameter awesome.upload_dir inside your container. It is not difficult but a little cumbersome:

First we need to define the configuration tree:
// src/My/AswesomeBundle/DependencyInjection/Configuration.php
namespace My/AwesomeBundle/DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * This is the class that validates and merges configuration from your app/config files
 *
 * To learn more see {@link 
http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
 */
class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritDoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('awesome');

        $rootNode
            ->children()
                ->scalarNode('upload_dir')
                    ->defaultValue('/tmp/uploads') // or whatever default value
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}

Then we need to read and assign the value to the container:
// src/My/AswesomeBundle/DependencyInjection/DependencyInjection.php 
namespace My/AwesomeBundle/DependencyInjection; 
 
use Symfony\Component\DependencyInjection\ContainerBuilder; 
use Symfony\Component\Config\FileLocator; 
use Symfony\Component\HttpKernel\DependencyInjection\Extension; 
use Symfony\Component\DependencyInjection\Loader; 
 
/** 
 * This is the class that loads and manages your bundle configuration 
 * 
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} 
 */ 
class MyAwesomeBundleExtension extends Extension 
{ 
    /** 
     * {@inheritDoc} 
     */ 
    public function load(array $configs, ContainerBuilder $container) 
    { 
        $configuration = new Configuration(); 
        $config = $this->processConfiguration($configuration, $configs); 
 
        $container->setParameter('awesome.upload_dir', $config['upload_dir']); 
         
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 
        $loader->load('services.yml'); 
    } 
} 

And finally we can configure the upload dir in the global config.yml:
# app/config/config.yml
awesome:
    upload_dir: %kernel.cache_dir%/uploads

Thats all. It may seem a little convoluted at first, but it provides the functionality of configuration parameters to classes where it normally cannot be used.

2 comments:

  1. Hello,

    I just tried your implementation and it works fine for me, many thanks for sharing ideas, it helped me to solve my problem and helped me to understand dependency injections.

    Just one remark: in your Globals class, the methods needs the "function" word to work fine, and they don't work as protected methods, i had to declare them as public ones.

    But except these typos, it's a very good job, thank you.

    Etienne

    ReplyDelete
    Replies
    1. Many thanks for your corrections. I'm very glad that you find my little contribution useful.

      Delete