Symfony2 - simple, yet useful tips
Thursday, 16 June 2016, 10:47
It's been a while since my last article and activity, as I was mainly concentrated on my private life and commercials projects, but even though my top commitment was updating projects dependencies on GitHub I also experimented with some different approaches for organising Symfony2 projects. I want to share some of my thoughts, so you may also benefit from using it or tell me why these ideas are stupid. I don't want to write here some obvious stuff like "write unit tests", "use Composer" etc. - you can read that in every single tutorial for Symfony2. Some of my tricks are plain simple but not obvious, some, on the other hand look strange, but saved my ass, so you can give them a try. Please excuse my a bit chaotic form (and content), but I'm writing this in my free time while commuting by train .
Configuration
One of the first steps to do in Symfony2 application is to configure it. There are so many bundles out there, that sometimes you even spend more time on configuring and tuning stuff, then on writing actual code. Symfony2 configuration is very flexible and you can customize it a lot - here are some tips related to it, that I'm using in my projects.
local.yml
This is one of the things I would expect to be introduced even in Symfony Standard Edition and something I can't live without - an intermediate configuration file for local configuration overrides. In the standard package you have environment files (eg. config_prod.yml) which includes general configuration file (usually config.yml) and there is parameters.yml (or other equivalent) for local variables (like database connection credentials etc.). But sometimes it's not enough - first of all with parameters.yml you can only customize simple values and furthermore you have to predict possibility of such customization (which sometimes doesn't belong to you if you use 3rd-party bundles).
My solution is to introduce an additional file, which I usually call local.yml (and work with it like with parameters file - create default local.yml.dist file, add local file to .gitignore). In my projects environment files (like config_prod.yml) I load local.yml instead of config.yml. I can put any configuration I want into that local file to override system configuration (and so can any other developer working on the same project).
When is it useful:
- when you have some more complex local configuration adaptation (for example: if you have different vhost configuration which you need to handle or you have some buggy PHP extension);
- when you have multiple frontend nodes which you have to configure in a different way (for example: one node running on different platform then others);
- for customizing your local copy the way you want without affecting other developers.
The recipe:
- create a app/config/local.yml.dist file (or in any other location where you have your configuration) which should contain:
imports: - { resource: "config.yml" }
- add app/config/local.yml to .gitignore;
- handle local file creation at project deployment; here is simple make rule:
init: app/config/local.yml app/config/%.yml: app/config/%.yml.dist cp $^ $@
Things to remember:
- don't put anything more, than importing main configuration into your local.yml.dist - if you put anything there you expect it to be a constant part of your application config and should go to config.yml;
- remember that your local.yml should load config.yml - without this line nothing will probably work (quite obvious, but…);
- if you need to just change single parameter, then maybe just make particular configuration part varying on some parameters and put these parameters into parameters.yml;
- as it's impossible to use placeholders in import resources locations this local file is not the "final" one, as there are still environment-specific files - one could think that as it's a local configuration it can replace environment settings, but this is not the purpose of this file.
Splitting configuration into multiple files
In the default distribution there are only environment-specific files and main configuration file (and security.yml). But your config.yml will very quickly grow to an enormous size, especially if you are starting your project or even starting playing with Symfony2. Before you will learn what should stay, what shouldn't be touched and what is completely not needed, you will probably edit the configuration file more often then any other file. However, the configuration in Symfony2 is very flexible and you can organize it however you want - in one huge file or in dozens of small files. For the time when you make more mistakes then successes you may want to split your configuration into several files (however you want it to look - for example administration panel configuration, assets configuration, separate files for external services like Redis etc.). Do that! It will make configuration management much easier until you come out with quite stable structure.
You don't have to worry about performance - Symfony2 merges all files and dumps compiled configuration (entire DI container) into cache, so it doesn't matter how many files it needs to load after configuration is built for the first time (until it changes).
Anyway you will most likely move back to a single configuration file and just make your configuration customizable through parameters. But before you will find out what parts of it change often and what are the best options, digging through two 10-line files is much easier then messing with 20 lines on a single, hundred-line long file.
Positioned array elements in YAML
If you will use my local.yml recipe (or in any other case, when you will override existing configuration) you may sometimes want to override elements of the configuration that is an indexed array (list, not a hash). How to override just a third element of that array in that case? Well, in YAML you still can define numeric keys and since in PHP both indexed and associative arrays are just arrays it will easily work! Here is example of what I mean:
sonata_admin:
dashboard:
blocks:
2: { position: "left", type: "sonata.admin.block.admin_list" }
Secured routing part
For things that involve sensitive data, like sending user credentials for logging in or registration process it's wise to use HTTPS. Symfony2 router allows you to do that easily. But very quickly you can get lost by adding single requirement line to multiple routes (for example: if you use FOSUserBundle there are at least few routes you would want to enclose within HTTPS). My solution for that is to put all the routes that should be protected into separate router configuration file (let's call it routing-secured.yml) and take benefit from configuration cascading - simply import this file with requirement and all of routes contained in that file will automatically be affected by requirements from parent route set (importing one):
_secured:
resource: "routing-secured.yml"
requirements:
_scheme: "https"
Dependencies
Use annotations with caution
Annotations are a very convenient way for providing metadata and configuration for your services, components and whatever else you need. For example you can configure even DI services using JMSDiExtraBundle, you can configure routing and caching with SensioFrameworkExtraBundle. I use annotations wherever I can - it's always handy to have metadata close to the code, so you don't need to touch multiple files when you work on new stuff. But they are also tricky! Remember that for each annotation type you need handlers. For some kinds it's not a problem (for example Doctrine ORM is a handler for own annotations, so you can use them freely). But for most other kinds of annotations you usually need an additional bundle that will handle them. As long as you work on just your own application it's fine - you can use whatever you want.
Problems start when you work on some shared bundle (internal solution for your company) or even publishing it as open source. When someone wants to use your bundle he will also need to load all the bundles that handle the annotations you used. But that's not even the end of our problems - of course if you define dependencies in composer.json, Composer will download all necessary bundles, but the user may not look into that file and may not notice these dependencies: they are kind of implicit dependencies - your code can't load additional bundles for application, so you need to explain in details which extra bundles user needs to add to his kernel.
Symfony Components dependencies
Symfony2 world is much more then just a great framework. It is built on top of Symfony Components so the code developed for it can also work without the full-stack framework. In your bundle you probably not use all of the components, not even most of them - just a few. And your code often doesn't really rely on the framework code (FrameworkBundle etc.). If you want to create a shared bundle, try to list particular components in your composer.json file instead of relying on an entire symfony/symfony as a dependency - this will allow to use your code in projects that don't use the full Symfony2 framework stack and, additionally, it will make your development easier, since you will need less dependencies to be downloaded for example in your CI environment to run tests.
git submodules
Git itself has a very powerful dependencies subsystem called submodules. Of course Composer is a great tool, but it can't handle everything. For example: for me handling JavaScript dependencies of PHP projects is still far easier using git submodule then any Composer-based packages that wraps that. Also for libraries that don't provide Composer packages it's plain easy to just use:
git submodule add https://github.com/sstephenson/prototype.git vendor/prototype
No need for "faking" repositories in composer.json.
Strict dependencies vs. flexible dependencies
When defining dependencies in composer.json every time I need to investigate what versions of given library/bundle are available, which of them are stable, which contain features I use in my code… what's worse, a lot of projects don't use tags or tag very rarely. How to choose correct dependencies then? The thing is, that it depends on what you are doing.
If you're creating an end-user application, a final application, which code won't be re-used, is self-contained or will be run in a production environment etc. then you need maximum stability. You should put as strict constraints as possible - you must be sure that whenever you install your application it will run with tested versions of dependencies, with same libraries that you expected it to be built against:
- if dependency provides a stable tag that fulfils you needs - use it!;
- if it doesn't, and you need to use the dev- branch, you better stick to particular commit (dev-branch#commit) - this will make you sure that branch update won't accidentally ruin your build;
- if it doesn't, and even conflicts with your other dependencies, you can even use more nasty structure: dev-branch#commit as version.
Don't hesitate to use any other hacks to enforce Composer to use very particular commit you want. Not so long ago there was a real dependency hell if you tried to use FOSUserBundle, HWIOAuthBundle and SonataUserBundle together for example. Here is what I had in my composer.json (now they are provide compatible stable tags fortunately):
"friendsofsymfony/user-bundle": "dev-master#7efda22426c8aee51752d708485283d47496b709 as 1.3.1",
"sonata-project/user-bundle": "dev-master#25db098aaf210e458e29fcb381c4c9a2e22f15aa",
"hwi/oauth-bundle": "dev-master#93b73b46ed20b960a9b2a2a005bfcded1e58558d",
You can also add your indirect dependencies into composer.json to make sure you use the expected version (for example even if you don't use the SonataBlockBundle on your own, you might want to ensure some interface compatibility). Of course once your project is built it's good to keep composer.lock file, but it doesn't solve the problem, because whenever you will try to update dependencies your game will start from the beginning.
Ok, this is for applications. But such approach is, on the other hand, absolutely wrong when you develop a shared bundle. Shared bundles should be as flexible as possible. Don't limit your users - if only your bundle can work with different versions of dependent libraries mark that in composer.json. For example for Symfony, you should probably accept various 2.x versions (for instance ">=2.1,<2.4-dev"). You can even relax it more, to accept everything until Symfony3 is out: ~2.0. Whichever you decide to use, avoid strict constraints in shared bundles - even if you are unsure what will happen - don't restrict end-user: as long as it's just possible to download your dependency, your user can come out with even most dirty workarounds to make it running. Better have it running with dirty workarounds, then not running at all.
Project code organisation
Keep all views together
Since in Symfony2 everything is a bundle and views also belong to bundles, there are many possible locations for them. It's very useful for bundles developers to provide views resources together with their projects. Until you need to change them, you can use default views from vendor. But in your final application project you don't want to search multiple directories for views to adapt. Especially if you have some frontend developers that are not so aware of how your server-side application works, they just want to put HTML into your template. Thus better keep all your views in app/Resources/ directory.
Of course if you want to override vendor bundle view you have to use this directory, but this is not so obvious for your application bundles. When creating ApplicationAnyBundle you might think about putting views together with it, into src/Application/Bundle/AnyBundle/Resources/, but that will only bring chaos into your project. As the name says, Application*Bundles are obviously very specific for your application - not supposed to be re-used. It's totally fine to put their resources into app/Resources/ directory and this is where your application bundles view should be stored.
Deployment
To automate some tasks during deployment (or simply automate maintenance tasks) you can use Composer scripts or Symfony2 console commands (I mean - you can develop your own). SensioDistributionBundle is a great example. Remember that within your command you will have access to all DI services, all parameters and all external resources will be configured. If you need complex operations, that are handled already in your logic, write a command that will execute this logic. Don't waste time on complex build tools. A good example can be assets minification, distribution for static servers etc.
However you also need to remember that you can use console commands only for finalization tasks, not for initialization. Your Symfony2 application must be able to run in order to execute your command - you can't, for instance, use it to build your configuration, since it is needed to bootstrap your command.
3rd-party bundles
BerriartSitemapBundle
This is a great and easy-to-use sitemap generator bundle. Of course you have to read first how to provide URLs for sitemap, but it's plain easy. Populating the sitemap with URL is just a single command call:
app/console berriart:sitemap:populate
But there is one problem - the sitemap is not cleared before populating so before the next call you need to first clean the tables (you can simply enclose these commands within a make target for example):
app/console doctrine:query:sql "DELETE FROM berriart_sitemap_image_url"
app/console doctrine:query:sql "DELETE FROM berriart_sitemap_url"
app/console berriart:sitemap:populate
I hope to have more time soon to publish some more useful stuff.
Tags: Optimization, PHP, Production, Open Source, Symfony