diff --git a/README.md b/README.md index 30b1ec1..9c0a19f 100755 --- a/README.md +++ b/README.md @@ -24,6 +24,25 @@ The best way to install Kdyby/Console is using [Composer](http://getcomposer.or $ composer require kdyby/console ``` +Then enable the extension in your `config.neon`: + +```yml +extensions: + console: Kdyby\Console\DI\ConsoleExtension +``` + +And append your commands into `console` section: +```yml +console: + disable: false # optional, can be used to disable console extension entirely + helpers: # optional, helpers go here + - App\Console\FooHelper + commands: # define your commands in this section. Full Nette DI is supported. + - App\Console\SendNewslettersCommand + - App\Console\AnotherCommand + - App\Console\AnotherCommand2 +``` + Documentation ------------ diff --git a/docs/en/index.md b/docs/en/index.md index 8b6c42c..c547a0f 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -1,66 +1,83 @@ -Quickstart -========== +kdyby/console +============= -This extension is here to provide integration of [Symfony Console](https://github.com/symfony/console) into Nette Framework. +This extension provides integration of [Symfony Console](https://github.com/symfony/console) into [Nette Framework](https://www.nette.org). + +It allows you to create command-line commands directly within your application. These commands can be used for recurring tasks, as cronjobs, maintenances, imports and/or big things like sending newsletters. Installation ----------- -The best way to install Kdyby/Console is using [Composer](http://getcomposer.org/): +Fastest way is to use [Composer](http://getcomposer.org/) - run following command in your project root: ```sh $ composer require kdyby/console ``` -You can enable the extension using your neon config. +Minimal configuration +--------------------- -```yml +First register new extension in your `config.neon` + +``` extensions: console: Kdyby\Console\DI\ConsoleExtension ``` - -Minimal configuration ---------------------- - -This extension creates new configuration section `console`, the absolute minimal configuration might look like this +This creates new configuration section `console`, the absolute minimal configuration might look like this: ```yml console: url: http://www.kdyby.org ``` -The `url` key specifies reference url that allows you to generate urls using Nette `UI\Presenter` in CLI (which is not possible otherwise). Another useful key is `commands` where you can register new commands. Look at the [Extending](#extending) part. +The `url` key specifies reference url, that allows you to generate urls using Nette `UI\Presenter` in CLI (which is not possible otherwise). +Now, your nette installation is ready to run commands! Try it: + +```sh +$ php www/index.php +``` Writing commands ---------------- -Commands are like controllers, but for Symfony Console. Example command might look like this +Example command might look like this: ```php namespace App\Console; +use App\Models; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class SendNewslettersCommand extends Command { - protected function configure() + /** @var Models\NewsletterSender */ + private $newsletterSender; + + /** + * @param Models\NewsletterSender $sender + */ + protected function __construct(Models\NewsletterSender $sender) { - $this->setName('app:newsletter') - ->setDescription('Sends the newsletter'); + parent::__construct('app:newsletter'); // <-- run with `php www/index.php app:newsletter` + $this->setDescription('Sends the newsletter'); + $this->newsletterSender = $sender; } + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ protected function execute(InputInterface $input, OutputInterface $output) { - $newsletterSender = $this->getHelper('container')->getByType('Models\NewsletterSender'); - try { - $newsletterSender->sendNewsletters(); - $output->writeLn('Newsletter sended'); + $this->newsletterSender->sendNewsletters(); + $output->writeLn('Newsletter sent'); return 0; // zero return code means everything is ok } catch (\Nette\Mail\SmtpException $e) { @@ -71,29 +88,25 @@ class SendNewslettersCommand extends Command } ``` -The configure method is to name the command and specify arguments and options. -They have a lot of options and you can read about them in Symfony Documentation. -When the command is executed, the execute method is called with two arguments. -First one is command input, which contains all the parsed arguments and options. -The second one is command output which should be used to provide feedback to the developer which ran the command. +In `__construct`, we setup the name of the command, then set some description and set all dependencies. + +Every command contains an `execute` function, which is called by Symfony console, whenever the command should be executed. The arguments handle either input parameters, and/or allow you to write to output stream. + +Best practice is to return an exit code, which specifies if the command ran successfully or not. This code can be read by other applications, when they execute your app. This is useful for cronjobs. -Best practise is to return an exit code which specifies if the command ran successfully and can be read by other applications when executed. +Every command needs to be registered in commands section of `config.neon`: +```yml +console: + commands: + - App\Console\SendNewslettersCommand +``` Extending --------- -To add a command, simply register it as a service with tag `kdyby.console.command` - -```yml -services: - newsletterCommand: - class: App\Console\SendNewslettersCommand - tags: [kdyby.console.command] -``` - -Alternatively you can use shorter syntax for registering command (without tag). It's useful when you have a lot of commands: +To add a command, simply register it inside of the `console.commands` section: ```yml console: @@ -103,14 +116,10 @@ console: - App\Console\AnotherCommand2 ``` -This is called anonymous registration (look at hyphens). You can name your command (`newsletterCommand: App\Console\SendNewslettersCommand`) but mostly it's not necessary. - -To add a helper, simply register it as a service with tag `kdyby.console.helper` - +If you want to add a new [console helper](http://symfony.com/doc/current/components/console/helpers/index.html), use following syntax: ```yml -services: - fooHelper: - class: App\Console\FooHelper - tags: [kdyby.console.helper] +console: + helpers: + - App\Console\FooHelper ``` diff --git a/src/Kdyby/Console/DI/ConsoleExtension.php b/src/Kdyby/Console/DI/ConsoleExtension.php index 26e1658..d24dcd8 100644 --- a/src/Kdyby/Console/DI/ConsoleExtension.php +++ b/src/Kdyby/Console/DI/ConsoleExtension.php @@ -82,18 +82,16 @@ public function loadConfiguration() Nette\Utils\Validators::assert($config, 'array'); foreach ($config['commands'] as $i => $command) { $def = $builder->addDefinition($this->prefix('command.' . $i)); - list($def->factory) = Nette\DI\Compiler::filterArguments(array( - is_string($command) ? new Statement($command) : $command - )); - - if (class_exists($def->factory->entity)) { - $def->class = $def->factory->entity; - } - - $def->setAutowired(FALSE); - $def->setInject(FALSE); + Nette\DI\Compiler::parseService($def, $command); $def->addTag(self::TAG_COMMAND); } + + isset($config['helpers']) ?: $config['helpers'] = []; + foreach ($config['helpers'] as $i => $helper) { + $def = $builder->addDefinition($this->prefix('helper.' . $i)); + Nette\DI\Compiler::parseService($def, $helper); + $def->addTag(self::TAG_HELPER); + } } diff --git a/tests/KdybyTests/Console/DiWiring.phpt b/tests/KdybyTests/Console/DiWiring.phpt new file mode 100644 index 0000000..0d6634d --- /dev/null +++ b/tests/KdybyTests/Console/DiWiring.phpt @@ -0,0 +1,416 @@ + + * @package Kdyby\Console + */ + +namespace KdybyTests\Console\DI; + +use Kdyby; +use Nette; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Tester; +use Tester\Assert; +use Tracy\Debugger; + +require_once __DIR__ . '/../bootstrap.php'; +require_once __DIR__ . '/CliAppTester.php'; + + + +/** + * @author Pavel Ptacek + */ +class DiWiringTest extends Tester\TestCase +{ + /** + * @return Nette\DI\Container + */ + private function createContainer() + { + Debugger::$logDirectory = TEMP_DIR . '/log'; + Tester\Helpers::purge(Debugger::$logDirectory); + + $config = new Nette\Configurator(); + $config->setTempDirectory(TEMP_DIR); + $config->setDebugMode(TRUE); + $config->addConfig(__DIR__ . '/config/di-wiring.neon', $config::NONE); + $config->addConfig(__DIR__ . '/config/allow.neon', $config::NONE); + + return $config->createContainer(); + } + + /** + * Create containers from configuration + */ + public function testContainerCreation() + { + $app = $this->createContainer()->getService('console.application'); + Assert::true( + $app instanceof Kdyby\Console\Application, + 'Application was not created using container->getService(console.application)' + ); + + $app = $this->createContainer()->getByType('Kdyby\Console\Application'); + Assert::true( + $app instanceof Kdyby\Console\Application, + 'Application was not created using container->getByType(Kdyby\Console\Application)' + ); + } + + /** + * Create commands and get their execution + */ + public function testCommands() + { + /** @var $app Kdyby\Console\Application */ + $app = $this->createContainer()->getByType('Kdyby\Console\Application'); + + $tests = [ + CommandInCommands::COMMAND_NAME => CommandInCommands::RETURN_CODE, + CommandAsService::COMMAND_NAME => CommandAsService::RETURN_CODE, + CommandInCommandsNeedsAll::COMMAND_NAME => CommandInCommandsNeedsAll::RETURN_CODE, + CommandAsServiceNeedsAll::COMMAND_NAME => CommandAsServiceNeedsAll::RETURN_CODE, + CommandInCommandsNeedsAllWithInject::COMMAND_NAME => CommandInCommandsNeedsAllWithInject::RETURN_CODE, + CommandAsServiceNeedsAllWithInject::COMMAND_NAME => CommandAsServiceNeedsAllWithInject::RETURN_CODE, + ]; + + $null = new NullOutput(); + foreach($tests as $command => $returnCode) { + Assert::same( + $returnCode, + $app->run(new ArgvInput(['www/index.php', $command]), $null), + 'Exit code check: '.$command.' did not run properly' + ); + } + } + + /** + * Validate that helpers are injected properly + */ + public function testHelpers() + { + /** @var $app Kdyby\Console\Application */ + $app = $this->createContainer()->getByType('Kdyby\Console\Application'); + + /** @var $command CommandInCommands */ + $command = $app->find(CommandInCommands::COMMAND_NAME); + $command->validateHelpers(); + } + + /** + * Validate that all injections are properly inserted by DI + */ + public function testInjections() + { + /** @var $app Kdyby\Console\Application */ + $app = $this->createContainer()->getByType('Kdyby\Console\Application'); + + /** @var $command CommandInCommandsNeedsAll */ + $command = $app->get(CommandInCommandsNeedsAll::COMMAND_NAME); + $command->validateNotAnnotations(); + $command->validateConstructor(); + $command->validateNotInjector(); + + /** @var $command CommandAsServiceNeedsAll */ + $command = $app->find(CommandAsServiceNeedsAll::COMMAND_NAME); + $command->validateNotAnnotations(); + $command->validateConstructor(); + $command->validateNotInjector(); + + /** @var $command CommandInCommandsNeedsAll */ + $command = $app->get(CommandInCommandsNeedsAllWithInject::COMMAND_NAME); + $command->validateAnnotations(); + $command->validateConstructor(); + $command->validateInjector(); + + /** @var $command CommandAsServiceNeedsAll */ + $command = $app->find(CommandAsServiceNeedsAllWithInject::COMMAND_NAME); + $command->validateAnnotations(); + $command->validateConstructor(); + $command->validateInjector(); + } +} + +/** + * Basic test for return code on all commands, test for helper blueprint + * @package KdybyTests\Console\DI + */ +abstract class DiTestCommand extends Command +{ + const COMMAND_NAME = 'n/a'; + const RETURN_CODE = 10; + + public function __construct() + { + parent::__construct(static::COMMAND_NAME); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + return static::RETURN_CODE; + } + + public function validateHelpers() + { + Assert::type('KdybyTests\Console\DI\HelperInSection', $this->getHelper('HelperInSection'), 'helper in section'); + Assert::type('KdybyTests\Console\DI\HelperAsService', $this->getHelper('HelperAsService'), 'helper as service'); + } +} + +/** + * Basic class for dependant commands - those, that use the trait - boilerplate + * @package KdybyTests\Console\DI + */ +abstract class DiTestDependantCommand extends DiTestCommand +{ + /** + * Load dependency tester trait + */ + use DependencyTesterTrait { + DependencyTesterTrait::__construct as private __dttConstruct; + } + + /** + * @param CommandInCommands $commandInCommands + * @param TestService $testService + * @param CommandAsService $commandAsService + */ + public function __construct( + CommandInCommands $commandInCommands, + TestService $testService, + CommandAsService $commandAsService + ) + { + parent::__construct(); + $this->__dttConstruct($commandInCommands, $testService, $commandAsService); + } +} + +/** + * Used for testing the dependencies + * @package KdybyTests\Console\DI + */ +trait DependencyTesterTrait +{ + /** @var CommandInCommands */ + private $constructorCommandInCommands; + + /** @var TestService */ + private $constructorTestService; + + /** @var CommandAsService */ + private $constructorCommandAsService; + + /** @var CommandInCommands */ + private $injectorCommandInCommands; + + /** @var TestService */ + private $injectorTestService; + + /** @var CommandAsService */ + private $injectorCommandAsService; + + /** @var CommandInCommands @inject */ + public $annotationCommandInCommands; + + /** @var TestService @inject */ + public $annotationTestService; + + /** @var CommandAsService @inject */ + public $annotationCommandAsService; + + /** + * @param CommandInCommands $commandInCommands + * @param TestService $testService + * @param CommandAsService $commandAsService + */ + public function __construct( + CommandInCommands $commandInCommands, + TestService $testService, + CommandAsService $commandAsService + ) + { + $this->constructorCommandInCommands = $commandInCommands; + $this->constructorTestService = $testService; + $this->constructorCommandAsService = $commandAsService; + } + + /** + * @param CommandInCommands $injectorCommandInCommands + */ + public function injectInjectorCommandInCommands(CommandInCommands $injectorCommandInCommands) + { + $this->injectorCommandInCommands = $injectorCommandInCommands; + } + + /** + * @param TestService $injectorTestService + */ + public function injectInjectorTestService(TestService $injectorTestService) + { + $this->injectorTestService = $injectorTestService; + } + + /** + * @param CommandAsService $injectorCommandAsService + */ + public function injectInjectorCommandAsService(CommandAsService $injectorCommandAsService) + { + $this->injectorCommandAsService = $injectorCommandAsService; + } + + /** + * @throws \Tester\AssertException if constructor injects fail (NOT nonsense, since this is a trait -- constructor is overriden) + */ + public function validateConstructor() + { + Assert::type('KdybyTests\Console\DI\CommandInCommands', $this->constructorCommandInCommands, 'Constructor fail'); + Assert::type('KdybyTests\Console\DI\TestService', $this->constructorTestService, 'Constructor fail'); + Assert::type('KdybyTests\Console\DI\CommandAsService', $this->constructorCommandAsService, 'Constructor fail'); + } + + /** + * @throws \Tester\AssertException if annotation injects failed + */ + public function validateAnnotations() + { + Assert::type('KdybyTests\Console\DI\CommandInCommands', $this->annotationCommandInCommands, 'Annotation fail'); + Assert::type('KdybyTests\Console\DI\TestService', $this->annotationTestService, 'Annotation fail'); + Assert::type('KdybyTests\Console\DI\CommandAsService', $this->annotationCommandAsService, 'Annotation fail'); + } + + /** + * @throws \Tester\AssertException if annotation not injects failed + */ + public function validateNotAnnotations() + { + Assert::same(null, $this->annotationCommandInCommands, 'Annotation fail'); + Assert::same(null, $this->annotationTestService, 'Annotation fail'); + Assert::same(null, $this->annotationCommandAsService, 'Annotation fail'); + } + + /** + * @throws \Tester\AssertException if injector injects failed + */ + public function validateInjector() + { + Assert::type('KdybyTests\Console\DI\CommandInCommands', $this->injectorCommandInCommands, 'Injector fail'); + Assert::type('KdybyTests\Console\DI\TestService', $this->injectorTestService, 'Injector fail'); + Assert::type('KdybyTests\Console\DI\CommandAsService', $this->injectorCommandAsService, 'Injector fail'); + } + + /** + * @throws \Tester\AssertException if injector injects did not fail + */ + public function validateNotInjector() + { + Assert::same(null, $this->injectorCommandInCommands, 'Injector fail'); + Assert::same(null, $this->injectorTestService, 'Injector fail'); + Assert::same(null, $this->injectorCommandAsService, 'Injector fail'); + } + +} + +/** + * Basic command defined in commands section, without any dependencies + * @package KdybyTests\Console\DI + */ +class CommandInCommands extends DiTestCommand +{ + const COMMAND_NAME = 'app:commands:command'; + const RETURN_CODE = 20; +} + +/** + * Basic command defined in services, without any dependencies + * @package KdybyTests\Console\DI + */ +class CommandAsService extends DiTestCommand +{ + const COMMAND_NAME = 'app:services:command'; + const RETURN_CODE = 30; +} + +/** + * Command defined in commands section, that needs all dependencies + * @package KdybyTests\Console\DI + */ +class CommandInCommandsNeedsAll extends DiTestDependantCommand +{ + const COMMAND_NAME = 'app:commands:needsAll'; + const RETURN_CODE = 110; +} + +/** + * Command defined in services section, that needs all dependencies + * @package KdybyTests\Console\DI + */ +class CommandAsServiceNeedsAll extends DiTestDependantCommand +{ + const COMMAND_NAME = 'app:services:needsAll'; + const RETURN_CODE = 120; +} + +/** + * Command defined in services section, that needs all dependencies + * @package KdybyTests\Console\DI + */ +class CommandInCommandsNeedsAllWithInject extends DiTestDependantCommand +{ + const COMMAND_NAME = 'app:commands:needsAllWithInject'; + const RETURN_CODE = 130; +} + +/** + * Command defined in services section, that needs all dependencies + * @package KdybyTests\Console\DI + */ +class CommandAsServiceNeedsAllWithInject extends DiTestDependantCommand +{ + const COMMAND_NAME = 'app:services:needsAllWithInject'; + const RETURN_CODE = 140; +} + +/** + * Basic test service defined in services, without any dependencies + * @package KdybyTests\Console\DI + */ +class TestService +{ +} + +/** + * Helper defined in console.helpers section + * @package KdybyTests\Console\DI + */ +class HelperInSection extends Helper +{ + public function getName() + { + return 'HelperInSection'; + } +} + +/** + * Helper defined in services section + * @package KdybyTests\Console\DI + */ +class HelperAsService extends Helper +{ + public function getName() + { + return 'HelperAsService'; + } +} + +\run(new DiWiringTest()); diff --git a/tests/KdybyTests/Console/config/di-wiring.neon b/tests/KdybyTests/Console/config/di-wiring.neon new file mode 100644 index 0000000..9cbae4b --- /dev/null +++ b/tests/KdybyTests/Console/config/di-wiring.neon @@ -0,0 +1,28 @@ +console: + commands: + - KdybyTests\Console\DI\CommandInCommands + - KdybyTests\Console\DI\CommandInCommandsNeedsAll + - + class: KdybyTests\Console\DI\CommandInCommandsNeedsAllWithInject + inject: true + helpers: + - KdybyTests\Console\DI\HelperInSection + +extensions: + console: Kdyby\Console\DI\ConsoleExtension + +services: + service1: KdybyTests\Console\DI\TestService + - + class: KdybyTests\Console\DI\CommandAsService + tags: [kdyby.console.command] + - + class: KdybyTests\Console\DI\HelperAsService + tags: [kdyby.console.helper] + - + class: KdybyTests\Console\DI\CommandAsServiceNeedsAll + tags: [kdyby.console.command] + - + class: KdybyTests\Console\DI\CommandAsServiceNeedsAllWithInject + inject: true + tags: [kdyby.console.command]