Symfony2 Conceptual Issue: General Bundles Vs. Specific Ones

Symfony2 conceptual issue: general bundles vs. specific ones


The new approach

After several months since I wrote this answer, my approach has changed, so I'm sharing it with the community. This answer is still pretty popular and can lead newcomers to the approach I don't think is the best one anymore. So...

Now I have only one app specific bundle and I call it AppBundle. There were several problems with the old approach and here are some of them:

  • Creating a lot of bundles is tedious. You have to create a bundle class and a bunch of standard folders for each new bundle and then activate it and register its routes and DI and whatnot.

  • Unnecessary hardcore decision making process. Sometimes you just can't decide which bundle a particular thing belongs to because it's used by more than one bundle. And after you spend a half a day and finally make your hard decision on where to put it, you'll find that in a couple of days or weeks you won't be able to tell right away which bundle to look that thing in — because most of the times the decision wasn't based on pure logic and you had to choose based on a coin toss or whatever means you use to bring higher powers for help.

    I suggested using CommonBundle for common stuff in the past but doing that you'll have to do a lot of unnecessary refactorings moving a thing to and from CommonBundle based on how many or few bundles will use that thing later.

  • App specific bundles are interdependent anyway. When people meet the idea of bundles for the first time, one of the main thought that goes through their minds is something like “Yay! I'll have me a bunch of reusable bundles!” That idea is great and I have nothing against it; the problem is that app specific bundles are not that reusable anyway — there are interdependent. Forget about reuse in this case.

  • No idea where to put Behat features and step definitions. This problem is related to the previous ones: you have to repeat the same brainless motions for each bundle and then make hardcore decisions.

    When I started writing Behat features, I just couldn't decide where to put a lot of features and step definitions because they belonged to several bundles at a time. Putting them into CommonBundle seemed to be even worse, because that's the last bundle I would look for that stuff in. So, I ended up creating FeatureBundle for that.

Switching to a single bundle solved all these problems.

I've also seen some people having a separate bundle for, say, all the entities. I don't like this approach neither and actually suggest keeping entities and other non Symfony2 specific stuff out of the bundles.

Note again that this new approach applies to app specific bundles. Official docs and other places are full of great advice on how to structure bundles intended to be shared with others and reused across numerous projects. I write bundles of this type as well. But what I've found out after months of working on Symfony2 projects is that there is a difference between the bundles intended for reuse and the app specific ones — one approach doesn't fit all.

And, of course, when you see something reusable emerging in your app specific bundle, just extract it, put it in a separate repo and install as a vendor.

Also I've found myself using subnamespaces much more actively as a way to partition the bundle logically — instead of creating a bunch of bundles for that and going through all those troubles.

The old approach

There are no hard and fast rules or silver bullets, but I'll share my approach of doing things — maybe it will give you an insight or two.

First of all, I don't have two all-encompassing bundles like FrontendBundle and BackendBundle. Instead, my bundles have both frontend and backend controllers, views, etc. So, if I strip everything from my UserBundle except for controllers and views, its structure would look like this:

UserBundle
├── Controller
│ ├── Admin
│ │ └── UserController.php
│ └── UserController.php
├── Resources
│ └── views
│ ├── Admin
│ │ └── User
│ │ ├── add.html.twig
│ │ ├── delete.html.twig
│ │ ├── edit.html.twig
│ │ ├── form.html.twig
│ │ └── index.html.twig
│ └── User
│ ├── edit.html.twig
│ ├── sign-in.html.twig
│ ├── sign-up.html.twig
│ └── view.html.twig
└── UserBundle.php

Second, I have CommonBundle which I use for stuff shared by several bundles:

CommonBundle
├── Resources
│ ├── public
│ │ ├── css
│ │ │ ├── admin.css
│ │ │ ├── common.css
│ │ │ └── public.css
│ │ └── img
│ │ ├── add.png
│ │ ├── delete.png
│ │ ├── edit.png
│ │ ├── error.png
│ │ ├── return.png
│ │ ├── success.png
│ │ └── upload.png
│ └── views
│ ├── Admin
│ │ └── layout.html.twig
│ └── layout.html.twig
└── CommonBundle.php

