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

The Evolution of Symfony: Now and to the Future!

weaverryan
November 18, 2022

The Evolution of Symfony: Now and to the Future!

With the release of 6.2, Symfony will contain somewhere near *500* new features from just the past 12 months! Wow!

And, while many of these are small, the landscape of *how* Symfony apps are developed continues to evolve. In this talk, we'll look at some of the most important changes, from new dependency injection attributes, framework-extra bundle features in Symfony, Symfony UX, testing, and more! Basically... a quick catch-up on 12 months, *thousands* of commits, and hundreds of releases across numerous repositories. Let's go!

weaverryan

November 18, 2022
Tweet

More Decks by weaverryan

Other Decks in Programming

Transcript

  1. with your friend Ryan Weaver The Evolution of Symfony: Now

    and to the Future! @weaverryan
  2. with your friend Mickey Mouse The Evolution of Symfony: Now

    and to the Future! @weaverryan
  3. 
 > Author at SymfonyCasts.com symfonycasts.com twitter.com/weaverryan Hello there! I’m

    Ryan! > Husband of the talented and beloved @leannapelham > Father to my much more imaginative son, Beckett > I work at Disney, as a mouse Mickey > On the core team, Encore, UX, animated fi lms, etc
  4. The Latest in Mouse Animation @weaverryan (modernization: PHP 8+ &

    Symfony)
  5. PHP 8.0 (2 years old) - Attributes - Property Promotion

    - Named Arguments - $null?->safeOp() - Trailing arg commas - Stringable @weaverryan PHP 8.1 (1 year old) - Enums - Readonly props - Callable syntax - New in initializers PHP 8.2 (-1 week) - Readonly classes
  6. Adventure #1 Entities @weaverryan

  7. /** @ORM\Entity(repositoryClass=UserRepository::class) */ class User implements UserInterface, PasswordAuthenticatedUserInterface { /**

    * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private $id; /** @ORM\Column(type="string", unique=true) */ private $email; /** @ORM\Column(type="json") */ private $roles = []; /** @ORM\ManyToOne(targetEntity=Address::class) */ private $address; }
  8. #[ORM\Entity(repositoryClass: UserRepository::class)] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue]

    #[ORM\Column(type: Types::INTEGER)] private $id; #[ORM\Column(type: Types::STRING, unique: true)] private $email; #[ORM\Column(type: Types::JSON)] private $roles = []; #[ORM\ManyToOne(targetEntity: Address::class)] private $address; }
  9. #[ORM\Entity(repositoryClass: UserRepository::class)] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue]

    #[ORM\Column(type: Types::INTEGER)] private ?int $id = null; #[ORM\Column(type: Types::STRING, unique: true)] private ?string $email = null; #[ORM\Column(type: Types::JSON)] private array $roles = []; #[ORM\ManyToOne(targetEntity: Address::class)] private ?Address $address = null; }
  10. #[ORM\Entity(repositoryClass: UserRepository::class)] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue]

    #[ORM\Column()] private ?int $id = null; #[ORM\Column(unique: true)] private ?string $email = null; #[ORM\Column] private array $roles = []; #[ORM\ManyToOne()] private ?Address $address = null; }
  11. Adventure #2 Controllers @weaverryan

  12. /** * @Route("/products/edit/{id}", name="product_edit") * @IsGranted("product_edit", subject="product") */ public function

    edit(Product $product) { }
  13. #[Route('/products/edit/{id}', name: 'product_edit')] #[IsGranted('product_edit', subject: 'product')] public function edit(Product $product):

    Response { }
  14. So long, and thanks for all the fi sh

  15. #[Route('/products/edit/{id}')] public function edit(Product $product): Response { $user = $this->getUser();

    }
  16. #[Route('/products/edit/{id}')] public function edit(Product $product, #[CurrentUser] User $user) { }

    * #[CurrentUser] is not new. But playing nice with the "entity resolver" IS new
  17. Adventure #3 Services @weaverryan

  18. class PlotFactory { private CharacterGenerator $characterGenerator; private ConflictCreator $conflictCreator; private

    LoveableSidekickBuilder $sidekickBuilder; public function __construct( CharacterGenerator $characterGenerator, ConflictCreator $conflictCreator, LoveableSidekickBuilder $sidekickBuilder, ) { $this->characterGenerator = $characterGenerator; $this->conflictCreator = $conflictCreator; $this->sidekickBuilder = $sidekickBuilder; } } 🥱
  19. class PlotFactory { public function __construct( private CharacterGenerator $characterGenerator, private

    ConflictCreator $conflictCreator, private LoveableSidekickBuilder $sidekickBuilder, ) { } } Property Promotion!
  20. class PlotFactory { public function __construct( private readonly CharacterGenerator $characterGenerator,

    private readonly ConflictCreator $conflictCreator, private readonly LoveableSidekickBuilder $sidekickBuilder, ) { } } Hipster properties
  21. One of Symfony's Goals: Keep you working on YOUR code

    @weaverryan
  22. class PlotFactory { public function __construct( private bool $isDebug, private

    LoaderInterface $twigLoader, ) { } } Non-autowireable args
  23. services: # ... App\PlotFactory: arguments: $isDebug: '%kernel.debug%' $twigLoader: '@twig.loader' Go

    to a YAML fi le??
  24. class PlotFactory { public function __construct( private bool $isDebug, private

    LoaderInterface $twigLoader, ) { } } #[Autowire('%kernel.debug%')] #[Autowire(service: 'twig.loader')]
  25. Another need for YAML Tags that don't work with autocon

    fi guration @weaverryan
  26. class MovieCreditsListener { public function onKernelRequest(RequestEvent $event) { } }

  27. services: # ... App\MovieCreditsListener: tags: - name: 'kernel.event_listener' event: 'request.event'

    method: 'onKernelRequest'
  28. #[AutoconfigureTag('kernel.event_listener', [ 'event' => RequestEvent::class, 'method' => 'onKernelRequest', ])] class

    MovieCreditsListener { public function onKernelRequest(RequestEvent $event) { } }
  29. #[AsEventListener(RequestEvent::class, 'onKernelRequest')] class MovieCreditsListener { public function onKernelRequest(RequestEvent $event) {

    } }
  30. #[AsEntityListener(Events::prePersist, entity: User::class)] class UserEntityListener { public function prePersist(User $user)

    { } }
  31. #[AsEventListener(RequestEvent::class, 'onKernelRequest')] #[AsCommand(name: 'debug:form', description: '...')] #[AsController] #[AsMessageHandler] #[AsRoutingConditionService] #[AsEntityListener(Events::prePersist,

    entity: User::class)]
  32. Any YAML left? Service Decoration @weaverryan

  33. class DecoratedRouter implements RouterInterface { public function __construct( private RouterInterface

    $router, private LoggerInterface $logger ) { } public function generate(string $name, array $parameters = []) { $this->logger->info('Generating route', [ 'name' => $name, ]); return $this->router->generate($name, $parameters); } }
  34. services: # ... App\DecoratedRouter: decorates: 'router'

  35. #[AsDecorator(decorates: 'router')] class DecoratedRouter implements RouterInterface { public function __construct(

    private RouterInterface $router, private LoggerInterface $logger ) { } public function generate(string $name, array $parameters = []) { $this->logger->info('Generating route', [ 'name' => $name, ]); return $this->router->generate($name, $parameters); } }
  36. This is the way @weaverryan

  37. Bounty Hunting for Bugs @weaverryan (Debugging improvements)

  38. php bin/console mailer:test [email protected]

  39. php bin/console messenger:failed:show --class-filter=MyClass php bin/console messenger:failed:show --stats php bin/console

    messenger:count
  40. php bin/console debug:container ConflictCreator

  41. Completely Redesigned: Web Debug Toolbar & Pro fi ler

  42. None
  43. None
  44. None
  45. Bounty: Spoils of Hard Work @weaverryan (New Goodies)

  46. #[Route('/products/edit/{id}', name: 'product_edit')] public function editProduct(Product $product): Response { $form

    = $this->createForm(ProductFormType::class, $product); // ... return $this->render('product/edit.html.twig', [ 'form' => $form, ]); } $this->renderForm() $form->createView() ❌ ❌ 422 status code ✅
  47. class PlotFactory { public function __construct( private HeavyDependency $heavyDependency )

    { } } #[Autoconfigure(lazy: true)]
  48. #[Route('/movie/create', name: 'create_movie')] public function createMovieAction(PlotFactory $plotFactory) { dump($plotFactory); //

    ... }
  49. #[Route('/movie/create', name: 'create_movie')] public function createMovieAction(PlotFactory $plotFactory) { if ($someRareCondition)

    { $plotFactory->rewritePlot(); } // ... }
  50. #[Route('/register')] public function register(Security $security): Response { // ... //

    ... } $security->login($user, 'form_login');
  51. #[Route('/something')] public function something(Security $security): Response { // ... $security->logout(validateCsrfToken:

    true); // ... }
  52. security: # ... main: # ... access_token: token_handler: App\Security\AccessTokenHandler

  53. class AccessTokenHandler implements AccessTokenHandlerInterface { public function getUserIdentifierFrom(string $token): string

    { $accessToken = $this->repository->findOneByValue($token); if (null === $accessToken || !$accessToken->isValid()) { throw new BadCredentialsException('Invalid credentials.'); } return $accessToken->getUserId(); } }
  54. HtmlSanitizer Component https://symfony.com/blog/new-in-symfony-6-1-htmlsanitizer-component Clock Component https://symfony.com/blog/new-in-symfony-6-2-clock-component LocaleSwitcher https://symfony.com/blog/new-in-symfony-6-1-locale-switcher Emoji Insanity

    https://symfony.com/blog/new-in-symfony-6-2-better-emoji-support
  55. The Light Side of the Force @weaverryan UI's with Symfony

    UX
  56. 6 Big Releases Dec 2021 v2.0 Mar 2022 v2.1 Jun

    2022 v2.2 Jul 2022 v2.3 Aug 2022 v2.4 Nov 2022 v2.5
  57. 7 New Components! • autocomplete $builder ->add('foods', ChoiceType::class, [ 'autocomplete'

    => true ])
  58. #[AsTwigComponent] class SearchPackagesComponent { public function __construct(private PackageRepository $packageRepo) {

    } public function getPackages(): array { return $this->packageRepo->findAll(); } } <div {{ attributes }}> <input> {% for package in packages %} ... {% endfor %} </div> 7 New Components! • autocomplete • twig-component
  59. #[AsLiveComponent] class SearchPackagesComponent { #[LiveProp(writable: true)] public string $search =

    ''; // ... public function getPackages(): array { return $this->packageRepo->findAll($this->search); } } <div {{ attributes }}> <input data-model="search"> {% for package in packages %} ... {% endfor %} </div> Live Components
  60. <div {{ vue_component('PackageSearch', { packages: packagesData }) }}></div> <div {{

    react_component('PackageSearch', { packages: packagesData }) }}></div> • autocomplete • twig-component • live-component • react • vue • notify • typed 7 New Components!
  61. ux.symfony.com

  62. And…

  63. symfony-ux NO LONGER EXPERIMENTAL! it is* * except for LiveComponents

  64. Protecting the Galaxy @weaverryan (Celebrating the Changes in the Symfony

    Ecosystem)
  65. Webpack Encore Dec 2021 v1.7.0 May 2022 v2.0.0 Jul 2022

    v3.0.0 Sep 2022 v4.0.0 Easy Upgrade Path
  66. MakerBundle 10 new minor releases symfony/ fl ex composer recipes:update

    API Platform Major V3!!! EasyAdmin 4.0 -> 4.4 zenstruck/browser 1.0! Noti fi ers 25+ new noti fi er bridges!
  67. class BlogPostTest extends TestCase { use HasBrowser; public function testViewPostAndAddComment()

    { $post = PostFactory::new()->create(['title' => 'My First Post']); $this->browser() // $this->pantherBrowser() ->visit("/posts/{$post->getId()}") ->assertSuccessful() ->assertSeeIn('title', 'My First Post') ->assertSeeIn('h1', 'My First Post') ->assertNotSeeElement('#comments') ->fillField('Comment', 'My First Comment') ->click('Submit') ->assertOn("/posts/{$post->getId()}") ->assertSeeIn('#comments', 'My First Comment') ; } }
  68. Clone Wars @weaverryan

  69. symfony/symfony 500 contributors 4000+ commits symfony/symfony-docs 290 contributors 3,000+ commits

    symfony/ux 50+ contributors 500+ commits symfony/maker-bundle 30+ contributors 250+ commits
  70. "Through The Force, Things You Will See. Other Places. The

    Future, The Past" @weaverryan What's coming next?
  71. None
  72. None
  73. Symfony is YOU!

  74. Symfony UX • Svelte? • Form Collection? • Date Field?

    • Upload? • Rich Text Editor? UX Live Components: parity with Livewire
  75. zenstruck/document-library // $library wraps around Flysystem $document = $library->open('path/to/file.txt'); $document->path();

    $document->size(); $document->contents(); $document->publicUrl(); $document->temporaryUrl('+30 minutes'); $document->store('some/path.txt', $contents);
  76. #[ORM\Entity] class Product { #[Mapping(library: 'public')] #[ORM\Column(type: Document::class)] public ?Document

    $image = null; } /** @var UploadedFile $file */ $product->image = new PendingDocument($file); $entityManager->persist($product); $entityManager->flush(); // on next request $product = $repository->find($id); $product->image->temporaryUrl('+10 minutes'); $product->image->contents();
  77. None
  78. None
  79. But most of all @weaverryan

  80. It's Symfony so… @weaverryan

  81. What will we build together? @weaverryan

  82. Thank you! @weaverryan ❤❤❤

  83. Next year, where will the conference be? @weaverryan No idea

    I have. Hrmm