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