Tuesday, December 6, 2011

A week of symfony #256

The Symfony blog has picked up my article about creating a date helper in Symfony2. Have to be careful about the crap I post from now on...

Passing PHP data to Apache access logs

Recently, I worked on a project to collect performance data from an application at work and pass that data to Apache access logs as part of a performance improvement project. As it turns out, PHP has a built-in function called apache_note() that your can use for this purpose.

For example, if you wish to pass memory usage information to Apache access logs, add the following code to at the end of your front controller file:

if (php_sapi_name() != 'cli') {
    apache_note('PHPMemoryUsage', memory_get_peak_usage(true));
}

Then, update your Apache LogFormat directive to include this new piece of information:

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %{PHPMemoryUsage}n" combined

Saturday, November 26, 2011

Mapping embedded documents in Doctrine MongoDB ODM

If you have a MongoDB document that has another document embedded in it, you still have to create a separate XML mapping document for the embedded one. So if you have a parent document called Activity that has an embedded document called Coordinates, the configuration would look something like this:

src/Acme/ActivityBundle/Resources/config/doctrine/Activity.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\ActivityBundle\Document\Activity" db="acme" collection="activity" customId="true">
        
        <field fieldName="id" id="true" strategy="INCREMENT" />

        <embed-one target-document="Acme\ActivityBundle\Document\Coordinates" field="locationCoordinates" />

        <lifecycle-callbacks>
            <lifecycle-callback method="callbackPrePersist" type="prePersist" />
            <lifecycle-callback method="callbackPreUpdate" type="preUpdate" />
        </lifecycle-callbacks>
        
        <indexes>
            <index>
                <key name="locationCoordinates" order="2d" />
            </index>
        </indexes>
        
    </document>
    
</doctrine-mongo-mapping>

src/Acme/ActivityBundle/Resources/config/doctrine/Coordinates.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <embedded-document name="Acme\ActivityBundle\Document\Coordinates">
        <field fieldName="latitude" type="float" />
        <field fieldName="longitude" type="float" />
    </embedded-document>
    
</doctrine-mongo-mapping>

The XML mapping format is not included in the Doctrine MongoDB ODM documentation. After a bunch of tests, I realized the @EmbeddedDocument in the annotation format example and simply replaced the document tag with embedded-document in the mapping file.

Friday, November 25, 2011

Creating a Symfony2 helper

While working on a Symfony2 project yesterday, I realized I needed a better way to format dates in my templates. The reason behind this was the fact that my MongoDB document entities contained date values that were either null or DateTime instances. This entailed using a bunch of if statements for extra checks that made template code unnecessarily verbose.

My solution was to create a template helper in an application specific framework bundle that I had created previously to contain all common assets and other necessary common resources.

Create a Helper directory in the framework bundle directory and create a DateHelper class:

src/Acme/FrameworkBundle/Helper/DateHelper.php

namespace Acme\FrameworkBundle\Helper;

use Symfony\Component\Templating\Helper\Helper;

class DateHelper extends Helper
{
    protected $options;
    
    public function __construct(array $options)
    {
        $this->options = $options;
    }
    
    public function getName()
    {
        return 'date';
    }
    
    public function format($date, $detailed = false)
    {
        if (!empty($date)) {
            if ($date instanceof \DateTime) {
                if ($detailed === true) {
                    return $date->format($this->getOption('detailed_format'));
                } else {
                    return $date->format($this->getOption('default_format'));
                }
            }
        }
        
        return null;
    }
    
    protected function getOption($name)
    {
        if (array_key_exists($name, $this->options)) {
            return $this->options[$name];
        }
        
        throw new Exception('Options does not exist');
    }
}

Define the configuration options for your helper:

src/Acme/FrameworkBundle/DependencyInjection/Configuration.php

