Kris Wallsmith

Symfony Guru at opensky.com.
Discussing web development, Symfony and fatherhood.

Posts tagged Symfony2

Jan 16

Symfony2 Security Voters

I answered this question on StackOverflow today that is probably worth repeating here. The poster was asking how to implement subscription-based authorization logic in Symfony2. I imagine he models his problem something like this:

class Subscription
{
    const SECURED_AREA_FOO = 'FOO';
    const SECURED_AREA_BAR = 'BAR';

    // ...

    /** @ManyToOne(targetEntity="User", inversedBy="subscriptions") */
    public $user;

    /** @Column */
    public $securedArea;

    /** @Column(type="date") */
    public $start;

    /** @Column(type="date") */
    public $end;

    public function isActive()
    {
        $now = new DateTime();

        return $now >= $this->start && $now < $this->end;
    }

    // ...
}

class User implements UserInterface
{
    // ...

    public function getRoles()
    {
        $roles = $this->roles;

        foreach ($this->subscriptions as $subscription) {
            if ($subscription->isActive()) {
                $roles[] = 'ROLE_SUBSCRIPTION_'.$subscription->securedArea;
            }
        }

        return $roles;
    }

    // ...
}

He may then configure access control in his security.yml:

access_control:
    - { path: "^/sections/foo", roles: [ ROLE_SUBSCRIPTION_FOO ] }
    - { path: "^/sections/bar", roles: [ ROLE_SUBSCRIPTION_BAR ] }

This solution is nice enough, but we can do better. As the poster pointed out when someone suggested this solution, it doesn’t make sense to hydrate all of a user’s subscription objects (some of which may have expired years ago) just to fetch a string value. On top of that, fetching all of these objects every request is a waste of resources because the system will only be checking for the existence of one of them in any given request.

This authorization logic can be implemented much more lightly by encapsulating it in a custom security voter.

Voters

When questions about authorization are asked in Symfony2, the answer is arrived at by a process of voting. For example, when someone requests an URL that matches a configured access_control rule, a vote is held to decide whether to allow or deny access to that resource.

This voting process is similar in some ways to a US appeals court. There are a certain numbers of judges presiding over any given proceeding and in the end a decision is rendered. Occasionally judges recuse themselves from a case for this or that reason.

The authorization portion of the Symfony2 security component also includes a panel of judges called voters. These voters each have a say whenever questions of authorization come up in your application. Not every voter will have an opinion on every decision; some will abstain.

There are two basic types of votes: whether a user is granted a certain security attribute (i.e. the ROLE_USER attribute) and whether a user is granted a certain security attribute for a certain object (i.e. the EDIT attribute on blog post X). Each voter can be tuned to only chime in when certain votes are held. For example, you could write a voter that only participates when considering users with Gmail addresses.

In this particular case, we want to create a voter that only chimes in when questions regarding access to areas secured by subscriptions are raised. We can signify this by creating a new set of attributes that start with the string SUBSCRIPTION_.

access_control:
    - { path: "^/sections/foo", roles: [ SUBSCRIPTION_FOO ] }

This configuration proposes that only users with access to the security attribute SUBSCRIPTION_FOO should be granted access to any URL that starts with /sections/foo. Since there are no voters configured to evaluate this particular set of attributes, access with always be denied. But that is solved easy enough by creating a custom security voter.

This voter will look something like this:

class SubscriptionVoter implements VoterInterface
{
    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function supportsAttribute($attribute)
    {
        return 0 === strpos($attribute, 'SUBSCRIPTION_');
    }

    public function supportsClass($class)
    {
        return true;
    }

    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $user = $token->getUser();

        foreach ($attributes as $attribute) {
            if ($this->supportsAttribute($attribute)) {
                $securedArea = substr($attribute, strlen('SUBSCRIPTION_'));

                // use the entity manager to query for active
                // subscriptions that connect the current user to the
                // requested secured area

                return $success ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED;
            }
        }

        return VoterInterface::ACCESS_ABSTAIN;
    }
}

And be configured in the service container something like this:

services:
    subscription_voter:
        class: SubscriptionVoter
        public: false
        arguments:
            - @doctrine.orm.entity_manager
        tags:
            - { name: security.voter }

And that’s all there is to it. You have encapsulated your custom authorization logic in one clean class and added it to the Symfony2 security layer.

Other Applications

This is an example of one specific application of security voters, but there are many more. If you are struggling with how to implement some special access control logic that doesn’t fit nicely into either security roles or the security component’s ACL, consider creating a custom voter.


Oct 19
The truth shall set you free…

The truth shall set you free…


Oct 18
My last presentation during my month of travel was a tech talk on Symfony2 at the L//P offices in Zürich. I did some live coding and demonstrated how to reference a controller from the DIC and unit test that controller using PHPUnit.

My last presentation during my month of travel was a tech talk on Symfony2 at the L//P offices in Zürich. I did some live coding and demonstrated how to reference a controller from the DIC and unit test that controller using PHPUnit.


Oct 17

How to Test a Symfony2 Bundle

