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: Redirecting example.com et al. to www.example.com when behind an Elastic Load Balancer

If your production server is behind an AWS Elastic Load Balancer (ELB), you’ll want to ensure that you don’t have multiple URIs that are pointing at the same resource (e.g. http://111.111.111.111/contact-us and http://www.example.com/contact-us).

Even with only one instance behind your ELB, there are many ways of reaching the resource:

  • example.com
  • www.example.com
  • ec2-XXX-XXX-XXX-XXX.us-west-1.compute.amazonaws.com (the DNS name of your single instance)
  • XXX.XXX.XXX.XXX (the IP address of your single instance)
  • Example-PROD-XXXXXXXXX.us-west-1.elb.amazonaws.com (the DNS name of your ELB)

Normally, a run-of-the-mill Apache Rewrite would be capable of redirecting all these to www.example.com. However, the gotcha here is the ELB’s health check. Suppose your ping URI is /ping.php. Without accounting for this in your Rewrite, you’ll end up with a 301 redirect on this file which will cause the health check to fail. The solution is to exclude the ping file from the rewrite rule.

# Note: this will probably not work for HTTPS since "http://" is hard-coded
RewriteCond %{HTTP_HOST} !^www\.example\.com$ [NC]
RewriteCond %{REQUEST_URI} !^/ping\.php [NC]
RewriteRule ^/(.*)$ http://www.example.com/$1 [R=301,L]

As you’d expect, the ping file can still be accessed by all those DNS names (e.g. ec2-XXX-XXX-XXX-XXX.us-west-1.compute.amazonaws.com/ping.php), so you probably want to add it to your robots.txt.

User-Agent: *
Disallow: /ping.php

AWS: Restricting access to a beta server that is behind an Elastic Load Balancer

If you have a beta or UAT or test server, you’ll likely want to restrict public access for many reasons. Most notably, to keep the general public out and to stop search engines recording duplicates.

So you’ll likely have an EC2 Security Group with rules for port 80 which generally work fine. However, if you want to put your server behind an Elastic Load Balancer (ELB) (e.g. to make it as close to production as possible), you’ll run into a problem. The issue is that the ELB needs to ping your instances. Because port 80 has been restricted, the ELB cannot get through.

I found an old post that has the solution if you scroll down to the comments.

The solution is to add amazon-elb/amazon-elb-sg to your Security Group.

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.

Changing is_null($x) to null === $x or !is_null($x) to null !== $x in PHP files

Here is a script that can perform the change of is_null($x) to null === $x (and the negation).

1 2 3 4 5 6 7 8 9
#!/bin/bash
 
# Assume we look in tmp
 
# For is_null
find tmp/ -type f -name "*.php" -exec sed -i -r 's#\([^\!]\)is_null\s*(\([^()]*\))#\1\2 === null#g' {} \;
 
# For !is_null
find tmp/ -type f -name "*.php" -exec sed -i -r 's#[\!]is_null\s*(\([^()]*\))#\1 !== null#g' {} \;
view raw is_null.sh This Gist brought to you by GitHub.

Note I am not the original author of this script, however, I do not have the original source.

Note: this is a micro-optimisation, however, it does enable for nicer code in any case.

NetBeans with PHP on MacBook Air causing loud fan

I’ve been using NetBeans 7.1 on my MacBook Air and it seemed that the fan would eventually come on and it was quite loud while trying to work.

After disabling as many plugins as I didn’t need, I think that the issue may be solved by closing the Tasks window. The problem may have been created because I am using the phpMD and phpCodeSniffer plugins which both create a lot of tasks.

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.