Ole Rößner
?
BookingController
BookingRepository
Logger
BookingController
BookingRepository
Logger
<?php declare(strict_types=1);
final class BookingController
{
private RepositoryInterface $repository;
private LoggerInterface $logger;
public function __construct()
{
$this->repository = new BookingRepository();
$this->logger = new Logger();
}
}
BookingController
BookingRepository
Logger
<?php declare(strict_types=1);
final class BookingController
{
private RepositoryInterface $repository;
private LoggerInterface $logger;
public function __construct()
{
$this->repository = BookingRepository::getInstance();
$this->logger = Logger::getInstance();
}
}
BookingController
BookingRepository
Logger
Mandatory dependency
Optional dependency
Mandatory dependency
Optional dependency
<?php declare(strict_types=1);
final class BookingController
{
private ObjectRepository $repository;
private LoggerInterface $logger;
public function __construct(ObjectRepository $repository)
{
$this->repository = $repository;
$this->setLogger(new NullLogger());
}
public function setLogger(LoggerInterface $logger) : void
{
$this->logger = $logger;
}
}
As specific as possible,
as generic as needed.
Interface segregation principle
$ tree config/packages
config/packages
├── assets.yaml
├── commands.yaml
├── dev
│ ├── (...)
├── doctrine_migrations.yaml
├── doctrine.yaml
├── framework.yaml
├── prod
│ ├── (...)
├── routing.yaml
├── security_checker.yaml
├── security.yaml
├── sensio_framework_extra.yaml
├── swiftmailer.yaml
├── test
│ ├── (...)
├── translation.yaml
├── twig_extensions.yaml
├── twig.yaml
├── validator.yaml
└── webpack_encore.yaml
MessageHandler
Controllers
Commands
HTTP
CLI
QUEUE
class BlogController extends AbstractController
{
// code
}
// \Symfony\Bundle\FrameworkBundle\Controller\AbstractController
abstract class AbstractController implements ServiceSubscriberInterface
{
/**
* @var ContainerInterface
*/
protected $container;
/**
* @internal
* @required
*/
public function setContainer(ContainerInterface $container)
{
// setter code
}
}
// \Symfony\Bundle\FrameworkBundle\Controller\AbstractController
public static function getSubscribedServices()
{
return [
'router' => '?'.RouterInterface::class,
'request_stack' => '?'.RequestStack::class,
'http_kernel' => '?'.HttpKernelInterface::class,
'serializer' => '?'.SerializerInterface::class,
'session' => '?'.SessionInterface::class,
'security.authorization_checker' => '?'.AuthorizationCheckerInterface::class,
'twig' => '?'.Environment::class,
'doctrine' => '?'.ManagerRegistry::class,
'form.factory' => '?'.FormFactoryInterface::class,
'security.token_storage' => '?'.TokenStorageInterface::class,
'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class,
'parameter_bag' => '?'.ContainerBagInterface::class,
'message_bus' => '?'.MessageBusInterface::class,
'messenger.default_bus' => '?'.MessageBusInterface::class,
];
}
namespace App\Controller;
class BlogController extends AbstractController
{
public function index(Request $request, int $page, string $_format, PostRepository $posts, TagRepository $tags): Response
{ /* code */ }
public function postShow(Post $post): Response
{ /* code */ }
public function commentNew(Request $request, Post $post, EventDispatcherInterface $eventDispatcher): Response
{ /* code */ }
public function commentForm(Post $post): Response
{ /* code */ }
public function search(Request $request, PostRepository $posts): Response
{ /* code */ }
}
public function __construct(UriBuilder $uriBuilder, Tag $occasionTarget,
TagRepository $tagRepository, PageRenderer $pageRenderer,
TripRepository $tripRepository, LandingRepository $landingRepository,
LandingTeaserService $landingTeaserService, CityRepository $cityRepository,
CityDistrictRepository $cityDistrictRepository,
FileRepository $fileRepository
) {
parent::__construct();
$this->uriBuilder = $uriBuilder;
$this->occasionTarget = $occasionTarget;
$this->tagRepository = $tagRepository;
$this->pageRenderer = $pageRenderer;
$this->tripRepository = $tripRepository;
$this->landingRepository = $landingRepository;
$this->landingTeaserService = $landingTeaserService;
$this->cityRepository = $cityRepository;
$this->cityDistrictRepository = $cityDistrictRepository;
$this->fileRepository = $fileRepository;
}
[...]
# config/routes/annotations.yaml
actions:
resource: ../../src/Action/
type: annotation
namespace App\Action;
final class Hello
{
/**
* @Route("/hello/{who}")
* @Template("hello.html.twig")
*/
public function __invoke(string $who): array
{
return ['who' => $who];
}
}
# config/services.yaml
services:
App\Action\:
resource: '../src/Action'
tags: ['controller.service_arguments']
With great power comes great responsibility!
— Uncle Ben
class RoomService extends AbstractController
{
public function getAllRooms(): Collection
{
$rooms = $this->getDoctrine()->getRepository(Room::class)->findAll();
// do stuff
// wait! what?!
}
}
abstract class AbstractController implements ServiceSubscriberInterface
Autoconfigure!
use \Doctrine\Common\Persistence\ObjectRepository;
final class RoomService
{
private ObjectRepository $repository;
public function __construct(ObjectRepository $repository)
{
$this->repository = $repository;
}
public function getAllRooms(): Collection
{
$rooms = $this->repository->findAll();
// do stuff
}
}
class BlogController extends AbstractController {
public function commentNew(Request $request, Post $post, EventDispatcherInterface $eventDispatcher): Response
{
$comment = new Comment();
$comment->setAuthor($this->getUser());
$post->addComment($comment);
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
$eventDispatcher->dispatch(new CommentCreatedEvent($comment));
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
}
@event_dispatcher
@security.token_storage
@form.factory
@doctrine
@router
@twig
// Überall in der Doku:
// Entity laden
$entity = $repository->find(1);
// Entity speichern
$entityManager->persist($entity);
public interface IRepository<T>
{
T find(int id);
IEnumerable<T> findAll();
void add(T entity);
void edit(T entity);
void delete(T entity);
}
PHP, please giev Generics!
...or use Psalm
/**
* @method Comment find(int $id, $lockMode = null, $lockVersion = null)
* @method Comment[]|Collection findAll()
*/
final class CommentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Comment::class);
}
public function add(Comment $comment): void
{
$this->edit($comment);
$this->getEntityManager()->flush($comment);
}
public function edit(Comment $comment): void
{
$this->getEntityManager()->persist($comment);
}
public function delete(Comment $comment):void
{
$this->getEntityManager()->remove($comment);
}
}
abstract class ValidatingServiceEntityRepository extends ServiceEntityRepository
{
public function add($entity): void
{
$this->edit($entity);
$this->getEntityManager()->flush($entity);
}
public function edit($entity): void
{
$this->validateEntity($entity);
$this->getEntityManager()->persist($entity);
}
public function delete($entity): void
{
$this->validateEntity($entity);
$this->getEntityManager()->remove($entity);
}
private function validateEntity($entity): void
{
if (false === \is_a($entity, $this->getClassName())) {
throw new \TypeError('Entity is not a '.$this->getClassName());
}
}
}
class BlogController extends AbstractController {
public function commentNew(Request $request, Post $post, EventDispatcherInterface $eventDispatcher): Response
{
$comment = new Comment();
$comment->setAuthor($this->getUser());
$post->addComment($comment);
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
$eventDispatcher->dispatch(new CommentCreatedEvent($comment));
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
}
final class BlogController implements MessageBusAware
{
use MessageBusAwareTrait;
// [...]
if ($form->isSubmitted() && $form->isValid()) {
$this->dispatchMessage(new SaveCommentRequest($comment));
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
}
final class SaveCommentCommandHandler implements MessageHandlerInterface, MessageBusAware
{
use MessageBusAwareTrait;
public function __construct(CommentRepository $repository)
{ /* initialize fields */ }
public function __invoke(SaveCommentRequest $request)
{
$this->repository->saveAndFlush($request->getComment());
$this->dispatchMessage(new CommentSavedEvent($comment));
}
}
Der größte Antrieb des Programmierers, ist die Faulheit.
# config/services.yaml
services:
_instanceof:
Basster\SymfonyDiExtras\Messenger\MessageBusAwareInterface:
calls:
- [setMessageBus, ['@message_bus']]
Basster\SymfonyDiExtras\Event\EventDispatcherAwareInterface:
calls:
- [setEventDispatcher, ['@event_dispatcher']]
use Basster\SymfonyDiExtras\Messenger\MessageBusAwareInterface;
use Basster\SymfonyDiExtras\Messenger\MessageBusAwareTrait;
use Basster\SymfonyDiExtras\Messenger\NullBus;
final class BlogController implements MessageBusAwareInterface
{
use MessageBusAwareTrait;
public function __construct() {
$this->setMessageBus(new NullBus()); // NullObject Pattern!
}
}
composer req basster/symfony-di-extras
<?php
namespace Psr\Log;
/**
* This Logger can be used to avoid conditional log calls
*
* Logging should always be optional, and if no logger is provided to your
* library creating a NullLogger instance to have something to throw logs at
* is a good way to avoid littering your code with `if ($this->logger) { }`
* blocks.
*/
class NullLogger extends AbstractLogger
{
/**
* Logs with an arbitrary level.
*/
public function log($level, $message, array $context = array())
{
// noop
}
}
final class SomeService implements MessageBusAwareInterface
{
public function __construct()
{
$this->setMessageBus(new NullBus()); // <- NullObject by default!
}
public function setMessageBus(MessageBusInterface $messageBus): void {...}
public function doStuff(): void
{
// instead of...
if ($this->messageBus) {
$this->messageBus->dispatch(new FooMessage());
}
// just do
$this->messageBus->dispatch(new FooMessage());
}
}
interface LinkTypeParserInterface
{
/** @throws UnsupportedLinkTypeException */
public function getType(Link $link): string;
}
abstract class AbstractLinkTypeParser implements LinkTypeParserInterface
{
public function getType(Link $link): string { /* ... */ }
}
final class LinkTypeParser implements LinkTypeParserInterface
{
/** @var LinkTypeParserInterface[]|iterable */
private iterable $parserCollection;
public function __construct(iterable $parserCollection)
{
$this->parserCollection = $parserCollection;
}
public function getType(UserLink $link): string
{
$type = 'custom';
foreach ($this->parserCollection as $parser) {
try {
$type = $parser->getType($link);
} catch (UnsupportedLinkTypeException $ex) {
continue;
}
}
return $type;
}
}
Links
├── AbstractLinkTypeParser.php
├── BehanceLinkTypeParser.php
├── CreativeFabricaLinkParser.php
├── CreativeMarketLinkParser.php
├── DribbbleLinkParser.php
├── FacebookLinkTypeParser.php
├── FlickrLinkTypeParser.php
├── FontsComLinkParser.php
├── GoogleFontsLinkParser.php
├── GooglePlusLinkTypeParser.php
├── GraphicRiverLinkParser.php
├── InstagramLinkTypeParser.php
├── LinkTypeParserInterface.php
├── LinkTypeParser.php
├── MyFontsLinkParser.php
├── TwitterLinkTypeParser.php
└── UnsupportedLinkTypeException.php
services:
_defaults:
public: false
autowire: true
autoconfigure: true
_instanceof:
App\Links\AbstractLinkTypeParser:
tags:
- { name: app.link.parser }
services:
App\Links\LinkTypeParser:
arguments: [!tagged app.link.parser]
class BlogController extends AbstractController {
public function commentNew(Request $request, Post $post): Response
{
$comment = new Comment();
$comment->setAuthor($this->getUser());
$post->addComment($comment);
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->dispatchMessage(new SaveCommentRequest($comment); // NEU!
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
}
class BlogController extends AbstractController {
public function commentNew(Request $request, Post $post, FormFactoryInterface $formFactory): Response
{
$comment = new Comment();
$comment->setAuthor($this->getUser());
$post->addComment($comment);
$form = $formFactory->create(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->dispatchMessage(new SaveCommentRequest($comment); // NEU!
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
}
class BlogController extends AbstractController {
public function commentNew(FormRequest $request, Post $post): Response
{
$comment = new Comment();
$comment->setAuthor($this->getUser());
$post->addComment($comment);
if (!$formRequest->handle(CommentType::class, $comment)) {
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
// muss nicht, nur zur besseren Verständnis, da das Form Framework
// den State von $comment ändert
$comment = $formRequest->getValidData();
$this->dispatchMessage(new SaveCommentRequest($comment));
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
}
composer req qafoolabs/no-framework-bundle --no-scripts
# config/packages/no-framework.yaml
services:
_defaults:
autowire: true
autoconfigure: true
# di-factory
QafooLabs\Bundle\NoFrameworkBundle\Request\SymfonyFormRequest:
class: QafooLabs\Bundle\NoFrameworkBundle\Request\SymfonyFormRequest
factory: ['@App\Form\SymfonyFormRequestFactory', 'createFormRequest']
# di-alias
QafooLabs\MVC\FormRequest: '@QafooLabs\Bundle\NoFrameworkBundle\Request\SymfonyFormRequest'
composer req qafoolabs/no-framework-bundle --no-scripts
class BlogController extends AbstractController {
public function commentNew(FormRequest $request, Post $post): Response
{
$comment = new Comment();
$comment->setAuthor($this->getUser());
$post->addComment($comment);
if (!$formRequest->handle(CommentType::class, $comment)) {
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
// muss nicht, nur zur besseren Verständnis
$comment = $formRequest->getValidData();
$this->dispatchMessage(new SaveCommentRequest($comment));
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
}
class BlogController extends AbstractController {
public function commentNew(FormRequest $request, Post $post, TokenContext $context): Response
{
$comment = new Comment();
$comment->setAuthor($context->getCurrentUser());
$post->addComment($comment);
if (!$formRequest->handle(CommentType::class, $comment)) {
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
// muss nicht, nur zur besseren Verständnis
$comment = $formRequest->getValidData();
$this->dispatchMessage(new SaveCommentRequest($comment));
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
}
composer req qafoolabs/no-framework-bundle --no-scripts
class BlogController {
public function commentNew(FormRequest $request, Post $post,
TokenContext $context, Environment $twig,
UrlGeneratorInterface $router): Response
{
$comment = new Comment();
$comment->setAuthor($context->getCurrentUser());
$post->addComment($comment);
if (!$formRequest->handle(CommentType::class, $comment)) {
return new Response($twig->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
$this->dispatchMessage(new SaveCommentRequest($formRequest->getValidData());
return new RedirectResponse(
$router->generate('blog_post', ['slug' => $post->getSlug()])
);
}
}
use Basster\LazyResponseBundle\Response\LazyResponseInterface;
use Basster\LazyResponseBundle\Response\RedirectResponse;
use Basster\LazyResponseBundle\Response\TemplateResponse;
class BlogController {
public function commentNew(FormRequest $request, Post $post,
TokenContext $context): LazyResponseInterface
{
$comment = new Comment();
$comment->setAuthor($context->getCurrentUser());
$post->addComment($comment);
if (!$formRequest->handle(CommentType::class, $comment)) {
return new TemplateResponse('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
$this->dispatchMessage(new SaveCommentRequest($formRequest->getValidData());
return new RedirectResponse('blog_post', ['slug' => $post->getSlug()]);
}
}
composer require basster/lazy-response-bundle
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
final class RedirectResponseHandler extends AbstractLazyResponseHandler
{
private RouterInterface $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
protected function generateResponse(LazyResponseInterface $controllerResult): Response
{
return new SymfonyRedirectResponse(
$this->router->generate(
$controllerResult->getRouteName(),
$controllerResult->getRouteParams()
),
$controllerResult->getStatusCode(),
$controllerResult->getHeaders()
);
}
}
composer require basster/lazy-response-bundle
final class TwigResponseHandler extends AbstractLazyResponseHandler
{
private Environment $twig;
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
protected function generateResponse(LazyResponseInterface $controllerResult): Response
{
return new Response(
$this->twig->render(
$controllerResult->getTemplate(),
$controllerResult->getData()
),
$controllerResult->getStatus(),
$controllerResult->getHeaders()
);
}
}
composer require basster/lazy-response-bundle
use Basster\LazyResponseBundle\Response\LazyResponseInterface;
use Basster\LazyResponseBundle\Response\RedirectResponse;
use Basster\LazyResponseBundle\Response\TemplateResponse;
class BlogController {
public function commentNew(FormRequest $request, Post $post,
TokenContext $context): LazyResponseInterface
{
$comment = new Comment();
$comment->setAuthor($context->getCurrentUser());
$post->addComment($comment);
if ($formRequest->handle(CommentType::class, $comment)) {
$this->dispatchMessage(new SaveCommentRequest($formRequest->getValidData());
return new RedirectResponse('blog_post', ['slug' => $post->getSlug()]);
}
return new TemplateResponse('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
}
final class StaticPagesController {
private const NO_DATA = [];
/**
* @Route("/imprint")
* @Template("static-pages/imprint.html.twig")
*/
public function imprintAction() : array
{
return static::NO_DATA;
}
/**
* @Route("/terms-of-service")
* @Template("static-pages/terms-of-service.html.twig")
*/
public function termsOfServiceAction() : array
{
return static::NO_DATA;
}
}
# config/routes/static-pages.yaml
my_imprint:
path: /imprint
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: 'static-pages/imprint.html.twig'
maxAge: 86400
sharedAge: 86400
my_terms-of-service:
path: /terms-of-service
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: 'static-pages/terms-of-service.html.twig'
maxAge: 86400
sharedAge: 86400
final class LegacyRedirectController
{
/**
* @Route("/old-legacy-page.html")
*/
public function redirectToNewPage(UrlGeneratorInterface $router): RedirectRoute
{
return new RedirectResponse(
$router->generate('new-shiny-page'),
Response::HTTP_MOVED_PERMANENTLY
);
}
}
# config/routes/legacy-redirects.yaml
legacy_page:
path: /old-legacy-page.html
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController
defaults:
route: new-shiny-page
permanent: true