Upgrade to Pro — share decks privately, control downloads, hide ads and more …

What's new in Symfony 4.4/5.0

What's new in Symfony 4.4/5.0

Jan Schädlich

January 06, 2020
Tweet

More Decks by Jan Schädlich

Other Decks in Programming

Transcript

  1. View Slide

  2. Jan Schädlich
    [email protected]
    @jschaedl
    @janschaedlich

    View Slide

  3. Looking for a Job?
    https://sensiolabs.de/jobs/96454

    View Slide

  4. Agenda
    Deprecations

    Improvements

    New Components

    View Slide

  5. View Slide

  6. 4.4
    Deprecations
    Bugfixes Bugfixes
    Features Features
    Deprecations
    5.0
    What are the Differences?

    View Slide

  7. Deprecations
    Symfony 4.4
    from 4.3 to 4.4
    Code will be removed in Symfony 5.0

    View Slide

  8. Non-int return value in Command::execute()
    Contributed by jschaedl in #33775
    namespace App\Command;
    use Symfony\Component\Console\Command\Command;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Output\OutputInterface;
    class CreateUserCommand extends Command
    {
    protected static $defaultName = 'app:create-user';
    // ...
    protected function execute(InputInterface $input, OutputInterface $output)
    {
    // ...
    }
    }
    Console

    View Slide

  9. Non-int return value in Command::execute()
    Contributed by jschaedl in #33775
    protected function execute(InputInterface $input, OutputInterface $output)
    {
    // ...
    return 0;
    }
    Console

    View Slide

  10. Deprecated tag !tagged in favor of !tagged_iterator
    Contributed by jschaedl in #31321
    # Before
    services:
    App\Handler:
    tags: ['app.handler']
    App\HandlerCollection:
    arguments: [!tagged app.handler]
    # After
    services:
    App\Handler:
    tags: ['app.handler']
    App\HandlerCollection:
    arguments: [!tagged_iterator app.handler]
    DependencyInjection

    View Slide

  11. Deprecated short factories/configurators YAML syntax
    Contributed by nicolas-grekas in #31543
    # Before
    services:
    my_service:
    factory: factory_service:method
    # After
    services:
    my_service:
    factory: ['@factory_service', method]
    DependencyInjection

    View Slide

  12. Deprecate int/float for string input in NumberType
    Contributed by xabbuh in #32130
    use Symfony\Component\Form\Extension\Core\Type\NumberType;
    $form = $this->factory->create(NumberType::class, 2.99, [
    'input' => 'string',
    'scale' => 2,
    ]);
    Form

    View Slide

  13. Deprecated HeaderBag::get() returning an array
    Contributed by Simperfit in #32122
    // Symfony 4.3
    namespace Symfony\Component\HttpFoundation;
    class HeaderBag implements \IteratorAggregate, \Countable
    {
    /**
    * Returns a header value by name.
    *
    * @param string $key The header name
    * @param string|null $default The default value
    * @param bool $first Whether to return the first value or all header values
    *
    * @return string|string[]|null The first header value or default value if $first is true,
    * an array of values otherwise
    */
    public function get($key, $default = null, $first = true) {...}
    }
    HttpFoundation

    View Slide

  14. Deprecated HeaderBag::get() returning an array
    Contributed by Simperfit in #32122
    namespace Symfony\Component\HttpFoundation;
    class HeaderBag implements \IteratorAggregate, \Countable
    {
    // Symfony 4.4, using argument $first is deprecated
    /**
    * @return string|string[]|null The first header value or default value if $first is
    * true, an array of values otherwise
    */
    public function get($key, $default = null) {…}
    // Symfony 5.0
    /**
    * @return string|null The first header value or default value
    */
    public function get(string $key, string $default = null) {...}
    }
    HttpFoundation

    View Slide

  15. Deprecated HeaderBag::get() returning an array
    Contributed by Simperfit in #32122
    class HeaderBag implements \IteratorAggregate, \Countable
    {
    // Symfony 4.4
    public function all(/*string $key = null*/) {…}
    // Symfony 5.0
    public function all(string $key = null) {...}
    }
    HttpFoundation

    View Slide

  16. Deprecated passing arguments to Request::isMethodSafe()
    Contributed by dFayet in #31658
    // Symfony 4.3
    namespace Symfony\Component\HttpFoundation;
    class Request
    /**
    * Checks whether or not the method is safe.
    *
    * @param bool $andCacheable Adds the additional condition that the
    * method should be cacheable. True by default.
    *
    * @return bool
    */
    public function isMethodSafe(/* $andCacheable = true */) {...}
    }
    HttpFoundation

    View Slide

  17. Deprecated passing arguments to Request::isMethodSafe()
    Contributed by dFayet in #31658
    // Symfony 4.3
    $request->isMethodSafe(true); // throws BadMethodCallException
    $request->isMethodSafe(); // throws BadMethodCallException
    $request->isMethodSafe(false);
    // Symfony 4.4
    $request->isMethodSafe(false); // deprecated
    // use
    $request->isMethodSafe();
    $request->isMethodCachable();
    HttpFoundation

    View Slide

  18. Deprecated returning non-boolean
    values from checkCredentials()
    Contributed by derrabus in #33308
    # Symfony 4.3
    # Guard\AuthenticatorInterface.php
    /**
    * Returns true if the credentials are valid.
    *
    * If any value other than true is returned, authentication will
    * fail. You may also throw an AuthenticationException if you wish
    * to cause authentication to fail.
    *
    * The *credentials* are the return value from getCredentials()
    *
    * @param mixed $credentials
    * @return bool
    * @throws AuthenticationException
    */
    public function checkCredentials($credentials, UserInterface $user);
    Security

    View Slide

  19. Deprecated returning non-boolean
    values from checkCredentials()
    Contributed by derrabus in #33308
    # Symfony 4.4
    # Guard\AuthenticatorInterface.php
    /**
    * Returns true if the credentials are valid.
    *
    * If false is returned, authentication will fail. You may also throw
    * an AuthenticationException if you wish to cause authentication to fail.
    *
    * The *credentials* are the return value from getCredentials()
    *
    * @param mixed $credentials
    * @return bool
    * @throws AuthenticationException
    */
    public function checkCredentials($credentials, UserInterface $user);
    Security

    View Slide

  20. Deprecated isGranted() on more than one attribute
    Contributed by wouterj in #33584
    // Before
    if ($this->authorizationChecker->isGranted(['ROLE_USER', 'ROLE_ADMIN'])) {
    // ...
    }
    // After
    if ($this->authorizationChecker->isGranted(
    new Expression("is_granted('ROLE_USER') or is_granted(‘ROLE_ADMIN')"))
    ) {}
    // or
    if ($this->authorizationChecker->isGranted('ROLE_USER')
    || $this->authorizationChecker->isGranted('ROLE_ADMIN')) {}
    Security

    View Slide

  21. Debug Component
    deprecated
    Use the new ErrorHandler Component instead!

    View Slide

  22. WebServer Bundle
    $ curl -sS https://get.symfony.com/cli/installer | bash
    deprecated
    Use the Symfony CLI instead!

    View Slide

  23. └── MyBundle/
    ├── config/
    ├── public/
    ├── src/
    │ └── MyBundle.php
    ├── templates/
    └── translations
    class MyBundle extends Bundle
    {
    public function getPath(): string
    {
    return \dirname(__DIR__);
    }
    }
    Added new Bundle directory convention
    consistent with standard skeleton
    HttpKernel

    View Slide

  24. Type Declarations and Return Types
    https://symfony.com/blog/symfony-type-declarations-return-types
    https://github.com/symfony/symfony/issues/32179

    View Slide

  25. BC-Breaks
    Symfony 4.4
    from 4.3 to 4.4

    View Slide

  26. - [HttpClient] Added method cancel() to ResponseInterface.

    - [Mailer] Changed the DSN to use for disabling delivery (using the NullTransport) from smtp://null to null://null (host doesn't matter).

    - [Mailer] Renamed class SmtpEnvelope to Envelope and DelayedSmtpEnvelope to DelayedEnvelope.

    - [Mailer] Added a required string $transport argument to MessageEvent::__construct.

    - [Messenger] Removed SendersLocatorInterface::getSenderByAlias added in 4.3.

    - [Messenger] Removed $retryStrategies argument from Worker::__construct.

    - [Messenger] Changed arguments of ConsumeMessagesCommand::__construct.

    - [Messenger] Removed $senderClassOrAlias argument from RedeliveryStamp::__construct.

    - [Messenger] Removed UnknownSenderException.

    - [Messenger] Removed WorkerInterface.

    - [Messenger] Removed $onHandledCallback of Worker::run(array $options = [], callable $onHandledCallback = null).

    - [Messenger] Removed StopWhenMemoryUsageIsExceededWorker in favor of StopWorkerOnMemoryLimitListener.

    - [Messenger] Removed StopWhenMessageCountIsExceededWorker in favor of StopWorkerOnMessageLimitListener.

    - [Messenger] Removed StopWhenTimeLimitIsReachedWorker in favor of StopWorkerOnTimeLimitListener.

    - [Messenger] Removed StopWhenRestartSignalIsReceived in favor of StopWorkerOnRestartSignalListener.

    - [Mime] Removed NamedAddress, use Address instead (which supports a name now)

    View Slide

  27. There is even more
    https://github.com/symfony/symfony/blob/master/UPGRADE-4.4.md
    https://github.com/symfony/symfony/blob/master/UPGRADE-5.0.md

    View Slide

  28. Pause?

    View Slide

  29. Improvements

    View Slide

  30. Improved Type Constraint
    Contributed by jschaedl in #31351
    // src/Entity/Author.php
    use Symfony\Component\Validator\Constraints as Assert;
    class Author
    {
    /**
    * @Assert\Type("Ramsey\Uuid\UuidInterface")
    */
    protected $id;
    /**
    * @Assert\Type("string")
    */
    protected $firstName;
    // ...
    }
    Validator

    View Slide

  31. Improved Type Constraint
    Contributed by jschaedl in #31351
    // src/Entity/Author.php
    use Symfony\Component\Validator\Constraints as Assert;
    class Author
    {
    // ...
    /**
    * @Assert\Type(type={"alpha", "digit"})
    */
    protected $accessCode;
    // ...
    }
    Validator

    View Slide

  32. Week Form Type
    Contributed by dFayet in #32061
    $builder->add('startDateTime', WeekType::class, [
    // use this if you store week numbers as strings ('2011-W17')
    'input' => 'string',
    // use this if you store week numbers as arrays (e.g. [2011, 17])
    'input' => 'array',
    // renders two to select the year and week number
    'widget' => 'choice',
    // renders two to write the year and week number
    'widget' => 'text',
    // renders a which is properly rendered by most browsers
    'widget' => 'single_text',
    ]);
    Form

    View Slide

  33. PHP Assertions for Email Messages
    Contributed by fabpot in #32930
    // tests/Controller/DefaultControllerTest.php
    use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    class DefaultControllerTest extends WebTestCase
    {
    public function testSomething()
    {
    $client = static::createClient();
    $client->request('GET', '/newsletter-signup');
    // ...
    $this->assertEmailCount(2);
    $this->assertEmailIsQueued($this->getMailerEvent(0));
    $email = $this->getMailerMessage(0);
    $this->assertEmailHeaderSame($email, 'To', '[email protected]');
    $this->assertEmailTextBodyContains($email, 'Welcome to Symfony!');
    $this->assertEmailAttachementCount($email, 1);
    }
    }
    FrameworkBundle

    View Slide

  34. PHP Assertions for Email Messages
    Contributed by fabpot in #32930
    FrameworkBundle
    $this->assertEmailCount()
    $this->assertQueuedEmailCount()
    $this->assertEmailIsQueued()
    $this->assertEmailIsNotQueued()
    $this->assertEmailAttachementCount()
    $this->assertEmailTextBodyContains()
    $this->assertEmailTextBodyNotContains()
    $this->assertEmailHtmlBodyContains()
    $this->assertEmailHtmlBodyNotContains()
    $this->assertEmailHasHeader()
    $this->assertEmailNotHasHeader()
    $this->assertEmailHeaderSame()
    $this->assertEmailHeaderNotSame()
    $this->assertEmailAddressContains()

    View Slide

  35. Simpler Event Listeners
    Contributed by derrabus in #33851
    EventDispatcher
    use Symfony\Component\HttpKernel\Event\RequestEvent;
    final class MyRequestListener
    {
    public function __invoke(RequestEvent $event): void
    {
    // ...
    }
    }
    services:
    App\EventListener\MyRequestListener:
    tags:
    - - { name: kernel.event_listener, event: kernel.request }
    + - { name: kernel.event_listener }
    # config/services.yaml
    services:
    App\EventListener\:
    resource: ../src/EventListener/*
    tags: ['kernel.event_listener']

    View Slide

  36. Allow Binding Tagged Services
    Contributed by lyrixx in #33623
    services:
    _instanceof:
    App\Foo\Rule\RuleInterface:
    tags: ['app.foo.rule']
    _defaults:
    bind:
    iterable $rules: !tagged_iterator app.foo.rule
    # ...
    DependencyInjection

    View Slide

  37. Improved YAML Syntax for Method Calls
    Contributed by lyrixx in #33623
    services:
    App\Service\MessageGenerator:
    # ...
    calls:
    - method: setLogger
    arguments:
    - '@logger'
    DependencyInjection
    Contributed by nicolas-grekas in #33779.
    services:
    App\Service\MessageGenerator:
    # ...
    calls:
    - setLogger: ['@logger']

    View Slide

  38. Priorities for Tagged Services
    Contributed by lyrixx in #33623
    services:
    _instanceof:
    App\Handler:
    tags:
    - { name: 'app.handler', priority: 20 }
    App\HandlerCollection:
    arguments: [!tagged_iterator app.handler]
    DependencyInjection
    services:
    # ...
    App\HandlerCollection:
    arguments: [!tagged_iterator app.handler, default_priority_method: 'calculateServicePriority']
    final class MyService
    {
    public static function getDefaultPriority(): int
    {
    return 0; }
    }
    }

    View Slide

  39. Mailer Integration
    Contributed by fabpot in #32912
    WebProfilerBundle

    View Slide

  40. HttpClient Integration
    Contributed by tyx and jeremyFreeAgent in #33015
    WebProfilerBundle

    View Slide

  41. Clear Ajax Requests
    Contributed by Matts in #31876
    WebProfilerBundle

    View Slide

  42. Service Container Linter
    DependencyInjection
    Contributed by alcalyn, GuilhemN and nicolas-grekas in #33015
    $ bin/console lint:container

    View Slide

  43. Service Container Linter
    DependencyInjection
    Contributed by alcalyn, GuilhemN and nicolas-grekas in #33015
    namespace App\SomeNamespace;
    class SomeService
    {
    public function __construct(int $someProperty = 7)
    {
    // ...
    }
    }
    services:
    App\SomeNamespace\SomeService: ~
    Invalid definition for service "App\SomeNamespace\SomeService": argument 1 of
    "App\SomeNamespace\SomeService::__construct" accepts "int", "NULL" passed.

    View Slide

  44. Service Container Linter
    DependencyInjection
    Contributed by alcalyn, GuilhemN and nicolas-grekas in #33015
    namespace App\SomeNamespace;
    class SomeService
    {
    public function setSomeItems(
    SomeClass $item,
    SomeClass …$items
    ) {
    // ...
    }
    }
    Invalid definition for service "App\SomeNamespace\SomeService": argument 2 of
    "App\SomeNamespace\SomeService::setSomeItems" accepts "App\SomeNamespace\SomeClass",
    "App\AnotherNamespace\SomeDifferentClass" passed.
    services:
    foo:
    class: App\SomeNamespace\SomeClass
    bar:
    class: App\AnotherNamespace\SomeDifferentClass
    App\SomeNamespace\SomeService:
    calls:
    - method: setSomeItems
    arguments:
    - '@foo'
    - '@bar'

    View Slide

  45. Notification Emails
    Contributed by fabpot in #33605
    use Symfony\Bridge\Twig\Mime\NotificationEmail;
    $email = (new NotificationEmail())
    ->from('[email protected]')
    ->to('[email protected]')
    ->subject('My first notification email via Symfony')
    ->markdown(<<There is a **problem** on your website, you should investigate it
    right now. Or just wait, the problem might solves itself automatically,
    we never know.
    EOF
    )
    ->action('More info?', 'https://example.com/')
    ->importance(NotificationEmail::IMPORTANCE_HIGH)
    ;
    Mime

    View Slide

  46. Notification Emails
    Contributed by fabpot in #33605
    Mime

    View Slide

  47. Lazy Firewalls
    Contributed by nicolas-grekas in #33676
    # config/packages/security.yaml
    security:
    # ...
    firewalls:
    main:
    pattern: ^/
    anonymous: ~
    # ...
    Security
    # config/packages/security.yaml
    security:
    # ...
    firewalls:
    main:
    pattern: ^/
    anonymous: lazy
    # ...

    View Slide

  48. Added Support for Alpha-3 Codes
    Contributed by creiner in #33791
    use Symfony\Component\Form\Extension\Core\Type\CountryType;
    // ...
    $builder->add('country', CountryType::class, [
    'alpha3' => true, // ISO 3166-1, e.g deu
    ]);
    Form

    View Slide

  49. Password Migrations
    Contributed by nicolas-grekas in #31594, #31597 and #31843
    # config/packages/security.yaml
    security:
    # ...
    encoders:
    App\Entity\User:
    algorithm: auto
    cost: 14
    Security
    use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
    class UserRepository extends EntityRepository implements PasswordUpgraderInterface
    {
    public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
    {
    // this code is only an example; the exact code will depend on
    $user->setPassword($newEncodedPassword);
    $this->getEntityManager()->flush($user);
    }
    }

    View Slide

  50. Password Hashing
    Contributed by chalas_r in #34020 and #34139
    security:
    # ...
    encoders:
    App\Entity\User:
    algorithm: 'argon2i'
    algorithm: 'argon2id'
    algorithm: 'auto'
    algorithm: 'bcrypt'
    algorithm: 'sodium'
    Security
    # config/packages/security.yaml
    security:
    # ...
    encoders:
    App\Entity\User:
    algorithm: 'argon2i'
    migrate_from: 'bcrypt'

    View Slide

  51. Preloading Symfony
    Applications in PHP 7.4

    View Slide

  52. OPCache Preloading in Practice
    ; php.ini
    opcache.preload=/path/to/the/preload.php

    View Slide

  53. OPCache Preloading in Symfony
    ; php.ini
    opcache.preload=/path/to/project/var/cache/prod/App_KernelProdContainer.preload.php

    View Slide

  54. OPCache Preloading in Symfony
    namespace Symfony\Component\HttpKernel;
    abstract class Kernel implements KernelInterface, RebootableInterface, TerminableInterface
    {
    /**
    * Gets the classes to list in the preloading script.
    *
    * When a class is listed, all its parent classes or interfaces are automatically listed too.
    * Service classes are also automatically preloaded and don't need to be listed explicitly.
    */
    public function getClassesToPreload(): array
    {
    //...
    }
    }

    View Slide

  55. OPCache Preloading in Symfony
    namespace Symfony\Component\HttpKernel\DependencyInjection;
    abstract class Extension extends BaseExtension
    {
    /**
    * Adds classes to list in the preloading script.
    *
    * When a class is listed, all its parent classes or interfaces are automatically listed too.
    * Service classes are also automatically preloaded and don't need to be listed explicitly.
    */
    public function addClassesToPreload(array $preloadedClasses): void
    {
    //...
    }
    }

    View Slide

  56. There is even more
    https://symfony.com/blog/category/living-on-the-edge/5.0-4.4
    https://symfony.com/blog/symfony-4-4-curated-new-features
    https://symfony.com/blog/symfony-5-0-curated-new-features

    View Slide

  57. Pause?

    View Slide

  58. New Components

    View Slide

  59. ErrorHandler Component
    Symfony 4.4
    The ErrorHandler component provides tools to
    manage errors and ease debugging PHP code.
    $ composer require symfony/error-handler

    View Slide

  60. # public/index.php
    use App\Kernel;
    // Before
    use Symfony\Component\Debug\Debug;
    require dirname(__DIR__).'/config/bootstrap.php';
    if ($_SERVER['APP_DEBUG']) {
    umask(0000);
    Debug::enable();
    }
    // ...
    $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
    $request = Request::createFromGlobals();
    $response = $kernel->handle($request);
    $response->send();
    $kernel->terminate($request, $response);

    View Slide

  61. # public/index.php
    use App\Kernel;
    // After
    use Symfony\Component\ErrorHandler\Debug;
    require dirname(__DIR__).'/config/bootstrap.php';
    if ($_SERVER['APP_DEBUG']) {
    umask(0000);
    Debug::enable();
    }
    // ...
    $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
    $request = Request::createFromGlobals();
    $response = $kernel->handle($request);
    $response->send();
    $kernel->terminate($request, $response);

    View Slide

  62. Class Loading Debugger
    use Symfony\Component\ErrorHandler\DebugClassLoader;
    DebugClassLoader::enable();

    View Slide

  63. Turning PHP Errors into Exeptions
    use Symfony\Component\ErrorHandler\ErrorHandler;
    ErrorHandler::register();

    View Slide

  64. Catching PHP Function Errors and
    turning them into Exceptions
    data = json_decode(file_get_contents($filename), true);
    $data['read_at'] = date($datetimeFormat);
    file_put_contents($filename, json_encode($data));

    View Slide

  65. Catching PHP Function Errors and
    turning them into Exceptions
    $content = @file_get_contents($filename);
    if (false === $content) {
    throw new \RuntimeException('Could not load file.');
    }
    $data = @json_decode($content, true);
    if (null === $data) {
    throw new \RuntimeException('File does not contain valid JSON.');
    }
    $datetime = @date($datetimeFormat);
    if (false === $datetime) {
    throw new \RuntimeException('Invalid datetime format.');
    }

    View Slide

  66. Catching PHP Function Errors and
    turning them into Exceptions
    $content = ErrorHandler::call('file_get_contents', $filename);

    View Slide

  67. Catching PHP Function Errors and
    turning them into Exceptions
    $data = ErrorHandler::call(static function () use ($filename,
    $datetimeFormat) {
    // if any code executed inside this anonymous function fails,
    // a PHP exception will be thrown, even if the code
    // uses the '@' PHP silence operator
    $data = json_decode(file_get_contents($filename), true);
    $data['read_at'] = date($datetimeFormat);
    file_put_contents($filename, json_encode($data));
    return $data;
    });

    View Slide

  68. {
    "title": "Not Found",
    "status": 404,
    "detail": "Sorry, the page you are looking for could not be found"
    }
    Error pages for non-HTML formats
    templates/bundles/TwigBundle/Exception/error403.json.twig
    Request Format
    JSON XML ATOM TXT RFC 7807
    deprecated

    View Slide

  69. Deprecated error templates for non-html formats
    Contributed by yceruto in #31398
    # If you were not using this option previously, set it to `null`
    twig:
    exception_controller: null
    # If you were using this option previously, set it to `null`
    # and use `framework.error_controller` instead
    # Before
    twig:
    exception_controller: 'App\Controller\MyExceptionController'
    # After
    twig:
    exception_controller: null
    framework:
    error_controller: 'App\Controller\MyExceptionController'

    View Slide

  70. How to customize error pages for non-HTML formats?
    // templates/bundles/TwigBundle/Exception/error.json.twig
    {
    "type": "https://example.com/error",
    "title": "{{ status_text }}",
    "status": {{ status_code }}
    }

    View Slide

  71. How to customize error pages for non-HTML formats?
    class ProblemJsonNormalizer implements NormalizerInterface
    {
    public function normalize($exception, $format = null, array $context = [])
    {
    return [
    'type' => 'https://example.com/error',
    'title' => $exception->getStatusText(),
    'status' => $exception->getStatusCode(),
    ];
    }
    public function supportsNormalization($data, $format = null)
    {
    return 'json' === $format && $data instanceof FlattenException;
    }
    }

    View Slide

  72. Custom HTML error pages based on Twig
    keep working as before.
    templates/bundles/TwigBundle/Exception/error500.html.twig

    View Slide

  73. Error preview pages
    - # config/routes/dev/twig.yaml
    + # config/routes/dev/framework.yaml
    _errors:
    - resource: '@TwigBundle/Resources/config/routing/errors.xml'
    + resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
    prefix: /_error
    The error page preview feature keeps working as before,
    but some files have changed their location.

    View Slide

  74. Notifier Component
    Symfony 5.0
    experimental
    https://www.meetup.com/de-DE/sfughh/events/xqdjjrybcdbgb/

    View Slide

  75. String Component
    Symfony 5.0
    experimental
    https://www.meetup.com/de-DE/sfughh/events/xqdjjrybcdbgb/

    View Slide

  76. Questions?

    View Slide

  77. View Slide

  78. Thank you!
    https://speakerdeck.com/jschaedl

    View Slide