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

    View Slide

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

    View Slide


  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

    View Slide

  4. The Latest in Mouse
    Animation
    @weaverryan
    (modernization: PHP 8+
    & Symfony)


    View Slide

  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

    View Slide

  6. Adventure #1


    Entities
    @weaverryan

    View Slide

  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;


    }

    View Slide

  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;


    }

    View Slide

  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;


    }

    View Slide

  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;


    }

    View Slide

  11. Adventure #2


    Controllers
    @weaverryan

    View Slide

  12. /**


    * @Route("/products/edit/{id}", name="product_edit")


    * @IsGranted("product_edit", subject="product")


    */


    public function edit(Product $product)


    {


    }


    View Slide

  13. #[Route('/products/edit/{id}', name: 'product_edit')]


    #[IsGranted('product_edit', subject: 'product')]


    public function edit(Product $product): Response


    {


    }


    View Slide

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

    View Slide

  15. #[Route('/products/edit/{id}')]


    public function edit(Product $product): Response


    {


    $user = $this->getUser();


    }

    View Slide

  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

    View Slide

  17. Adventure #3


    Services
    @weaverryan

    View Slide

  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;


    }


    }


    🥱

    View Slide

  19. class PlotFactory


    {


    public function __construct(


    private CharacterGenerator $characterGenerator,


    private ConflictCreator $conflictCreator,


    private LoveableSidekickBuilder $sidekickBuilder,


    )


    {


    }


    }


    Property Promotion!

    View Slide

  20. class PlotFactory


    {


    public function __construct(


    private readonly CharacterGenerator $characterGenerator,


    private readonly ConflictCreator $conflictCreator,


    private readonly LoveableSidekickBuilder $sidekickBuilder,


    )


    {


    }


    }
    Hipster properties

    View Slide

  21. One of Symfony's Goals:


    Keep you working on YOUR
    code
    @weaverryan

    View Slide

  22. class PlotFactory


    {


    public function __construct(


    private bool $isDebug,


    private LoaderInterface $twigLoader,


    )


    {


    }


    }
    Non-autowireable args

    View Slide

  23. services:


    # ...


    App\PlotFactory:


    arguments:


    $isDebug: '%kernel.debug%'


    $twigLoader: '@twig.loader'
    Go to a YAML
    fi
    le??

    View Slide

  24. class PlotFactory


    {


    public function __construct(


    private bool $isDebug,


    private LoaderInterface $twigLoader,


    )


    {


    }


    }
    #[Autowire('%kernel.debug%')]
    #[Autowire(service: 'twig.loader')]

    View Slide

  25. Another need for YAML


    Tags that don't work with
    autocon
    fi
    guration
    @weaverryan

    View Slide

  26. class MovieCreditsListener


    {


    public function onKernelRequest(RequestEvent $event)


    {


    }


    }

    View Slide

  27. services:


    # ...


    App\MovieCreditsListener:


    tags:


    -


    name: 'kernel.event_listener'


    event: 'request.event'


    method: 'onKernelRequest'


    View Slide

  28. #[AutoconfigureTag('kernel.event_listener', [


    'event' => RequestEvent::class,


    'method' => 'onKernelRequest',


    ])]


    class MovieCreditsListener


    {


    public function onKernelRequest(RequestEvent $event)


    {


    }


    }

    View Slide

  29. #[AsEventListener(RequestEvent::class, 'onKernelRequest')]


    class MovieCreditsListener


    {


    public function onKernelRequest(RequestEvent $event)


    {



    }


    }

    View Slide

  30. #[AsEntityListener(Events::prePersist, entity: User::class)]


    class UserEntityListener


    {


    public function prePersist(User $user)


    {


    }


    }

    View Slide

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

    View Slide

  32. Any YAML left?


    Service Decoration
    @weaverryan

    View Slide

  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);


    }


    }

    View Slide

  34. services:


    # ...


    App\DecoratedRouter:


    decorates: 'router'

    View Slide

  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);


    }


    }

    View Slide

  36. This is the way
    @weaverryan

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  40. php bin/console debug:container ConflictCreator

    View Slide

  41. Completely Redesigned:


    Web Debug Toolbar & Pro
    fi
    ler

    View Slide

  42. View Slide

  43. View Slide

  44. View Slide

  45. Bounty: Spoils of Hard Work
    @weaverryan
    (New Goodies)

    View Slide

  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

    View Slide

  47. class PlotFactory


    {


    public function __construct(


    private HeavyDependency $heavyDependency


    )


    {


    }


    }
    #[Autoconfigure(lazy: true)]

    View Slide

  48. #[Route('/movie/create', name: 'create_movie')]


    public function createMovieAction(PlotFactory $plotFactory)


    {


    dump($plotFactory);




    // ...


    }

    View Slide

  49. #[Route('/movie/create', name: 'create_movie')]


    public function createMovieAction(PlotFactory $plotFactory)


    {


    if ($someRareCondition) {


    $plotFactory->rewritePlot();


    }




    // ...


    }

    View Slide

  50. #[Route('/register')]


    public function register(Security $security): Response


    {


    // ...





    // ...


    }
    $security->login($user, 'form_login');

    View Slide

  51. #[Route('/something')]


    public function something(Security $security): Response


    {


    // ...


    $security->logout(validateCsrfToken: true);




    // ...


    }

    View Slide

  52. security:


    # ...


    main:


    # ...


    access_token:


    token_handler: App\Security\AccessTokenHandler


    View Slide

  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();


    }


    }


    View Slide

  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

    View Slide

  55. The Light Side of the Force
    @weaverryan
    UI's with Symfony UX

    View Slide

  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

    View Slide

  57. 7 New Components!
    • autocomplete
    $builder


    ->add('foods', ChoiceType::class, [


    'autocomplete' => true


    ])


    View Slide

  58. #[AsTwigComponent]


    class SearchPackagesComponent


    {


    public function __construct(private PackageRepository $packageRepo)


    {


    }


    public function getPackages(): array


    {


    return $this->packageRepo->findAll();


    }


    }








    {% for package in packages %}


    ...


    {% endfor %}





    7 New Components!
    • autocomplete


    • twig-component

    View Slide

  59. #[AsLiveComponent]


    class SearchPackagesComponent


    {


    #[LiveProp(writable: true)]


    public string $search = '';


    // ...


    public function getPackages(): array


    {


    return $this->packageRepo->findAll($this->search);


    }


    }






    {% for package in packages %}


    ...


    {% endfor %}





    Live Components

    View Slide



  60. packages: packagesData


    }) }}>


    packages: packagesData


    }) }}>
    • autocomplete


    • twig-component


    • live-component


    • react


    • vue


    • notify


    • typed
    7 New Components!

    View Slide

  61. ux.symfony.com

    View Slide

  62. And…

    View Slide

  63. symfony-ux


    NO LONGER EXPERIMENTAL! it is*
    * except for LiveComponents

    View Slide

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

    View Slide

  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

    View Slide

  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!

    View Slide

  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')


    ;


    }


    }

    View Slide

  68. Clone Wars
    @weaverryan

    View Slide

  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

    View Slide

  70. "Through The Force, Things
    You Will See. Other Places. The
    Future, The Past"
    @weaverryan
    What's coming next?

    View Slide

  71. View Slide

  72. View Slide

  73. Symfony is YOU!

    View Slide

  74. Symfony UX
    • Svelte?


    • Form Collection?


    • Date Field?


    • Upload?


    • Rich Text Editor?
    UX Live Components: parity with Livewire

    View Slide

  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);

    View Slide

  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();

    View Slide

  77. View Slide

  78. View Slide

  79. But most of all
    @weaverryan

    View Slide

  80. It's Symfony so…
    @weaverryan

    View Slide

  81. What will we build together?
    @weaverryan

    View Slide

  82. Thank you!
    @weaverryan
    ❤❤❤

    View Slide

  83. Next year, where will


    the conference be?
    @weaverryan
    No idea I have. Hrmm

    View Slide