namespace Acme\FrameworkBundle\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_framework');

        $rootNode->children()
            ->arrayNode('helper')
                ->isRequired()
                ->children()
                    ->arrayNode('date')
                        ->isRequired()
                        ->children()
                            ->scalarNode('default_format')->defaultValue('Y-m-d')->cannotBeEmpty()->end()
                            ->scalarNode('detailed_format')->defaultValue('Y-m-d H:i:s')->cannotBeEmpty()->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ->end();

        return $treeBuilder;
    }
}

Add the new configuration section that you have defined for your helper to your config.yml:

app/config/config.yml

acme_framework:
    helper:
        date:
            default_format: "Y-m-d"
            detailed_format: "Y-m-d H:i:s"

The above approach is a very simplistic approach and would not be the correct one if you are dealing with localization. However, in my case, this is more than enough.

Add the necessary code to your extension class to load your new configuration:

src/Acme/FrameworkBundle/DependencyInjection/AcmeFrameworkExtension.php

namespace Acme\FrameworkBundle\DependencyInjection;

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

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

The next step is to define our service and tag it as a helper:

src/Acme/FrameworkBundle/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">

    <services>
        
        <service id="acme_framework.helper.date" class="Acme\FrameworkBundle\Helper\DateHelper">
            <tag name="templating.helper" alias="date" />
            <argument>%acme_framework.helper.date%</argument>
        </service>
        
    </services>

</container>

And that is pretty much it. You can now call your helper in your templates like this:

echo $view['date']->format($activity->getStartedAt());

Wednesday, November 2, 2011

Adding post-login logic to FOSUserBundle

Having finally figured out how to use FOSUserBundle in my project, I decided to keep track of all logins next. The implementation turned out to be a breeze thanks to Symfony2's security listener mechanism.

As usual, the first step is to create a MongoDB document for this purpose. This is a very simple document that contains a user's id, session id, IP address, and login date.

src/Acme/UserBundle/Document/LoginHistory.php

namespace Acme\UserBundle\Document;

class LoginHistory
{
    protected $id;
    protected $userId;
    protected $sessionId;
    protected $ip;
    protected $createdAt;
   
    /**
     * Get id
     *
     * @return custom_id $id
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set userId
     *
     * @param int $userId
     */
    public function setUserId($userId)
    {
        $this->userId = $userId;
    }

    /**
     * Get userId
     *
     * @return int $userId
     */
    public function getUserId()
    {
        return $this->userId;
    }

    /**
     * Set sessionId
     *
     * @param string $sessionId
     */
    public function setSessionId($sessionId)
    {
        $this->sessionId = $sessionId;
    }

    /**
     * Get sessionId
     *
     * @return string $sessionId
     */
    public function getSessionId()
    {
        return $this->sessionId;
    }

    /**
     * Set ip
     *
     * @param string $ip
     */
    public function setIp($ip)
    {
        $this->ip = $ip;
    }

    /**
     * Get ip
     *
     * @return string $ip
     */
    public function getIp()
    {
        return $this->ip;
    }

