Taming monolithic Symfony Bundles

posted 25.03.2016

Bundles are the basic building block of every Symfony app. With a recommended and adopted directory structure and auto-config, you can get loads of new features into your app just by adding one line to composer.json and another to your AppKernel.

You wonder how the creators of the bundles create such components? Sticking to a few rules will get you most of the way there. The rest is up to your coding skills!

One Bundle to rule them all

Let me start with the typical pattern I see in small to mid-size Symfony apps.

src
└── SymfonyApp
    ├── CoreBundle
    ├── ApiBundle
    └── FrontBundle

You see an attempt to separate the code into multiple bundles, to separate the concerns each one of them have. The dependency graph of the Bundle is intended to look like this:

       +---------+       
    +-->  Core   <--+    
    |  +---------+  |    
    |               |    
 +--+----+     +----+--+ 
 |  API  |     | Front | 
 +-------+     +-------+ 

You have two bundles, one for the REST API and a second one for the rendered front-end site. The idea is that the common logic sits in a core and you can only require classes from Core in API and Front, but not from each other.

Not rarely, this grows out of control and the following happens.

      +---------+
   +-->  Core   <--+
   |  +---------+  |
   |               |
+--v----+     +----+--+
|  API  <-----+ Front |
+-------+     +-------+

Suddenly, in the API Bundle you have this convenient class which one time you include in Core and call it a hack. Then you do the same for the Frontend bundle. During the next few iterations you do it a few more times and everything starts to depend on each other. Then you just give up and put everything into a single Bundle.

+-------+
|  App  |
+-------+

Hey! That’s exactly what Symfony best practices recommend. So it must not be a bad thing? It’s ok and it’s a pragmatic approach and if you keep your code clean otherwise, you can create a maintainable app.

Microbundles for re-usable components

If you think you can do better than having one bundle for everything, you’re right. Let’s explore two simple possibilites.

The first one is actually similar to what was above. Split the app into multiple, more focused bundles. With one subtle but very important difference.

The example above tried to split the app into a few equally-sized pieces. Instead, cut off small pieces. It’s much easier to create these miniature bundles with only one small task. You will probably not use the full directory structure, you might not have any controllers, you typically won’t have any templates.

Rules:

  • the bundle has one task only
  • the bundle cannot require code from any other bundle
  • that means it could be extracted to another repo at any time
  • be careful about hidden dependencies when using parameters from the global configuration like %environment%, always expose semantic configuration

To give an example, this is the smallest Bundle of one of our projects:

TimeBundle/
├── DependencyInjection
│   ├── Configuration.php
│   └── TimeExtension.php
├── EventSubscriber
│   └── KernelDefaultTimezoneEventSubscriber.php
├── TimeBundle.php
└── Resources
    └── config
        └── services.yml

The sole and only task of this bundle is to get a timezone in config.yml, put it as a parameter to the event subscriber listening to kernel.request and set the appropriate timezone for the whole script execution. Nothing is too small to be extracted. It’s actually quite the opposite, the smaller the component, the easier to extract.

Bridge Bundles, providing glue code for a standalone library

The second approach is to move away from Symfony for the bulk of your code. Sounds weird? If you look at popular Symfony bundles, you’ll see they do not contain the feature themselves, e.g.:

Each of the bundle repositories just contains the necessary bridging to Symfony like Event Listeners, Service definitions, semantic configuration etc.

And with in another framework you wouldn’t use the bundle but just directly use the library without any Symfony bindings.

You can do exactly the same with your code. Often, there is no reason for your code to be tied to Symfony specifics. It could be put into a ZF2 app and called from any other PHP framework. If you then want to make the code auto-wired and enhanced by Symfony features, write a bridging Bundle which makes both ends meet.