Every website uses some form of contextualization. In other words, users see information based on their context. A user being tied to a "company" or "organization", which sandboxes the data they see is one example of this.

!=

Adding context to the service container allows you to pass the current user or organization as dependencies to objects requiring a context in order to function. This post aims to show the ease of contextualizing your website with the Symfony2 Service Container, and will demonstrate how to:

  • add context to the container
  • switch context for admin users
  • reference context in twig templates
  • implement context security

Add context to the container

As with most things in Symfony2, it all starts with the container. The primary context object is your user. We will add this to the container with a User factory:

<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
<services>
    <!-- Current User Factory -->
    <service id="acme.user_factory" class="Acme\Demo\Bundle\Factory\UserFactory">
        <argument type="service" id="security.context" />
    </service>

    <!-- The Current User -->
    <service id="context.user" class="Acme\DemoBundle\Entity\User"
         factory-service="acme.user_factory"
         factory-method="get">
    </service>
</services>

Note: By nature, factories must always be declared public, even if they are never retrieved from the container.

Factory classes allow the creation of a service when retrieving that service requires some additional logic to do so. The cookbook article How to Use A Factory To Create a Service outlines this concept in detail. The UserFactory class in this case will retrieve the logged-in user from the security context.

namespace Acme\DemoBundle\Factory;

use Symfony\Component\Security\Core\SecurityContextInterface;

class UserFactory
{
    private $context;

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

    public function get()
    {
        if (null === $token = $this->context->getToken()) {
            return null;
        }

        if (!is_object($user = $token->getUser())) {
            return null;
        }

        return $user;
    }
}

This class requests a token from the security context, and verifies the user retrieved from the token is an object. The factory then returns the contextualized user, or null if no such user exists.

Note: Factories need to handle the possibility of null context. Login screens, public landing pages, and console tasks are just a few examples of how an application can function outside of a context.

Any other contextual objects pertaining to the logged in user, such as a company or organization, can be added to the service container in a similar fashion. The example below shows how to add a user's company to the container.

<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
<services>
    <!-- … -->

    <!-- Current Company Factory -->
    <service id="acme.company_factory" class="Acme\DemoBundle\Factory\CompanyFactory">
        <argument type="service" id="context.user" />
    </service>

    <!-- The Current Company -->
    <service id="context.company" class="Acme\DemoBundle\Entity\Company"
         factory-service="acme.company_factory"
         factory-method="get">
    </service>
</services>

And create the factory class:

namespace Acme\DemoBundle\Factory;

use Acme\DemoBundle\Entity\User;

class CompanyFactory
{
    private $user;

    public function __construct(User $user = null)
    {
        $this->user = $user;
    }

    public function get()
    {
        $company = null;

        if ($this->user) {
            $company = $this->user->getCompany();
        }

        return $company;
    }
}

Note: The user argument in the constructor must be optional in the case no user exists in the context.

Now that the contextualized company and user exist in the container, they can be accessed anywhere that has access to the container. The controller is a great place for this. For example, the "Contact List" shown above could be implemented like so:

namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class ContactController extends Controller
{
    public function listAction()
    {
        $company = $this->get('context.company');
        $contacts = $this->get('doctrine.orm.entity_manager')
            ->getRepository('AcmeDemoBundle:Contact')
            ->getContactsForCompany($company);

        return $this->render('AcmeDemoBundle:Contact:list.html.twig', array(
            'user'       => $this->get('context.user'),
            'contacts'   => $contacts,
        ));
    }
}

Switching context for admin users

The CompanyFactory class can be modified in order to allow for context switching. An admin user will want to switch between companies for troubleshooting and administration. This will ward against having ambiguous context (in our case, this would be a user not tied to a company). Certain areas of the site depend on a context, and therefore admin users should switch their context rather than be allowed outside of it.

namespace Acme\DemoBundle\Factory;

use Acme\DemoBundle\Entity\User;
use Symfony\Component\HttpFoundation\Session;
use Doctrine\Common\Persistence\ObjectManager;

class CompanyFactory
{
    private $user;
    private $session;
    private $entity_manager;

    public function __construct(User $user = null, Session $session, ObjectManager $entityManager)
    {
        $this->user           = $user;
        $this->session        = $session;
        $this->entity_manager = $entityManager;
    }

    public function get()
    {
        $company = null;

        if ($companyId = $this->session->get('context.company_id')) {
            $company = $this->entity_manager
                ->getRepository('AcmeDemoBundle:Company')
                ->find($companyId);
        }

        if (!$company && $this->user) {
            $company = $this->user->getCompany();
        }

        return $company;
    }
}

The session storage service and the entity manager service are now passed to the factory. This enables the factory to check the http session for the existence of a company identifier and ask the entity manager for this company. If this is null, the user's default company is used instead.

Because additional dependencies have been added to the factory, the service definition needs to be updated:

