Symfony2 Bundle Structure, a use case

  • Thomas Botton

(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:  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: martinsikora.com/symfony2-and-dependency-injection / 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”.


Tell us what you think