Skip to content

Commit bcaad99

Browse files
committed
Extension: commands are lazy by default
1 parent 2c719a4 commit bcaad99

File tree

5 files changed

+59
-130
lines changed

5 files changed

+59
-130
lines changed

.docs/README.md

Lines changed: 32 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Console
1+
# Contributte Console
2+
3+
Integration of [Symfony Console](https://symfony.com/doc/current/components/console.html) into Nette Framework.
24

35
## Content
46

@@ -30,7 +32,6 @@ console:
3032
catchExceptions: true / false
3133
autoExit: true / false
3234
url: https://example.com
33-
lazy: false
3435
```
3536

3637
In SAPI (CLI) mode, there is no HTTP request and thus no URL address.
@@ -43,27 +44,24 @@ console:
4344

4445
### Helpers
4546

46-
You could also define you custom `helperSet` just in case. There are 2 possible approaches. You can register your
47-
`App\Model\MyCustomHelperSet` as a service under the `services` section or provide it directly to the extension config `helperSet`.
48-
49-
Already defined service:
47+
You have the option to define your own helperSet if needed. There are two methods to do this. One way is to register your `App\Model\MyCustomHelperSet` as a service in the services section.
48+
Alternatively, you can directly provide it to the extension configuration helperSet.
5049

5150
```neon
52-
services:
53-
customHelperSet: App\Model\MyCustomHelperSet
54-
5551
console:
56-
helperSet: @customHelperSet
57-
```
52+
# directly
53+
helperSet: App\Model\MyCustomHelperSet
5854
59-
Directly defined helperSet:
55+
# or reference service
56+
helperSet: @customHelperSet
6057
61-
```neon
62-
console:
63-
helperSet: App\Model\MyCustomHelperSet
58+
services:
59+
customHelperSet: App\Model\MyCustomHelperSet
6460
```
6561

66-
By default, helperSet contains 4 helpers defined in `Symfony\Component\Console\Application`. You can add more helpers, if need them.
62+
By default, helperSet contains 4 helpers defined in `Symfony\Component\Console\Application`. You can add your own helpers to the helperSet.
63+
64+
```php
6765

6866
```neon
6967
console:
@@ -73,24 +71,17 @@ console:
7371

7472
### Lazy-loading
7573

76-
From version 3.4 `Symfony\Console` uses command lazy-loading. This extension fully supports this feature and
77-
you can enable it in the NEON file.
78-
79-
```neon
80-
console:
81-
lazy: true
82-
```
83-
84-
From this point forward, all commands are instantiated only if needed. Don't forget that listing all commands will instantiate them all.
85-
86-
How to define command names? Define `$defaultName` in the command or via the `console.command` tag on the service.
74+
By default, all commands are registered in the console application during the extension registration. This means that all commands are instantiated and their dependencies are injected.
75+
This can be a problem if you have a lot of commands and you don't need all of them at once. In this case, this extension setup lazy-loading of commands.
76+
This means that commands are instantiated only when they are needed.
8777

8878
```php
8979
use Symfony\Component\Console\Command\Command;
80+
use Symfony\Component\Console\Attribute\AsCommand;
9081

82+
#[AsCommand(name: 'app:foo')]
9183
class FooCommand extends Command
9284
{
93-
protected static $defaultName = 'app:foo';
9485
}
9586
```
9687

@@ -116,35 +107,28 @@ use Symfony\Component\Console\Command\Command;
116107
use Symfony\Component\Console\Input\InputArgument;
117108
use Symfony\Component\Console\Input\InputInterface;
118109
use Symfony\Component\Console\Output\OutputInterface;
110+
use Symfony\Component\Console\Attribute\AsCommand;
119111

112+
#[AsCommand(
113+
name: 'app:foo',
114+
description: 'Adds user with given username to database',
115+
)]
120116
final class AddUserCommand extends Command
121117
{
122118

123-
private UsersModel $usersModel;
119+
private UserFacade $userFacade;
124120

125-
/**
126-
* Pass dependencies with constructor injection
127-
*/
128-
public function __construct(UsersModel $usersModel)
121+
public function __construct(UserFacade $userFacade)
129122
{
130-
parent::__construct(); // don't forget parent call as we extends from Command
131-
$this->usersModel = $usersModel;
123+
parent::__construct();
124+
$this->userFacade = $usersFacade;
132125
}
133126

134127
protected function configure(): void
135128
{
136-
// choose command name
137-
$this->setName('user:add')
138-
// description (optional)
139-
->setDescription('Adds user with given username to database')
140-
// arguments (maybe required or not)
141-
->addArgument('username', InputArgument::REQUIRED, 'User\'s username');
142-
// you can list options as well (refer to symfony/console docs for more info)
129+
$this->addArgument('username', InputArgument::REQUIRED, "User's username");
143130
}
144131

145-
/**
146-
* Don't forget to return 0 for success or non-zero for error
147-
*/
148132
protected function execute(InputInterface $input, OutputInterface $output): int
149133
{
150134
// retrieve passed arguments/options
@@ -173,14 +157,15 @@ final class AddUserCommand extends Command
173157
}
174158
```
175159

176-
### Register command
160+
Register your command as a service in NEON file.
177161

178162
```neon
179163
services:
180164
- App\Console\AddUserCommand
181165
```
182166

183-
Maybe you will have to flush the `temp/cache` directory.
167+
> [!IMPORTANT]
168+
> Remember! Flush `temp/cache` directory before running the command.
184169
185170
## Entrypoint
186171

@@ -190,8 +175,6 @@ You can copy & paste it to your project, for example to `<root>/bin/console`.
190175

191176
Make sure to set it as executable. `chmod +x <root>/bin/console`.
192177

193-
##### Nette 3.0+
194-
195178
```php
196179
#!/usr/bin/env php
197180
<?php declare(strict_types = 1);
@@ -203,14 +186,3 @@ exit(App\Bootstrap::boot()
203186
->getByType(Contributte\Console\Application::class)
204187
->run());
205188
```
206-
207-
##### Nette <= 2.4
208-
209-
```php
210-
#!/usr/bin/env php
211-
<?php declare(strict_types = 1);
212-
213-
$container = require __DIR__ . '/../app/bootstrap.php';
214-
215-
exit($container->getByType(Contributte\Console\Application::class)->run());
216-
```

src/DI/ConsoleExtension.php

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use Nette\Http\UrlScript;
1414
use Nette\Schema\Expect;
1515
use Nette\Schema\Schema;
16-
use Nette\Utils\Arrays;
1716
use stdClass;
1817
use Symfony\Component\Console\Command\Command;
1918
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
@@ -25,8 +24,6 @@
2524
class ConsoleExtension extends CompilerExtension
2625
{
2726

28-
public const COMMAND_TAG = 'console.command';
29-
3027
private bool $cliMode;
3128

3229
public function __construct(bool $cliMode = false)
@@ -46,7 +43,6 @@ public function getConfigSchema(): Schema
4643
'helpers' => Expect::arrayOf(
4744
Expect::anyOf(Expect::string(), Expect::array(), Expect::type(Statement::class))
4845
),
49-
'lazy' => Expect::bool(true),
5046
]);
5147
}
5248

