High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

142
The Naked Bundle Matthias Noback

description

Slides for my talk "High Quality Symfony Bundles" tutorial at the Dutch PHP Conference 2014 (http://phpconference.nl).

Transcript of High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Page 1: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

The Naked BundleMatthias Noback

Page 2: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014
Page 3: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Assuming you all have aworking project

https://github.com/matthiasnoback/high-quality-bundles-project

Page 4: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Generate a bundleUse app/console generate:bundle

Namespace: Dpc/Bundle/TutorialBundleBundle name: DpcTutorialBundleConfiguration: ymlWhole directory structure: yes

Page 5: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

The full directory structure of a bundle:

Page 6: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

What's wrong?Too many commentsRouting and a controllerTranslationsTwig templatesA useless test

Page 7: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

You are not going to use it all,but it will be committed!

Page 8: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Before we continue, clean up yourbundle

Remove the following files and directories:ControllerResources/docResources/publicResources/translationsResources/viewsTests

Also remove any superfluous comments!

Page 9: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

The officialview onbundles

Page 10: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

First-class citizens

Documentation » The Quick Tour » The Architecture

Page 11: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

I think your code is more important than the framework,which should be considered an implementation detail.

Page 12: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

All your code lives in abundle

Documentation » The Book » Creating Pages in Symfony2

Page 13: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

I don't think that's a good idea.It contradicts the promise of reuse of "pre-built feature

packages".

Page 14: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Almost everything livesinside a bundle

Documentation » Glossary

Page 15: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Which is not really true, because many things live insidelibraries (e.g. the Symfony components), which is good.

Page 16: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Best practicesDocumentation » Cookbook » Bundles

Page 17: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Controllers

Controllers don't need to extend anything at all.ContainerAware* should be avoided in all cases.

Page 18: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Tests

What's up with the 95%?

Page 19: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Twig

Why Twig? I though Symfony didn't care about this.

Documentation » The Book » Creating and Using Templates

Page 20: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

The old view on bundles isnot sufficient anymorePeople are reimplementing things because existing

solutions are too tightly coupled to a framework (or even aspecific version).

Why is it necessary to do all these things again for Symfony,Laravel, Zend, CodeIgniter, CakePHP, etc.?

Page 21: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Last year I started workingon this

Page 22: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Then it became this

Page 23: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

About bundles

Page 24: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

A bundle is...A thin layer of Framework-specific

configuration to make resources from somelibrary available in a Symfony2 application.

Page 25: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

A "Symfony application"meaning:

A project that depends on the Symfony FrameworkBundle.

Page 26: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Resources areRoutes (Symfony Routing Component)Services (Symfony DependencyInjection Component)Templates (Twig)Form types (Symfony Form Component)Mapping metadata (Doctrine ORM, MongoDB ODM, etc.)Translations (Symfony Translation Component)Commands (Symfony Console Component)...?

Page 27: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

So: a bundle is mainly configuration to make these resourcesavailable, the rest is elsewhere in a library.

Page 28: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

I also wrote

Page 29: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

The challengeMake the bundle as clean as possible

Page 30: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Entities

Page 31: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create an entityUse app/console doctrine:generate:entity

Specs

The entity shortcut name: DpcTutorialBundle:Post.Configuration format: annotationIt has a title (string) field.Run app/console doctrine:schema:create orupdate --force and make sure your entity has acorresponding table in your database.

Page 32: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Let's say you've modelled the Postentity very well

You may want to reuse this in other projects.Yet it's only useful if that project uses Doctrine ORM too!

Page 33: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Why?Annotations couple the Post class to Doctrine ORM.

(Since annotations are classes!)

Page 34: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Also: why are my entities inside abundle?

They are not only useful inside a Symfony project.

Page 35: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Move the entity to anothernamespace

E.g. Dpc\Tutorial\Model\Post.

Page 36: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create an XML mapping fileE.g. Dpc\Tutorial\Model\Mapping\Post.orm.xml<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> <entity name="Dpc\Tutorial\Model\Post"> <id name="id" type="integer"> <generator strategy="AUTO"/> </id> <field name="title" type="string"/> </entity></doctrine-mapping>

You can copy the basic XML from/vendor/doctrine/orm/docs/en/reference/xml-

mapping.rst.

Page 37: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

In factAlways use XML mapping, it makes a lot of sense, and you

get auto-completion in your IDE!

Page 38: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Remove all ORM things (annotations) from the Post class

Page 39: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

If you are going to try the following at home:

Update DoctrineBundleModify composer.json:

{ "require": { ... "doctrine/doctrine-bundle": "~1.2@dev" }}

Run composer update doctrine/doctrine-bundle

Page 40: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Add a compiler pass to your bundleIt will load the XML mapping files

use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass;

class DpcTutorialBundle{ public function build(ContainerBuilder $container) { $container->addCompilerPass($this->buildMappingCompilerPass()); }

private function buildMappingCompilerPass() { return DoctrineOrmMappingsPass::createXmlMappingDriver( array( __DIR__ . '/../../Test/Model/Mapping/' => 'Dpc\Tutorial\Model' ) ); }}

Page 41: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

What have we won?Clean model classesThey are reusable in non-Symfony projectsThey are reusable with different persistence libraries

Documentation » The Cookbook » Doctrine » How to provide model classes for several Doctrineimplementations

Page 42: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Controllers

Page 43: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create a controllerUse app/console generate:controller

Specs

Name: DpcTutorialBundle:PostConfiguration: annotationTemplate: twigThe route contains an id parameter.Action: showActionRoute: /post/{id}/show

Page 44: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Implement the following logicModify the action to retrieve a Post entity from the

database:public function showAction(Post $post){ return array('post' => $post);}

Page 45: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Don't forget to register the route# in the bundle's routing.yml file:DpcTutorialBundle_Controllers: resource: "@DpcTutorialBundle/Controller" type: "annotation"

Page 46: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

By the wayConsider using XML for routing too!

For the same reasons

Page 47: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Does all of this really needto be inside the bundle?

Page 48: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Move the controller class to thelibrary

Page 49: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Remove parent Controller classWe are going to inject every dependency by hand instead of

relying on the service container.

Page 50: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create a service for the controllerservices: dpc_tutorial.post_controller: class: Dpc\Tutorial\Controller\PostController

Page 51: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Remove @Route annotationsInstead: define actual routes in the bundle's routing.yml

file.Use the service id of the controller instead of its class name.

dpc_tutorial.post_controller.show: path: /post/{id}/show defaults: _controller: dpc_tutorial.post_controller:showAction

Page 52: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Remove @Template annotationsInject the templating service instead and use it to render

the template.use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Templating\EngineInterface;

class PostController{ public function __construct(EngineInterface $templating) { $this->templating = $templating; }

public function showAction(Post $post) { return new Response( $this->templating->render( 'DpcTutorialBundle:Post:show.html.twig', array('post' => $post) ) ); }}

Page 53: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

services: dpc_tutorial.post_controller: class: Dpc\Tutorial\Controller\PostController arguments: - @templating

Page 54: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

What about the

Templates

Page 55: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Move the template to the libraryE.g. from Dpc/Bundle/TutorialBundle/Resources/views/Post/show.html.twig to

Dpc/Tutorial/View/Post/show.html.twig

Page 56: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Change the template reference$this->templating->render( '@DpcTutorial/Post/show.html.twig', array('post' => $post))

Page 57: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Register the new location of thetemplates

# in config.ymltwig: ... paths: "%kernel.root_dir%/../src/Dpc/Tutorial/View": DpcTutorial

Documentation » The Cookbook » Templating » How to use and Register namespaced Twig Paths

Page 58: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Well...We don't want to ask users to modify their config.yml!

Page 59: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Let's prependconfiguration

use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;

class DpcTutorialExtension extends ConfigurableExtension implements PrependExtensionInterface{ ... public function prepend(ContainerBuilder $container) { $bundles = $container->getParameter('kernel.bundles'); if (!isset($bundles['TwigBundle'])) { return; }

$container->prependExtensionConfig( 'twig', array( 'paths' => array( "%kernel.root_dir%/../src/Dpc/Tutorial/View" => 'DpcTutorial' ) ) ); }}

Documentation » The Cookbook » Bundles » How to simplify configuration of multiple Bundles

Page 60: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

One last step!The action's $post argument relies on something called

.param convertersThose convert the id from the route to the actual Post

entity.This is actually Symfony framework-specific behavior

Page 61: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Rewrite the controller to make useof a repository

use Doctrine\Common\Persistence\ObjectRepository;

class PostController{ public function __construct(..., ObjectRepository $postRepository) { ... $this->postRepository = $postRepository; }

public function showAction($id) { $post = $this->postRepository->find($id); if (!($post instanceof Post)) { throw new NotFoundHttpException(); } ... }}

Page 62: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

services: dpc_tutorial.post_controller: class: Dpc\Tutorial\Controller\PostController arguments: - @templating - @dpc_tutorial.post_repository

dpc_tutorial.post_repository: class: Doctrine\Common\Persistence\ObjectRepository factory_service: doctrine factory_method: getRepository arguments: - Dpc\Tutorial\Model\Post

Page 63: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

What do we have now?

Page 64: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Reusable templates

Page 65: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Reusable controllersThey work with Silex too!

Who would have though that was possible?

Page 66: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Console commands

Page 67: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create a console commandUse app/console generate:console-command

Make it insert a new post in the database.It takes one argument: the post's title.

Page 68: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Something like thisuse Dpc\Tutorial\Model\Post;use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;use Symfony\Component\Console\Input\InputArgument;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Output\OutputInterface;

class CreatePostCommand extends ContainerAwareCommand{ protected function configure() { $this ->setName('post:create') ->addArgument('title', InputArgument::REQUIRED); }

protected function execute(InputInterface $input, OutputInterface $output) { $manager = $this->getContainer() ->get('doctrine') ->getManagerForClass('Dpc\Tutorial\Model\Post');

$post = new Post(); $post->setTitle($input->getArgument('title')); $manager->persist($post); $manager->flush();

$output->writeln('New post created: '.$post->getTitle()); }}

Page 69: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Why is it inside a bundle?Because it is automatically registered when it's in the

Command directory.

Page 70: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

So let's move it out!

Page 71: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Move the command to the library

Page 72: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create a service for itGive it the tag console.command.

Or else it won't be recognized anymore!

services: dpc_tutorial.create_post_command: class: Dpc\Tutorial\Command\CreatePostCommand tags: - { name: console.command }

Page 73: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

What about ContainerAware?It couples our command to the Symfony framework.

Which is not needed at all.

Page 74: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Extend from CommandThen inject dependencies instead of fetching them from the

container.use Doctrine\Common\Persistence\ManagerRegistry;

class CreatePostCommand extends Command{ private $doctrine;

public function __construct(ManagerRegistry $doctrine) { parent::__construct();

$this->doctrine = $doctrine; } ... protected function execute(InputInterface $input, OutputInterface $output) { $manager = $this->doctrine->getManager(); ... }}

Page 75: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

services: dpc_tutorial.create_post_command: class: Dpc\Tutorial\Command\CreatePostCommand arguments: - @doctrine tags: - { name: console.command }

Page 76: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

What do we have?Explicit dependenciesReusable commands that works in all projects that use theSymfony Console Component (like )A bit less magic (no auto-registering commands)Which means now we can put anything we want in theCommand directory

Cilex

Page 77: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Testing abundle

Or: testing configuration

Page 78: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

The Configuration class

Page 79: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

I don't get it!

Page 80: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

I don't trust myself with it.And when I don't trust myself, I write tests

Page 81: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

SymfonyConfigTestOn GitHub: SymfonyConfigTest

{ "require-dev": { "matthiasnoback/symfony-config-test": "~0.1" }}

Page 82: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Prepare a test suite for yourConfiguration class

Create a directory Tests/DependencyInjection insidethe bundle.In that directory create a new class:ConfigurationTest.

Page 83: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create the test classThe ConfigurationTest should extend fromAbstractConfigurationTestCaseImplement the missing method getConfiguration()namespace Dpc\Bundle\TutorialBundle\Tests\DependencyInjection;

use Dpc\Bundle\TutorialBundle\DependencyInjection\Configuration;use Matthias\SymfonyConfigTest\PhpUnit\AbstractConfigurationTestCase;

class ConfigurationTest extends AbstractConfigurationTestCase{ protected function getConfiguration() { return new Configuration(); }}

Page 84: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Desired structure in config.ymldpc_tutorial: # host should be a required key host: localhost

Page 85: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

A required value: hostTest first

/** * @test */public function the_host_key_is_required(){ $this->assertConfigurationIsInvalid( array( array() ), 'host' );}

If we provide no values at all, we expect an exceptioncontaining "host".

Page 86: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

See it failbin/phpunit -c app

Page 87: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Make the test pass$rootNode ->children() ->scalarNode('host') ->isRequired() ->end() ->end();

Page 88: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Trial and errorYou're done when the test passes!

Page 89: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Repeated configuration valuesDesired structure in config.yml

dpc_tutorial: servers: a: host: server-a.nobacksoffice.nl port: 2730 b: host: server-b.nobacksoffice.nl port: 2730 ...

host and port are required keys for each serverconfiguration

Page 90: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Test first/** * @test */public function host_is_required_for_each_server(){ $this->assertConfigurationIsInvalid( array( array( 'servers' => array( 'a' => array() ) ) ), 'host' );}

Page 91: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Run the testsbin/phpunit -c app

Page 92: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Write the code$rootNode ->children() ->arrayNode('servers') ->useAttributeAsKey('name') ->prototype('array') ->children() ->scalarNode('host') ->isRequired() ->end()

Page 93: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Run the tests

Page 94: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Test firstRepeat these steps for port

Make sure your test first failsThen you add some codeThen the test should pass

Page 95: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Merging config values$this->assertConfigurationIsInvalid( array( array( ... // e.g. values from config.yml ), array( ... // e.g. values from config_dev.yml ) ), 'host');

Page 96: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Disable mergingTest first

/** * @test */public function server_configurations_are_not_merged(){ $this->assertProcessedConfigurationEquals( array( array( 'servers' => array( 'a' => array('host' => 'host-a', 'port' => 1) ) ), array( 'servers' => array( 'b' => array('host' => 'host-b', 'port' => 2) ) ) ), array( 'servers' => array( 'b' => array('host' => 'host-b', 'port' => 2) ) ) );}

Page 97: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Add some code$rootNode ->children() ->arrayNode('servers') ->useAttributeAsKey('name') // don't reindex the array ->prototype('array') // means: repeatable ->children() ->scalarNode('host')->end() ->scalarNode('port')->end() ->end() ->end() ->end() ->end();

Page 98: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Run the testsbin/phpunit -c app

Page 99: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Disable deep mergingValues from different configuration sources should not be

merged.$rootNode ->children() ->arrayNode('servers') ->performNoDeepMerging() ... ->end() ->end();

Page 100: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Advantages of TDD forConfiguration classes

We gradually approach our goal.We immediately get feedback on what's wrong.We can test different configuration values withoutchanging config.yml manually.We can make sure the user gets very specific errormessages about wrong configuration values.Learn more about all the options by reading the

.offical

documentation of the Config component

Page 101: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Testing Extensionclasses

dpc_tutorial: servers: a: host: localhost port: 2730

Should give us a dpc_tutorial.a_server service withhost and port as constructor arguments.

Page 102: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Create a test class for your extensionDirectory: Tests/DependencyInjectionClass name: [NameOfTheExtension]TestClass should extend AbstractExtensionTestCaseImplement getContainerExtensions(): return aninstance of your extension classnamespace Dpc\Bundle\TutorialBundle\Tests\DependencyInjection;

use Dpc\Bundle\TutorialBundle\DependencyInjection\DpcTutorialExtension;use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;

class DpcTutorialExtensionTest extends AbstractExtensionTestCase{ protected function getContainerExtensions() { return array( new DpcTutorialExtension() ); }}

Page 103: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Test first/** * @test */public function it_creates_service_definitions_for_each_server(){ $this->load( array( 'servers' => array( 'a' => array('host' => 'host-a', 'port' => 123), 'b' => array('host' => 'host-b', 'port' => 234) ) ) );

$this->assertContainerBuilderHasServiceDefinitionWithArgument( 'dpc_tutorial.a_server', 0, 'host-a' ); $this->assertContainerBuilderHasServiceDefinitionWithArgument( 'dpc_tutorial.a_server', 1, 123 );

$this->assertContainerBuilderHasServiceDefinitionWithArgument( 'dpc_tutorial.b_server', 0, 'host-b' ); $this->assertContainerBuilderHasServiceDefinitionWithArgument( 'dpc_tutorial.b_server', 1, 234 );}

Page 104: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

See it fail

Page 105: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Write the codeuse Symfony\Component\DependencyInjection\Definition;

public function load(array $configs, ContainerBuilder $container){ $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs);

foreach ($config['servers'] as $name => $serverConfig) { $serverDefinition = new Definition(); $serverDefinition->setArguments( array( $serverConfig['host'], $serverConfig['port'], ) );

$container->setDefinition( 'dpc_tutorial.' . $name . '_server', $serverDefinition ); }}

Page 106: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

See it pass

Page 107: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Refactor!public function load(array $configs, ContainerBuilder $container){ $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs);

$this->configureServers($container, $config['servers']);}

private function configureServers(ContainerBuilder $container, array $servers){ foreach ($servers as $name => $server) { $this->configureServer($container, $name, $server['host'], $server['port']); }}

private function configureServer(ContainerBuilder $container, $name, $host, $port){ $serverDefinition = new Definition(null, array($host, $port)); $container->setDefinition( 'dpc_tutorial.' . $name . '_server', $serverDefinition );}

Page 108: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Shortcuts versus the Real dealThe base class provides some useful shortcutsTo get the most out of testing your extension:Read all about classes like Definition in the officialdocumentation

Page 109: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Patterns of Dependency Injection

Page 110: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

A Bundlecalled Bandle

Page 111: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

I thought a bundle is just a class thatimplements BundleInterface...

Page 112: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Why the suffix isnecessary

abstract class Bundle extends ContainerAware implements BundleInterface{ public function getContainerExtension() { ... $basename = preg_replace('/Bundle$/', '', $this->getName());

$class = $this->getNamespace() . '\\DependencyInjection\\' . $basename . 'Extension';

if (class_exists($class)) { $extension = new $class(); ... } ... }} Line 6: '/Bundle$/'

Page 113: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

But: no need to guess, youalready know which class

it is, right?

Page 114: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Override thegetContainerExtension() of your

bundle classThen make it return an instance of your extension class.

Page 115: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

use Dpc\Bundle\TutorialBundle\DependencyInjection\DpcTutorialExtension;

class DpcTutorialBundle extends Bundle{ public function getContainerExtension() { return new DpcTutorialExtension(); }}

Now the extension doesn't need to be in theDependencyInjection directory anymore!

Page 116: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

It still needs to have the Extension suffix though...

Page 117: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Open the Extension class (from theHttpKernel component)Take a look at the getAlias() method.

Page 118: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

abstract class Extension implements ExtensionInterface, ConfigurationExtensionInterface{ public function getAlias() { $className = get_class($this); if (substr($className, -9) != 'Extension') { throw new BadMethodCallException( 'This extension does not follow the naming convention;' . 'you must overwrite the getAlias() method.' ); } $classBaseName = substr(strrchr($className, '\\'), 1, -9);

return Container::underscore($classBaseName); }}

Page 119: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

The alias is used to find out which configuration belongs towhich bundle:

# in config.ymldpc_tutorial: ...

By convention it's the lowercase underscored bundle name.

Page 120: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

But what happens when Irename the bundle?

The alias changes too, which means configuration inconfig.yml won't be recognized anymore.

Page 121: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Also:The extension needs to be renamed too, because of the

naming conventions...DpcTutorialBundle, DpcTutorialExtension, dpc_tutorial

NobackTestBundle, NobackTestExtension, noback_test

Page 122: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

So: open your extension classOverride the getAlias() method.

Make it return the alias of your extension (a string).E.g. DpcTutorialBundle::getAlias() returns

dpc_tutorial.

Page 123: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

class DpcTutorialExtension extends Extension{ public function getAlias() { return 'dpc_tutorial'; }}

Page 124: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

But now we have someduplication of information

The alias is also mentioned inside the Configurationclass.

Page 125: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

class Configuration implements ConfigurationInterface{ public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('dpc_tutorial'); ...

return $treeBuilder; }}

Page 126: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Modify extension and configurationHow can we make sure that the name of the root node inthe configuration class is the same as the alias returned by

getAlias()?

Page 127: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

class Configuration implements ConfigurationInterface{ private $alias;

public function __construct($alias) { $this->alias = $alias; }

public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root($this->alias); ... }}