My app/Resources/views/base.html.twig is almost the same as it comes with Symfony Standard distribution:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{ block('title') | striptags | raw }}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

Both CommonBundle/Resources/views/layout.html and CommonBundle/Resources/views/Admin/layout.html extend app/Resources/views/base.html.twig. Other bundles' templates extend one of these two layouts, depending on whether they are for frontend or backend. Basically, this is how I'm using the Three-level Inheritance approach.

So, I'd put your date displayer into CommonBundle. Depending on its complexity it could be just a template, a macro or a Twig extension.

Pagination is a common problem, so I suggest you to use one of the existing bundles instead of reinventing the wheel — if they suite your needs, of course.

And yes, it's perfectly okay to have bundles without controllers or views, etc.

Confused with symfony2 bundles

Symfony2 is bundle-based framework
=> So, everything, as well as the core itself is a bundle.

You can see which bundle is loaded by default in app/AppKernel.php.

But Symfony2 does also include a library, organized by "components (vendor/symfony/src/Symfony/Components). Code in bundle can use this library..

Should everything really be a bundle in Symfony 2.x?

I've written a more thorough and updated blog post on this topic: http://elnur.pro/symfony-without-bundles/


No, not everything has to be in a bundle. You could have a structure like this:

  • src/Vendor/Model — for models,
  • src/Vendor/Controller — for controllers,
  • src/Vendor/Service — for services,
  • src/Vendor/Bundle — for bundles, like src/Vendor/Bundle/AppBundle,
  • etc.

This way, you would put in the AppBundle only that stuff that is really Symfony2 specific. If you decide to switch to another framework later, you would get rid of the Bundle namespace and replace it with the chosen framework stuff.

Please note that what I'm suggesting here is for app specific code. For reusable bundles, I still suggest using the best practices.

Keeping entities out of bundles

To keep entities in src/Vendor/Model outside of any bundle, I've changed the doctrine section in config.yml from

doctrine:
# ...
orm:
# ...
auto_mapping: true

to

doctrine:
# ...
orm:
# ...
mappings:
model:
type: annotation
dir: %kernel.root_dir%/../src/Vendor/Model
prefix: Vendor\Model
alias: Model
is_bundle: false

Entities's names — to access from Doctrine repositories — begin with Model in this case, for example, Model:User.

You can use subnamespaces to group related entities together, for example, src/Vendor/User/Group.php. In this case, the entity's name is Model:User\Group.

Keeping controllers out of bundles

First, you need to tell JMSDiExtraBundle to scan the src folder for services by adding this to config.yml:

jms_di_extra:
locations:
directories: %kernel.root_dir%/../src

Then you define controllers as services and put them under the Controller namespace:

<?php
namespace Vendor\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\SecurityExtraBundle\Annotation\Secure;
use Elnur\AbstractControllerBundle\AbstractController;
use Vendor\Service\UserService;
use Vendor\Model\User;

/**
* @Service("user_controller", parent="elnur.controller.abstract")
* @Route(service="user_controller")
*/
class UserController extends AbstractController
{
/**
* @var UserService
*/
private $userService;

/**
* @InjectParams
*
* @param UserService $userService
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
}

/**
* @Route("/user/add", name="user.add")
* @Template
* @Secure("ROLE_ADMIN")
*
* @param Request $request
* @return array
*/
public function addAction(Request $request)
{
$user = new User;
$form = $this->formFactory->create('user', $user);

if ($request->getMethod() == 'POST') {
$form->bind($request);

if ($form->isValid()) {
$this->userService->save($user);
$request->getSession()->getFlashBag()->add('success', 'user.add.success');

return new RedirectResponse($this->router->generate('user.list'));
}
}

return ['form' => $form->createView()];
}

/**
* @Route("/user/profile", name="user.profile")
* @Template
* @Secure("ROLE_USER")
*
* @param Request $request
* @return array
*/
public function profileAction(Request $request)
{
$user = $this->getCurrentUser();
$form = $this->formFactory->create('user_profile', $user);

if ($request->getMethod() == 'POST') {
$form->bind($request);

if ($form->isValid()) {
$this->userService->save($user);
$request->getSession()->getFlashBag()->add('success', 'user.profile.edit.success');

return new RedirectResponse($this->router->generate('user.view', [
'username' => $user->getUsername()
]));
}
}

return [
'form' => $form->createView(),
'user' => $user
];
}
}