<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
<services>
    <!-- … -->

    <!-- Current Company Factory -->
    <service id="acme.company_factory" class="Acme\DemoBundle\Factory\CompanyFactory">
        <argument type="service" id="context.user" />
        <argument type="service" id="session" />
        <argument type="service" id="doctrine.orm.default_entity_manager" />
    </service>

    <!-- The Current Company -->
    <service id="context.company" class="Acme\DemoBundle\Entity\Company"
         factory-service="acme.company_factory"
         factory-method="get">
    </service>
</services>

Use the Service as a Dependency

The context classes can now be used like the dependency it is. For example, to filter objects in a list by context using the SonataAdminBundle, the current context can be passed to the admin service through the container

<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
<services>
    <!-- … -->

    <service id="acme.admin.contact" class="Acme\DemoBundle\Admin\ContactAdmin">

        <tag name="sonata.admin" manager_type="orm" group="admin" label="contact"/>

        <argument />
        <argument>Acme\DemoBundle\Entity\Contact</argument>
        <argument>SonataAdminBundle:CRUD</argument>
        <property name="company"  type="service" id="context.company" />
    </service>
</services>

The context company is now available in the admin class, and filtering the admin list is as easy as doing the following:

namespace Acme\DemoBundle\Admin;

class CompanyAdmin extends Sonata\AdminBundle\Admin\Admin
{
    public $company;

    //...
    public function createQuery($context = 'list')
    {
        $query = parent::createQuery($context);
        $query->getQueryBuilder()
            ->andWhere('o.company = :company')
            ->setParameter('company', $this->company)
        ;

        return $query;
    }
}

Reference context in twig templates

It is very handy to have context objects available inside the twig templates. To do this, set the services as globals in the twig configuration.

# app/config/config.yml
twig:
    globals:
      currentUser:    @context.user
      currentCompany: @context.company

This enables you to do very handy things like display the context in a twig block (like in the "Contact List" example above).

 # app/Resources/views/user_block.html.twig
 {% block user_block %}
     <h3>logged in as {{ currentUser.username }} ({{ currentCompany.name }})</h3>
 {% endblock %}

Implement context security

Any website implementing context will need to protect items existing outside the current context. This step in the process is easy to dismiss when deadlines and other priorities jockey for attention, but is a must have for any contextual application. Your client doesn't want curious users engineering URLs and reeking havoc on your other users.

The first step will be to create a Voter class. Voter classes are used by the security context to manage access. They have three possible outcomes, Grant, Deny, and Abstain.

 namespace Acme\DemoBundle\Security\Authorization\Voter;

 use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
 use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

 class ContextVoter implements VoterInterface, ContainerAwareInterface
 {
     public $container;

     public function supportsAttribute($attribute)
     {
         return $attribute === 'CONTEXT';
     }

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

     public function vote(TokenInterface $token, $object, array $attributes)
     {
         if ($this->supportsClass($object) && $company = $this->container->get('context.company')) {
             foreach ($attributes as $attribute) {
                 if ($this->supportsAttribute($attribute)) {
                     if ($company == $object->getCompany()) {
                         return VoterInterface::ACCESS_GRANTED;
                     }
                     return VoterInterface::ACCESS_DENIED;
                 }
             }
         }

         return VoterInterface::ACCESS_ABSTAIN;
     }
 } 

Note: The service_container service is passed in rather than the context.company service to avoid a ServiceCircularReferenceException. In this case, context.user depends on the security context, which depends on voters. Requesting the company at runtime solves this issue.

The voter loops through the requested attributes and determines whether or not it can grant access to this object. If the attribute being requested is CONTEXT, the voter grants or denies access accordingly.

This can now be configured in the service container:

<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
<services>
    <!-- … -->

    <!-- Context Voter -->
    <service id="context.voter" class="Acme\DemoBundle\Security\Authorization\Voter\ContextVoter" public="false">
        <tag name="security.voter" />
        <property name="container" type="service" id="service_container" />
    </service>
</services>

Pass the CONTEXT attribute and appropriate object to the isGranted function anywhere the security context is present:

public function showAction($id)
{
    //...
    if (!$this->get('security.context')->isGranted(array("CONTEXT"), $entity)) {
        throw new Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
    }
    //...
}

This may seem overkill. After all, the call to isGranted could simply be replaced by explicitly checking the company, no voters, security context, or dependency injection required! Working with the security context now will save a lot of headache in the future. Context may be more complicated than a simple "company" class. Consider GooglePlus circles, or Facebook's privacy policy, as complex examples of context. Using a voter will allow this logic to stay outside the controller.

Postmortem

Context is complicated, and almost every SaaS application requires it. The service container in Symfony2 helps mitigate these complexities, and allows the current context to behave as a dependency. Take the extra time to include context in your container, and your application will be much healthier because of it.

3 comments

  • Antonio Jesús - May 8, 2012

    Excelent post! It will be to me very helpful

  • Paulo - June 14, 2012

    Very very nice article!

  • murat - September 27, 2012

    This is great article. Thank you for effort. For newbies like me, if you can add sample entities and relation annotations, this article would be greater.