-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BC BREAK] Full blown DI for console commands #50
Changes from all commits
2a07c97
e147480
2884357
c8182af
10e2002
efb42d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,66 +1,83 @@ | ||
Quickstart | ||
========== | ||
kdyby/console | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please leave the title as it was before |
||
============= | ||
|
||
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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe better would be "register the extension" ? not sure about this one |
||
|
||
``` | ||
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). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is gonna need more rewriting, but that's definitely out of the scope of this PR |
||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was definitely bad, but I really don't wanna encourage constructor injection in console commands There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allright, recommended approach would be using (I hate it but that's a matter of preference :) ) |
||
|
||
/** | ||
* @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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
where have you found this information? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I do constructor dependencies as a matter of religion. :) When you override the method, autogenerated stub contains parameter $name. Also, I don't like using arbitraty Neither of which is reflected in Symfony documentation. I suppose I should also create a PR for SF/console :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree about the configure method #50 (comment). Anyway, this PR is tryting to solve too many issues at once, making it hard to discuss them all at once and come up with a solution :-/ |
||
|
||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that you encourage registering it through the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a new section or a specific note then? Will update the docs accoardingly. Also, is this used and/or do you want to encourage this? Because if I'd write a Nette extension, that uses console, I'd include adding it to config.neon into my extension readme. IMHO update of config.neon should be best-practice of nextensions, shouldn't it? My personal opinion is: do not use tags, but keep them for BC. Since all commands are also available as a service, you can inject them into your presenter/whatever and use them, as I did: BTW -- do you want to add this to docs? Could add a section class CleanupFocus extends Command
{
/** @var Paths */
private $pathsCommand;
public function execute(InputInterface $input, OutputInterface $output)
{
// ...
$frameInput = new ArgvInput(['actionname', '--all'], $this->pathsCommand->getDefinition());
$this->pathsCommand->execute($frameInput, $output);
// ...
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Definitely not, if you install an extension, you don't wanna copy boilerplate to your config - one line with extension registration should be enough in most cases. It should just work™. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Tags are a big part of binding extensions together, even in symfony. I'm all for encouraging the end-users to add commands in config section, but the tags are a vital part to it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Let's be hones, that's a pretty nasty hack (which I've also commited myself several times). The clean solution to this is to extract the logic to some facade and use that facade in both commands.
I think I've already answered this one :) |
||
|
||
```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 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need the disabled inject here, it's an optimalization - it's meant to allow you get your sevices using property injection which is called in runtime There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can discuss if it's needed or not, but I would like to leave it out of the scope of this PR, as I like the parsing the service using the "smart way". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, isn't the service graph initialized only when parsing If it's during the generation only, it doesn't matter if the graph is or isn't init'ed -- it should be in fact better this way, as the container code will match the use-case of your command more exactly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you misunderstood me. If you run the app through console, Sf application takes over and loads all commands to ask them about their names and descriptions (at least) - this list is not cached and is fetched over and over in runtime. Which means the command has to be instantiated - which means all it's constructor dependencies have to be instantiaded, and if it has any inject properties or methods, that were wired in compile-time, they're now part of instantiating that service and will be also instantiated in memory. Which means all their dependencies are too. If you're following the rule to have light constructors, this shouldn't be a big deal for you, as it only creates "a few" (depending on the console command dependencies) services. But it has other problems, or you might be using some library that doesn't have exactly light constructors. Having the That's a choice you can make by simply using a constructor or inject properties. Both will work. Both has advantages/disadvantages. It's up to the programmer to choose one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyway, that should be out of the scope of this PR. We can continue this discussion in separate thread (if you still think that it would be good to change it). I would like to make more smaller steps with PR's. |
||
} | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want this in the readme, it adds work for me to maintain two documents :-/