    /**
     * Set createdAt
     *
     * @param date $createdAt
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;
    }

    /**
     * Get createdAt
     *
     * @return date $createdAt
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    
    public function callbackPrePersist()
    {
        $this->setCreatedAt(new \DateTime());
    }
}

Second step is to map this document to a MongoDB collection:

src/Acme/UserBundle/Resources/config/doctrine/LoginHistory.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\UserBundle\Document\LoginHistory" db="acme" collection="loginHistory" customId="true">
        
        <field fieldName="id" id="true" strategy="INCREMENT" />
        <field fieldName="userId" type="int" />
        <field fieldName="sessionId" type="string" />
        <field fieldName="ip" type="string" />
        <field fieldName="createdAt" type="date" />
        
        <lifecycle-callbacks>
            <lifecycle-callback method="callbackPrePersist" type="prePersist" />
        </lifecycle-callbacks>
        
        <indexes>
            <index>
                <key name="userId" order="asc" />
            </index>
            <index>
                <key name="createdAt" order="desc" />
            </index>
        </indexes>
        
    </document>
    
</doctrine-mongo-mapping>

Next step is to create a new listener to do the work for us when a user successfully logs in to the application:

src/Acme/UserBundle/Resources/Core/Listenere/InteractiveLogin.php

namespace Acme\UserBundle\Core\Listener;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use DateTime;
use Acme\UserBundle\Document\LoginHistory;

class InteractiveLogin
{
    protected $container;
    protected $dm;

    public function __construct(ContainerInterface $container, DocumentManager $dm)
    {
        $this->container = $container;
        $this->dm = $dm;
    }
    
    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();

        if ($user instanceof UserInterface) {
            
            $loginHistory = new LoginHistory();
            $loginHistory->setUserId($user->getId());
            $loginHistory->setSessionId($this->container->get('session')->getId());
            $loginHistory->setIp($this->container->get('request')->getClientIp());
            
            $this->dm->persist($loginHistory);
            $this->dm->flush();
            
        }
    }
}

The last step is to attach this listener to Symfony's security.interactive_login event:

src/Acme/UserBundle/Resources/config/doctrine/LoginHistory.mongodb.xml

<service id="acme.security.login_listener" class="Acme\UserBundle\Core\Listener\InteractiveLogin">
    <argument type="service" id="service_container" />
    <argument type="service" id="fos_user.document_manager" />
    <tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" />
</service>

That's it!

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

Saturday, August 27, 2011

Symfony 2 + DoctrineMongoDBBundle + FOSUserBundle Tutorial

It's been a while I have not added an entry to my blog. I have been busy playing with Symfony 2, MongoDB, and FOSUserBundle for a while now so here is a tutorial to integrate Symfony 2, MongoDB, and FOSUserBundle.

Objectives

* Install DoctrineMongoDBBundle
* Install FOSUserBundle
* Create user model
** Utilize groups
** Add additional properties to user model
* Create customized registration form and handler

At this point I am going to assume that you have a running Symfony 2 and MongoDB installation in place already and a basic understanding of how to configure Symfony 2 services. I will also exclude view related steps from this tutorial.

Setting Up DoctrineMongoDBBundle

Add the following to your deps file:
[doctrine-mongodb]
    git=http://github.com/doctrine/mongodb.git

[doctrine-mongodb-odm]
    git=http://github.com/doctrine/mongodb-odm.git

[DoctrineMongoDBBundle]
    git=http://github.com/symfony/DoctrineMongoDBBundle.git
    target=/bundles/Symfony/Bundle/DoctrineMongoDBBundle
Run the vendors script to install DoctrineMongoDBBundle:
php vendors/vendors install
Once the installation is complete, update your app/AppKernel.php file to register your DoctrineMongoDBBundle:
public function registerBundles()
{
    $bundles = array(
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Symfony\Bundle\SecurityBundle\SecurityBundle(),
        new Symfony\Bundle\TwigBundle\TwigBundle(),
        new Symfony\Bundle\MonologBundle\MonologBundle(),
        new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
        new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
        new Symfony\Bundle\AsseticBundle\AsseticBundle(),
        new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
        new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
        new Symfony\Bundle\DoctrineMongoDBBundle\DoctrineMongoDBBundle(),
    );

    if (in_array($this->getEnvironment(), array('dev', 'test'))) {
        $bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
        $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
        $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
    }

    return $bundles;
}
Now, let's update the app/autoload.php file:
AnnotationRegistry::registerLoader(function($class) use ($loader) {
    $loader->loadClass($class);
    return class_exists($class, false);
});

AnnotationRegistry::registerFile(__DIR__.'/../vendor/doctrine-mongodb-odm/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php');
The AnnotationRegistry::registerLoader() call simply registers Symfony loader in Doctrine with a simple anonymous callback function. This is all fine as long as you have a single class definition per file along with consistent namespacing; however, if this is not the case as can be seen in DoctrineAnnotations.php, you need to include the file separately as shown above via the AnnotationRegistry::registerFile() call. Otherwise, you will end up receiving error messages related to auto-loading:
[Semantical Error] The annotation "@Sensio\Bundle\FrameworkExtraBundle\Configuration\Route" in class Acme\DemoBundle\Controller\SecuredController does not exist, or could not be auto-loaded.  
Finally, update your configuration file with your connection information:
doctrine_mongodb:
    connections:
        default:
            server: mongodb://localhost:27017
            options:
                connect: true
    default_database: acme
    document_managers:
        default:
            auto_mapping: true

Setting up FOSUserBundle

FOSUserBundle installation steps are the same as the ones for Doctrine-MognoDB package. First, update your deps file:
[FOSUserBundle]
    git=git://github.com/FriendsOfSymfony/FOSUserBundle.git
    target=/bundles/FOS/UserBundle
Run the vendors script to install FOSUserBundle:
php vendors/vendors install
Add the FOSUserBundle routes to your app/config/routing.yml file:
fos_user_profile:
    resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
    prefix: /user/profile

fos_user_register:
    resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
    prefix: /user/register

fos_user_resetting:
    resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
    prefix: /user/resetting

fos_user_change_password:
    resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
    prefix: /user/change-password
    
fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"
    prefix: /user
    
fos_user_group:
    resource: "@FOSUserBundle/Resources/config/routing/group.xml"
    prefix: /group
Create a new user bundle that is going to extend the FOSUserBundle:
php app/console generate:bundle --namespace=Acme/UserBundle
Update src/Acme/UserBundle/AcmeUserBundle.php and add a getParent() method as illustrated below:
namespace Acme\UserBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeUserBundle extends Bundle
{
    public function getParent()
    {
        return 'FOSUserBundle';
    }
}
Add your new bundles (FOSUserBundle and AcmeUserBundle) to your app/AppKernel.php:
public function registerBundles()
{
    $bundles = array(
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Symfony\Bundle\SecurityBundle\SecurityBundle(),
        new Symfony\Bundle\TwigBundle\TwigBundle(),
        new Symfony\Bundle\MonologBundle\MonologBundle(),
        new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
        new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
        new Symfony\Bundle\AsseticBundle\AsseticBundle(),
        new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
        new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
        new Symfony\Bundle\DoctrineMongoDBBundle\DoctrineMongoDBBundle(),
        new FOS\UserBundle\FOSUserBundle(),
        new Acme\UserBundle\AcmeUserBundle(),
    );

    if (in_array($this->getEnvironment(), array('dev', 'test'))) {
        $bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
        $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
        $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
    }

    return $bundles;
}
Now, create your model files under the src/Acme/UserBundle/Document directory. Again, we are going to extend the base classes provided in the FOSUserBundle.

src/Acme/UserBundle/Document/User.php
namespace Acme\UserBundle\Document;

use FOS\UserBundle\Document\User as BaseUser;

class User extends BaseUser
{
    protected $sex;
    protected $dob;
    protected $termsOfService;
    protected $groups = array();

    public function __construct()
    {
        parent::__construct();
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Set dob
     *
     * @param date $dob
     */
    public function setDob($dob)
    {
        $this->dob = $dob;
    }

