Testing feature flags with Symfony2 and PHPUnit

Marc Weistroff wrote a really good article on using feature flags in a PHP application. The idea is that you can enable certain features for certain users only (for example enable feature X only for your beta testers).

Marc’s article is complete in itself, however I wanted to add a test to ensure that my controller action was truly inaccessible by another other than beta testers.

Here is the controller action and a corresponding test that ensures that a 404 is shown if the user is not a beta tester. I probably could have used the security firewall to do this or a JMS security annotation as an alternative to specifically writing the conditional in the controller action.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
<?php
 
namespace Acme\Bundle\WebBundle\Controller;
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 
use JMS\DiExtraBundle\Annotation as DI;
 
/**
* @Route("/feature-x")
*/
class FeatureXController
{
/**
* @Route("/{someSlug}/{anotherSlug}")
*
* @Template()
*/
public function featureXAction(Request $request, $someSlug, $anotherSlug)
{
if (!$this->getSecurityContext()->isGranted('FEATURE_BETA')) { // or, say, ROLE_BETA_TESTER depending on your needs
throw new NotFoundHttpException();
}
 
// do something for feature X
 
return array();
}
 
/**
* @DI\LookupMethod("router")
*
* @return RouterInterface
*/
protected function getRouter()
{
}
 
/**
* @DI\LookupMethod("security.context")
*
* @return SecurityContextInterface
*/
protected function getSecurityContext()
{
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
<?php
 
namespace Acme\Bundle\WebBundle\Tests\Controller;
 
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 
class FeatureXControllerTest extends WebTestCase
{
/**
* @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function testFeatureXThrowsNotFoundExceptionWhenUserIsNotBetaTester()
{
$securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface');
 
$securityContext
->expects($this->once())
->method('isGranted')
->will($this->returnValue(false))
;
 
$controller = $this->getMockBuilder('Acme\Bundle\WebBundle\Controller\FeatureXController')
->setMethods(array('getSecurityContext'))
->getMock();
 
$controller
->expects($this->once())
->method('getSecurityContext')
->will($this->returnValue($securityContext))
;
 
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
 
$controller->featureXAction($request, 'some-slug', 'another-slug');
}
}

Feel free to comment if you have any suggestions or improvements. Thanks to Marc for the original article.

Symfony2, Composer, Capifony and an EC2 Micro instance

When deploying a Symfony2 application to an EC2 micro instance via Capifony, the Composer install step sometimes failed with an error like this:

--> Installing Composer dependencies
  * executing "cd /var/www/beta.example.com/releases/20120828043222 && php composer.phar install -v --no-interaction"
    servers: ["beta-1.www.example.com"]
    [beta-1.www.example.com] executing command
 ** [out :: beta-1.www.example.com] Loading composer repositories with package information
 ** [out :: beta-1.www.example.com] Installing dependencies from lock file
 ** [out :: beta-1.www.example.com] - Installing example/simple-linkedin-php (v3.2.0)
 ** [out :: beta-1.www.example.com] Downloading: connection...
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 0%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 30%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 35%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 60%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 65%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 95%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 100%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Downloading: 100%
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Unpacking archive
 ** [out :: beta-1.www.example.com] Cleaning up
 ** [out :: beta-1.www.example.com] PHP Fatal error:  Uncaught exception 'ErrorException' with message 'proc_open(): fork failed - Cannot allocate memory' in phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php:943
 ** [out :: beta-1.www.example.com] Stack trace:
 ** [out :: beta-1.www.example.com] #0 [internal function]: Composer\Util\ErrorHandler::handle(2, 'proc_open(): fo...', 'phar:///var/www...', 943, Array)
 ** [out :: beta-1.www.example.com] #1 phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php(943): proc_open('stty -a | grep ...', Array, NULL, NULL, NULL, Array)
 ** [out :: beta-1.www.example.com] #2 phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php(848): Symfony\Component\Console\Application->getSttyColumns()
 ** [out :: beta-1.www.example.com] #3 phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php(771): Symfony\Component\Console\Application->getTerminalWidth()
 ** [out :: beta-1.www.example.com] #4 phar:///var/www/beta.example.com/relea in phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php on line 943
 ** [out :: beta-1.www.example.com] 
 ** [out :: beta-1.www.example.com] Fatal error: Uncaught exception 'ErrorException' with message 'proc_open(): fork failed - Cannot allocate memory' in phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php:943
 ** [out :: beta-1.www.example.com] Stack trace:
 ** [out :: beta-1.www.example.com] #0 [internal function]: Composer\Util\ErrorHandler::handle(2, 'proc_open(): fo...', 'phar:///var/www...', 943, Array)
 ** [out :: beta-1.www.example.com] #1 phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php(943): proc_open('stty -a | grep ...', Array, NULL, NULL, NULL, Array)
 ** [out :: beta-1.www.example.com] #2 phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php(848): Symfony\Component\Console\Application->getSttyColumns()
 ** [out :: beta-1.www.example.com] #3 phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php(771): Symfony\Component\Console\Application->getTerminalWidth()
 ** [out :: beta-1.www.example.com] #4 phar:///var/www/beta.example.com/relea in phar:///var/www/beta.example.com/releases/20120828043222/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php on line 943
    command finished in 32287ms
*** [deploy:update_code] rolling back

The error message is pretty obvious: memory has run out. I thought it could be solved by increasing PHP’s memory limit, but that didn’t work. A Google search lead me to this comment on the Composer PHP repository. So I tried adding a swap to the EC2 micro instance and it appears to have solved the problem.

/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024
/sbin/mkswap /var/swap.1
/sbin/swapon /var/swap.1

AWS EC2: ensure that the user data you send has UNIX line endings

I’ve just created a Symfony2 console command that fires up an EC2 instance and sends a user_data.sh file to it. The command has a method that renders a Twig template (user_data.sh.twig) into user_data.sh. However, I couldn’t figure out why the script was hanging on my heredocs. For instance, the user data script would hang on:

1 2 3 4 5 6 7 8 9 10 11
#!/bin/bash
 
# Configure logrotate
 
touch /etc/logrotate.d/example.com
 
read -r -d '' LOGROTATE_EXAMPLE <<EOF
; Nothing to see here (yet)
EOF
 
echo "$LOGROTATE_EXAMPLE" | tee /etc/logrotate.d/example.com
view raw user_data.sh This Gist brought to you by GitHub.

I suspected that the script was waiting for input, which lead me to believe that it was something to do with line endings. I was right. It’s a simple update to my method to ensure that all line endings are UNIX.

1 2 3
<?php
 
$userData = str_replace(array("\r\n", "\r"), "\n", $userData);

It’s a rookie piece of code but hopefully this helps anyone having problems with hanging user data on an EC2 instance. An alternative would be to ensure that the files have the correct line endings in the first place.

Symfony2: selecting a bundle namespace

In my latest Symfony2 application I’ve been trying to figure out the best way to organise my namespaces/structure. I’ve decided that I don’t particularly like the bundle namespace Acme\UserBundle (for an example bundle AcmeUserBundle). Instead, I am leaning more towards Acme\Bundle\UserBundle.

Why? Because in the top-level namespace (i.e. all namespaces in Acme), we often have many other namespaces, not just Symfony2 bundles. For example, we might have something looking like the following:

Acme
  AdminBundle
  BlogBundle
  Component
    Form
      Extension
        Core
          EventListener
            TrimUriProtocolListener.php
  Domain
  Persistence
  UserBundle

I’ve added in some other bundles just to demonstrate that when you look at this, it seems to me that the bundles are out of place. Note: these namespaces could very well be in separate repositories and imported (and “glued”) together by something like Composer (i.e. they could be practically separated). However, when you use references to the namespaces in your code (e.g. think auto-complete), you will have a lot of bundles in the top-level namespace.

So it seems like a better structure would be the following:

Acme
  Bundle
    AdminBundle
    BlogBundle
    UserBundle
  Component
    Form
      Extension
        Core
          EventListener
            TrimUriProtocolListener.php
  Domain
  Persistence

Notice how the bundles now live in a Bundle namespace. Anytime you want to reference a namespace (e.g. with autocomplete), you are not flooded with a million bundles.

It’s not going to work for everyone and everything (and everyone has their own preference), however, I am enjoying having this structure moreso than my previous Symfony2 application.

Overriding the ObjectIdentityRetrievalStrategy to check if a domain object is a Doctrine proxy

I created this issue because my ACL checks were failing. It turns out that the ObjectIdentityRetrievalStrategy fails for Doctrine proxies because internally, get_class is used and this will return the class of the Doctrine proxy, not the class of the entity.

So, here is how to create your own strategy to override the default:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
<?php
 
namespace Jbi\CoreBundle\Security\Acl\Domain;
 
use Symfony\Component\Security\Acl\Exception\InvalidDomainObjectException;
use Symfony\Component\Security\Acl\Model\ObjectIdentityRetrievalStrategyInterface;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
 
use Doctrine\ORM\EntityManager;
 
/**
* Strategy to be used for retrieving object identities from domain objects
* where the domain object may be a Doctrine proxy.
*/
class ObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategyInterface
{
/**
* @var \Doctrine\ORM\EntityManager $em
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {@inheritDoc}
*/
public function getObjectIdentity($domainObject)
{
try {
if ($domainObject instanceof \Doctrine\ORM\Proxy\Proxy) {
return $this->fromDomainObject($domainObject);
}
return ObjectIdentity::fromDomainObject($domainObject);
} catch (InvalidDomainObjectException $failed) {
return null;
}
}
private function fromDomainObject($domainObject)
{
if (!is_object($domainObject)) {
throw new InvalidDomainObjectException('$domainObject must be an object.');
}
 
try {
if ($domainObject instanceof DomainObjectInterface) {
return new ObjectIdentity($domainObject->getObjectIdentifier(), $this->em->getClassMetadata(get_class($domainObject))->getName());
} elseif (method_exists($domainObject, 'getId')) {
return new ObjectIdentity($domainObject->getId(), $this->em->getClassMetadata(get_class($domainObject))->getName());
}
} catch (\InvalidArgumentException $invalid) {
throw new InvalidDomainObjectException($invalid->getMessage(), 0, $invalid);
}
 
throw new InvalidDomainObjectException('$domainObject must either implement the DomainObjectInterface, or have a method named "getId".');
}
}
view raw gistfile1.aw This Gist brought to you by GitHub.

Then, in your services file, register and override the default service, passing the entity manager as an argument and with your new class:

1 2 3
<service id="security.acl.object_identity_retrieval_strategy" class="Jbi\CoreBundle\Security\Acl\Domain\ObjectIdentityRetrievalStrategy" public="false">
<argument type="service" id="doctrine.orm.entity_manager" />
</service>
view raw gistfile1.xml This Gist brought to you by GitHub.

Using Twig and Symfony2 to create a page title

In the layout, we check if a view has set the title block. If so, we use a hyphen to separate the view and the layout title. If not, we just use the layout’s title. You can extend the example to translate the layout title.

1 2 3 4 5 6 7 8 9 10 11 12
<!DOCTYPE html>
<html>
<head>
{% if block('title') %}
{% set title = block('title') ~ ' - My App' %}
{% else %}
{% set title = 'My App' %}
{% endif %}
<title>{{ title }}</title>
</head>
<body></body>
</html>

And in your view, you can set the title:

1 2 3 4 5
{% extends 'MyAppCoreBundle::layout.html.twig' %}
 
{% block title %}
{{ 'my_app.core.view.somewhere.something.title'|trans }}{{ parent() }}
{% endblock %}