SOLID Symfony Applications

Ein

 

 

Freitagsfrühstück

Agenda

  • SOLID in aller Kürze
  • Symfony in aller Kürze
  • SOLID in Symfony
  • Symfony 3.3 - Was ändert sich?
  • The twelve-factor app (wenn Zeit)

SOLID in aller Kürze

  • Gutes OOP Design
  • Langlebig
  • Wartungsfreundlich
  • Akronym für...

Single responsibility principle

Es sollte nie mehr als einen Grund dafür geben, eine Klasse/ein Modul/eine Funktion zu ändern.

Open/closed principle

Module sollten sowohl offen (für Erweiterungen), als auch geschlossen (für Modifikationen) sein.

Liskov substitution principle

Objekte in einem Programm sollten durch Instanzen ihrer Subtypen ersetzbar sein, ohne die Korrektheit des Programms zu ändern.

Interface segregation principle

Clients sollten nicht dazu gezwungen werden, von Interfaces abzuhängen, die sie nicht verwenden.

Dependency inversion principle

Module höherer Ebenen sollten nicht von Modulen niedrigerer Ebenen abhängen.
Beide sollten von Abstraktionen abhängen.


Abstraktionen sollten nicht von Details abhängen.
Details sollten von Abstraktionen abhängen.

Symfony in aller Kürze

  • PHP (HTTP) Application Framework
  • Komponentenbasiert
  • V1: 2007, V2: 2011, V3: 2015

Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony

Symfony Best Practices?

  • "Best" für Rapid Application Development*
  • Stellenweise strittig in der Community
  • Trotzdem quasi Pflichtlektüre!
 * "symfony/symfony": "<3.3"

SOLID in Symfony

Single responsibility principle?

Viele kleine Services!

Dependency Injection!

DI Container

Gib mir 'controller.booking'

controller.booking

  • repository.booking
  • logger

repository.booking

  • entity_manager

logger

entity_manager

Dependency Injection

Dependency Injection

So spezifisch, wie möglich.

So generell, wie nötig.

services.yml

# app/config/services.yml
imports:
    # SoC your service files
    - { resource: "../Resources/services/repository.services.yml" }

parameters:
  image_driver: gmagick
  # merges with the one from config.yml!

services:
    image_processor:
        class: AppBundle\Image\Processor
        arguments: ['%image_driver%'] # parameter
        calls:
            - [setLogger, ['@logger']] # services

config_test.yml

# app/config/config_test.yml
imports:
    - { resource: config_dev.yml }

web_profiler:
    toolbar: false
    intercept_redirects: false

parameters:
  image_driver: gd

services:
    image_processor:
        class: AppBundle\Image\DebugProcessor
    cache.images.txt:
        class: Symfony\Component\Cache\Adapter\NullAdapter

# [...]

services.yml

# app/config/services.yml

parameters:
  image_processor_class: AppBundle\Image\Processor

services:
    image_processor:
        class: '%image_processor_class%'
  • Nur in Shared Bundles!
  • Da wiederum nützlich!

Private Services

# app/config/services.yml

services:
    thumbnailer:
        class: Appbundle\Image\Thumbnailer
        arguments: ['@image_processor']

    image_processor:
        class: AppBundle\Image\Processor
        arguments: ['%image_driver%'] # parameter
        public: false

    
    logstash_logger:
        class: AppBundle\Logging\LogstashLogger
        public: false    

Private Services

<?php
declare(strict_types=1);

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class ImageController extends Controller 
{
    public function someAction(): Response
    {
        $logstashLogger = $this->get('logstash_logger');
    }
}

Private Services nur per DI!

Apropos $this->get()

  • Schlecht testbar
  • Fördert schlechtes Design

Controller as Service!

/**
 * Displays a form to edit an existing car entity.
 *
 * @Route("/{id}/edit", name="car_edit")
 * @Method({"GET", "POST"})
 */
