the naked bundle - symfony live london 2014

Post on 27-Nov-2014

931 Views

Category:

Technology

4 Downloads

Preview:

Click to see full reader

DESCRIPTION

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.

TRANSCRIPT

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/framework­bundle ;)

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="UTF­8" ?><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<doctrine­mapping>

<entity name=”My\Bundle\Entity\Article”>    <id name="id" type="integer" column="id">        <generator strategy="AUTO"/>    </id>    <field name=”title” type=”string”></entity>    </doctrine­mapping>

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/doctrine­bundle": "~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('/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

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"        }    }}

Twig templates// in composer.json{    "extra": {        "resources": {            "/my­library/views": "src/MyLibrary/Views"        }    }}

// in the controllerreturn $this­>templating    ­>render('/my­library/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

top related