Thursday, October 20, 2011

Symfony2 choice list type for MongoDB backed forms

A couple of days ago, I had to implement a select field representing a one-to-one MongoDB relationship in one of my Symfony2 forms. Having spent some time reading the documentation, I decided to use the entity form field type for this purpose.

class AcmeFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('parent', 'entity', array(
                'class' => 'Acme\MyBundle\Document\MyDocument',
                'property' => 'name',
                'label' => 'Parent',
                'empty_value' => 'Root'
            ));
     }
}

And I got presented with this exception:

Class Acme\MyBundle\Document\MyDocument is not a valid entity or mapped super class. 

I realized Symfony2 was trying to load the necessary class using the default entity manager defined by the Doctrine ORM and failing utterly. After digging through the code, I found out what I was looking for inside the vendor/bundles/Symfony/Bundle/DoctrineMongoDBBundle/Form/Type/DocumentType.php class. This class is the "entity" type equivalent for MongoDB backed forms. It's alias is document instead of entity, and it is registered into the service container with the usual tag/name/alias combination as can be seen in the service configuration XML snippet below.

src/vendor/bundles/Symfony/Bundle/DoctrineMongoDBBundle/Resources/config/mongodb.xml

<service id="form.type.mongodb_document" class="SymfonyBundleDoctrineMongoDBBundleFormTypeDocumentType">
    <tag name="form.type" alias="document" />
    <argument type="service" id="doctrine.odm.mongodb.document_manager" />
</service>

Long story short, if you are using MongoDB as your backend, your forms must use the document type for your choice lists.

class AcmeFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('parent', 'document', array(
                'class' => 'Acme\MyBundle\Document\MyDocument',
                'property' => 'name',
                'label' => 'Parent',
                'empty_value' => 'Root'
            ));
     }
}

The configuration for document choice lists is the same as entity choice lists. See http://symfony.com/doc/current/reference/forms/types/entity.html for more information.

Tuesday, October 11, 2011

Integrating Memcached with Symfony2

Overview

This is a short tutorial to add Memcached support to your Symfony 2 application. This is actually not a really good way to add caching support to your application as your API should support multiple caching mechanisms just like Zend_Cache does; however, all I need for my application is a way to access a Memcached singleton instance and this setup satisfies that requirement.

Configuration

Generate a new bundle in your application source directory using the command below.

php app/console generate:bundle

In your app/config.yml, create a section for your bundle:

app/config/config.yml

acme_memcached:
    servers:
        server1:
            host: localhost
            port: 11211
            weight: 25
        server2:
            host: localhost
            port: 11211
            weight: 75

Note: You should also add a persistence flag to this configuration; however, I am going to omit this flag to keep this tutorial simple.

Once we have a format that we like, we need to update the Configuration class in our Memcached bundle to validate our configuration format. Here is what it should look like:

src/Acme/MemcachedBundle/DependencyInjection/Configuration.php

namespace Acme\MemcachedBundle\DependencyInjection;

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

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_memcached');

        $rootNode->children()
            ->arrayNode('servers')
                ->isRequired()
                ->requiresAtLeastOneElement()
//                ->useAttributeAsKey('host', false)
                ->prototype('array')
                    ->children()
                        ->scalarNode('host')->isRequired()->cannotBeEmpty()->end()
                        ->scalarNode('port')->defaultValue(11211)->cannotBeEmpty()->end()
                        ->scalarNode('weight')->defaultValue(0)->end()
                    ->end()
                ->end()
            ->end()
        ->end();

        return $treeBuilder;
    }
}

The important thing to notice here is the prototype() call which defines a prototype for multiple children nodes (server) under a parent node (servers).

Next step is to update the AcmeMemcachedExtension class and assign a parameter name to our servers array as illustrated below.

src/Acme/MemcachedBundle/DependencyInjection/AcmeMemcachedExtension.php

namespace Acme\MemcachedBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

class AcmeMemcachedExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);
        
        $container->setParameter('acme_memcached.servers', $config['servers']);
        unset($config['servers']);
        
        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');
    }
}