public function editAction(Request $request, int $id) : Response
{
    $em = $this->getDoctrine()->getManager();
    $car = $em->getRepository('AppBundle:Car')->find($id);
    $editForm = $this->createForm('AppBundle\Form\CarType', $car);
    $editForm->handleRequest($request);

    if ($editForm->isSubmitted() && $editForm->isValid()) {
        $em->persist($car);
        $em->flush();

        return $this->redirectToRoute('car_edit', ['id' => $car->getId()]);
    }

    return $this->render('car/edit.html.twig', [
        'car' => $car,
        'form' => $editForm->createView(),
    ]);
}

In einem typischen Controller

Wie viele Services?

/**
 * Car controller.
 *
 * @Route("car")
 */
class CarController
{
    /**
     * CarController constructor.
     */
    public function __construct(
        EntityManager $entityManager,
        FormFactory $formFactory,
        Router $router,
        Environment $twig
    )
    {
        // ...
    }

    // ...
}

Service Controller Constructor

class CarRepository extends EntityRepository
{
    public function persist(Car $car) : void
    {
        $this->getEntityManager()->persist($car);
    }

    public function flush(Car $car = null) : void
    {
        $this->getEntityManager()->flush($car);
    }
}

Typisierte Repositories

// lesen
$car = $repository->find($id);
// aber schreiben
$entityManager->persist($car);

Problem:

Lösung:

