Skip to content

Commit

Permalink
Extension: commands are lazy by default
Browse files Browse the repository at this point in the history
  • Loading branch information
f3l1x committed Jan 3, 2024
1 parent 2c719a4 commit bcaad99
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 130 deletions.
92 changes: 32 additions & 60 deletions .docs/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Console
# Contributte Console

Integration of [Symfony Console](https://symfony.com/doc/current/components/console.html) into Nette Framework.

## Content

Expand Down Expand Up @@ -30,7 +32,6 @@ console:
catchExceptions: true / false
autoExit: true / false
url: https://example.com
lazy: false
```

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

### Helpers

You could also define you custom `helperSet` just in case. There are 2 possible approaches. You can register your
`App\Model\MyCustomHelperSet` as a service under the `services` section or provide it directly to the extension config `helperSet`.

Already defined service:
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.
Alternatively, you can directly provide it to the extension configuration helperSet.

```neon
services:
customHelperSet: App\Model\MyCustomHelperSet
console:
helperSet: @customHelperSet
```
# directly
helperSet: App\Model\MyCustomHelperSet
Directly defined helperSet:
# or reference service
helperSet: @customHelperSet
```neon
console:
helperSet: App\Model\MyCustomHelperSet
services:
customHelperSet: App\Model\MyCustomHelperSet
```

By default, helperSet contains 4 helpers defined in `Symfony\Component\Console\Application`. You can add more helpers, if need them.
By default, helperSet contains 4 helpers defined in `Symfony\Component\Console\Application`. You can add your own helpers to the helperSet.

```php

```neon
console:
Expand All @@ -73,24 +71,17 @@ console:

### Lazy-loading

From version 3.4 `Symfony\Console` uses command lazy-loading. This extension fully supports this feature and
you can enable it in the NEON file.

```neon
console:
lazy: true
```

From this point forward, all commands are instantiated only if needed. Don't forget that listing all commands will instantiate them all.

How to define command names? Define `$defaultName` in the command or via the `console.command` tag on the service.
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.
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.
This means that commands are instantiated only when they are needed.

```php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(name: 'app:foo')]
class FooCommand extends Command
{
protected static $defaultName = 'app:foo';
}
```

Expand All @@ -116,35 +107,28 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(
name: 'app:foo',
description: 'Adds user with given username to database',
)]
final class AddUserCommand extends Command
{

private UsersModel $usersModel;
private UserFacade $userFacade;

/**
* Pass dependencies with constructor injection
*/
public function __construct(UsersModel $usersModel)
public function __construct(UserFacade $userFacade)
{
parent::__construct(); // don't forget parent call as we extends from Command
$this->usersModel = $usersModel;
parent::__construct();
$this->userFacade = $usersFacade;
}

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

/**
* Don't forget to return 0 for success or non-zero for error
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
// retrieve passed arguments/options
Expand Down Expand Up @@ -173,14 +157,15 @@ final class AddUserCommand extends Command
}
```

### Register command
Register your command as a service in NEON file.

```neon
services:
- App\Console\AddUserCommand
```

Maybe you will have to flush the `temp/cache` directory.
> [!IMPORTANT]
> Remember! Flush `temp/cache` directory before running the command.
## Entrypoint

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

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

##### Nette 3.0+

```php
#!/usr/bin/env php
<?php declare(strict_types = 1);
Expand All @@ -203,14 +186,3 @@ exit(App\Bootstrap::boot()
->getByType(Contributte\Console\Application::class)
->run());
```

##### Nette <= 2.4

```php
#!/usr/bin/env php
<?php declare(strict_types = 1);

$container = require __DIR__ . '/../app/bootstrap.php';

exit($container->getByType(Contributte\Console\Application::class)->run());
```
78 changes: 24 additions & 54 deletions src/DI/ConsoleExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use Nette\Http\UrlScript;
use Nette\Schema\Expect;
use Nette\Schema\Schema;
use Nette\Utils\Arrays;
use stdClass;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
Expand All @@ -25,8 +24,6 @@
class ConsoleExtension extends CompilerExtension
{

public const COMMAND_TAG = 'console.command';

private bool $cliMode;

public function __construct(bool $cliMode = false)
Expand All @@ -46,7 +43,6 @@ public function getConfigSchema(): Schema
'helpers' => Expect::arrayOf(
Expect::anyOf(Expect::string(), Expect::array(), Expect::type(Statement::class))
),
'lazy' => Expect::bool(true),
]);
}

Expand Down Expand Up @@ -102,13 +98,11 @@ public function loadConfiguration(): void
}

// Commands lazy loading
if ($config->lazy) {
$builder->addDefinition($this->prefix('commandLoader'))
->setType(CommandLoaderInterface::class)
->setFactory(ContainerCommandLoader::class);
$builder->addDefinition($this->prefix('commandLoader'))
->setType(CommandLoaderInterface::class)
->setFactory(ContainerCommandLoader::class);

$applicationDef->addSetup('setCommandLoader', ['@' . $this->prefix('commandLoader')]);
}
$applicationDef->addSetup('setCommandLoader', ['@' . $this->prefix('commandLoader')]);

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

// Register all commands (if they are not lazy-loaded)
// otherwise build a command map for command loader
// Add all commands to map for command loader
$commands = $builder->findByType(Command::class);

if (!$config->lazy) {
// Iterate over all commands and add to console
foreach ($commands as $serviceName => $service) {
$applicationDef->addSetup('add', [$service]);
}
} else {
$commandMap = [];

// Iterate over all commands and build commandMap
foreach ($commands as $serviceName => $service) {
$tags = $service->getTags();
$entry = ['name' => null, 'alias' => null];

if (isset($tags[self::COMMAND_TAG])) {
// Parse tag's name attribute
if (is_string($tags[self::COMMAND_TAG])) {
$entry['name'] = $tags[self::COMMAND_TAG];
} elseif (is_array($tags[self::COMMAND_TAG])) {
$entry['name'] = Arrays::get($tags[self::COMMAND_TAG], 'name', null);
}
} else {
// Parse it from static property
$entry['name'] = call_user_func([$service->getType(), 'getDefaultName']); // @phpstan-ignore-line
}

// Validate command name
if (!isset($entry['name'])) {
throw new ServiceCreationException(
sprintf(
'Command "%s" missing tag "%s[name]" or variable "$defaultName".',
$service->getType(),
self::COMMAND_TAG
)
);
}

// Append service to command map
$commandMap[$entry['name']] = $serviceName;
$commandMap = [];

// Iterate over all commands and build commandMap
foreach ($commands as $serviceName => $service) {
$commandName = call_user_func([$service->getType(), 'getDefaultName']); // @phpstan-ignore-line

if ($commandName === null) {
throw new ServiceCreationException(
sprintf(
'Command "%s" missing #[AsCommand] attribute',
$service->getType(),
)
);
}

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

/** @var ServiceDefinition $commandLoaderDef */
$commandLoaderDef = $builder->getDefinition($this->prefix('commandLoader'));
$commandLoaderDef->getFactory()->arguments = ['@container', $commandMap];

// Register event dispatcher, if available
try {
$dispatcherDef = $builder->getDefinitionByType(EventDispatcherInterface::class);
Expand Down
1 change: 0 additions & 1 deletion tests/cases/Command/Command.HelperSet.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ Toolkit::test(function (): void {
$compiler->addExtension('console', new ConsoleExtension(true));
$compiler->loadConfig(FileMock::create('
console:
lazy: false
services:
- Tests\Fixtures\HelperSetCommand
', 'neon'));
Expand Down
1 change: 0 additions & 1 deletion tests/cases/DI/ConsoleExtension.lazy.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ Toolkit::test(function (): void {
$compiler->addExtension('console', new ConsoleExtension(true));
$compiler->loadConfig(FileMock::create('
console:
lazy: true
services:
foo: Tests\Fixtures\FooCommand
', 'neon'));
Expand Down
17 changes: 3 additions & 14 deletions tests/cases/DI/ConsoleExtension.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ Toolkit::test(function (): void {
$compiler->addExtension('console', new ConsoleExtension(true));
$compiler->loadConfig(FileMock::create('
console:
lazy: false
services:
foo: Tests\Fixtures\FooCommand
', 'neon'));
Expand All @@ -48,7 +47,7 @@ Toolkit::test(function (): void {
$container = new $class();

Assert::type(Application::class, $container->getByType(Application::class));
Assert::true($container->isCreated('foo'));
Assert::false($container->isCreated('foo'));
Assert::count(1, $container->findByType(Command::class));
Assert::type(FooCommand::class, $container->getByType(Command::class));
});
Expand Down Expand Up @@ -118,15 +117,8 @@ Toolkit::test(function (): void {
$compiler->addExtension('console', new ConsoleExtension(true));
$compiler->loadConfig(FileMock::create('
console:
lazy: true
services:
defaultName: Tests\Fixtures\FooCommand
tagNameString:
factory: Tests\Fixtures\FooCommand
tags: [console.command: bar]
tagNameArray:
factory: Tests\Fixtures\FooCommand
tags: [console.command: [name: baz]]
', 'neon'));
}, [getmypid(), 6]);

Expand All @@ -136,10 +128,8 @@ Toolkit::test(function (): void {
$application = $container->getByType(Application::class);
Assert::type(Application::class, $application);
Assert::false($container->isCreated('defaultName'));
Assert::count(3, $container->findByType(Command::class));
Assert::count(1, $container->findByType(Command::class));
Assert::true($application->has('app:foo'));
Assert::true($application->has('bar'));
Assert::true($application->has('baz'));
});

// Invalid command
Expand All @@ -150,12 +140,11 @@ Toolkit::test(function (): void {
$compiler->addExtension('console', new ConsoleExtension(true));
$compiler->loadConfig(FileMock::create('
console:
lazy: true
services:
noName: Tests\Fixtures\NoNameCommand
', 'neon'));
}, [getmypid(), 7]);
}, ServiceCreationException::class, 'Command "Tests\Fixtures\NoNameCommand" missing tag "console.command[name]" or variable "$defaultName".');
}, ServiceCreationException::class, 'Command "Tests\Fixtures\NoNameCommand" missing #[AsCommand] attribute');
});

// Always exported
Expand Down

0 comments on commit bcaad99

Please sign in to comment.