The setParameter() call above is an important step for our implementation, because it will allow us to inject an array containing multiple server definitions into our Memcached service as a single argument.

Now, let's define a class that extends the base PECL Memcached class. This is going to be the class that will be instantiated by the Symfony service container.

src/Acme/MemcachedBundle/Core/Memcached.php

namespace Acme\MemcachedBundle\Core;

class Memcached extends \Memcached
{
    public function __construct($servers) 
    {
        parent::__construct();
                
        foreach ($servers as $server) {
            
            if (!isset($server['host'])) {
                throw new \LogicException("Memcached host must be defined for server $server");
            }
            
            if (!isset($server['port'])) {
                throw new \LogicException("Memcached port must be defined for server $server");
            }
            
            if (!isset($server['weight'])) {
                $server['weight'] = 0;
            }
            
            $this->addServer($server['host'], $server['port'], $server['weight']);
            
        }
    }
}

The next step is to update our services.xml and tie configuration with implementation.

src/Acme/MemcachedBundle/Resources/config/services.xml

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="acme_memcached.class">Acme\MemcachedBundle\Core\Memcached</parameter>
    </parameters>

    <services>
        <service id="acme_memcached" class="%acme_memcached.class%">
            <argument>%acme_memcached.servers%</argument>
        </service>
    </services>

</container>

That's it. Now we can access our Memcached singleton with the usual mechanism in our actions:

$memcached = $this->get('acme_memcached');

Sunday, October 9, 2011

Symfony2 SecurityBundle and FOSUserBundle integration: How does it work?

Overview

A couple of days ago, I realized I needed to add some new functionality to the login process. Specifically, I needed to track all previous login attempts. Not knowing anything about the new Symfony2 SecurityBundle, I had to go through the underlying code to understand what was going on. In the process, I think got a basic idea about how the new SecurityBundle interacts with FOSUserBundle.

Configuration

I have a basic security configuration as illustrated below.

app/config/security.yml

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN:  ROLE_ADMIN
        
    providers:
        fos_userbundle:
            id: fos_user.user_manager

    firewalls:
        main:
            pattern: .*
            form_login:
                provider:   fos_userbundle
                check_path: /user/login_check
                login_path: /user/login
            logout:
                path: /user/logout
            anonymous:    true
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            pattern: ^/user/login$
            security: false

Full security configuration reference can be found at here

app/config/routing.yml

fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"
    prefix: /user

As you can see above, we are importing all FOSUserBundle security routing rules with the /user prefix. Now, let's have a look at the security routing rules under FOSUserBundle.

vendor/bundles/FOS/UserBundle/Resources/config/routing/security.xml

<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="fos_user_security_login" pattern="/login">
        <default key="_controller">FOSUserBundle:Security:login</default>
    </route>

    <route id="fos_user_security_check" pattern="/login_check">
        <default key="_controller">FOSUserBundle:Security:check</default>
    </route>

    <route id="fos_user_security_logout" pattern="/logout">
        <default key="_controller">FOSUserBundle:Security:logout</default>
    </route>

</routes>

It should be pretty self explanatory up to this point. We have a default security routing config file that we are importing into our application with the /user prefix and we have all the matching paths (login_path, check_path, etc.) defined in our app/config/config.yml for a working FOSUserBundle implementation. Now, let's have a look at FOSUserBundle SecurityController.

vendor/bundles/FOS/UserBundle/Controller/SecurityController.php

namespace FOS\UserBundle\Controller;

use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class SecurityController extends ContainerAware
{
    public function loginAction()
    {
        $request = $this->container->get('request');
        /* @var $request \Symfony\Component\HttpFoundation\Request */
        $session = $request->getSession();
        /* @var $session \Symfony\Component\HttpFoundation\Session */

        // get the error if any (works with forward and redirect -- see below)
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } elseif (null !== $session && $session->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = '';
        }

        if ($error) {
            // TODO: this is a potential security risk (see http://trac.symfony-project.org/ticket/9523)
            $error = $error->getMessage();
        }
        // last username entered by the user
        $lastUsername = (null === $session) ? '' : $session->get(SecurityContext::LAST_USERNAME);

        return $this->container->get('templating')->renderResponse('FOSUserBundle:Security:login.html.'.$this->container->getParameter('fos_user.template.engine'), array(
            'last_username' => $lastUsername,
            'error'         => $error,
        ));
    }

    public function checkAction()
    {
        throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
    }

    public function logoutAction()
    {
        throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
    }
}