    /**
     * Get dob
     *
     * @return date $dob
     */
    public function getDob()
    {
        return $this->dob;
    }

    /**
     * Set sex
     *
     * @param string $sex
     */
    public function setSex($sex)
    {
        $this->sex = $sex;
    }

    /**
     * Get sex
     *
     * @return string $sex
     */
    public function getSex()
    {
        return $this->sex;
    }

    /**
     * Add groups
     *
     * @param Acme\UserBundle\Document\Group $groups
     */
    public function addGroups(\Acme\UserBundle\Document\Group $groups)
    {
        $this->groups[] = $groups;
    }

    /**
     * Get groups
     *
     * @return Doctrine\Common\Collections\Collection $groups
     */
    public function getGroups()
    {
        return $this->groups;
    }

    /**
     * Set termsOfService
     *
     * @param boolean $termsOfService
     */
    public function setTermsOfService($termsOfService)
    {
        $this->termsOfService = $termsOfService;
    }

    /**
     * Get termsOfService
     *
     * @return boolean $termsOfService
     */
    public function getTermsOfService()
    {
        return $this->termsOfService;
    }
}
src/Acme/UserBundle/Document/Group.php
namespace Acme\UserBundle\Document;

use FOS\UserBundle\Document\Group as BaseGroup;

class Group extends BaseGroup
{

}
A couple of things to remember at this point. I have added custom properties to my user class. These are the additional properties that I would like to store in my MongoDB collection. There is one exception: "termsOfService". This property is not going to be persisted; however, it is needed since Symfony 2 utilizes models for form validation purposes.