The Symfony2 Framework is fully unit tested using PHPUnit. When you create a Symfony2 bundle to share with the community, it’s important that your bundle also be fully unit tested. It’s also important that users be able to run your bundle’s test suite without having to wrap it in a dummy project. This blog post is about how to set that up.

PHPUnit Configuration

Configuration for PHPUnit should be in a file named phpunit.xml.dist in your bundle’s root directory. This file is suffixed .dist since it is the distributed configuration. Users can copy this default configuration to phpunit.xml and make modifications there for their environment. This is necessary when testing bundles, as we’ll see below.

The distributed configuration file should look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./Tests/bootstrap.php">
  <php>
    <!-- <server name="SYMFONY" value="/path/to/symfony" /> -->
  </php>
  <testsuites>
    <testsuite name="FacebookBundle Test Suite">
      <directory suffix="Test.php">./Tests</directory>
    </testsuite>
  </testsuites>
  <filter>
    <whitelist>
      <directory>./</directory>
      <exclude>
        <directory>./Tests</directory>
      </exclude>
    </whitelist>
  </filter>
</phpunit>

This configuration instructs PHPUnit to include a bootstrap file located at the base of your bundles Tests/ directory before running any tests. We’ll take a look at this file below.

The <php> portion of the configuration will define the $_SERVER['SYMFONY'] variable. After copying this file to phpunit.xml, users will need to uncomment this line and enter the actual path to the Symfony2 src/ directory here.

The <testsuites> section of the file tells PHPUnit where to find your test cases. The <filter> section defines a whitelist of files that the test suite is covering, which excludes the test cases themselves.

Autoloading

The purpose of the bootstrap file referenced in the PHPUnit configuration is to initialize autoloading of classes from the Symfony core and from your bundle. The former can be done using the UniversalClassLoader:

require_once $_SERVER['SYMFONY'].'/Symfony/Component/HttpFoundation/UniversalClassLoader.php';

use Symfony\Component\HttpFoundation\UniversalClassLoader;

$loader = new UniversalClassLoader();
$loader->registerNamespace('Symfony', $_SERVER['SYMFONY']);
$loader->register();

Notice the use of $_SERVER['SYMFONY'], which we defined earlier in the PHPUnit configuration?

Loading of your bundle’s classes is a bit more complicated since we can’t rely on them be installed in any particular directory, but a hack like this will do the trick:

spl_autoload_register(function($class)
{
    if (0 === strpos($class, 'Bundle\\Kris\\FacebookBundle\\')) {
        $path = implode('/', array_slice(explode('\\', $class), 3)).'.php';
        require_once __DIR__.'/../'.$path;
        return true;
    }
});

With this configuration and bootstrap script in place, running your bundle’s test suite is easy as pie:

$ cd ~/Sites/FacebookBundle
$ phpunit

Jul 29

How to create a Symfony2 templating helper

As you get started with Symfony2 you will probably find yourself getting stuck on some tasks that are second-nature to you when developing in symfony 1. One of these will probably be adding a custom helper to your view layer. Hopefully this quick article will clarify that particular process.

Create a helper class

In order to work with the Symfony2 view layer your helper class must implement the HelperInterface interface. This interface is quite simple:

namespace Symfony\Components\Templating\Helper;

interface HelperInterface
{
    function getName();
    function setCharset($charset);
    function getCharset();
}

Most of the time you’ll be extending the abstract base helper class provided in the core, which provides concrete implementations of the latter two methods. This remaining method, getName(), should return the name your helper can be called by from the view layer.

For example, a helper that outputs the Google Analytics tracking code could look something like this:

namespace Application\HelloBundle\Helper;

use Symfony\Components\Templating\Helper\Helper;

class TrackerHelper extends Helper
{
    public function getName()
    {
        return 'tracker';
    }
}

Register your helper

Once you have a concrete helper class you could add it directly to the templating engine using the $engine->set($helper) method. However, Symfony2 provides another, more scalable way to register helpers using the dependency injection container’s tagging mechanism.

To register a service as a templating helper, add a templating.helper tag that includes an alias attribute of the name you would like to use to reference this helper. For example, in hello/config/config.xml:

<services>
    <service id="tracker_helper" class="Application\HelloBundle\Helper\TrackerHelper">
        <tag name="templating.helper" alias="tracker" />
    </service>
</services>

Make it do something!

Now that your helper is registered you can access it in your template files via the templating engine (i.e. $view['tracker']). Let’s make it do something now so this isn’t just academic.

In order to output the correct tracking code, our helper will need to know what site id to use. We can pass this value to our helper’s constructor:

class TrackerHelper extends Helper
{
    protected $profileId;

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

    public function __toString()
    {
        // return tracking code that includes $this->profileId
    }

    // ...
}

Last we just need to configure the constructor parameter in the service container configuration:

<parameters>
    <parameter key="tracker.profile_id">UA-XXXXX-XX</parameter>
</parameters>

<services>
    <service id="tracker_helper" class="Application\HelloBundle\Helper\TrackerHelper">
        <tag name="templating.helper" alias="tracker" />
        <argument>%tracker.profile_id%</argument>
    </service>
</services>

With that, you can now output the tracking code in your template:

<?php echo $view['tracker'] ?>

Voilà! You can now get started developing your own Symfony2 templating helpers.