Note that I'm using my ElnurAbstractControllerBundle to simplify defining controllers as services.

The last thing left is to tell Symfony to look for templates without bundles. I do this by overriding the template guesser service, but since the approach is different between Symfony 2.0 and 2.1, I'm providing versions for both of them.

Overriding the Symfony 2.1+ template guesser

I've created a bundle that does that for you.

Overriding the Symfony 2.0 template listener

First, define the class:

<?php
namespace Vendor\Listener;

use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Sensio\Bundle\FrameworkExtraBundle\EventListener\TemplateListener as FrameworkExtraTemplateListener;
use JMS\DiExtraBundle\Annotation\Service;

class TemplateListener extends FrameworkExtraTemplateListener
{
/**
* @param array $controller
* @param Request $request
* @param string $engine
* @throws InvalidArgumentException
* @return TemplateReference
*/
public function guessTemplateName($controller, Request $request, $engine = 'twig')
{
if (!preg_match('/Controller\\\(.+)Controller$/', get_class($controller[0]), $matchController)) {
throw new InvalidArgumentException(sprintf('The "%s" class does not look like a controller class (it must be in a "Controller" sub-namespace and the class name must end with "Controller")', get_class($controller[0])));

}

if (!preg_match('/^(.+)Action$/', $controller[1], $matchAction)) {
throw new InvalidArgumentException(sprintf('The "%s" method does not look like an action method (it does not end with Action)', $controller[1]));
}

$bundle = $this->getBundleForClass(get_class($controller[0]));

return new TemplateReference(
$bundle ? $bundle->getName() : null,
$matchController[1],
$matchAction[1],
$request->getRequestFormat(),
$engine
);
}

/**
* @param string $class
* @return Bundle
*/
protected function getBundleForClass($class)
{
try {
return parent::getBundleForClass($class);
} catch (InvalidArgumentException $e) {
return null;
}
}
}

And then tell Symfony to use it by adding this to config.yml:

parameters:
jms_di_extra.template_listener.class: Vendor\Listener\TemplateListener

Using templates without bundles

Now, you can use templates out of bundles. Keep them under the app/Resources/views folder. For example, templates for those two actions from the example controller above are located in:

  • app/Resources/views/User/add.html.twig
  • app/Resources/views/User/profile.html.twig

When referring to a template, just omit the bundle part:

{% include ':Controller:view.html.twig' %}

For what would I make a bundle? (Symfony 2)

According to symfony documentation a bundle should be consistent and closed structure. So, if for example "store" and "gallery" are related in some way (eg. use the same model), then they should be in one bundle (AppBundle, CoreBundle, PlatformBundle - whatever you want). But if gallery is completely separate piece of code and can be easily join to another project - then you should consider exclude it to separate bundle.

I think a good idea is look at some projects on github and look how others handle this.

Where do you put your homepage in Symfony2 Project?

See these two questions and my answers to them:

  • Symfony2 conceptual issue: general bundles vs. specific ones
  • Should everything really be a bundle in Symfony 2?

TL;DR

Create one app-specific bundle called AppBundle to avoid the hardcore decision making process on where to put and then find an entity.

Symfony2 bundle understanding

Since you're speaking of different designs and functionality, I assume the two parts won't really have much in common - at least from a front-end point of view. What will be shared essentially sums up to DB access, etc. - which is pretty much global stuff.

So yup, I'd go with two bundles, it'll be even more instructive to you IMHO.



Related Topics



Leave a reply



Submit