Leider kennt PHP (noch) keine Generics :(

abstract class TypedEntityRepository extends EntityRepository
{
    public function persist($entity): void
    {
        $this->validateEntity($entity);
        $this->getEntityManager()->persist($entity);
    }

    public function flush($entity = null): void
    {
        $this->validateEntity($entity);
        $this->getEntityManager()->flush($entity);
    }

    private function validateEntity($entity = null): void
    {
        if ($entity && !is_a($entity, $this->getClassName())) {
            throw new \InvalidArgumentException(
                'Entity is not a '.$this->getClassName()
            );
        }
    }
}

Alternative

Prüfung zur Laufzeit, nicht zur Interpreterzeit!

# app/Resources/services/repository.services.yml

services:
  repo.car:
      class: AppBundle\Repository\CarRepository
      factory: ['@doctrine.orm.entity_manager', getRepository]
      arguments: [AppBundle:Car]

Repositories im DI-Container?

Problem:

$repo = new CarRepository(); // geht nicht, denn...
 
/**
 * \Doctrine\ORM\EntityRepository
 *
 * @param EntityManager         $em    The EntityManager to use.
 * @param Mapping\ClassMetadata $class The class descriptor.
 */
public function __construct($em, Mapping\ClassMetadata $class) { }

// daher holen wir Repos bislang immer so:

$repo = $entityManager->getRepository(AppBundle\Entity\Car::class);

Lösung:

/**
 * Car controller.
 *
 * @Route("car")
 */
class CarController
{
    /**
     * CarController constructor.
     */
    public function __construct(
        CarRepository $carRepository, // <- spezifischer
        FormFactory $formFactory,
        Router $router,
        Environment $twig
    )
    {
        // ...
    }

    // ...
}

Controller Constructor II

# app/Resources/services/forms.services.yml

services:
  form.car:
      class: Symfony\Component\Form\Form
      factory: ['@form.factory', create]
      arguments: [AppBundle\Form\CarType]

Forms im DI-Container?

Problem:

$car = new Car;
/** @var \Symfony\Component\Form\Form $editForm */
$editForm = $this->get('form.factory')->create(AppBundle\Form\CarType::class, $car);

Lösung:

...aber noch nicht ganz...

class CarController
{
    public function __construct(
        CarRepository $carRepository, // <- spezifischer
        Form $carForm, // <- spezifischer
        Router $router,
        Environment $twig
    )
    {
        // ...
    }
}

Controller Constructor III

// vorher
$editForm = $this->createForm('AppBundle\Form\CarType', $car);
$editForm->handleRequest($request);

// nachher
$this->carForm->setData($car);
$this->carForm->handleRequest($request);
class CarController
{
    public function __construct(
        CarRepository $carRepository, // spezifischer
        Form $carForm, // spezifischer
        Router $router
        /* Twig Engine ist weg */
    ) {  /*...*/ }

    /**
     * @Route("/{id}/edit", name="car_edit")
     * @Method({"GET", "POST"})
     * @Template("car/edit.html.twig")
     */
    public function editAction(Request $request, int $id)
    {
        $car = $this->carRepository->find($id);
        $this->carForm->setData($car);
        $this->carForm->handleRequest($request);
    
        if ($this->carForm->isSubmitted() && $this->carForm->isValid()) {
            $this->carRepository->persist($car);
            $this->carRepository->flush();
    
            return new RedirectResponse(
                $this->router->generate('car_edit', ['id' => $car->getId()])
            );
        }
    
        return ['car' => $car, 'form' => $this->carForm->createView()]; // <- gibt array zurück
    }
}

Controller Constructor IV

Events

Der Symfony Lebenszyklus

Name Constant Argument passed to the listener
kernel.request KernelEvents::REQUEST GetResponseEvent
kernel.controller KernelEvents::CONTROLLER FilterControllerEvent
kernel.view KernelEvents::VIEW GetResponseForControllerResultEvent
kernel.response KernelEvents::RESPONSE FilterResponseEvent
kernel.finish_request KernelEvents::FINISH_REQUEST FinishRequestEvent
kernel.terminate KernelEvents::TERMINATE PostResponseEvent
kernel.exception KernelEvents::EXCEPTION GetResponseForExceptionEvent

Kernel Events

EventDispatcher

<!-- Symfony/Bundle/FrameworkBundle/Resources/config/services.xml -->
<services>
        <service id="event_dispatcher" 
            class="Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher">
            <argument type="service" id="service_container" />
        </service>
</services>

Eventbasierte Architektur

SOLID

Open/Closed Principle

class CarController
{
    public function __construct(
        CarRepository $carRepository, // spezifischer
        Form $carForm, // spezifischer
        Router $router,
        EventDispatcher $dispatcher // <- neu!
    ) {  /*...*/ }

    /**
     * @Route("/{id}/edit", name="car_edit")
     * @Method({"GET", "POST"})
     * @Template("car/edit.html.twig")
     */
    public function editAction(Request $request, int $id)
    {
        $car = $this->carRepository->find($id);
        $this->carForm->setData($car);
        $this->carForm->handleRequest($request);
    
        if ($this->carForm->isSubmitted() && $this->carForm->isValid()) {
            $this->dispatcher->dispatch('app.car_edited', new CarEvent($car)); // <- neu!
    
            return new RedirectResponse(
                $this->router->generate('car_edit', ['id' => $car->getId()])
            );
        }
    
        return ['car' => $car, 'form' => $this->carForm->createView()];
    }
}

Eventbasierte Controller

EventDispatcher in jeden Controller injecten?

...das muss doch auch anders gehen!

Tagged Services?

# app/config/services.yml
services:
    app.twig_extension:
        class: AppBundle\Twig\AppExtension
        public: false
        tags:
            - { name: twig.extension }

Dependency Injection Container in Symfony

  • Registry
  • Definitions
  • Parameters
  • Wird kompiliert
  • Wird gecached

Der DI-Container

Kernel

Container Builder

Container Compiler

Container

CompilerPass[]

CompilerPass für twig.extension

namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler;

class TwigEnvironmentPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (false === $container->hasDefinition('twig')) {
            return;
        }

        $definition = $container->getDefinition('twig');

        // ...
        foreach ($container->findTaggedServiceIds('twig.extension') as $id => $attributes) {
            $definition->addMethodCall('addExtension', array(new Reference($id)));
        }
        // ...
    }
}

CompilerPass für controller.event_aware

namespace AppBundle\DI\Compiler;

class EventDispatcherAwarePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (false === $container->hasDefinition('event_dispatcher')) {
            return;
        }

        $eventDispatcher = $container->getDefinition('event_dispatcher');

        foreach ($container->findTaggedServiceIds('controller.event_aware') as $id => $attributes) {
            $controller = $container->getDefinition($id);
            $controller->addMethodCall('setEventDispatcher', [new Reference($eventDispatcher)];
        }
    }
}
# app/config/services.yml
services:
    app.controller.car:
        class: AppBundle\Controller\CarController
        arguments:
            - '@repo.car'
            - '@form.car'
            - '@router'
        tags:
            - { name: controller.event_aware }

Viel gewonnen haben wir nicht...

Der größte Antrieb des Programmierers, ist die Faulheit.

Prof. Dr. rer. nat. Helmut Eirund (HS-Bremen)

EventDispatcherAware

<?php

namespace DI\Event;

interface EventDispatcherAwareInterface
{
    /**
     * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
     */
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void;
}

trait EventDispatcherAwareTrait
{
    /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */
    protected $eventDispatcher;

    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
    {
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * Convenience function to dispatch an event.
     */
    protected function dispatchEvent(string $eventName, Event $event): Event
    {
        return $this->eventDispatcher->dispatch($eventName, $event);
    }
}

EventDispatcherAwarePass

<?php

namespace AppBundle\DI\Compiler;

class EventDispatcherAwarePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('event_dispatcher')) {
            return;
        }

        $eventDispatcher = $container->getDefinition('event_dispatcher');
        $definitions     = $container->getDefinitions();

        foreach ($definitions as $definition) {
            $className = $definition->getClass();

            if ($className && class_exists($className)) {
                $refClass = $container->getReflectionClass($className);
                if ($refClass->implementsInterface(DI\Event\EventDispatcherAwareInterface::class)) {
                    $definition->addMethodCall(
                        'setEventDispatcher', 
                        [$container->findDefinition($eventDispatcher)]
                    );
                }
            }
        }
    }
}

CompilerPass registrieren

class AppBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new EventDispatcherAwarePass());
    }
}
class CarController implements EventDispatcherAwareInterface
{
    use EventDispatcherAwareTrait;

    public function __construct(
        CarRepository $carRepository, // spezifischer
        Form $carForm, // spezifischer
        Router $router
    ) {  /*...*/ }

    /**
     * @Route("/{id}/edit", name="car_edit")
     * @Method({"GET", "POST"})
     * @Template("car/edit.html.twig")
     */
    public function editAction(Request $request, int $id)
    {
        // ...
    
        if ($this->carForm->isSubmitted() && $this->carForm->isValid()) {
            $this->dispatchEvent('app.car_edited', new CarEvent($car)); // <- neu!
    
            // ...
        }
    
        return ['car' => $car, 'form' => $this->carForm->createView()];
    }
}

EventDispatcherAware Controller

class CarController implements EventDispatcherAwareInterface
{
    use EventDispatcherAwareTrait; // <- enthält setEventDispatcher()

    public function __construct(
        CarRepository $carRepository, // spezifischer
        Form $carForm, // spezifischer
        Router $router
    ) {
        $this->carRepository = $carRepository;
        $this->carForm = $carForm;
        $this->router = $router
    }

    // ...
}

ACHTUNG!

class CarController implements EventDispatcherAwareInterface
{
    use EventDispatcherAwareTrait; // <- enthält setEventDispatcher()

    public function __construct(
        CarRepository $carRepository, // spezifischer
        Form $carForm, // spezifischer
        Router $router
    ) {
        $this->carRepository = $carRepository;
        $this->carForm = $carForm;
        $this->router = $router
        $this->eventDispatcher = new NullDispatcher;
    }

    // ...
}

Der NullDispatcher

class NullDispatcher implements EventDispatcherInterface
{
    /** {@inheritdoc} */
    public function dispatch($eventName, Event $event = null): Event
    {
        return new Event;
    }

    /** {@inheritdoc} */
    public function addListener($eventName, $listener, $priority = 0): void
    {
        // noop;
    }

