@afup
@florianm__
- Un itinéraire 🗺️
- Des hôtels 💤
- Du carburant ⛽
- De la nourriture 🍗
De quoi va t'on avoir besoin ?
@afup
@florianm__
✨ Une application ✨
Florian Merle
@florianm__
Florian-Merle
AKAWAKA
@afup
@florianm__
✨ Road Trip Planner ✨
-
Découvrir ce que cache l'injection de dépendances 🔗
- Conteneur,
-
Services,
-
Paramètres,
-
Tags,
-
Etc.
-
Pour le défi technique 🏋️
@afup
@florianm__
PSR-11
namespace Psr\Container;
/**
* Describes the interface of a container that exposes methods to read its entries.
*/
interface ContainerInterface
{
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id Identifier of the entry to look for.
*
* @return mixed Entry.
*/
public function get($id);
/**
* Returns true if the container can return an entry for the given identifier.
* Returns false otherwise.
*
* @param string $id Identifier of the entry to look for.
*
* @return bool
*/
public function has($id);
}
@afup
@florianm__
@afup
@florianm__
- Single responsibility principle
- Open/closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
Construire notre conteneur
@afup
@florianm__
ContainerBuilder.php 👷
config.yaml 🔧
Container.php 📦
::buildcontainer(): Container
Implemente PSR-11
@afup
@florianm__
ContainerBuilder
final class ContainerBuilder
{
public function buildContainer(): Container
{
$config = \yaml_parse_file('config.yaml');
return new Container(
config: $config,
serviceIds: $this->resolveServiceIds($config),
);
}
/** @param string[] $config */
private function resolveServiceIds(array $config): array
{
$serviceConfigList = $config['services'] ?? [];
return array_keys($serviceConfigList);
}
}
services:
App\SmsSender: ~
App\Mailer: ~
app.weather_listener:
class: App\Listener\Weather
parameters:
notifier: '@App\Mailer'
- App\SmsSender
- App\Mailer
- app.weather_listener
@afup
@florianm__
final class Container implements ContainerInterface
{
public function get(string $id): mixed
{
if (false === $this->has($id)) {
throw new NotFoundException();
}
if (false === array_key_exists($id, $this->services)) {
$this->services[$id] = $this->buildInstance($id);
}
return $this->services[$id];
}
}
Container
public function has(string $id): bool
{
return in_array($id, $this->serviceIds);
}
@afup
@florianm__
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
private function buildInstance(string $id): mixed
{
$serviceConfig = $this->config['services'][$id] ?? [];
$reflector = new \ReflectionClass($serviceConfig['class'] ?? $id);
if (!$reflector->isInstantiable()) {
throw new ContainerException("Service $id is not instantiable");
}
$constructor = $reflector->getConstructor();
if (null === $constructor || empty($constructor->getParameters())) {
$instance = $reflector->newInstance();
} else {
$parameters = $this->resolveDependencies($constructor->getParameters(), $serviceConfig);
$instance = $reflector->newInstanceArgs($parameters);
}
return $instance;
}
}
services:
App\Mailer: ~
App\Listener\Weather:
parameters:
notifier: '@App\Mailer'
Container
@afup
@florianm__
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
{
$dependencies = [];
foreach ($constructorParameterList as $constructorParameter) {
$dependencies[] = $this->resolveDependency($constructorParameter, $serviceConfig);
}
return $dependencies;
}
}
Container
@afup
@florianm__
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
{
$value = $serviceConfig['parameters'][$constructorParameter->getName()];
// To-do: services, parameters, etc.
return $value;
}
}
Container
@afup
@florianm__
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
{
$value = $serviceConfig['parameters'][$constructorParameter->getName()];
// service
if (str_starts_with($value, '@')) {
return $this->get(substr($value, 1));
}
return $value;
}
}
Container
services:
App\Mailer: ~
App\Listener\Weather:
parameters:
notifier: '@App\Mailer'
class Mailer implements NotifierInterface
{
}
class WeatherListener
{
public function __construct(private NotifierInterface $notifier) {}
}
Tout configurer à la main ?
@afup
@florianm__
Tout configurer à la main ?
Autowiring
@florianm__
@afup
services:
App\Controller\RoutePlannerController:
parameters:
- '@App\Provider\WeatherProvider'
- '@App\PointOfInterestFinder'
- '@App\Repository\RouteRepository'
App\Provider\WeatherProvider:
parameters:
apiKey: '%weather_provider.api_key%'
App\PointOfInterestFinder: ~
App\Repository\RouteRepository:
parameters:
entityManager: '@App\EntityManager'
App\EntityManager: ~
$ tree src/
src/
├── Controller
│ ├── RoutePlannerController.php
├── Provider
│ ├── WeatherProvider.php
├── Repository
│ ├── RouteRepository.php
├── EntityManager.php
├── PointOfInterestFinder.php
└── Kernel.php
3 directories, 6 files
Container
- App\Controller\RoutePlannerController
- App\Provider\WeatherProvider
- App\PointOfInterestFinder
- App\Repository\RouteRepository
- App\EntityManager
Services
@afup
@florianm__
services:
App\Controller\RoutePlannerController:
parameters:
- '@App\Provider\WeatherProvider'
- '@App\PointOfInterestFinder'
- '@App\Repository\RouteRepository'
App\Provider\WeatherProvider:
parameters:
apiKey: '%weather_provider.api_key%'
App\PointOfInterestFinder: ~
App\Repository\RouteRepository:
parameters:
entityManager: '@App\EntityManager'
App\EntityManager: ~
autowiring:
App\:
resource: '../src/'
exclude:
- '../src/Command/'
- '../src/Entity/'
- '../src/Kernel.php'
services:
App\Provider\WeatherProvider:
parameters:
apiKey: '%weather_provider.api_key%'
@afup
@florianm__
@afup
@florianm__
Choses à faire
-
Trouver les services à "autowire"
-
Autowire les dépendances
@afup
@florianm__
Étapes pour "get" un service
has(id) ?
@afup
@florianm__
final class ContainerBuilder
{
public function buildContainer(): Container
{
$config = \yaml_parse_file('config.yaml');
return new Container(
config: $config,
serviceIds: $this->resolveServiceIds($config),
);
}
/**
* @param string[] $config
*/
private function resolveServiceIds(array $config): array
{
return array_keys($config['services']);
}
}
1- Trouver les services à "autowire"
@afup
@florianm__
final class ContainerBuilder
{
public function buildContainer(): Container
{
$config = \yaml_parse_file('config.yaml');
return new Container(
config: $config,
serviceIds: $this->resolveServiceIds($config),
);
}
/**
* @param string[] $config
*/
private function resolveServiceIds(array $config): array
{
$ids = [
...array_keys($config['services']),
...ContainerBuilderHelper::findAutowirableServiceIds($config),
];
return array_unique($ids);
}
}
1- Trouver les services à "autowire"
@afup
@florianm__
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
{
$value = $serviceConfig['parameters'][$constructorParameter->getName()] ?? null;
// service
if (str_starts_with($value, '@')) {
return $this->get(substr($value, 1));
}
return $value;
}
}
2- Autowire les dépendances
@afup
@florianm__
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
{
$value = $serviceConfig['parameters'][$constructorParameter->getName()] ?? null;
if (null === $value) {
try {
return $this->get($constructorParameter->getType()->__toString());
} catch(NotFoundExceptionInterface) {
throw new ContainerException('Cannot autowire parameter');
}
}
// service
if (str_starts_with($value, '@')) {
return $this->get(substr($value, 1));
}
return $value;
}
}
2- Autowire les dépendances
services:
App\Controller\RoutePlannerController:
parameters:
- '@App\Provider\WeatherProvider'
- '@App\PointOfInterestFinder'
- '@App\Repository\RouteRepository'
App\Provider\WeatherProvider:
parameters:
apiKey: '%weather_provider.api_key%'
App\PointOfInterestFinder: ~
App\Repository\RouteRepository:
parameters:
entityManager: '@App\EntityManager'
App\EntityManager: ~
autowiring:
App\:
resource: '../src/'
exclude:
- '../src/Command/'
- '../src/Entity/'
- '../src/Kernel.php'
services:
App\Provider\WeatherProvider:
parameters:
apiKey: '%weather_provider.api_key%'
@afup
@florianm__
Recherche de points d'intérêt
📍
@afup
@florianm__
final class PointOfInterestHandler
{
public function __construct(
private HotelHandler $hotelHandler,
private FuelHandler $fuelHandler,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
if ($pointOfInterest instanceof Hotel) {
return $this->hotelHandler->handle($pointOfInterest);
}
if ($pointOfInterest instanceof Fuel) {
return $this->fuelHandler->handle($pointOfInterest);
}
throw new \LogicException("No handler found");
}
}
@afup
@florianm__
final class PointOfInterestHandler
{
public function __construct(
private HotelHandler $hotelHandler,
private FuelHandler $fuelHandler,
private FoodHandler $foodHandler,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
if ($pointOfInterest instanceof Hotel) {
return $this->hotelHandler->handle($pointOfInterest);
}
if ($pointOfInterest instanceof Fuel) {
return $this->fuelHandler->handle($pointOfInterest);
}
if ($pointOfInterest instanceof Food) {
return $this->foodHandler->handle($pointOfInterest);
}
throw new \LogicException("No handler found");
}
}
@afup
@florianm__
Chaine de responsabilités
⛓️
@afup
@florianm__
final class PointOfInterestHandler
{
/** @param iterable<HandlerInterface> $handlers */
public function __construct(
private iterable $handlers,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
foreach ($this->handlers as $handler) {
if ($handler->can($pointOfInterest)) {
return $handler->handle($pointOfInterest);
}
}
throw new \LogicException("No handler found");
}
}
@afup
@florianm__
interface HandlerInterface
{
public function can(PointOfInterest $pointOfInterest): bool;
public function handle(PointOfInterest $pointOfInterest): mixed;
}
@afup
@florianm__
services:
App\PointOfInterestHandler:
parameters:
handlers: '!tagged_iterator point_of_interest_handler'
App\HotelHandler:
tags:
- { name: 'point_of_interest_handler' }
App\FuelHandler:
tags:
- { name: 'point_of_interest_handler' }
App\FoodHandler:
tags:
- { name: 'point_of_interest_handler' }
🚩
🚩
🚩
🪣
🚩
🚩
🚩
@afup
@florianm__
Étapes pour "get" un service
@afup
@florianm__
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
{
$value = $serviceConfig['parameters'][$constructorParameter->getName()];
// service
if (str_starts_with($value, '@')) {
return $this->get(substr($value, 1));
}
return $value;
}
}
services:
App\PointOfInterestHandler:
parameters:
handlers: '!tagged_iterator point_of_interest_handler'
@afup
@florianm__
// tagged iterator
if (str_starts_with($value, '!tagged_iterator ')) {
$tag = substr($value, 17);
// find service with a specific tag
$taggedServiceListConfig = ContainerHelper::findTaggedServices(
$tag,
$this->config['services']
);
$taggedServices = [];
foreach ($taggedServiceListConfig as $id => $taggedServiceConfig) {
$taggedServices[] = $this->get($id);
}
return $taggedServices;
}
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
{
$value = $serviceConfig['parameters'][$constructorParameter->getName()];
// service
if (str_starts_with($value, '@')) {
return $this->get(substr($value, 1));
}
return $value;
}
}
services:
App\PointOfInterestHandler:
parameters:
handlers: '!tagged_iterator point_of_interest_handler'
Recherche de points d'intérêt
📍
@afup
@florianm__
final class PointOfInterestHandler
{
public function __construct(
private HotelHandler $hotelHandler,
private FuelHandler $fuelHandler,
private FoodHandler $foodHandler,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
if ($pointOfInterest instanceof Hotel) {
return $this->hotelHandler->handle($pointOfInterest);
}
if ($pointOfInterest instanceof Fuel) {
return $this->fuelHandler->handle($pointOfInterest);
}
if ($pointOfInterest instanceof Food) {
return $this->foodHandler->handle($pointOfInterest);
}
throw new \LogicException("No handler found");
}
}
@afup
@florianm__
final class HotelHandler implements HandlerInterface
{
public function __construct(
private ?HandlerInterface $nextHandler,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
if ($pointOfInterest instanceof Hotel) {
// do stuff
return 'hotel';
}
if ($this->nextHandler) {
return $this->nextHandler->handle($pointOfInterest);
}
throw new \LogicException("No handler found");
}
}
@afup
@florianm__
final class HotelHandler implements HandlerInterface
{
public function __construct(
private ?HandlerInterface $nextHandler,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
if ($pointOfInterest instanceof Hotel) {
// do stuff
return 'hotel';
}
if ($this->nextHandler) {
return $this->nextHandler->handle($pointOfInterest);
}
throw new \LogicException("No handler found");
}
}
@afup
@florianm__
final class FuelHandler implements HandlerInterface
{
public function __construct(
private ?HandlerInterface $nextHandler,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
if ($pointOfInterest instanceof Fuel) {
// do stuff
return 'fuel';
}
if ($this->nextHandler) {
return $this->nextHandler->handle($pointOfInterest);
}
throw new \LogicException("No handler found");
}
}
final class FoodHandler implements HandlerInterface
{
public function __construct(
private ?HandlerInterface $nextHandler,
) {
}
public function handle(PointOfInterest $pointOfInterest): mixed
{
if ($pointOfInterest instanceof Food) {
// do stuff
return 'food';
}
if ($this->nextHandler) {
return $this->nextHandler->handle($pointOfInterest);
}
throw new \LogicException("No handler found");
}
}
Décoration
@afup
@florianm__
@afup
@florianm__
Configuration
HotelHandler
FuelHandler
FoodHandler
HandlerInterface
@afup
@florianm__
Configuration
services:
point_of_interest_handler:
class: App\HotelHandler
App\FuelHandler:
decorates: point_of_interest_handler
parameters:
nextHandler: '@.inner'
App\FoodHandler:
decorates: point_of_interest_handler
parameters:
nextHandler: '@.inner'
HotelHandler
FuelHandler
FoodHandler
point_of_interest_handler
@afup
@florianm__
Étapes pour "get" un service
@afup
@florianm__
final class Container implements ContainerInterface
{
public function get(string $id): mixed
{
if (false === $this->has($id)) {
throw new NotFoundException();
}
if (false === array_key_exists($id, $this->services)) {
$this->services[$id] = $this->buildInstance($id);
}
return $this->services[$id];
}
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
// private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
}
services:
point_of_interest_handler:
class: App\HotelHandler
App\FuelHandler:
decorates: point_of_interest_handler
parameters:
nextHandler: '@.inner'
App\FoodHandler:
decorates: point_of_interest_handler
parameters:
nextHandler: '@.inner'
@afup
@florianm__
final class Container implements ContainerInterface
{
public function get(string $id): mixed
{
if (false === $this->has($id)) {
throw new NotFoundException();
}
if (false === array_key_exists($id, $this->services)) {
$this->services[$id] = $this->buildInstance(
ContainerHelper::getMainId($id, $this->serviceIds, $this->config),
);
}
return $this->services[$id];
}
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
// private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
}
@afup
@florianm__
// decoration
if (str_starts_with($value, '@.inner')) {
$stack = ContainerHelper::getDecorationStack($id, $this->serviceIds, $this->config);
$currentPosition = array_search($id, array_keys($stack));
return $this->buildInstance(array_keys($stack)[$currentPosition - 1]);
}
final class Container implements ContainerInterface
{
// public function get(string $id): mixed
// private function buildInstance(string $id): mixed
// private function resolveDependencies(array $constructorParameterList, array $serviceConfig): array
private function resolveDependency(ReflectionParameter $constructorParameter, array $serviceConfig): mixed
{
$value = $serviceConfig['parameters'][$constructorParameter->getName()];
// service
if (str_starts_with($value, '@')) {
return $this->get(substr($value, 1));
}
return $value;
}
}
- point_of_interest_handler
- App\FuelHandler
- App\FoodHandler
Récupérer la météo
🌞
@afup
@florianm__
composer require cmfcmf/openweathermap-php-api
@afup
@florianm__
@afup
@florianm__
<?php
use Cmfcmf\OpenWeatherMap;
use Cmfcmf\OpenWeatherMap\Exception as OWMException;
use Http\Factory\Guzzle\RequestFactory;
use Http\Adapter\Guzzle6\Client as GuzzleAdapter;
$httpRequestFactory = new RequestFactory();
$httpClient = GuzzleAdapter::createWithConfig([]);
$owm = new OpenWeatherMap('YOUR-API-KEY', $httpClient, $httpRequestFactory);
$weather = $owm->getWeather('Berne', 'metric', 'ch');
echo $weather->temperature;
Factory
@afup
@florianm__
services:
weather.client:
factory: ['@App\Weather\ClientFactory', 'create']
@afup
@florianm__
Étapes pour "get" un service
@afup
@florianm__
services:
weather.client:
factory: ['@App\Weather\ClientFactory', 'create']
final class Container implements ContainerInterface
{
// ...
private function buildInstance(string $id): mixed
{
$serviceConfig = $this->config['services'][$id] ?? [];
$reflector = new \ReflectionClass($serviceConfig['class'] ?? $id);
if (!$reflector->isInstantiable()) {
throw new ContainerException("Service $id is not instantiable");
}
$constructor = $reflector->getConstructor();
if (null === $constructor || empty($constructor->getParameters())) {
$instance = $reflector->newInstance();
} else {
$parameters = $this->resolveDependencies($constructor->getParameters(), $serviceConfig);
$instance = $reflector->newInstanceArgs($parameters);
}
return $instance;
}
}
@afup
@florianm__
final class Container implements ContainerInterface
{
// ...
private function buildInstance(string $id): mixed
{
$serviceConfig = $this->config['services'][$id] ?? [];
$factoryConfig = $serviceConfig['factory'] ?? null;
if ($factoryConfig !== null) {
$factoryServiceId = substr($factoryConfig[0], 1);
$factoryMethod = $factoryConfig[1];
return $this->get($factoryServiceId)->$factoryMethod();
}
$reflector = new \ReflectionClass($serviceConfig['class'] ?? $id);
if (!$reflector->isInstantiable()) {
throw new ContainerException("Service $id is not instantiable");
}
$constructor = $reflector->getConstructor();
if (null === $constructor || empty($constructor->getParameters())) {
$instance = $reflector->newInstance();
} else {
$parameters = $this->resolveDependencies($constructor->getParameters(), $serviceConfig);
$instance = $reflector->newInstanceArgs($parameters);
}
return $instance;
}
}
services:
weather.client:
factory: ['@App\Weather\ClientFactory', 'create']
@afup
@florianm__
class ClientFactory
{
public function __construct(private string $apiKey) {}
pulbic function create(): OpenWeatherMap
{
return new OpenWeatherMap(
$this->apiKey,
GuzzleAdapter::createWithConfig([]),
new RequestFactory(),
);
}
}
services:
weather.client:
factory: ['@App\Weather\ClientFactory', 'create']
Integration avec Symfony
@afup
@florianm__
@afup
@florianm__
CustomContainer
- App\Controller\RoutePlannerController
- App\Provider\WeatherProvider
- App\PointOfInterestFinder
Services
Container
- kernel
- event_dispatcher
- request_stack
- router.default
Services
@afup
@florianm__
CustomContainer
- App\Controller\RoutePlannerController
- App\Provider\WeatherProvider
- App\PointOfInterestFinder
Services
Container
- kernel
- event_dispatcher
- request_stack
- router.default
- App\Controller\RoutePlannerController
- App\Provider\WeatherProvider
- App\PointOfInterestFinder
Services
@afup
@florianm__
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}
@afup
@florianm__
use App\DependencyInjection\Container as CustomContainer;
use App\DependencyInjection\ContainerBuilder as CustomContainerBuilder;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
private CustomContainer $customContainer;
public function boot(): void
{
$containerBuilder = new CustomContainerBuilder();
$this->customContainer = $containerBuilder->buildContainer();
parent::boot();
}
}
@afup
@florianm__
class Kernel extends BaseKernel
{
use MicroKernelTrait;
private CustomContainer $customContainer;
// public function boot(): void
protected function initializeContainer(): void
{
parent::initializeContainer();
foreach ($this->customContainer->getIds() as $id) {
$this->container->set($id, $this->customContainer->get($id));
}
}
}
@afup
@florianm__
class Kernel extends BaseKernel
{
use MicroKernelTrait;
private CustomContainer $customContainer;
// public function boot(): void
// protected function initializeContainer(): void
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(
new RegisterCustomServicesCompilerPass($this->customContainer)
);
}
}
class RegisterCustomServicesCompilerPass implements CompilerPassInterface
{
public function __construct(
private readonly CustomContainer $customContainer
) {
}
public function process(ContainerBuilder $container): void
{
foreach ($this->customContainer->getIds() as $id) {
$service = $this->customContainer->get($id);
$definition = new Definition(get_class($service));
$definition->setSynthetic(true);
$container->setDefinition($id, $definition);
}
}
}
Démo'
@afup
@florianm__
🎬
Services
Paramètres
Tags
Compiler pass
Référence circulaire
Alias
Configurator
Factory
Lazy Services
Immutable-setter Injection
Service Closures
Decoration
Service locator
Non Shared Services
Synthetic services
Parent Services
@afup
@florianm__
Autowiring
-
SOLID 💪
-
Différents environnements 🎭
-
Optimisation 🚀
-
Configuration facilitée👌
@afup
@florianm__
Merci 🙏
[AFUP Day Lyon 2024] DI
By Florian David Merle