Ein
Freitagsfrühstück
Es sollte nie mehr als einen Grund dafür geben, eine Klasse/ein Modul/eine Funktion zu ändern.
Module sollten sowohl offen (für Erweiterungen), als auch geschlossen (für Modifikationen) sein.
Objekte in einem Programm sollten durch Instanzen ihrer Subtypen ersetzbar sein, ohne die Korrektheit des Programms zu ändern.
Clients sollten nicht dazu gezwungen werden, von Interfaces abzuhängen, die sie nicht verwenden.
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 is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony
* "symfony/symfony": "<3.3"
DI Container
Gib mir 'controller.booking'
controller.booking
repository.booking
logger
entity_manager
So spezifisch, wie möglich.
So generell, wie nötig.
# 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
# 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
# [...]
# app/config/services.yml
parameters:
image_processor_class: AppBundle\Image\Processor
services:
image_processor:
class: '%image_processor_class%'
# 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
<?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!
/**
* 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(),
]);
}
Wie viele Services?
/**
* Car controller.
*
* @Route("car")
*/
class CarController
{
/**
* CarController constructor.
*/
public function __construct(
EntityManager $entityManager,
FormFactory $formFactory,
Router $router,
Environment $twig
)
{
// ...
}
// ...
}
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);
}
}
// 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()
);
}
}
}
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]
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
)
{
// ...
}
// ...
}
# app/Resources/services/forms.services.yml
services:
form.car:
class: Symfony\Component\Form\Form
factory: ['@form.factory', create]
arguments: [AppBundle\Form\CarType]
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
)
{
// ...
}
}
// 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
}
}
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 |
<!-- 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>
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()];
}
}
# app/config/services.yml
services:
app.twig_extension:
class: AppBundle\Twig\AppExtension
public: false
tags:
- { name: twig.extension }
Kernel
Container Builder
Container Compiler
Container
CompilerPass[]
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)));
}
// ...
}
}
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)
<?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);
}
}
<?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)]
);
}
}
}
}
}
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()];
}
}
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
}
// ...
}
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;
}
// ...
}
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;
}
}
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');
}
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()])
);
}
// ...
}
}
<?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'
));
}
}
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()];
}
}
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()];
}
}
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()]);
}
}
wiederum dank QafooLabsNoFrameworkBundle
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);
}
# app/config/services.yml
services:
my_converter:
class: MyBundle\Request\ParamConverter\MyConverter
tags:
- { name: request.param_converter, priority: 10, converter: my_converter }
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;
}
}
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?
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()
{
// ...
}
}
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)
{
// ...
}
}
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:
# 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:
29.05.2017
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'
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'
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!)
<?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'