This Case Study is a guest post written by Antoni Orfin, Co-Founder and Software Architect at Octivi. Want your company featured on the official Symfony blog? Send a proposal or case study to fabien.potencier@sensiolabs.com

For most people, using full-stack frameworks equals slowing down websites. At Octivi, we think that it depends on correctly choosing the right tools for specific projects.

When we were asked to optimize a website for one of our clients, we analyzed their setup from the ground up. The result: migrate them toward Service Oriented Architecture and extract their core-business system as a separate service.

In this Case Study, we'll reveal some architecture details of 1 Billion Symfony2 Application. We'll show you the project big-picture then focus on features we really like in Symfony2. Don't worry; we'll also talk about the things that we don't really use.

Key numbers to give you an overview of the described platform's scale:

Our system development had to fulfill both of the following requirements:

So, as you can see, some unusual things are happening here - Redis is our primary data store and MySQL is our last cache layer .

Of course, every web-application uses some kind of data-storage. We've managed a nice setup here, too.

To maintain high performance and high-availability, the Symfony2 service uses several application's servers - in redundant configuration.

As we've said – the platform is structured into separate webservices serving as a REST APIs and Symfony2 application is one of the services.

Things we liked the most¶

Clear project structure - Bundles¶ Symfony2 doesn't impose full structure of your project, it's actually quite flexible. Basically, you can structure your code into Bundles which maintains Symfony-related code and more generic Components which can handle common tasks, including those not necessarily strictly related to the Symfony2 ecosystem. Following this concept, we're mostly using bundles to divide our Project into logically-connected parts. We barely modified the Symfony2 Standard structure, so our code base is very easy to understand for developers with Symfony2 experience.

Extending codebase - EventDispatcher Component¶ Do you need to change the response format in all your controllers? It's easy, just add new ResponseFormatListener and listen for kernel.response event. We have seen so much spaghetti code in so many projects that we really liked the popularization of Events concepts in Symfony. It's nothing new in software patterns, just old Observer Pattern, but before, in legacy PHP frameworks it wasn't so commonly used. In addition to origin Symfony2 events, we've also chosen to add our custom ones. Using Event Listeners we can keep code clean, methods dispatch their specific events and that way new parts of the code can connect to the existing parts without actually making in-code changes. Because performance is crucial in the project, we're evaluating the performance impact. The nice thing is that this mechanism comes with hardly any performance overhead. Internally it's simple array that stores event listeners' instances. Virtually worry-free!

Retrieving requests' data - OptionsResolver Component¶ While designing this application, we're also considering the most efficient way of retrieving and validating data from request content. We needed to smoothly transform request's data into DTO (Data Transfer Object). That way we won't get stuck with hard-to-maintain associative arrays and we'll stick to the structure of Request classes. Basically, we ended with passing queries with Query String and considered different Symfony2 mechanisms... You could use Form Component, build request structure in a Form Type and pass Request to it. It's nice and has rich features, but comes with huge overhead. We also didn't need or want advanced options like having nested fields. Another way would be to pass data in a request's content in a JSON structure. Use a Serializer component (like great JMSSerializer or even Symfony Serializer Component) and validate resulting DTO Request's objects. There are still two points that could lead to performance bottleneck: serializing and validating with the Validator Component. Thus, we didn't need any advanced validation (just checking required options and some basic format validation). Requests' format structure is also designed to be simple, and we've chosen... OptionsResolver Component. It's the same one you use when making options for your forms. We pass to it the GET array and on the output we receive a nicely validated and structured model object (it's an array to DTO normalization). One nice thing about handling validation, exceptions come with verbose messages so they're ideal for debugging purposes and for presenting readable exceptions for API Endpoints. Example of a handling request 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 62 63 64 65 66 67 68 69 70 71 <? php namespace Octivi\WebserviceBundle\Controller ; /** * @Route("/foo") */ class FooController extends BaseController { /** * Sample requests: * - OK: api.com/foo/bars?type=simple&offset=10 * - OK: api.com/foo/bars?type=simple * - Wrong: api.com/foo/bars?offset=10 * - Wrong: api.com/foo/bars?limit=many * * @Route("/bars") */ public function barsAction () { $request = new GetBarsRequest ( $this -> getRequest () -> query -> all ()); $results = $this -> get ( 'main.foo' ) -> getBars ( $request ); return $results ; } } <? php namespace Octivi\WebserviceBundle\Request ; class GetBarsRequest extends AbstractRequest { protected $type ; protected $limit ; protected $offset ; public function __construct ( array $options = array ()) { parent :: __construct ( $options ); } protected function setDefaultOptions ( OptionsResolverInterface $resolver ) { parent :: setDefaultOptions ( $resolver ); $resolver -> setRequired ( array ( 'type' , )); $resolver -> setOptional ( array ( 'limit' , 'offset' )); $resolver -> setAllowedTypes ( array ( 'limit' => array ( 'numeric' ), 'offset' => array ( 'numeric' ) )); $resolver -> setDefaults ( array ( 'limit' => 10 , 'offset' => 0 )); $resolver -> setAllowedValues ( array ( 'type' => array ( 'simple' , 'extended' ), )); } // ... } Pretty straightforward, isn't it? And we've achieved everything we want: Basic validation Setting required fields Optional parameters Handling data types (numeric) and allowed values Default parameters

