Slide 1

Slide 1 text

Console Revisitée

Slide 2

Slide 2 text

chalasr ROBIN CHALAS @chalas_r les-tilleuls.coop

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

@chalas_r

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Laziness? @chalas_r

Slide 13

Slide 13 text

Laziness - Round 1 @chalas_r

Slide 14

Slide 14 text

Laziness Round 1 ROUTING EN AMONT @chalas_r

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Commands = Controllers @chalas_r

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Il est temps de changer çà. @chalas_r

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

#[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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Merci ! @chalas_r