The Bundle system is one of the greatest and most powerful features of Symfony2. Bundles contain all the files related to a single feature of your application: controllers, entities, event listeners, form types, Twig templates, etc. But how much of that actually needs to be inside a bundle?
In this talk we’ll take a bundle, containing all those different types of classes, configuration files and templates, and strip it down to the bare necessities. And I promise that after moving many files out of the bundle, everything still works.
While looking for ways to move things out of the bundle, I will discuss some of the more advanced features of bundle design, like prepending configuration, compiler passes and Doctrine mapping drivers. We will end with a very lean bundle, surrounded by a few highly reusable, maximally decoupled libraries.
22. But a framework is just a framework
● Quickstarter for your projects
● Prevents and solves big security
issues for you
● Has a community you can rely on
31. Behavioral conventions
Controller:
● Is allowed to return an array
● Actions can type-hint to objects which will be
fetched based on route parameters (??)
32. Configuration conventions
Use lots of annotations!
/**
* @Route("/{id}")
* @Method("GET")
* @ParamConverter("post", class="SensioBlogBundle:Post")
* @Template("SensioBlogBundle:Annot:show.html.twig")
* @Cache(smaxage="15", lastmodified="post.getUpdatedAt()")
* @Security("has_role('ROLE_ADMIN')")
*/
public function showAction(Post $post)
{}
47. What do we rely on
HttpKernel
namespace SymfonyComponentHttpKernel;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
interface HttpKernelInterface
{
/**
* Handles a Request to convert it to a Response.
*/
public function handle(Request $request, ...);
}
48. Why? My secret missions
“Let's rebuild the application, but
this time we use Zend4 instead of
Symfony2”
57. use SymfonyComponentHttpFoundationRequest;
class ArticleController
{
public function editAction(Request $request)
{
$em = $this->get('doctrine')->getManager();
...
if (...) {
throw $this->createNotFoundException();
}
...
return array(
'form' => $form->createView()
);
}
}
Zooming in a bit
58. TODO
✔ Inject dependencies
✔ Don't use helper methods
✔ Render the template manually
✔ Keep using Request
(not really a TODO)
59. use DoctrineORMEntityManager;
class ArticleController
{
function __construct(
EntityManager $em,
) {
$this->em = $em;
}
...
}
Inject dependencies
60. Inline helper methods
use SymfonyComponentHttpKernelExceptionNotFoundHttpException
class ArticleController
{
...
public function editAction(...)
{
...
throw new NotFoundHttpException();
...
}
}
61. use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentTemplatingEngineInterface;
class ArticleController
{
function __construct(..., EngineInterface $templating)
{
...
}
public function editAction(...)
{
...
return new Response(
$this->templating->render(
'@MyBundle:Article:edit.html.twig',
array(
'form' => $form->createView()
)
)
);
}
}
Render the template manually
62. Dependencies are explicit now
Also: no mention of a “bundle”
anywhere!
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentTemplatingEngineInterface;
use DoctrineORMEntityManager;
63. Dependency overflow
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentTemplatingEngineInterface;
class ArticleController
{
function __construct(
EntityManager $entityManager,
EngineInterface $templating,
TranslatorInterface $translator,
ValidatorInterface $validator,
Swift_Mailer $mailer,
RouterInterface $router
) {
...
}
...
}
70. Bundle stuff: routing.xml
<!-- in MyBundle/Resources/config/routing.xml →
<?xml version="1.0" encoding="UTF-8" ?>
<routes>
<route id="edit_article"
path="/article/edit/{id}">
<default key="_controller">
edit_article_controller:__invoke
</default>
</route>
</routes>
Pull request by Kevin Bond allows you to leave out the “:__invoke” part!
71. Controller – Achievements
● Can be anywhere
● No need to follow naming conventions
(“*Controller”, “*action”)
● Dependency injection, no service location
● Reusable in any application using
HttpFoundation
78. … are you ever going to use
anything else than Doctrine ORM?”
79. Well...
Think about Doctrine MongoDB ODM,
Doctrine CouchDB ODM, etc.
namespace MyBundleEntity;
use DoctrineORMMapping as ORM;
use DoctrineODMMongoDBMappingAnnotations as MongoDB;
use DoctrineODMCouchDBMappingAnnotations as CoucheDB;
class Article
{
/**
* @ORMColumn
* @MognoDBField
* @CoucheDBField
*/
private $title;
}
80. TODO
✔ Remove annotations
✔ Find another way to map the data
81. namespace MyBundleEntity;
class Article
{
private $id;
private $title;
}
Nice and clean
A true POPO, the ideal of the data
mapper pattern
82. Use XML for mapping metadata
<doctrine-mapping>
<entity name=”MyBundleEntityArticle”>
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name=”title” type=”string”>
</entity>
</doctrine-mapping>
83. Conventions for XML metadata
● For MyBundleEntityArticle
● Put XML here:
@MyBundle/Resources/config/doctrine/
Article.orm.xml
84. We don't want it in the bundle!
There's a nice little trick
85. You need DoctrineBundle >=1.3
{
"require": {
...,
"doctrine/doctrine-bundle": "~1.3@dev"
}
}
86. use DoctrineBundleDoctrineBundleDependencyInjectionCompiler
DoctrineOrmMappingsPass;
class MyBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(
$this->buildMappingCompilerPass()
);
}
private function buildMappingCompilerPass()
{
$xmlPath = '%kernel.root_dir%/../src/MyLibrary/Doctrine';
$namespacePrefix = 'MyLibraryModel';
return DoctrineOrmMappingsPass::createXmlMappingDriver(
array($xmlPath => $namespacePrefix)
);
}
}
87. Now:
● For MyLibraryModelArticle
● Put XML here:
src/MyLibrary/Doctrine/Article.orm.xml
88. Entities - Achievements
● Entity classes can be anywhere
●Mapping metadata can be
anywhere and in different formats
● Entities are true POPOs
90. Conventions
● In /Resources/views/[Controller]
● Filename: [Action].[format].[engine]
91. The difficulty with templates
They can have all kinds of implicit
dependencies:
● global variables, e.g. {{ app.request }}
● functions, e.g. {{ path(...) }}
● parent templates, e.g. {% extends
“::base.html.twig” %}
93. Documentation » The Cookbook » Templating »
How to use and Register namespaced Twig Paths
# in config.yml
twig:
...
paths:
Twig namespaces
"%kernel.root_dir%/../src/MyLibrary/Views": MyLibrary
// in the controller
return $this->templating->render('@MyLibrary/Template.html.twig');
94. Get rid of absolute paths
Using Puli, created by Bernhard
Schüssek (Symfony Forms,
Validation)
95. What Puli does
Find the absolute paths of
resources in a project
96. use WebmozartPuliRepositoryResourceRepository;
$repo = new ResourceRepository();
$repo->add('/my-library/views', '/absolute/path/to/views/*');
/my-library/views /index.html.twig
/absolute/path/to/views /index.html.twig
echo $repo
->get('/my-library/views/index.html.twig')
->getRealPath();
// => /absolute/path/to/views/index.html.twig
97. Register “prefixes”
Manually, or using the
Puli Composer plugin
// in the composer.json file of a package or project
{
"extra": {
"resources": {
"/my-library/views": "src/MyLibrary/Views"
}
}
}
98. Twig templates
// in composer.json
{
"extra": {
"resources": {
"/my-library/views": "src/MyLibrary/Views"
}
}
}
Puli Twig extension
// in the controller
return $this->templating
->render('/my-library/views/index.html.twig');
99. Many possibilities
● Templates
● Translation files
●Mapping metadata
● Service definitions
● And so on!
100. The future is bright
● Puli is not stable yet
● But I expect much from it: