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, likesrc/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' %}
Of course you can decouple your application. Just develop it as a library and integrate it into symfony vendor/
-folder (either by using the deps
or composer.json
, depending wether you use Symfony2.0 or Symfony2.1). However, you need at least one bundle, that acts as the "frontend" of your library, where Symfony2 finds the controller (and such).