Clearcode, PHP

Symfony - project tamed


Symfony - project tamed: March 27, 2014 by Paweł Barański Software Manager at Clearcode

When managing projects based on Symfony2, from the technical side, it is a good idea to establish a set of rules for the project. If you haven’t established and implemented such rules yet, then they should be created as soon as possible. Why? Well, no matter how many people are working on the project, the code needs to look like it was written by one person. Every programmer who contributes to the project needs to be able to simply pick up where the other developer left off, regardless if they implemented the module or not. This allows the project to progress faster, not only with implementation, but also when it comes to code review.

Symfony documentation doesn’t specifically focus on this issue, and the bundles that are written by the community have their own set of rules. Of course, there are the main, common points, but these alone do not define a good set of rules that are able to be checked. Learning from mistakes as you go along cannot only be costly, but also time consuming. It is good to have a starting point, something that at least has worked for someone else. This is how the idea to share the guidelines on the Taming Symfony Project came about.

Bundle structure

We used the following guidelines for determining the structure of the bundle:

  1. Number of directories should not be “too large” (in most cases, maximum 10)
  2. Opening the directory should not bring about any surprises regarding its contents or where they are located. For this reason, the first level directory is generic, rather than specific to the domain.
  3. Unless specified in the description of the directory, the class definition must be placed in a folder that reflects the structure of the Symfony or folder structure associated with the component that is related to the class (Twig, Doctrine). In the latter case, the main folder must match the name of the component.

The base was a basic bundle which is generated by the task generate:bundle. Below is what was created as a result of these assumptions:

/Command – symfony tasks.

/Controller – standard symfony folder. Controllers should have a small amount of actions and the actions themselves should not be large.

/DependencyInjection – standard symfony folder. It consists primarily of the bundle configuration, occasionally CompilerPass.

/Document – model classes managed by the ODM and their repositories.

/Entity – model classes managed by an ORM model and their repositories.

/Event – event classes.

/EventListener – listeners / subscribers. It can contain subfolders, e.g. for Doctrine or Form listeners, etc. You would usually see a reversed directory structure. In other libraries Form/EventListener is more common than EventListener/Form. I’d recommend the latter in order to avoid spreading the EventListener directory within a single bundle. This also goes hand in hand with the guidelines, which refer to a small amount of directories and their generic nature.

/Form – form related class. It contains subfolders, in particular Form/Type and Form/DataTransformer.

/Model – model classes that are not managed by ORM/ODM.

/Resources – standard symfony folder.

/Service – service classes, domain specific to the bundle which are not listeners and do not match other folders. It may contain subfolders.

/Tests – unit tests. The folder structure corresponds to the structure of the folders in the bundle, so if you want to test a class located in the Service/MyService.php, put the test case in the file Tests/Service/MyServiceTest.php.

/Twig/Extensions – classes that are extensions for Twig. It is placed here mainly as an example of the 2nd case in step 3 of the guidelines.

/Validator – validators.

Events.php – final class containing constants with the names of the events defined in the bundle.

Regarding the placement of interfaces, they should be placed in directories that would normally contain their implementations. Deviations from this structure should be considered for specific cases.

Doctrine

‘flush’ only from the controller and commands. The rule is definite, mainly due to limit attempts to tamper with it, because although there are exceptions to the rule, they should be treated carefully. Only a handful of services actually need to write data to the database. Preferably, they would only need to call ‘persist’ and wait for the flush until the flow returns to the controller. Sometimes it is necessary, for example, for us to have the ‘id’ before we go any further, however, in most cases, it can wait. The advantage of adhering to this principle is that we do not have ‘flushes’ spread all over the different types of classes and we do not have to worry about handling transactions manually – ‘flush’ wraps its queries in the transaction by default.

public function someAction($entity)
{
$this->container->get('my_service')->somethingHappensToEntity($entity);
$manager = $this->getDoctrine()->getManager();
$manager->flush($entity);
//...
}

Table names must be named in the plural, using lowercase letters and words separated by underscores ‘_’.

entity 'UserGroup' -> table 'user_groups'

Column names must be written with lowercase letters and words separated by underscores ‘_’.

property class 'createdAt' -> name of the coloumn 'created_at'

Entities should not contain any logic (the less the better, preferably not at all). The logic should be included in the services.

Queries must be constructed in a repository class using QueryBuilder. It is also a good practice to separate construction of a query from its execution using separate functions for each part. Developers are divided into two groups – the first writes using pure DQL, the second uses QueryBuilder. Using QueryBuilder provides greater control over building correct DQL and allows easier isolation of logic, common to several queries, so that the same logic can be used multiple times without code repetition.

public function getRate(Event $event, Member $member)
{
$qb = $this->getUserRateQueryBuilder($event, $member);
return $qb->getQuery()->getOneOrNullResult();
}

protected function getUserRateQueryBuilder(Event $event, Member $member)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('er')
->from($this->getClassName(), 'er')
->leftJoin('er.event', 'e')
->leftJoin('er.owner', 'o')
->where('e.id = :eventId')
->andWhere('o.id = :ownerId');
$qb->setParameter('eventId', $event->getId())
->setParameter('ownerId', $member->getId());
return $qb;
}

Do not use the Query\Expr. I am not questioning the usefulness of this class, but there are people who abuse it, which causes the expressions to lose their readability. A simple expression is able to grow from a few to a dozen characters. The general rule not to use this class works in most cases.

good: $qb->where('op.status IS NULL')
bad: $qb->where($this->getQueryBuilder()->expr()->isNull('op.status'))

Twig

Use the tag {% include% } instead of the include() function. Choose one of these solutions to maintain consistency. As I mentioned earlier, the code needs to look like one person wrote it. It should opt for a tag, because the purpose of ‘include’ (tag or function) has a lot to do with the way the final template is generated, which again brings it closer to tags such as {% block %} and {% extends %}.

Do not abuse the function / filters. Extensions in the form of a function can cause logic, which belongs to the controller, to be moved to the template. In order to decide whether to use the functions in twig, or return the already processed object from the action, it is worth asking the following question: “if we had to return JSON instead of HTML with the same data that we pass to the twig template, will it be sufficient or would we be missing something?”. If it turns out that regardless of whether we return HTML or JSON, the function should be used, then it should be done in the controller (or to be more specific, before the final response).

Service

Use the name of the vendor and the name of the Bundle (without the word Bundle) in the name of the service and in the names of the parameters.

class: Acme/DemoBundle/Service/MyService
service: acme.demo.my_service

List the service arguments and methods calls one by one in services.yml. You need to choose a style, otherwise you will end up having mixed single-line and multi-line calls. I chose multi-line due to the greater readability.

services:

acme.demo.example:
class: Acme\DemoBundle\Service\Example
arguments:
     - '@some_service'
     - '@another_service'
     - '%some_parameter%'
calls:
     - [setSomething, [%some_value%]]
     - [setSomethingElse, [%another_value%]]


This makes it easier to understand which services / parameters are ours, and which are someone else’s.

It works

This collection has proven itself in our case. It limited the number of questions about what to place where, and kept out code concise. Code actually looks very similar throughout the whole project, which makes adding a new member to the team a very quick and easy process. By adding code review to it, we are able to react right away to any attempts to break these rules. Checking some of them can be automated which allows us to save time. I recommend implementing these rules in your projects, I am also eager to know what your recommendations are on taming Symfony in your projects, because like with most things, these can surely be improved.

You can read more about Symfony (in polish) on my blog Devhelp.pl.