@@ -102,13 +98,11 @@ public function loadConfiguration(): void
10298
}
10399

104100
// Commands lazy loading
105-
if ($config->lazy) {
106-
$builder->addDefinition($this->prefix('commandLoader'))
107-
->setType(CommandLoaderInterface::class)
108-
->setFactory(ContainerCommandLoader::class);
101+
$builder->addDefinition($this->prefix('commandLoader'))
102+
->setType(CommandLoaderInterface::class)
103+
->setFactory(ContainerCommandLoader::class);
109104

110-
$applicationDef->addSetup('setCommandLoader', ['@' . $this->prefix('commandLoader')]);
111-
}
105+
$applicationDef->addSetup('setCommandLoader', ['@' . $this->prefix('commandLoader')]);
112106

113107
// Export types
114108
$this->compiler->addExportedType(Application::class);
@@ -137,55 +131,31 @@ public function beforeCompile(): void
137131
$httpDef->setFactory(Request::class, [new Statement(UrlScript::class, [$config->url])]);
138132
}
139133

140-
// Register all commands (if they are not lazy-loaded)
141-
// otherwise build a command map for command loader
134+
// Add all commands to map for command loader
142135
$commands = $builder->findByType(Command::class);
143-
144-
if (!$config->lazy) {
145-
// Iterate over all commands and add to console
146-
foreach ($commands as $serviceName => $service) {
147-
$applicationDef->addSetup('add', [$service]);
148-
}
149-
} else {
150-
$commandMap = [];
151-
152-
// Iterate over all commands and build commandMap
153-
foreach ($commands as $serviceName => $service) {
154-
$tags = $service->getTags();
155-
$entry = ['name' => null, 'alias' => null];
156-
157-
if (isset($tags[self::COMMAND_TAG])) {
158-
// Parse tag's name attribute
159-
if (is_string($tags[self::COMMAND_TAG])) {
160-
$entry['name'] = $tags[self::COMMAND_TAG];
161-
} elseif (is_array($tags[self::COMMAND_TAG])) {
162-
$entry['name'] = Arrays::get($tags[self::COMMAND_TAG], 'name', null);
163-
}
164-
} else {
165-
// Parse it from static property
166-
$entry['name'] = call_user_func([$service->getType(), 'getDefaultName']); // @phpstan-ignore-line
167-
}
168-
169-
// Validate command name
170-
if (!isset($entry['name'])) {
171-
throw new ServiceCreationException(
172-
sprintf(
173-
'Command "%s" missing tag "%s[name]" or variable "$defaultName".',
174-
$service->getType(),
175-
self::COMMAND_TAG
176-
)
177-
);
178-
}
179-
180-
// Append service to command map
181-
$commandMap[$entry['name']] = $serviceName;
136+
$commandMap = [];
137+
138+
// Iterate over all commands and build commandMap
139+
foreach ($commands as $serviceName => $service) {
140+
$commandName = call_user_func([$service->getType(), 'getDefaultName']); // @phpstan-ignore-line
141+
142+
if ($commandName === null) {
143+
throw new ServiceCreationException(
144+
sprintf(
145+
'Command "%s" missing #[AsCommand] attribute',
146+
$service->getType(),
147+
)
148+
);
182149
}
183150

184-
/** @var ServiceDefinition $commandLoaderDef */
185-
$commandLoaderDef = $builder->getDefinition($this->prefix('commandLoader'));
186-
$commandLoaderDef->getFactory()->arguments = ['@container', $commandMap];
151+
// Append service to command map
152+
$commandMap[$commandName] = $serviceName;
187153
}
188154

155+
/** @var ServiceDefinition $commandLoaderDef */
156+
$commandLoaderDef = $builder->getDefinition($this->prefix('commandLoader'));
157+
$commandLoaderDef->getFactory()->arguments = ['@container', $commandMap];
158+
189159
// Register event dispatcher, if available
190160
try {
191161
$dispatcherDef = $builder->getDefinitionByType(EventDispatcherInterface::class);

tests/cases/Command/Command.HelperSet.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ Toolkit::test(function (): void {
2121
$compiler->addExtension('console', new ConsoleExtension(true));
2222
$compiler->loadConfig(FileMock::create('
2323
console:
24-
lazy: false
2524
services:
2625
- Tests\Fixtures\HelperSetCommand
2726
', 'neon'));

tests/cases/DI/ConsoleExtension.lazy.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ Toolkit::test(function (): void {
2323
$compiler->addExtension('console', new ConsoleExtension(true));
2424
$compiler->loadConfig(FileMock::create('
2525
console:
26-
lazy: true
2726
services:
2827
foo: Tests\Fixtures\FooCommand
2928
', 'neon'));

tests/cases/DI/ConsoleExtension.phpt

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ Toolkit::test(function (): void {
3838
$compiler->addExtension('console', new ConsoleExtension(true));
3939
$compiler->loadConfig(FileMock::create('
4040
console:
41-
lazy: false
4241
services:
4342
foo: Tests\Fixtures\FooCommand
4443
', 'neon'));
@@ -48,7 +47,7 @@ Toolkit::test(function (): void {
4847
$container = new $class();
4948

5049
Assert::type(Application::class, $container->getByType(Application::class));
51-
Assert::true($container->isCreated('foo'));
50+
Assert::false($container->isCreated('foo'));
5251
Assert::count(1, $container->findByType(Command::class));
5352
Assert::type(FooCommand::class, $container->getByType(Command::class));
5453
});
@@ -118,15 +117,8 @@ Toolkit::test(function (): void {
118117
$compiler->addExtension('console', new ConsoleExtension(true));
119118
$compiler->loadConfig(FileMock::create('
120119
console:
121-
lazy: true
122120
services:
123121
defaultName: Tests\Fixtures\FooCommand
124-
tagNameString:
125-
factory: Tests\Fixtures\FooCommand
126-
tags: [console.command: bar]
127-
tagNameArray:
128-
factory: Tests\Fixtures\FooCommand
129-
tags: [console.command: [name: baz]]
130122
', 'neon'));
131123
}, [getmypid(), 6]);
132124

@@ -136,10 +128,8 @@ Toolkit::test(function (): void {
136128
$application = $container->getByType(Application::class);
137129
Assert::type(Application::class, $application);
138130
Assert::false($container->isCreated('defaultName'));
139-
Assert::count(3, $container->findByType(Command::class));
131+
Assert::count(1, $container->findByType(Command::class));
140132
Assert::true($application->has('app:foo'));
141-
Assert::true($application->has('bar'));
142-
Assert::true($application->has('baz'));
143133
});
144134

145135
// Invalid command
@@ -150,12 +140,11 @@ Toolkit::test(function (): void {
150140
$compiler->addExtension('console', new ConsoleExtension(true));
151141
$compiler->loadConfig(FileMock::create('
152142
console:
153-
lazy: true
154143
services:
155144
noName: Tests\Fixtures\NoNameCommand
156145
', 'neon'));
157146
}, [getmypid(), 7]);
158-
}, ServiceCreationException::class, 'Command "Tests\Fixtures\NoNameCommand" missing tag "console.command[name]" or variable "$defaultName".');
147+
}, ServiceCreationException::class, 'Command "Tests\Fixtures\NoNameCommand" missing #[AsCommand] attribute');
159148
});
160149

161150
// Always exported

0 commit comments

Comments
 (0)