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

Better Console Applications

Better Console Applications

Console applications - whether part of a larger (Symfony-)application or standalone-tool - usually are the bash-script of PHP developers. Thereby one often leaves the path of clean code and hacks a very pragmatic solution. Despite the fact that a lot of these fast solutions remain in project and need to be maintained for longer. What to reason about while developing a console application and which simple tricks help to clean up your code, is shown by examples in this talk. How do I decouple my code from CLI runtime, how do I optimize long running processes and so on.

Conference: SymfonyLive Phantasialand 2018
Example Application: https://github.com/chr-hertel/console-example

Christopher Hertel

May 04, 2018
Tweet

More Decks by Christopher Hertel

Other Decks in Programming

Transcript

  1. app

  2. • combining both • w/ or w/o interaction • used

    while development & CI • e.g. Composer, PHPUnit
  3. • Generate Invoices • Charge payment • Generate PDF •

    Mail to customer • Export postal data
  4. use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; use Symfony\Component\Stopwatch\Stopwatch; /** *

    Monthly Billing run for all active subscribers of our magazines. * * - Invoice gets generated * - Payment is executed * - Email is sent * - Magazine export is generated * * @author C▪▪▪▪▪▪▪▪▪▪ H▪▪▪▪▪ <c▪▪▪▪▪▪▪▪▪▪.h▪▪▪▪▪@▪▪▪▪▪▪▪▪▪▪.de> */ class BillingRunCommand extends ContainerAwareCommand { protected function configure(): void { $this->setName('app:billing:run'); $this->addArgument('period', InputArgument::OPTIONAL, 'Billing Period', ''); } protected function execute(InputInterface $input, OutputInterface $output): int { if ('dev' === $this->getContainer()->getParameter('kernel.environment')) { $stopwatch = new Stopwatch(); $stopwatch->start('billing-run');
  5. } protected function execute(InputInterface $input, OutputInterface $output): int { if

    ('dev' === $this->getContainer()->getParameter('kernel.environment')) { $stopwatch = new Stopwatch(); $stopwatch->start('billing-run'); } $period = \DateTimeImmutable::createFromFormat('m-Y', $input->getArgument('per if (false === $period) { $questionHelper = $this->getHelper('question'); $question = new Question('Which period do you want? (format: mm-yyyy)'); $question->setNormalizer(function ($period) { return \DateTimeImmutable::createFromFormat('m-Y', (string) $period); }); $question->setValidator(function ($period) { if (false === $period) { throw new \InvalidArgumentException('The given value was not a val } return $period; }); $period = $questionHelper->ask($input, $output, $question); } $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->for
  6. $period = $questionHelper->ask($input, $output, $question); } $output->writeln(sprintf('<info>Start billing run for

    %s</info>', $period->for $output->writeln('============================='.PHP_EOL); $customers = $this->fetchActiveCustomer(); $output->writeln(sprintf('<info>Loaded %d customers to process</info>'.PHP_EOL $invoices = $this->generateInvoice($output, $customers, $period); $this->payInvoices($output, $invoices); $this->sendInvoices($output, $invoices); $this->exportMagazines($output, $period, $invoices); $output->writeln(['', '<info>Done.</info>', '']); if ('dev' === $this->getContainer()->getParameter('kernel.environment')) { $output->writeln((string) $stopwatch->stop('billing-run')); } return 0; } /** * @return Customer[] */ private function fetchActiveCustomer(): array {
  7. /** * @return Invoice[] */ private function generateInvoice(OutputInterface $output, array

    $customers, \DateT { $entityManager = $this->getContainer()->get('doctrine.orm.default_entity_manag $output->writeln('Generate Invoices:'); $invoices = []; $progressBar = new ProgressBar($output, count($customers)); $progressBar->start(); foreach ($customers as $i => $customer) { $invoice = Invoice::forCustomer($customer, $period); $invoices[] = $invoice; $entityManager->persist($invoice); $progressBar->advance(); } $progressBar->finish(); $output->writeln(''); $output->writeln(sprintf('<comment>Generated %d invoices to pay</comment>'.PHP $entityManager->flush(); return $invoices; }
  8. use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; use Symfony\Component\Stopwatch\Stopwatch; /** *

    Monthly Billing run for all active subscribers of our magazines. * * - Invoice gets generated * - Payment is executed * - Email is sent * - Magazine export is generated * * @author C▪▪▪▪▪▪▪▪▪▪ H▪▪▪▪▪ <c▪▪▪▪▪▪▪▪▪▪.h▪▪▪▪▪@▪▪▪▪▪▪▪▪▪▪.de> */ class BillingRunCommand extends ContainerAwareCommand { protected function configure(): void { $this->setName('app:billing:run'); $this->addArgument('period', InputArgument::OPTIONAL, 'Billing Period', ''); } protected function execute(InputInterface $input, OutputInterface $output): int { if ('dev' === $this->getContainer()->getParameter('kernel.environment')) { $stopwatch = new Stopwatch(); $stopwatch->start('billing-run');
  9. } protected function execute(InputInterface $input, OutputInterface $output): int { if

    ('dev' === $this->getContainer()->getParameter('kernel.environment')) { $stopwatch = new Stopwatch(); $stopwatch->start('billing-run'); } $period = \DateTimeImmutable::createFromFormat('m-Y', $input->getArgument('per if (false === $period) { $questionHelper = $this->getHelper('question'); $question = new Question('Which period do you want? (format: mm-yyyy)'); $question->setNormalizer(function ($period) { return \DateTimeImmutable::createFromFormat('m-Y', (string) $period); }); $question->setValidator(function ($period) { if (false === $period) { throw new \InvalidArgumentException('The given value was not a val } return $period; }); $period = $questionHelper->ask($input, $output, $question); } $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->for
  10. class ExampleCommand extends Command { protected function configure(): void {

    // TODO: IMPLEMENT } protected function execute(InputInterface $input, OutputInterface $output): int { // TODO: IMPLEMENT } }
  11. class ExampleCommand extends Command { protected function configure(): void {

    // TODO: IMPLEMENT } protected function initialize(InputInterface $input, OutputInterface $output): void { // TODO: IMPLEMENT } protected function interact(InputInterface $input, OutputInterface $output): void { // TODO: IMPLEMENT } protected function execute(InputInterface $input, OutputInterface $output): int { // TODO: IMPLEMENT } } Lazy Commands
  12. protected function interact(InputInterface $input, OutputInterface $output) { $period = \DateTimeImmutable::createFromFormat('m-Y',

    (string) $input->getArgument('period')); if (false === $period) { $questionHelper = $this->getHelper('question'); $question = new Question('Which period do you want? (format: mm-yyyy)'); $question->setNormalizer(function ($period) { return \DateTimeImmutable::createFromFormat('m-Y', (string) $period); }); $question->setValidator(function ($period) { if (false === $period) { throw new \InvalidArgumentException('The given value was not a valid period, use mm-y } return $period; }); $period = $questionHelper->ask($input, $output, $question); } $input->setArgument('period', $period); }
  13. protected function execute(InputInterface $input, OutputInterface $output): int { if ('dev'

    === $this->getContainer()->getParameter('kernel.environment')) { $stopwatch = new Stopwatch(); $stopwatch->start('billing-run'); } $period = $input->getArgument('period'); $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->format(' $output->writeln('============================='.PHP_EOL); // ... protected function configure(): void { $this->setName('app:billing:run'); $this->addArgument('period', InputArgument::REQUIRED, 'Billing Period'); }
  14. protected function execute(InputInterface $input, OutputInterface $output): int { if ('dev'

    === $this->getContainer()->getParameter('kernel.environment')) { $stopwatch = new Stopwatch(); $stopwatch->start('billing-run'); } $period = $input->getArgument('period'); $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->format( $output->writeln('============================='.PHP_EOL); // ... $output->writeln(['', '<info>Done.</info>', '']); if ('dev' === $this->getContainer()->getParameter('kernel.environment')) { $output->writeln((string) $stopwatch->stop('billing-run')); } return 0; }
  15. class StopwatchListener implements EventSubscriberInterface { private $stopwatch; public function __construct(Stopwatch

    $stopwatch) { $this->stopwatch = $stopwatch; } public static function getSubscribedEvents() { return [ ConsoleEvents::COMMAND => 'startStopwatch', ConsoleEvents::TERMINATE => 'stopStopwatch', ]; } public function startStopwatch(ConsoleCommandEvent $event): void { $this->stopwatch->start($event->getCommand()->getName()); } public function stopStopwatch(ConsoleTerminateEvent $event): void { $name = $event->getCommand()->getName();
  16. } public static function getSubscribedEvents() { return [ ConsoleEvents::COMMAND =>

    'startStopwatch', ConsoleEvents::TERMINATE => 'stopStopwatch', ]; } public function startStopwatch(ConsoleCommandEvent $event): void { $this->stopwatch->start($event->getCommand()->getName()); } public function stopStopwatch(ConsoleTerminateEvent $event): void { $name = $event->getCommand()->getName(); if (!$this->stopwatch->isStarted($name)) { return; } $event->getOutput()->writeln((string) $this->stopwatch->stop($name)); } }
  17. protected function execute(InputInterface $input, OutputInterface $output): int { $period =

    $input->getArgument('period'); $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->format('m-Y'))); $output->writeln('============================='.PHP_EOL); $customers = $this->fetchActiveCustomer(); $output->writeln(sprintf('<info>Loaded %d customers to process</info>'.PHP_EOL, count($cust $invoices = $this->generateInvoice($output, $customers, $period); $this->payInvoices($output, $invoices); $this->sendInvoices($output, $invoices); $this->exportMagazines($output, $period, $invoices); $output->writeln(['', '<info>Done.</info>', '']); return 0; }
  18. protected function execute(InputInterface $input, OutputInterface $output): int { $period =

    $input->getArgument('period'); $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->format('m-Y'))); $output->writeln('============================='.PHP_EOL); $customers = $this->fetchActiveCustomer(); $output->writeln(sprintf('<info>Loaded %d customers to process</info>'.PHP_EOL, count($cust $invoices = $this->generateInvoice($output, $customers, $period); $this->payInvoices($output, $invoices); $this->sendInvoices($output, $invoices); $this->exportMagazines($output, $period, $invoices); $output->writeln(['', '<info>Done.</info>', '']); return 0; }
  19. private function generateInvoice(OutputInterface $output, array $customers, \DateTimeImmutable { $entityManager =

    $this->getContainer()->get('doctrine.orm.default_entity_manager'); $output->writeln('Generate Invoices:'); $invoices = []; $progressBar = new ProgressBar($output, count($customers)); $progressBar->start(); foreach ($customers as $i => $customer) { $invoice = Invoice::forCustomer($customer, $period); $invoices[] = $invoice; $entityManager->persist($invoice); $progressBar->advance(); } $progressBar->finish(); $output->writeln(''); $output->writeln(sprintf('<comment>Generated %d invoices to pay</comment>'.PHP_EOL, count($i $entityManager->flush(); return $invoices; }
  20. class BillingRunCommand extends Command { private $billingRun; public function __construct(BillingRun

    $billingRun) { parent::__construct('app:billing:run'); $this->billingRun = $billingRun; } protected function configure(): void { $this->addArgument('period', InputArgument::REQUIRED, 'Billing Period'); } protected function execute(InputInterface $input, OutputInterface $output): int { $period = $input->getArgument('period'); $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->format('m-Y' $output->writeln('============================='.PHP_EOL); $this->billingRun->start($period, $output); $output->writeln(['', '<info>Done.</info>', '']); return 0; }
  21. class BillingRun { private $entityManager; private $paymentProvider; private $mailer; private

    $exporter; public function __construct( EntityManagerInterface $entityManager, PaymentProvider $paymentProvider, Mailer $mailer, Exporter $exporter ) { $this->entityManager = $entityManager; $this->paymentProvider = $paymentProvider; $this->mailer = $mailer; $this->exporter = $exporter; } public function start(\DateTimeImmutable $period, OutputInterface $output): void { $customers = $this->fetchActiveCustomer(); $output->writeln(sprintf('<info>Loaded %d customers to process</info>'.PHP_EOL, count($c $invoices = $this->generateInvoice($output, $customers, $period); $this->payInvoices($output, $invoices); $this->sendInvoices($output, $invoices); $this->exportMagazines($output, $period, $invoices); }
  22. <?php declare(strict_types = 1); namespace App; use App\Entity\Customer; use App\Entity\Invoice;

    use App\Invoice\Exporter; use App\Invoice\Mailer; use App\Payment\Exception as PaymentException; use App\Payment\Provider as PaymentProvider; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; class BillingRun { private $entityManager; private $paymentProvider; private $mailer; private $exporter; public function __construct( EntityManagerInterface $entityManager, PaymentProvider $paymentProvider, Mailer $mailer, Exporter $exporter
  23. class BillingRun { private $entityManager; private $paymentProvider; private $mailer; private

    $exporter; private $logger; public function __construct( EntityManagerInterface $entityManager, PaymentProvider $paymentProvider, Mailer $mailer, Exporter $exporter, LoggerInterface $logger ) { $this->entityManager = $entityManager; $this->paymentProvider = $paymentProvider; $this->mailer = $mailer; $this->exporter = $exporter; $this->logger = $logger; } public function start(\DateTimeImmutable $period): void { $customers = $this->fetchActiveCustomer(); $this->logger->info(sprintf('Loaded %d customers to process', count($customers))); $invoices = $this->generateInvoice($customers, $period); $this->payInvoices($invoices); $this->sendInvoices($invoices);
  24. public function start(\DateTimeImmutable $period, callable $progress, callable $error): { $customers

    = $this->entityManager->getRepository(Customer::class)->findByActive(tru $customerNum = count($customers); $invoices = []; foreach ($customers as $i => $customer) { $invoice = Invoice::forCustomer($customer, $period); try { $this->paymentProvider->authorize($invoice); } catch (PaymentException $exception) { $error($exception); } $this->mailer->sendInvoice($invoice); $this->entityManager->persist($invoice); $invoices[] = $invoice; $progress(++$i, $customerNum); } $this->exporter->export($period, $invoices); $this->entityManager->flush(); }
  25. protected function execute(InputInterface $input, OutputInterface $output): int { $period =

    $input->getArgument('period'); $output->writeln(sprintf('<info>Start billing run for %s</info>', $period->format('m-Y'))); $output->writeln('============================='.PHP_EOL); $progress = new ProgressBar($output); $onProgress = function (int $count, int $max) use ($output, $progress) { $this->onProgress($output, $progress, $count, $max); }; $onError = function (PaymentException $exception) use ($output) { $output->writeln('<error>'.$exception->getMessage().'</error>'); }; $this->billingRun->start($period, $onProgress, $onError); $output->writeln(['', '<info>Done.</info>', '']); return 0; }