    /** {@inheritdoc} */
    public function addSubscriber(EventSubscriberInterface $subscriber): void
    {
        // noop;
    }

    /** {@inheritdoc} */
    public function removeListener($eventName, $listener): void
    {
        // noop;
    }

    /** {@inheritdoc} */
    public function removeSubscriber(EventSubscriberInterface $subscriber): void
    {
        // noop;
    }

    /** {@inheritdoc} */
    public function getListeners($eventName = null): array
    {
        return [];
    }

    /** {@inheritdoc} */
    public function getListenerPriority($eventName, $listener): ?int
    {
        return null;
    }

    /** {@inheritdoc} */
    public function hasListeners($eventName = null): bool
    {
        return false;
    }
}

Der NullDispatcher

if ($this->eventDispatcher) { // <- ohne NullDispatcher
    $this->eventDispatcher->dispatch('app.car_edited', new CarEvent($car));
}

if ($this->logger) { // <- ohne z.B. Psr\Log\NullLogger
    $this->logger->info('Foobar');
}

Warum Null Objekte?

class CarController implements EventDispatcherAwareInterface
{
    use EventDispatcherAwareTrait;

    public function __construct(
        CarRepository $carRepository,
        Form $carForm, 
        Router $router // <- der stört mich noch
    ) {  /*...*/ }

    /**
     * @Route("/{id}/edit", name="car_edit")
     * @Method({"GET", "POST"})
     * @Template("car/edit.html.twig")
     */
    public function editAction(Request $request, int $id)
    {
        // ...
    
        if ($this->carForm->isSubmitted() && $this->carForm->isValid()) {
            $this->dispatcher->dispatch('app.car_edited', new CarEvent($car)); // <- neu!
    
            return new RedirectResponse(
                $this->router->generate('car_edit', ['id' => $car->getId()])
            );
        }
    
        // ...
    }
}

Den Router loswerden

<?php
# src/Acme/DemoBundle/Controller/DefaultController.php
namespace Acme\DemoBundle\Controller;

use QafooLabs\Views\RedirectRoute;

class DefaultController
{
    public function redirectAction()
    {
        return new RedirectRoute('hello', array(
            'name' => 'Fabien'
        ));
    }
}

RedirectRoute

class CarController implements EventDispatcherAwareInterface
{
    use EventDispatcherAwareTrait;

    public function __construct(CarRepository $carRepository, Form $carForm) {  /*...*/ }

    /**
     * @Route("/{id}/edit", name="car_edit")
     * @Method({"GET", "POST"})
     * @Template("car/edit.html.twig")
     */
    public function editAction(Request $request, int $id)
    {
        $car = $this->carRepository->find($id);
        $this->carForm->setData($car);
        $this->carForm->handleRequest($request);
    
        if ($this->carForm->isSubmitted() && $this->carForm->isValid()) {
            $this->dispatcher->dispatch('app.car_edited', new CarEvent($car)); // <- neu!
    
            return new RedirectRoute('car_edit', ['id' => $car->getId()]);
        }
    
        return ['car' => $car, 'form' => $this->carForm->createView()];
    }
}

Unser Controller ohne Router

class CarController implements EventDispatcherAwareInterface
{
    use EventDispatcherAwareTrait;

    public function __construct(Form $carForm) {  /*...*/ }

    /**
     * @Route("/{id}/edit", name="car_edit")
     * @Method({"GET", "POST"})
     * @Template("car/edit.html.twig")
     */
    public function editAction(Request $request, Car $car)
    {
        $this->carForm->setData($car);
        $this->carForm->handleRequest($request);
    
        if ($this->carForm->isSubmitted() && $this->carForm->isValid()) {
            $this->dispatcher->dispatch('app.car_edited', new CarEvent($car)); // <- neu!
    
            return new RedirectRoute('car_edit', ['id' => $car->getId()]);
        }
    
        return ['car' => $car, 'form' => $this->carForm->createView()];
    }
}

Das Repository ist überflüssig

use QafooLabs\MVC\FormRequest;