In the security controller, we have both check and logout action methods included but they are not defined. Yet, the security process works. How is this possible?

How does it work?

Well, a single word: listeners.

  • First, an appropriate listener factory class is identified based on the authentication type.
  • Second, this listener factory class registers a user provider identified by the provider key under the firewall definition in security.yml. In our case, our provider is called fos_userbundle. This process basically links SecurityBundle to FOSUserBundle for authenticating submitted from values against a database.
  • Next, the listener factory class registers an authentication listener identified by "security.authentication.listener.form". This code listens to login attempts at the /user/login path.
  • Finally, FOSUserBundle registers a login listener for security.interactive_login event. This event indicates a successful login and the listener is used to execute post login code.

Let's go back to the beginning; everything starts when SecurityBundle is initiated and our security factory definitions are loaded from security_factories.xml.

Symfony/Bundle/SecurityBundle/Resources/config/security_factories.xml

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="security.authentication.factory.form" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.x509" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.basic" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.digest" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpDigestFactory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.remember_me" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory">
            <tag name="security.listener.factory" />
        </service>
    </services>
</container>

Basically, the call to load the SecurityBundle (see SecurityExtension::load()) simply calls the SecurityExtension::createFirewalls() method which in turn makes a call to the SecurityExtension::createListenerFactories() method. This method then loads all services tagged with "security.listener.factory" in the security_factories.xml file which is included above.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createFirewalls($config, ContainerBuilder $container)
{
    // ...

    // create security listener factories
    $factories = $this->createListenerFactories($container, $config);

    // ...
}

Listener factories are classes that are responsible for initiating listeners based on the authentication type. Symfony SecurityBundle provides factories for multiple authentication types. These authentication types include HTTP Basic, HTTP Digest, X509, RememberMe, and FormLogin. In our case, we are interested in the FormLogin type.

Once all listener factories are loaded, the code then loops over the firewall definitions in our app/config/security.yml file and calls SecurityExtension::createFirewall() method for each one found. In our app/config/config.yml file, we have three firewalls defined: main, dev, and login.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createFirewalls($config, ContainerBuilder $container)
{
    // ...

    foreach ($firewalls as $name => $firewall) {
        list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $factories);

        $contextId = 'security.firewall.map.context.'.$name;
        $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context'));
        $context
            ->replaceArgument(0, $listeners)
            ->replaceArgument(1, $exceptionListener)
        ;
        $map[$contextId] = $matcher;
    }

    // ...
}

Subsequently, the SecurityExtension::createFirewall() method initiates authentication listeners for each particular firewall by calling the SecurityExtension::createAuthenticationListeners() method.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds, array $factories)
{
    // ...

    // Authentication listeners
    list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $factories);

    // ...
}

The SecurityExtension::createAuthenticationListeners() method identifies the correct factory classes for a firewall by simply looping over all factories loaded into memory previously in a specific order (see SecurityBundle::$listenerPositions property and getPosition() methods inside factory classes) and then matching the keys found under the firewall definition (in our case, we have pattern, form_login, logout, and anonymous keys under the main firewall) to a factory instance (see getKey() methods inside factory classes). At the end, there is only one factory class defined with the form_login key and it is located at Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider, array $factories)
{
    // ...

    foreach ($this->listenerPositions as $position) {
        foreach ($factories[$position] as $factory) {
            $key = str_replace('-', '_', $factory->getKey());

            if (isset($firewall[$key])) {
                $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider;

                list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);

                $listeners[] = new Reference($listenerId);
                $authenticationProviders[] = $provider;
                $hasListeners = true;
            }
        }
    }

    // ...
}

Note: SecurityExtension::createAuthenticationListeners() mehod also creates a listener for anonymous authentication. Code is excluded for the sake of brevity.

