Blog Posts

Symfony2 Bundle Structure, a use case

(Written by Thomas Botton and Patrick Zahnd)

Symfony2 was released as Beta 1 few weeks ago. Since it starts to be our main PHP framework here at Liip, we decided to dive deeper into the core component of Symfony2: the bundles. Indeed, the structure of it needs to be clearly understood in order to have maintainable and sustainable code.

This blogpost aims to propose a project that one can take as a real life example for building its own bundle set with a correct structure - at least avoiding the main common mistakes.

In general, a bundle is meant to be a standalone component that can be reused and implemented by anyone in their project - as nobody wants to reinvent the wheel.
Bundles can have controllers, entities, forms, views, etc... To prevent these components to end up in a mess, Symfony2 comes with some rules and architecture to follow.

We recommend reading the Symfony2 "Bundle Directory Structure" post before starting at this point: http://symfony.com/doc/2.0/book/page_creation.html#bundle-directory-structure


Full bundle structure

Liip/
    EventBundle/
        EventBundle.php
        Command/
            AddEventCommand.php
        Controller/
            Backend/
                ConcertController.php
            Frontend/
                ConcertController.php
        DependencyInjection/
            ConcertExtension.php
        Document/
            Registration.php
        Entity/
            Concert.php
            ConcertManager.php
        Form/
            ConcertForm.php
            RegistrationForm.php
        Resources/
            meta/
                LICENSE
            config/
                config.xml
                doctrine/
                    metadata/
                        orm/
                            Liip.EventBundle.Entity.Concert.dcm.xml
                routing.xml
                validation.xml
            doc/
                index.rst
            translations/
                messages.en.xliff
            views/
                Backend/
                    concert_new.html.twig
                Frontend/
                    concert_show.html.twig
            public/
                css/
                    main.css
                js/
                    form.js
        Tests/
            Controller/
                Backend/
                    ConcertControllerTest.php
                Frontend/
                    ConcertControllerTest.php

/Command

Used to expose commands to the console (app/console).

/Controller

The place you put your controllers. You should separate Frontend and Backend controllers into subdirectories - if you have both - in order to have it more readable.

/DependencyInjection

Here you can load your own services by creating a so-called Extension.

/Document

Mapping for ODM objects. For example Contact Messages which you send as an email.

/Entity

Mappings for your ORM objects.

/Form

Form definitions.

/Resources

Configuration of your project.

/Resources/config

XML (or YML, or PHP) configuration files, like the routing.xml for example. Default is to use XML, and you have to use XML in Bundles you'd like to commit.

/Resources/views

The templates in the format you prefer (PHP, Twig, ...).

/Resources/translations

Translations file in XLIFF format.

/Resources/public

Public files like images, CSS stylesheets and Javascript files.

/Tests

Bundle functionality tests.


Sample project

As an example, we decided to go with a web application handling various music events and the user comments.
The following shows how to translate/divide a simple feature list within a set of bundles.

  • Events
    • Frontend
    • Event list
    • Search event
    • User can register for event
    • Backend
    • CRUD event over an admin interface and on the console
  • Visitor Echoes
    • Users can write a comment
  • Contact form
  • Pages displaying static content

Mockup:


Described in bundles

  • EventBundle
  • EchoBundle
  • ContactBundle
  • StaticBundle


Event bundle

The EventBundle encloses

  • commands to access the backend function
  • frontend and backend controllers
  • entity for the concerts
  • form to add concerts (backend)
  • form to register for an event (frontend)

Structure

