Console Revisitée

Robin Chalas

@chalas_r
les-tilleuls.coop
chalasr

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

@chalas_r

en CHIFFRES

500M

AU TOTAL

@chalas_r

450k

PAR JOUR

@chalas_r

Composer

Symfony

Doctrine

Laravel

API Platform

Drupal

Sylius

Magento

PHPStan

...

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

@chalas_r
@chalas_r

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

@chalas_r

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

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

Commands as services à la rescousse !

services:
    App\Command\FooCommand:
        tags:
            - { name: 'console.command' }
class FooCommand extends Command
{
    protected function configure()
    {
        $this->setName('app:foo');
    }

    protected function execute()
    {
       // ...
    }
}
@chalas_r

Laziness?

@chalas_r

Laziness - Round 1

@chalas_r
namespace Symfony\Component\Console\CommandLoader;

interface CommandLoaderInterface
{
    public function get($name);

    public function has($name);

    public function getNames();
}

Laziness Round 1

ROUTING EN AMONT

@chalas_r

ROUTING EN AMONT

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

Laziness Round 2 : AutoConfiguration

class FooCommand extends Command
{
    public static $defaultName = 'app:foo';

    protected function execute($input, $output)
    {
    }
}
@chalas_r

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

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

Commands = Controllers

 

@chalas_r

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

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

 

@chalas_r
  • 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

Il est temps de changer çà.

@chalas_r
  • 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

Invokable Command

#[AsCommand(name: "user:create", description: "Creates a user")]
final class CreateUserCommand
{
    public function __invoke(...): int {
        // ...
    }
}
@chalas_r
#[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

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

Stratégie

  • Rétrocompatibilité ? Oui !
     
  • Pour quand ? 6.4 🤞
@chalas_r

Help welcome

Feedback,
Discussion,
Review,
Sponsoring :)

@chalas_r

Merci !

@chalas_r

reinvent-symfony-console

By Robin Chalas

reinvent-symfony-console

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 !

  • 551