Once an appropriate factory class is identified, the FormLoginFactory::create() method is called. This registers a "security.authentication.listener.form" authentication listener that intercepts login attempts.

The FormLoginFactory::create() method also calls createAuthProvider() which is responsible for registering our custom user provider (fos_userbundle). This user provider class is used to verify login credentials against a database.

The FormLoginFactory class also declares security.authentication.listener.form as our listener id. (See FormLoginFactory::getListenerId())

Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>

        <parameter key="security.authentication.listener.form.class">Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener</parameter>

    </parameters>

    <services>

        <service id="security.authentication.listener.form"
                 class="%security.authentication.listener.form.class%"
                 parent="security.authentication.listener.abstract"
                 abstract="true">
        </service>

    </services>
</container>

As you can see in the security_listeners.xml, our listener class is Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener. Things get easier after this point. Any call to our UsernamePasswordFormAuthenticationListener::handle() method triggers attemptAuthentication() method. This method is where the interaction between FOSUserBundle and SecurityBundle begins.

First, the authenticate() method is called on our authentication provider, which then makes a call to our user provider. This is the UserManager class included in the FOSUserBundle package.

Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php

protected function attemptAuthentication(Request $request)
{
    // ...

    return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
}

Then, based on return value of the authenticate() call, onSuccess() or onFailure() method is called. If user login is a success, the AbstractAuthenticationListener::onSuccess() method dispatches an event identfied by the SecurityEvents::INTERACTIVE_LOGIN constant.

Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php

private function onSuccess(GetResponseEvent $event, Request $request, TokenInterface $token)
{
    // ...

    if (null !== $this->dispatcher) {
        $loginEvent = new InteractiveLoginEvent($request, $token);
        $this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent);
    }

    // ...
}

SecurityEvents::INTERACTIVE_LOGIN constant has a value of security.interactive_login so we can easily locate where the login listener registration happens:

vendor/bundles/FOS/UserBundle/Resources/config/security.xml

<?xml version="1.0" encoding="UTF-8"?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="fos_user.encoder_factory.class">FOS\UserBundle\Security\Encoder\EncoderFactory</parameter>
        <parameter key="fos_user.security.interactive_login_listener.class">FOS\UserBundle\Security\InteractiveLoginListener</parameter>
    </parameters>

    <services>
        <service id="fos_user.encoder_factory" class="%fos_user.encoder_factory.class%" public="false">
            <argument>%security.encoder.digest.class%</argument>
            <argument>%fos_user.encoder.encode_as_base64%</argument>
            <argument>%fos_user.encoder.iterations%</argument>
            <argument type="service" id="fos_user.encoder_factory.parent" />
        </service>

        <service id="fos_user.security.interactive_login_listener" class="%fos_user.security.interactive_login_listener.class%">
            <argument type="service" id="fos_user.user_manager" />
            <tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" />
        </service>
    </services>

</container>

And here is the custom listener code attached to the security.interactive_login event. It is purpose is to update the last login date for our users after a successful login.

FOS/UserBundle/Security/InteractiveLoginListener.php

namespace FOS\UserBundle\Security;

use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use DateTime;

class InteractiveLoginListener
{
    protected $userManager;

    public function __construct(UserManagerInterface $userManager)
    {
        $this->userManager = $userManager;
    }

    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();

        if ($user instanceof UserInterface) {
            $user->setLastLogin(new DateTime());
            $this->userManager->updateUser($user);
        }
    }
}

In my next post, I will illustrate how we can extend this behavior to implement additional functionality after a successful login.

Wednesday, October 5, 2011

Unexpected token "name" of value "if" ("end of statement block" expected) in "WebProfilerBundle:Collector:logger.html.twig"

Encountered this WebProfilerBundle error message when I ran the bin/vendors script to update my Symfony2 bundles. Make sure your deps file is up to date; you need to pay special attention to your version values. In this case, update your twig version to v1.2.0 as illustrated below:
[twig]
    git=http://github.com/fabpot/Twig.git
    version=v1.2.0
Run the vendors script to update your bundle and the error message should disappear. You can get the most up to date deps file from the symfony-standard repository located at: https://github.com/symfony/symfony-standard/blob/master/deps