Liip/ 
    EventBundle/ 
        EventBundle.php 
        Command/ 
            AddEventCommand.php 
        Controller/ 
            Backend/ 
                ConcertController.php 
            Frontend/ 
                ConcertController.php 
        DependencyInjection/ 
            ConcertExtension.php 
        Document/ 
            Registration.php 
        Entity/ 
            Concert.php 
            ConcertManager.php 
        Form/ 
            ConcertForm.php 
            RegistrationForm.php 
        Resources/ 
            meta/ 
                LICENSE 
            config/ 
                config.xml 
                doctrine/ 
                    metadata/ 
                        orm/ 
                            Liip.EventBundle.Entity.Concert.dcm.xml 
                routing.xml 
                validation.xml 
            doc/ 
                index.rst 
            translations/ 
                messages.en.xliff 
            views/ 
                Backend/ 
                    concert_new.html.twig 
                Frontend/ 
                    concert_show.html.twig 
            public/ 
                css/ 
                    main.css 
                js/ 
                    form.js 
        Tests/ 
            Controller/ 
                Backend/ 
                    ConcertControllerTest.php 
                Frontend/ 
                    ConcertControllerTest.php 

EchoBundle

The EchoBundle encloses

  • frontend controller
  • entity for the echoes
  • form to add an echo

Structure

Liip/ 
    EchoBundle/ 
        EchoBundle.php 
        Controller/ 
            FrontendController.php 
        Entity/ 
            Echo.php 
            EchoManager.php 
        Form/ 
            EchoForm.php 
        Resources/ 
            meta/ 
                LICENSE 
            config/ 
                doctrine/ 
                    metadata/ 
                        orm/ 
                            Liip.EchoBundle.Entity.Echo.dcm.xml 
                routing.xml 
            doc/ 
                index.rst 
            translations/ 
                messages.en.xliff 
            views/ 
                Frontend/ 
                    echo_new.html.twig 
                    echo_index.html.twig 
            public/ 
                css/ 
                    main.css 
                js/ 
                    form.js 
        Tests/ 
            Controller/ 
                FrontendControllerTest.php

ContactBundle

The ContactBundle encloses

  • a frontend controller
  • contact form

Structure

Liip/ 
    ContactBundle/ 
        ContactBundle.php 
        Controller/ 
            FrontendController.php 
        Document/ 
            Message.php 
        Form/ 
            ContactForm.php 
        Resources/ 
            meta/ 
                LICENSE 
            config/ 
                routing.xml 
            doc/ 
                index.rst 
            views/ 
                Frontend/ 
                    contact_form.html.twig 
            public/ 
                css/ 
                    main.css 
                js/ 
                    form.js 
        Tests/ 
            Controller/ 
                FrontendControllerTest.php

StaticBundle

You should not use this approach in production, this is just for example

The StaticBundle only encloses a frontend controller which will be responsible to take a page name as routing argument and then to display the corresponding template. If none is matching then 404 is displayed.

Structure

Liip/ 
    StaticBundle/ 
        StaticBundle.php 
        Controller/ 
            PageController.php 
        Resources/ 
            meta/ 
                LICENSE 
            config/ 
                routing.xml 
            doc/ 
                index.rst 
            views/ 
                Frontend/ 
                    static_home.html.twig 
                    static_about.html.twig 
            public/ 
                css/ 
                    main.css 
                js/ 
                    fancy.js 
        Tests/ 
            Controller/ 
                PageControllerTest.php


CONCLUSION

From this small case study, we can see some "bundle properties" drawing up which can be pick up depending on the bundle you are building:

  • Backend/Frontend differentiation: this structure allows you the get more readability and help you find faster what you want to work on, depending on if it is front or backend. This splitting usually occurs to the following directories: /Controller, /Ressources/views and /Tests
  • DB interaction based bundle (i.e. Entity/Document and Forms): that is the case for a lot of bundles which rely on one or more entities needing forms to interact with (create, modify or delete)
  • Customizable/Dependency Injection: this is the place where you can handle your own services. Therefore you have to create a so-called Extension. To understand that, you might want to read the following two blog posts: http://www.martinsikora.com/symfony2-and-dependency-injection / http://fabien.potencier.org/article/11/what-is-dependency-injection

To sum up, we could say that the bundles' goal is to split feature/functionality in order to be reusable as much as possible. If you end up with only one bundle for your web application, you might be wrong. Just get your head back from code and think: it must be a possibility to break your "SiteBundle" into at least 2 bundles. Else it means that you are having a static website, then you should go with a "StaticBundle".

About the author

Comments [14]

John Kary, 19.05.2011 12:42 CET