Nice looking DTO representation of a request

Keeping configuration in-code - Annotations¶ Yes, we're using annotations in a high-performance application. Does that sound weird? Maybe, but we love this mechanism! Annotations are just PHPDoc-like texts. They're parsed under a cache warm-up process and transformed to plain PHP files. As a matter of fact, it doesn't matter if you use XML, YAML or annotations, because they all end up transformed into plain PHP files. We're using annotations as much as possible: Routing - as we've shown in previous barsAction, we declare our routes via @Route annotations. We have found it cleaner to keep such declarations within controllers' code as opposed to dividing it into separate YML/XML files. That way, developers don't have to jump from file to file searching for routes. Keep in mind, that we have declared loading of such annotations in a global app/config/routing.yml file.

- as we've shown in previous barsAction, we declare our routes via @Route annotations. We have found it cleaner to keep such declarations within controllers' code as opposed to dividing it into separate YML/XML files. That way, developers don't have to jump from file to file searching for routes. Keep in mind, that we have declared loading of such annotations in a global file. Services - it's the next thing you can place in- code. Using JMSDiExtraBundle we don't have to worry about maintaining YAML files with Service Container declaration. Everything ends in service classes. Still, configuration of DI for external classes is done via XML files. Example event listener configured with Annotations: 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 <? php namespace Octivi\WebserviceBundle\EventListener ; use JMS\DiExtraBundle\Annotation\Service ; use JMS\DiExtraBundle\Annotation\Observe ; use JMS\DiExtraBundle\Annotation\InjectParams ; use JMS\DiExtraBundle\Annotation\Inject ; use Symfony\Component\HttpKernel\Event\FilterResponseEvent ; /** * @Service */ class FormatListener { /** * Constructor uses JMSDiExtraBundle for dependencies injection. * * @InjectParams({ * "em" = @Inject("doctrine.orm.entity_manager"), * "security" = @Inject("security.context") * }) */ function __construct ( EntityManager $em , SecurityContext $security ) { $this -> em = $em ; $this -> security = $security ; } /** * @Observe("kernel.response", priority = 0) */ public function onKernelResponse ( FilterResponseEvent $event ) { $this -> em ->... ; } }

Making rich CLI commands - Console Component¶ Another extensively-used Symfony component is Console Component, our most used component since most of the new features for the application come as CLI commands. In previous-gen PHP frameworks or other popular PHP-based software (e.g. Wordpress, Magento) making CLI commands gave us a serious headache. There weren't any standardized components or they were so lacking in features that everyone had to present his own solutions. In Symfony2, we found a cool framework for making CLI commands. We can set command names, required options and arguments. We discovered a good practice to make accurate descriptions of the commands. They're self-documenting and there's no reason to add them to the standard form of documentation. It's especially great when you're developing with Agile methods. When new functionalities come fast, you'll end up with lots of out-dated versions of your paper-documentation. We're using Console Component to create administrative tools and even long running processes. The longest one took about 6 days. Nice proof of lack of memory-leaks in the Symfony ecosystem! 1 2 3 4 5 6 7 8 9 10 $ php app / console octivi : test - command -- help Usage : octivi : test - command [ - l |-- limit [ = "..." ]] [ - o |-- offset [ = "..." ]] table Arguments : table Database table to process Options : -- limit ( - l ) Limit per SQL query . ( default : 10 ) -- offset ( - o ) Offset for the first statement ( default : 0 )