$configuration = new Configuration($this->getAlias());

Page 128: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

This introduces a bugRun app/console config:dump-reference

[extension-alias]

Page 129: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014
Page 130: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Open the Extension classTake the one from the DependencyInjection

component.

Page 131: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

public function getConfiguration(array $config, ContainerBuilder $container){ $reflected = new \ReflectionClass($this); $namespace = $reflected->getNamespaceName(); $class = $namespace.'\\Configuration'; if (class_exists($class)) { $r = new \ReflectionClass($class); $container->addResource(new FileResource($r->getFileName()));

if (!method_exists($class, '__construct')) { $configuration = new $class();

return $configuration; } }}

Our Configuration class has a constructor...

Page 132: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Override getConfiguration() inyour extension

Also: make sure only one instance of Configuration iscreated in the extension class.

Page 133: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

class DpcTutorialExtension extends Extension{ public function load(array $configs, ContainerBuilder $container) { $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); ... }

public function getConfiguration(array $config, ContainerBuilder $container) { return new Configuration($this->getAlias()); } ...}

Now we are allowed to rename Configuration or put itsomewhere else entirely!

Page 134: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Some last improvementExtend from ConfigurableExtension.

Page 135: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

abstract class ConfigurableExtension extends Extension{ final public function load(array $configs, ContainerBuilder $container) { $this->loadInternal( $this->processConfiguration( $this->getConfiguration($configs, $container), $configs ), $container ); }

abstract protected function loadInternal(array $mergedConfig, ContainerBuilder $container}

Page 136: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

It will save you a call to processConfiguration().

Page 137: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

class DpcTutorialExtension extends ConfigurableExtension{ public function loadInternal(array $mergedConfig, ContainerBuilder $container) { // $mergedConfig has already been processed

$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config' $loader->load('services.xml'); } ...}

Page 138: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

We introduced flexibility...By hard-coding the alias

And by skipping all the magic stuff

Now we canChange *Bundle into *Bandle

Change *Extension into *PluginChange Configuration into Complexity

If we want...

Page 140: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

€ 15,00

Page 141: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

I’m impressed. — Robert C. Martin

leanpub.com/principles-of-php-package-design/c/dpc2014

Page 142: High Quality Symfony Bundles tutorial - Dutch PHP Conference 2014

Feedbackjoind.in/10849

Twitter@matthiasnoback