You advocate against a StaticBundle in production. So what would be your suggested approach when something like that is in fact needed?

Tamer Ibrahim, 19.05.2011 14:50 CET

Very important article.#Bookmarked#
Thank you, guys u saved my day.

patrick.zahnd, 19.05.2011 17:26 CET

@John

You should at least do an abstraction for the pages. For example:

[Bundle directory]/Document/Page.php
[Bundle directory]/Document/PageManager.php

- The page defines the fields (here just $content).
- The page manager has the methods to get the content (or throw the exception).

You should also not save the pages as template files in the views directory.

I hope this was helpful.

Ivo Azirjans, 13.12.2011 08:54 CET

How do you render or configure actions in routing, when they are placed in a subdirectory?

Ivo Azirjans, 13.12.2011 09:09 CET

Found it, just use the backslash, like so:
EventBundle:Backend\Concert:index

Stephen Prytherch, 15.12.2011 21:23 CET

I'm working with a neighbor who runs GMCSVT. They provide event services and currently have some asp pages to help them.

It's a similar set of needs to what you have here. I started some prototyping in 2.0 and then went back to 1.4 which I've been using for the last year. Have you got much further with this project and would you consider sharing some of your findings/code? I been playing with an event Datatable showing the upcoming events and am working on a Calendar view just now..

Thomas Botton, 16.12.2011 10:06 CET

@stephen

Unfortunately we didn't (yet?) wrote such an application in Symfony2.
We took this use case for having real world example to base our blogpost on.

Nevertheless, the Symfony2 project has great documentation that we advice you to read through [1].
It should provide you most of the answers you need to complete your project.

We wish you all the best for your further development. Don't hesitate to keep us updated with your results.

[1]- http://symfony.com/doc/current/book/index.html

Kuba, 17.10.2012 19:56 CET

I'm newbie in Symfony2. I'm curious where to put classes like for example Cart in Webstore implementation. Can anyone dispel my doubts?

patrick.zahnd, 17.10.2012 22:42 CET

Hello @Kuba

Since "Cart" would probably be an entity, it goes into the "Entity" or "Document" folder in your Bundle.

If you have helper classes, then you can create separate folders within your Bundle to structure them. Therefore you can take the "Util" folder in the UserBundle as an example: https://github.com/paza/UserBundle/tree/master/Util

Zawla, 17.02.2013 14:48 CET

Thanks for this helpful post !

I'd like to know how you manage Documents here : Registration.php and Message.php ? Are they message templates (writen in PHP) or you stock it in no-sql db (like mongoDB) ?

Thanks ;-)

patrick.zahnd, 17.02.2013 15:02 CET

Hello @zawla

I am happy to hear our blog post was helpful. I guess this totally depends on the amount of signups you expect:

Only some per week: I would just send an email in this case - which you can format using a twig template. I would send it to myself/my client and a confirmation to the person who signs up.

Several every day: I would either save the message fileds into a SQL database or the formatted message into a nosql database. I would not send a message to myself/my client anymore but rather create a backend where you can find and manage the signups.

Lots of mails per day: I would probably add a queue system, which handles the sending of the signup mails.

Zawla, 17.02.2013 16:47 CET

Thank you @Patrick for your advise

Yes it's helpful because the most tutorials I've read talk about ONE bundle (in this case we don't take advantage of using symfony2 framework!)

What I don't understand in this use case is the aim, reason behind adding such directory "Document" if we can format the Message using a twig template...

patrick.zahnd, 17.02.2013 19:42 CET

You are welcome @Zawla ;)

The aim of having the Document directory is, that you
- define what fields you need in the given "Documents",
- define the field formats
- define the field validation
- and can generate a form from it directly.
Also it makes it easier to extend. If you for example want to save the given values to the database you can just extend the Document.

The folder you do not really have to name "Document", this is just a proposal.

Can Berkol, 21.05.2013 06:53 CET

Great article. We were considering to switch Symfony2; and now I feel more confident about some important aspects & setups. Thanks.

Add a comment

Your email adress will never be published. Comment spam will be deleted!