class CarController implements EventDispatcherAwareInterface
{
    use EventDispatcherAwareTrait;

    /**
     * @Route("/{id}/edit", name="car_edit")
     * @Method({"GET", "POST"})
     * @Template("car/edit.html.twig")
     */
    public function editAction(FormRequest $request, Car $car)
    {
        if (!$formRequest->handle(CarType::class, $car)) {
            return ['car' => $car, 'form' => $formRequest->createFormView()];
        }

        $car = $formRequest->getValidData(); // muss nicht, nur zur besseren Verständnis

        $this->dispatchEvent('app.car_edited', new CarEvent($car));

        return new RedirectRoute('car_edit', ['id' => $car->getId()]);
    }
}

...die Form auch!

namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;

interface ParamConverterInterface
{
    /**
     * Stores the object in the request.
     *
     * @param Request        $request       The request
     * @param ParamConverter $configuration Contains the name, class and options of the object
     *
     * @return bool True if the object has been successfully set, else false
     */
    function apply(Request $request, ParamConverter $configuration);

    /**
     * Checks if the object is supported.
     *
     * @param ParamConverter $configuration Should be an instance of ParamConverter
     *
     * @return bool True if the object is supported, else false
     */
    function supports(ParamConverter $configuration);
}

Custom ParamConverter

# app/config/services.yml
services:
    my_converter:
        class: MyBundle\Request\ParamConverter\MyConverter
        tags:
            - { name: request.param_converter, priority: 10, converter: my_converter }

Custom ParamConverter

class TypefaceFontParamConverter implements ParamConverterInterface
{
    /** @var \Entity\Repository\TypefaceFontRepository */
    private $fontRepository;

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

    public function apply(Request $request, ParamConverter $configuration): bool
    {
        $name = $configuration->getName();

        $attributes = $request->attributes;

        $typefaceName = $attributes->get('typeface');
        $fontName = $attributes->get('font');
        $fontFormat = $attributes->get('fontFormat');

        $success = true;

        try {
            $font = $this->fontRepository->findByUriParams($typefaceName, $fontName, $fontFormat);
            $attributes->set($name, $font);
        } catch (NoResultException $exception) {
            $success = false;
        } catch (\TypeError $error) {
            $success = false;
        }

        return $success;
    }

    public function supports(ParamConverter $configuration): bool
    {
        return $configuration->getClass() === TypefaceFont::class;
    }
}

TypefaceFontParamConverter

Wenn wir so viel Dependency Injection, mit so vielen, kleinen Services machen, wird unsere services.yml ganz schnell ganz groß und unübersichtlich.

Gibt es da nicht was?

Na klar!

JMSDiExtraBundle

Unsere Doctrine Mappings machen wir ja auch nicht in yml.

JMSDiExtraBundle Beispiele

use JMS\DiExtraBundle\Annotation as DI;

/**
 * @DI\Service("some.service.id", parent="another.service.id", public=false)
 * @DI\Tag("twig.extension")
 */
class MyService
{
    /**
     * @DI\InjectParams({
     *     "em" = @DI\Inject("doctrine.entity_manager")
     * })
     */
    public function __construct(EntityManager $em, Session $session)
    {
        // ...
    }

    /**
     * @DI\Observe("kernel.request", priority = 255)
     */
    public function onKernelRequest()
    {
        // ...
    }
}

JMSDiExtraBundle & Controller Services

use JMS\DiExtraBundle\Annotation as DI;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @DI\Service("my.awesome.controller")
 * @Route("/my-prefix", service="my.awesome.controller")
 */
class MyController
{
    /**
     * @DI\InjectParams({
     *     "em" = @DI\Inject("doctrine.entity_manager")
     * })
     */
    public function __construct(EntityManager $em, Session $session)
    {
        // ...
    }
}

Static Content anyone?

