The Naked Bundle - Symfony Live London 2014
-
Upload
matthiasnoback -
Category
Technology
-
view
931 -
download
4
description
Transcript of The Naked Bundle - Symfony Live London 2014
The Naked BundleMatthias Noback@matthiasnoback
What's it all about?
An actual naked bundle
I could've called it
BundleLitetm
The No Code Bundle
The Clean Bundle
But “naked” is catchy and controversial
The official view on bundles
First-class citizensDocumentation » The Quick Tour » The Architecture
Importance
Your code is more important than the framework,
which is an implementation detail
Reuse
Nice!
All your code lives in a bundleDocumentation » The Book » Creating Pages in Symfony2
Reuse
“All your code in a bundle” contradicts the promise of reuse
Everything lives inside a bundleDocumentation » Glossary
Not really true
Many things live inside libraries
(the Symfony components are libraries too!)
Which is good!
But you probably know that already
“libraries first”
What about...● Controllers
● Entities
● Templates
● ...
They just need to be in a bundle
Or do they?
Don't get me wrong
I love Symfony!
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
A framework is there for you
Your code doesn't need a framework
Noback's Principle
Code shouldn't rely on something
it doesn't truly need
Bundle conventions
Things in a bundle often rely on conventions to work
Conventions aren't necessary at all
So according to Noback's Principle,code shouldn't rely on bundle conventions too
Naming conventionsControllers:
● *Controller classes
● *action methods
Templates:
● in /Resources/views
● name: Controller/Action.html.twig
Structural conventionsController:
● Extends framework Controller class
● Is ContainerAware
Behavioral conventionsController:
● Is allowed to return an array
● Actions can type-hint to objects which will be fetched based on route parameters (??)
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){}
These conventions are what makes an application a Symfony2 application
A Year With Symfony
About bundles
A bundle exposes resources
Resources● Service definitions
● Controllers
● Routes
● Templates
● Entities
● Form types
● Event listeners
● Translations
● ...
No need for them to be inside a bundle
When placed outside the bundlethe resources could be reused separately
The bundle would be really small
And could just as well be a:Laravel package,
Zend or Drupal module,CakePHP plugin,
...
So the challenge is to
Make the bundle as clean as possible
Move the “misplaced” things to● a library
● with dependencies
● but not symfony/frameworkbundle ;)
Being realistic
Practical reusability
Reuse within the Symfony family
Think: Silex, Laravel, etc.
Allowed dependency
HttpFoundation● Request● Response
● Exceptions● etc.
What do we rely on
HttpKernelnamespace Symfony\Component\HttpKernel;
use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;
interface HttpKernelInterface{ /** * Handles a Request to convert it to a Response. */ public function handle(Request $request, ...);}
Why? My secret missions
“Let's rebuild the application, but this time we use Zend4 instead of Symfony2”
And of course
Education
You need a strong coupling radar
Explicit dependencies● Function calls
● Imported classes (“use”)
● Included files
● ...
Implicit dependencies● File locations
● File, class, method names
● Structure of return values
● ...
There we go!
use Symfony\Bundle\FrameworkBundle\Controller\Controller;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/** * @Route(“/article”) */class ArticleController extends Controller{
/** * @Route(“/edit”) * @Template() */function editAction(...){
...}
}
Controller
TODO✔ Don't rely on things that may not
be there in another context:✔ Parent Controller class
✔ Routing, template, annotations, etc.
class ArticleController{
function editAction(...){
...}
}
Nice and clean ;)
use Symfony\Component\HttpFoundation\Request;
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
TODO✔ Inject dependencies
✔ Don't use helper methods
✔ Render the template manually
✔ Keep using Request (not really a TODO)
use Doctrine\ORM\EntityManager;
class ArticleController{
function __construct(EntityManager $em,
) {$this>em = $em;
}
...}
Inject dependencies
Inline helper methodsuse Symfony\Component\HttpKernel\Exception\NotFoundHttpException
class ArticleController{ ...
public function newAction(...) { ... throw new NotFoundHttpException(); ... }}
use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Templating\EngineInterface;
class ArticleController{
function __construct(..., EngineInterface $templating) {}
public function newAction(...){
...return new Response(
$this>templating>render('@MyBundle:Article:new.html.twig',array(
'form' => $form>createView())
));
}}
Render the template manually
Dependencies are explicit now
use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Templating\EngineInterface;use Doctrine\ORM\EntityManager;
Also: no mention of a “bundle” anywhere!
Dependency overflowuse Symfony\Component\HttpFoundation\Response;use Symfony\Component\Templating\EngineInterface;
class ArticleController{
function __construct(EntityManager $entityManager,EngineInterface $templating,TranslatorInterface $translator,ValidatorInterface $validator,Swift_Mailer $mailer,RouterInterface $router
) {...
}
...}
The cause?
Convention
One controller, many actions
one action!
__invoke()
namespace MyLibrary\Controller\Article;
class New{
function __construct(...) {
// only what's necessary for this “action”}
public function __invoke(...){
...}
}
Nice!
└─Controller └─Article ├─New.php ├─Edit.php ├─Archive.php └─Delete.php
TODO✔ Set up routing
✔ Create a service and provide the right arguments
Bundle stuff: services.xml<! in MyBundle/Resources/config/services.xml →
<?xml version="1.0" ?><container><services>
<service id="new_article_controller" class="MyBundle\Controller\Article\New"> <argument type="service" id="doctrine.orm.default_entity_manager" /> <argument type="service" id="templating" /></service>
</services></container>
Bundle stuff: routing.xml<! in MyBundle/Resources/config/routing.xml →
<?xml version="1.0" encoding="UTF8" ?><routes>
<route id="new_article" path="/article/new"> <default key="_controller"> new_article_controller:__invoke </default></route>
</routes>
Pull request by Kevin Bond allows you to leave out the “:__invoke” part!
Controller – Achievements● Can be anywhere
● No need to follow naming conventions (“*Controller”, “*action”)
● Dependency injection, no service location
● Reusable in any application using HttpFoundation
Next up: Entities
namespace My\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class Article{ ...
/** * @ORM\Column(type=”string”) */ private $title;}
Entity conventions
What's wrong with annotations?
namespace My\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class Article{ ...
/** * @ORM\Column(type=”string”) */ private $title;}
Annotations are classes
use Doctrine\Common\Annotations\AnnotationReader;
$reader = new AnnotationReader();
$class = new \ReflectionClass('Article');
$reader>getClassAnnotations($class);
BANG
Class Doctrine\ORM\Mapping\Column
not found
Well, uhm, yes, but...
Are you ever going to use anything else than Doctrine ORM?
Well...Think about Doctrine MongoDB ODM, Doctrine CouchDB ODM, etc.namespace My\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;use Doctrine\ODM\CouchDB\Mapping\Annotations as CoucheDB;
class Article{ /** * @ORM\Column * @MognoDB\Field * @CoucheDB\Field */ private $title;}
TODO✔ Remove annotations
✔ Find another way to map the data
namespace My\Bundle\Entity;
class Article{ private $id; private $title;}
Nice and clean
A true POPO, the ideal of the data mapper pattern
Use XML for mapping metadata<doctrinemapping>
<entity name=”My\Bundle\Entity\Article”> <id name="id" type="integer" column="id"> <generator strategy="AUTO"/> </id> <field name=”title” type=”string”></entity> </doctrinemapping>
Conventions for XML metadata● For MyBundle\Entity\Article
● Put XML here: @MyBundle/Resources/config/doctrine/ Article.orm.xml
We don't want it in the bundle!There's a nice little trick
You need DoctrineBundle >=1.2
{ "require": { ..., "doctrine/doctrinebundle": "~1.2@dev" }}
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ 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 = 'MyLibrary\Model';
return DoctrineOrmMappingsPass::createXmlMappingDriver( array($xmlPath => $namespacePrefix) ); }}
Now:● For MyLibrary\Model\Article
● Put XML here: src/MyLibrary/Doctrine/Article.orm.xml
Entities - Achievements● Entity classes can be anywhere● Mapping metadata can be
anywhere and in different formats● Entities are true POPOs
Finally: Templates
Conventions● In /Resources/views/[Controller]
● Filename: [Action].[format].[engine]
The difficulty with templatesThey 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” %}
Still, we want them out!
And it's possible
# in config.ymltwig: ... paths: "%kernel.root_dir%/../src/MyLibrary/Views": MyLibrary
Twig namespacesDocumentation » The Cookbook » Templating » How to use and Register namespaced Twig Paths
// in the controllerreturn $this>templating>render('@MyLibrary/Template.html.twig');
Get rid of absolute paths
Using Puli, created by Bernhard Schüssek (Symfony Forms, Validation)
What Puli does
Find the absolute paths of resources in a project
use Webmozart\Puli\Repository\ResourceRepository;
$repo = new ResourceRepository();$repo>add('/mylibrary/views', '/absolute/path/to/views/*');
/my-library/views /index.html.twig
/absolute/path/to/views /index.html.twig
echo $repo>get('/mylibrary/views/index.html.twig')>getRealPath();
// => /absolute/path/to/views/index.html.twig
Register “prefixes”
Manually, or using the Puli Composer plugin
// in the composer.json file of a package or project{ "extra": { "resources": { "/mylibrary/views": "src/MyLibrary/Views" } }}
Twig templates// in composer.json{ "extra": { "resources": { "/mylibrary/views": "src/MyLibrary/Views" } }}
// in the controllerreturn $this>templating >render('/mylibrary/views/index.html.twig');
Puli Twig extension
Many possibilities● Templates
● Translation files
● Mapping metadata
● Service definitions
● And so on!
The future is bright● Puli is not stable yet
● But I expect much from it:
Puli will be the ultimate tool
to create NAKED BUNDLES
and to enable reuse of many kinds of resources
not limited byproject,
framework,even language
boundaries!
But even without Puli
There's a whole lot you can do to make your code not rely on the framework
Remember
The framework is for youYour code doesn't need it
Questions?
Get a $7,50 discount:http://leanpub.com/a-year-with-symfony/c/SymfonyLiveLondon2014
Get a $10,00 introduction discount:http://leanpub.com/principles-of-php-package-design/c/SymfonyLiveLondon2014
Thank you
Feedback: joind.in/11553
Talk to me: @matthiasnoback
Images
Sally MacKenzie: www.amazon.com