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

Réinventer le composant Console de Symfony

Réinventer le composant Console de Symfony

Console est le composant Symfony le plus utilisé. Des outils critiques comme Composer jusqu'aux autres frameworks PHP populaires, en passant par nos applications finales, il est omniprésent. L'inconvénient de cela est que changer quoi que ce soit n'est pas une mince affaire. Même le plus petit bug fix est susceptible de casser des milliers d'usages. Néanmoins, le composant s'améliore constamment grâce aux innombrables contributions qu'il reçoit depuis son introduction en 2010, tout en conservant sa rétrocompatibilité. Mais nous pensons qu'il est temps de faire peau neuve, notamment pour ouvrir le composant à davantage de possibilités et le débarrasser de certains problèmes de design. C'est pourquoi nous, quelques contributeurs clés dont Théo Fidry, Kevin Bond et moi-même, avons travaillé intensivement à le revisiter. C'est ce ce que je vais vous présenter dans ce talk. Préparez-vous à redécouvrir la Console !

Robin Chalas

March 24, 2023
Tweet

More Decks by Robin Chalas

Other Decks in Programming

Transcript

  1. Console Revisitée

    View Slide

  2. chalasr
    ROBIN CHALAS
    @chalas_r
    les-tilleuls.coop

    View Slide

  3. The Console component eases the
    creation of beautiful and testable
    command line interfaces.

    @chalas_r

    View Slide

  4. EN CHIFFRES
    500M
    AU TOTAL
    @chalas_r
    450k
    PAR JOUR

    View Slide

  5. @chalas_r
    Composer
    Symfony
    Doctrine
    Laravel
    API Platform
    Drupal
    Sylius
    Magento
    PHPStan
    ...

    View Slide

  6. Conséquence :
    Le moindre bugfix est un BC break.
    @chalas_r

    View Slide

  7. @chalas_r

    View Slide

  8. Tu ne le sais peut-être pas,
    mais ton projet en dépend !
    @chalas_r

    View Slide

  9. Enregistrer une commande, c'était
    comment avant ?
    // src/AppBundle/Command/FooCommand.php
    namespace AppBundle\Command;
    class FooCommand extends Command
    {
    protected function configure()
    {
    $this->setName('app:foo');
    }
    protected function execute()
    {
    // ...
    }
    }
    @chalas_r

    View Slide

  10. Tout était convention
    // src/Symfony/Component/HttpKernel/Bundle/Bundle.php
    public function registerCommands(Application $application)
    {
    if (!is_dir($dir = $this->getPath().'/Command')) {
    return;
    }
    $finder = new Finder();
    $finder->files()->name('*Command.php')->in($dir);
    foreach ($finder as $file) {
    $class = $file->getBasename('.php');
    $r = new \ReflectionClass($class);
    $application->add($r->newInstance());
    }
    }
    @chalas_r

    View Slide

  11. Commands as services à la
    rescousse !
    class FooCommand extends Command
    {
    protected function configure()
    {
    $this->setName('app:foo');
    }
    protected function execute()
    {
    // ...
    }
    }
    services:
    App\Command\FooCommand:
    tags:
    - { name: 'console.command' }
    @chalas_r

    View Slide

  12. Laziness?
    @chalas_r

    View Slide

  13. Laziness - Round 1
    @chalas_r

    View Slide

  14. Laziness Round 1
    ROUTING EN AMONT
    @chalas_r

    View Slide

  15. ROUTING EN AMONT
    class FooCommand extends Command
    {
    protected function configure()
    {
    // ...
    }
    }
    App\Command\FooCommand:
    tags:
    - { name: 'console.command', command: 'app:foo' }
    @chalas_r

    View Slide

  16. Laziness Round 2 :
    AutoConfiguration
    class FooCommand extends Command
    {
    public static $defaultName = 'app:foo';
    protected function execute($input, $output)
    {
    }
    }
    @chalas_r

    View Slide

  17. Snippet montrant propriété static $defaultName
    et $defaultDescription et la méthode configure()
    mais cette fois avec le setName() et
    setDescription() barrées genre effacé parce que
    plus utile)
    class FooCommand extends Command
    {
    public static $defaultName = 'app:foo';
    public static $defaultDescription = 'A foo command';
    protected function configure()
    {
    $this->setName('app:foo');
    $this->setDescription('A foo command');
    }
    }
    @chalas_r
    Encore plus lazy

    View Slide

  18. Attributes FTW
    screenshot de #[AsCommand] en remplacement
    de $defaultName et $defaultDescription
    #[AsCommand(
    name: 'app:foo',
    description: 'A foo command',
    )]
    class FooCommand extends Command
    {
    protected function execute($input, $output)
    {
    }
    }
    @chalas_r

    View Slide

  19. Commands = Controllers
    @chalas_r

    View Slide

  20. En CLI, les commandes sont les points d’entrée
    et de sortie de nos traitements métiers.
    Tout comme le sont les contrôleurs dans un
    contexte HTTP.

    @chalas_r

    View Slide

  21. Dans les faits, commandes et
    contrôleurs ne sont pas logés à
    la même enseigne.
    @chalas_r

    View Slide

  22. Un Controller peut être n'importe qu'elle callable,
    tandis que les Commandes doivent étendre la classe Command.
    De fait, une Commande hérite de méthodes dont elle n'a
    potentiellement pas besoin (90% du temps).
    Il existe des Controller Argument Value Resolvers, rien de tel
    pour les commandes.
    DIFFÉRENCES MAJEURES
    @chalas_r

    View Slide

  23. Il est temps de changer çà.
    @chalas_r

    View Slide

  24. Déclaration d'une commande via
    #[AsCommand] uniquement.
    Définition de l'input via
    #[InputArgument] et #[InputOption]
    Command Argument Value Resolvers
    Going full attributes
    @chalas_r

    View Slide

  25. Invokable Command
    #[AsCommand(name: "user:create", description: "Creates a user")]
    final class CreateUserCommand
    {
    public function __invoke(...): int {
    // ...
    }
    }
    @chalas_r

    View Slide

  26. #[AsCommand(name: "user:create")
    final class CreateUserCommand
    {
    public function __invoke(
    OutputInterface $output,
    UserRepository $userRepository,
    #[Autowire(service: 'mailer')]
    MailerInterface $mailer,
    #[InputArgument(mode: InputArgument::REQUIRED)]
    string $email,
    #[InputOption(mode: InputOption::VALUE_IS_ARRAY)]
    array $roles,
    ): int {
    $user = new User($email, $role);
    $userRepository->save($user);
    $mailer->send(...);
    }
    }
    @chalas_r

    View Slide

  27. Autres choses à revoir
    Abolir le couplage étroit entre Command & Application
    Simplifier l'usage de SymfonyStyle
    Améliorer l’API de sélection du flux de sortie (stdout vs stderr)
    Enrichir l'API de Testing
    @chalas_r

    View Slide

  28. Stratégie
    Rétrocompatibilité ? Oui !
    Pour quand ? 6.4
    🤞
    @chalas_r

    View Slide

  29. Help welcome
    Feedback,
    Discussion,
    Review,
    Sponsoring :)
    @chalas_r

    View Slide

  30. Merci !
    @chalas_r

    View Slide