Der TemplateController

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class StaticPagesController {
    const NO_DATA = [];
    
    /**
     * @Route("/imprint")
     * @Template("static-pages/imprint.html.twig")
     */
    public function imprintAction() : array
    {
        return self::NO_DATA;
    }
   
    /**
     * @Route("/terms-of-service")
     * @Template("static-pages/terms-of-service.html.twig")
     */
    public function termsOfServiceAction() : array
    {
        return self::NO_DATA;
    }
}

Anstatt:

Der TemplateController

# app/config/routing.yml
my_imprint:
    path: /imprint
    defaults:
        _controller:  FrameworkBundle:Template:template
        template:     'static-pages/imprint.html.twig'
        maxAge:       86400
        sharedAge:    86400

my_terms-of-service:
    path: /terms-of-service
    defaults:
        _controller:  FrameworkBundle:Template:template
        template:     'static-pages/terms-of-service.html.twig'
        maxAge:       86400
        sharedAge:    86400

Lieber so:

...und dann kam Symfony 3.3

29.05.2017

3.3 services.yml

parameters:
    #parameter_name: value

services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Repository,Tests}'

    # controllers are imported separately to make sure they're public
    # and have a tag that allows actions to type-hint services
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

    # add more services, or override services that need manual wiring
    # AppBundle\Service\ExampleService:
    #     arguments:
    #         $someArgument: 'some_value'

3.3 services.yml

services:
    _instanceof:
      Psr\Log\LoggerAwareInterface:
        calls:
          - [setLogger, ['@logger']]
      DI\Event\EventDispatcherAwareInterface:
        calls:
          - [setEventDispatcher, ['@event_dispatcher']]
      ImageProcessingBundle\Rendering\Image\Image: # <- Klasse, nicht Interface
        calls:
          - [setDrivers, ['@ImageProcessingBundle\Rendering\Image\Drivers']]
      Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface:
        tags:
          - { name: request.param_converter }

    AppBundle\Service\ExampleService:
        arguments:
            $someArgument: 'some_value'
    
    AppBundle\Service\FooService: ['%some_param%', '@AppBundle\Service\BarService']

    ThirdParty\Class\As\Service:

    # easy aliases
    markdown_transformer: '@AppBundle\Service\MarkdownTransformer'

3.3 Service ParamConverters

use Psr\Log\LoggerInterface;

class InvoiceController extends Controller
{
    public function listAction(LoggerInterface $logger)
    {
        $logger->info('A new way to access services!');
    }
}

// Aus der Doku: "This is only possible in a controller, and your controller service must be 
// tagged with controller.service_arguments to make it happen."
//
// Streng genommen jeder Service, der mit "controller.service_arguments" getaggt ist und entweder 
// ContainerAwareInterface implementiert oder von AbstractController erbt.
// (Nur aus dem Code rausgelesen, nicht selbst getestet!)

3.3 DotEnv

<?php // web/app.php

use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\HttpFoundation\Request;

/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require __DIR__ . '/../vendor/autoload.php';
if (PHP_VERSION_ID < 70000) {
    include_once __DIR__ . '/../var/bootstrap.php.cache';
}

(new Dotenv)->load(__DIR__ . '/../.env');

$env = getenv('APP_ENV') ?: 'prod';
$isDebug = $env === 'dev';

$kernel = new AppKernel($env, $isDebug);
if (PHP_VERSION_ID < 70000) {
    $kernel->loadClassCache();
}
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
# .env
DATABASE_HOST=127.0.0.1
DATABASE_NAME=symfony_test
DATABASE_USER=root
DATABASE_PASSWORD=123
REDIS_HOST=redis
# app/config/config.yml
parameters:
    database_host: '%env(DATABASE_HOST)%'
    database_name: '%env(DATABASE_NAME)%'
    database_user: '%env(DATABASE_USER)%'
    database_password: '%env(DATABASE_PASSWORD)%'
    env(DATABASE_HOST): '127.0.0.1'

Happy Coding!

Fragen?

Nachtrag - wenn Zeit

SOLID Symfony Applications

By Ole Rößner

SOLID Symfony Applications

  • 1,536