Another thing to notice is the use of "groups" property. I am going to use this field to assign groups - which are basically collection of roles - to my users.

Lastly, I have not added the "id" property to my models. The reason is simple; I am not going to use annotations to map my class properties. Instead, I will use the XML configuration as seen in vendor/bundles/FOS/UserBundle/Resources/config/doctrine/*.mongodb.xml to keep it consistent.

src/Acme/UserBundle/Resources/config/doctrine/User.mongodb.xml
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\UserBundle\Document\User" db="acme" collection="user" customId="true">
        
        <field fieldName="id" id="true" strategy="INCREMENT" />
        <field fieldName="dob" type="date" />
        <field fieldName="sex" type="string" />
        
        <reference-many target-document="Acme\UserBundle\Document\Group" field="groups"> />
       
    </document>
    
</doctrine-mongo-mapping>
Here, I have defined the "id" property as an increment type and mapped the "dob" and "sex" properties to the collection. You can also see how the "groups" property references entries in another collection in MongoDB. Finally, as explained previously, I have excluded the termsOfService field since I do not want it stored.

src/Acme/UserBundle/Resources/config/doctrine/Group.mongodb.xml
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\UserBundle\Document\Group" db="acme" collection="group" customId="true">
        <field fieldName="id" id="true" strategy="INCREMENT" />
    </document>
    
</doctrine-mongo-mapping>
Now that we are done with data mapping, we can move on to dealing with data collection. Create a new form type class at src/Acme/UserBundle/Form/Type/RegistrationFormType.php:
namespace Acme\UserBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('username', null, array(
                'attr' => array('class' => 'validate[required]'),
            ))
            ->add('email', null, array(
                'attr' => array('class' => 'validate[required,custom[email]]'),
            ))
            ->add('dob', 'date', array(
                'years' => range(date('Y') - 90, date('Y')),
                'data_timezone' => 'UTC',
                'user_timezone' => 'UTC'
            ))
            ->add('sex', 'choice', array(
                'choices'   => array('m' => 'Male', 'f' => 'Female'),
                'expanded' => true,
                'attr' => array('class' => 'validate[required]'),
            ))
            ->add('plainPassword', 'repeated', array(
                'type' => 'password',
                'first_name' => 'Password',
                'second_name' => 'Confirm Password',
                'attr' => array('class' => 'validate[required]'),
            ))
            ->add('termsOfService', 'checkbox', array(
                'label' => 'I agree to the terms and service',
            ));
        
    }

    public function getName()
    {
        return 'fos_user_registration';
    }
}
Next step is to validate our form. Create a new XML file located at src/Acme/UserBundle/Resources/config/validation.xml:
<?xml version="1.0" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
        http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">

    <class name="Acme\UserBundle\Document\User">

        <property name="username">
            <constraint name="Regex">
                <option name="pattern">/^[0-9a-z]+$/i</option>
                <option name="message">Username should consist of alpha-numeric characters only</option>
                <option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                </option>
            </constraint>
        </property>
        
        <property name="sex">
            <constraint name="NotBlank">
                <option name="message">Please select sex</option>
                <option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                </option>
            </constraint>
        </property>

        <property name="plainPassword">
            <constraint name="MinLength">
                <option name="limit">8</option>
                <option name="message">Password should be at least 8 characters</option>
                <option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                </option>
            </constraint>
        </property>
        
        <property name="termsOfService">
            <constraint name="True">
                <option name="message">Please agree to our terms and conditions</option>
                <option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                </option>
            </constraint>
        </property>
        
    </class>

</constraint-mapping>
As you can see in the validation file, I have updated the validation rules and messages for existing properties such as "username" and added new validation rules for properties such as "termsOfService".

We now have custom a custom form implemented. The next step would be to create a new handler for this form. It is possible to use the default handler provided in FOSUserBundle; however, in this case I have to create custom one to be able to populate the "groups" property in the user class.

Create a new form type class at src/Acme/UserBundle/Form/Type/RegistrationFormHandler.php:
namespace Acme\UserBundle\Form\Handler;

use FOS\UserBundle\Form\Handler\RegistrationFormHandler as BaseRegistrationFormHandler;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Mailer\MailerInterface;
use FOS\UserBundle\Model\GroupManagerInterface;

class RegistrationFormHandler extends BaseRegistrationFormHandler
{
    protected $groupManager;
    
    public function __construct(Form $form, Request $request, UserManagerInterface $userManager, MailerInterface $mailer, GroupManagerInterface $groupManager)
    {
        parent::__construct($form, $request, $userManager, $mailer);
        $this->groupManager = $groupManager;
    }
    
    protected function onSuccess(UserInterface $user, $confirmation)
    {
        $memberGroup = $this->groupManager->findGroupByName('member');
        
        $user->addGroup($memberGroup);
        
        parent::onSuccess($user, $confirmation);
    }
    
}
In our handler, we have overridden two methods of the default registration form handler provided in FOSUserBundle: the constructor and the onSuccess method. The onSuccess method is overridden because I want to assign a default group to my users when they register. This is possible but it would not work in the default setup since the the group manager service is not available for our handler. The solution is to update our constructor and our service definition as described below.

src/Acme/UserBundle/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">

    <services>
        
        <service id="acme.registration.form.type" class="Acme\UserBundle\Form\Type\RegistrationFormType">
            <tag name="form.type" alias="acme_user_registration_form_type" />
            <argument>%fos_user.model.user.class%</argument>
        </service>
        
        <service id="acme.registration.form.handler" class="Acme\UserBundle\Form\Handler\RegistrationFormHandler" scope="request" public="false">
            <argument type="service" id="fos_user.registration.form" />
            <argument type="service" id="request" />
            <argument type="service" id="fos_user.user_manager" />
            <argument type="service" id="fos_user.mailer" />
            <argument type="service" id="fos_user.group_manager" />
        </service>
        
    </services>
    
</container>
This little XML file summarizes the major paradigm shift from Symfony 1 to Symfony 2. Pretty much every class in our bundle is defined as a service. If you need to access a resource, simply update the the service definition and "inject" the required resource to another service - hence the term "dependency injection". The important point is to understand how these service definition files and the service container work together.

The last step is to set up our security.yml and bind everything together in our config.yml file.

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
                login_path:     /user/login
                use_forward:    false
                check_path:     /user/login_check
                failure_path:   null
            logout:       true
            anonymous:    true

        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

    access_control:
        # The WDT has to be allowed to anonymous users to avoid requiring the login with the AJAX request
        - { path: ^/_wdt/, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/_profiler/, role: IS_AUTHENTICATED_ANONYMOUSLY }
        # AsseticBundle paths used when using the controller for assets
        - { path: ^/js/, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/css/, role: IS_AUTHENTICATED_ANONYMOUSLY }
        # URL of FOSUserBundle which need to be available to anonymous users
        - { path: ^/user/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
#        - { path: ^/user/logout, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/user/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/user/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        # Secured part of the site
        # This config requires being logged for the whole site and having the admin role for the admin part.
        # Change these rules to adapt them to your needs
        - { path: ^/admin/, role: ROLE_ADMIN }
        - { path: ^/.*, role: ROLE_USER }
app/config/config.yml
fos_user:
    db_driver: mongodb
    firewall_name: main
    user_class: Acme\UserBundle\Document\User
    group:
        group_class: Acme\UserBundle\Document\Group
    from_email:
        address: info@acme.com
        sender_name: Acme
    encoder:
        algorithm: sha512
        encode_as_base64: false
        iterations: 10
    template:
        engine: php
    registration:
        confirmation:
            enabled: true
            template: AcmeUserBundle:Registration:email.txt.php
        form:
            type: acme_user_registration_form_type
            handler: acme.registration.form.handler
    resetting:
        token_ttl: 86400
        email:
            template: AcmeUserBundle:Resetting:email.txt.php

Conclusion

Overall, Symfony 2 is impressive. Having used Symfony 1.0, 1.2, and 1.4, I was looking for PHP5.3 and Doctrine 2 support. The new service container concept is elegant and, simply put, icing on the cake. On the other hand, having never used a similar component like sfGuardPlugin, I was challenged by FOSUserBundle. After all is said and done though, it is a welcome component that I will be using from now on.

Below is a couple of minor issues/questions that have come up while I was writing this post:

* Why can't I use a service id (acme.registration.form.type) for fos_user.registration.form.type? Why do I have to create an alias?
* Twig... Reminds me of my Smarty days. I want nothing to do with it. Yet, it is given priority over PHP templates as can be seen in the Symfony 2 documentation. Obviously, this ends up affecting the bundle developers. Maybe fos_user.template.engine can support an array like [php, twig] so I don't have to rewrite each template in PHP again?
* Multiple configuration schemes (XML, YAML, etc) sound really good until you have to actually deal with them. I wanted to stick with YAML until I realized XML was the default configuration scheme for FOSUserBundle.
* What is the purpose of storing non-canonicalized versions of email and username? I have always stored the normalized version of usernames and emails.

Saturday, May 7, 2011

Exim posing as Sendmail: Fixing Sender and Return-Path headers

Exim


If you are having problems changing the Sender and Return-Path headers, make sure that you are editing the right configuration file. On my CentOS 5.6:

[root@server mail]# ll /usr/sbin/sendmail
lrwxrwxrwx 1 root root 21 Oct 26  2009 /usr/sbin/sendmail -> /etc/alternatives/mta
[root@server mail]# ll /etc/alternatives/mta
lrwxrwxrwx 1 root root 23 Apr  9 07:48 /etc/alternatives/mta -> /usr/sbin/sendmail.exim
[root@server mail]# ll /usr/sbin/sendmail.exim 
lrwxrwxrwx 1 root root 4 Apr  9 07:45 /usr/sbin/sendmail.exim -> exim

I spent some time trying to figure out why my changes to the sendmail.mc file were being ignored. Naturally, Exim configuration is different than Sendmail. You need to edit the /etc/exim/exim.conf file:

remote_smtp:
  driver = smtp
  return_path = bounce@domain.com
  headers_rewrite = apache@* info@domain.com s

Don't forget the "s" at the end. See this page for more information: http://www.exim.org/exim-html-2.00/doc/html/spec_32.html#SEC669

If you are OK with displaying the apache user name (ie "Sender: apache@subdomain.domain.com") in the email header, then just update the qualify_domain configuration option in the same file.

qualify_domain = domain.com

This will fix the domain only (ie "Sender: apache@domain.com").

Sendmail


If you are indeed using Sendmail, then update your /etc/mail/sendmail.mc file.

LOCAL_DOMAIN(`domain.com')dnl
MASQUERADE_AS(`domain.com')dnl
FEATURE(`masquerade_envelope')dnl
FEATURE(masquerade_entire_domain)dnl
MASQUERADE_DOMAIN(`domain.com')dnl

If this configuration file does not exist, you need to install the sendmail-cf package first. Once you are done updating all the relevant configuration options, run the following command to generate your /etc/mail/sendmail.cf file.

make -C /etc/mail

Finally, restart sendmail:

service sendmail restart

Thursday, March 10, 2011

PHP API for Solr

I finally had some time to finish my PHP client for Solr. It is still in alpha but I am planning to finalize testing for a beta release soon. The source code can be found at https://github.com/buraks78/Logic-Solr-API.