Skip to content

Support richer command metadata and true lazy console registration #89

@ohmyfelix

Description

@ohmyfelix

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:

  • help
  • usages

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions