Motivation
contributte/console already provides lazy command services, but its current integration is still much thinner than Symfony's console DI integration used by FrameworkBundle.
Today, contributte/console:
- scans
Command services and builds only a simple name => serviceId map in src/DI/ConsoleExtension.php
- reads only the command name from the
console.command tag in src/DI/ConsoleExtension.php
- returns the real command service directly from
src/CommandLoader/ContainerCommandLoader.php
- documents lazy-loading in
.docs/README.md
That works, but it is not metadata-first lazy loading like Symfony. Because Symfony Console calls the command loader from Application::has() / Application::all(), command discovery (list, completion, namespace lookup, etc.) can still instantiate real commands early if the loader returns concrete services instead of LazyCommand proxies.
Detailed comparison with Symfony
Relevant Symfony classes:
Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass
Symfony\\Component\\Console\\Command\\LazyCommand
Symfony\\Bundle\\FrameworkBundle\\Console\\Application
Current behavior vs Symfony
| Area |
contributte/console |
Symfony / FrameworkBundle |
| Command discovery |
Scans DI services by Command type |
Uses compile-time registration via console compiler passes |
| Loader map |
name => serviceId |
command map + metadata-aware lazy references |
| Lazy loading |
Loader returns the real command service |
Loader can return LazyCommand proxy with metadata |
console.command tag |
Supports only name / {name: ...} |
Supports richer metadata (command, description, help, usages, method-based commands, etc.) |
#[AsCommand] support |
Mainly used for resolving the command name |
Metadata is consumed at compile time and reused for lazy registration |
| Aliases / hidden |
Works mainly through encoded AsCommand->name semantics |
First-class metadata handled during registration |
| Invokable services |
Not supported |
Tagged invokable services / methods can become commands |
| Collision handling |
Later command can silently overwrite earlier entries in the map |
Compiler pass validates and fails earlier |
| Broken command during discovery |
Can affect discovery/listing if the real command must be created |
FrameworkBundle keeps the console usable and reports registration errors |
Why this matters
The current implementation is "service-lazy", but not fully "metadata-lazy".
That means:
list
- shell completion
- command discovery
- namespace/abbreviation resolution
can still instantiate real commands instead of staying lightweight.
This is especially visible in larger apps or when a command has expensive dependencies.
Proposal
1) Extend console.command tag support
Support Symfony-like metadata in the tag, not only the name.
For example:
services:
app.foo:
class: App\\Console\\FooCommand
tags:
- console.command:
name: app:foo
aliases: [foo, bar]
description: Foo command
hidden: false
help: |
Extra help text
At minimum, supporting these fields would be useful:
name
aliases
description
hidden
Optional follow-up fields:
2) Extract full #[AsCommand] metadata at compile time
ConsoleExtension currently reads the attribute mainly to determine the command name.
It would be better to also extract:
- aliases
- description
- hidden
- help
- usages
This would align the extension more closely with Symfony's metadata model.
3) Use Symfony\\Component\\Console\\Command\\LazyCommand for metadata-rich registrations
Instead of returning the real command service directly for metadata-capable commands, wrap them in LazyCommand.
That would make command discovery truly lazy while keeping:
- name
- aliases
- description
- hidden state
available without constructing the real command.
This is probably the highest-value improvement.
4) Detect duplicate names and aliases during container compilation
The current command map is built by plain assignment, so collisions can be overwritten silently.
It would be safer to fail fast with a clear ServiceCreationException when:
- two commands resolve to the same name
- an alias conflicts with another command name
- two aliases collide
5) Consider invokable services only as a follow-up
Symfony also supports tagged invokable services / tagged methods as commands.
That looks useful, but it is a bigger design step and does not need to block the metadata + LazyCommand improvements.
Expected benefits
- truly lazy
list / completion / discovery
- better compatibility with Symfony console conventions
- more portable examples and configs
- clearer compile-time errors for collisions
- better ergonomics for larger Nette applications using many commands
Out of scope
This issue is not about backporting Symfony Kernel-specific behavior such as:
--env
--no-debug
--profile
- bundle-based command discovery
- full FrameworkBundle compiler-pass / service-locator machinery
Those depend on Symfony Kernel / FrameworkBundle semantics and do not map cleanly to contributte/console.
Related issues
This issue is intended as a follow-up focused on metadata-first lazy registration and richer Symfony-compatible command metadata, not on the original lazy-loading rollout.
Motivation
contributte/consolealready provides lazy command services, but its current integration is still much thinner than Symfony's console DI integration used by FrameworkBundle.Today,
contributte/console:Commandservices and builds only a simplename => serviceIdmap insrc/DI/ConsoleExtension.phpconsole.commandtag insrc/DI/ConsoleExtension.phpsrc/CommandLoader/ContainerCommandLoader.php.docs/README.mdThat works, but it is not metadata-first lazy loading like Symfony. Because Symfony Console calls the command loader from
Application::has()/Application::all(), command discovery (list, completion, namespace lookup, etc.) can still instantiate real commands early if the loader returns concrete services instead ofLazyCommandproxies.Detailed comparison with Symfony
Relevant Symfony classes:
Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPassSymfony\\Component\\Console\\Command\\LazyCommandSymfony\\Bundle\\FrameworkBundle\\Console\\ApplicationCurrent behavior vs Symfony
contributte/consoleCommandtypename => serviceIdLazyCommandproxy with metadataconsole.commandtag{name: ...}command,description,help,usages, method-based commands, etc.)#[AsCommand]supportAsCommand->namesemanticsWhy this matters
The current implementation is "service-lazy", but not fully "metadata-lazy".
That means:
listcan still instantiate real commands instead of staying lightweight.
This is especially visible in larger apps or when a command has expensive dependencies.
Proposal
1) Extend
console.commandtag supportSupport Symfony-like metadata in the tag, not only the name.
For example:
At minimum, supporting these fields would be useful:
namealiasesdescriptionhiddenOptional follow-up fields:
helpusages2) Extract full
#[AsCommand]metadata at compile timeConsoleExtensioncurrently reads the attribute mainly to determine the command name.It would be better to also extract:
This would align the extension more closely with Symfony's metadata model.
3) Use
Symfony\\Component\\Console\\Command\\LazyCommandfor metadata-rich registrationsInstead of returning the real command service directly for metadata-capable commands, wrap them in
LazyCommand.That would make command discovery truly lazy while keeping:
available without constructing the real command.
This is probably the highest-value improvement.
4) Detect duplicate names and aliases during container compilation
The current command map is built by plain assignment, so collisions can be overwritten silently.
It would be safer to fail fast with a clear
ServiceCreationExceptionwhen:5) Consider invokable services only as a follow-up
Symfony also supports tagged invokable services / tagged methods as commands.
That looks useful, but it is a bigger design step and does not need to block the metadata +
LazyCommandimprovements.Expected benefits
list/ completion / discoveryOut of scope
This issue is not about backporting Symfony Kernel-specific behavior such as:
--env--no-debug--profileThose depend on Symfony Kernel / FrameworkBundle semantics and do not map cleanly to
contributte/console.Related issues
This issue is intended as a follow-up focused on metadata-first lazy registration and richer Symfony-compatible command metadata, not on the original lazy-loading rollout.