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 full-size slide

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

    View full-size slide

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

    @chalas_r

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. 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 full-size slide

  9. 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 full-size slide

  10. 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 full-size slide

  11. Laziness?
    @chalas_r

    View full-size slide

  12. Laziness - Round 1
    @chalas_r

    View full-size slide

  13. Laziness Round 1
    ROUTING EN AMONT
    @chalas_r

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. Commands = Controllers
    @chalas_r

    View full-size slide

  19. 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 full-size slide

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

    View full-size slide

  21. 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 full-size slide

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

    View full-size slide

  23. 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 full-size slide

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

    View full-size slide

  25. #[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 full-size slide

  26. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  29. Merci !
    @chalas_r

    View full-size slide