console-commands
Skill35 starsChanged 5 months agoMITView on GitHub
Use when creating or refactoring Symfony console commands — never extends Command. On Symfony 8.1+ put #[AsCommand] on a method of a service class (even for a single command); on 7.x use the invokable attribute class. Also covers typed CLI inputs via value resolvers and #[MapInput] DTOs. Use when the task mentions console command, #[AsCommand], bin/console, #[Argument], #[Option], or a CLI task.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- orangehrm/orangehrmtheir own version, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- orangehrm/orangehrm★ 1.1ksame name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- api-platform-resourcesSkill
- bundle-organizationSkill
- doctrine-migrationsSkill
- doctrine-ormSkill
- doctrine-query-optimizationSkill
- domain-driven-designSkill
- dto-and-validationSkill
- hexagonal-architectureSkill
- jwt-authenticationSkill
- layered-architectureSkill
- messenger-asyncSkill
- problem-details-rfc9457Skill
- rest-controller-conventionsSkill
- security-authenticationSkill
- testing-pyramidSkill
- transactional-patternsSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 0 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 24
Before writing anything, read the project's symfony/console constraint in composer.json and choose:
- Very specific: line 34
If composer.json is missing or the constraint is ambiguous, check symfony/console in composer.lock, or ask which version the project …
- Very specific: line 125
Use the invokable command (__invoke) — no extends Command, no configure()/execute() boilerplate.
- Very specific: line 188
Never
extends Command: Import Symfony\Component\Console\Command\Command only for the return constants. - Very specific: line 237
Use Symfony Validator constraints (#[Assert\Url], #[Assert\Email], …) for validation — idiomatic and consistent with the rest of Symfony.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Symfony Console commands
A console command is just another transport into the service layer — treat the CLI like a controller: thin, typed at the edges, delegating to services.
Never extends Command. No configure(), no execute(), no
addArgument()/addOption(), no #[AsCommand(name: ..., description: ...)]
named parameters. That is the legacy Symfony 6/7 boilerplate this skill exists to
replace — on every supported version, including a single one-off command.
Step 1 — detect the Symfony version, then pick the form
Before writing anything, read the project's symfony/console constraint in
composer.json and choose:
- Symfony 8.1 or newer → always the method-based form (
#[AsCommand]on a method of a service class). This holds even for a single command — one method on one class. Do not drop to the class-based form just because there is only one command. - Symfony 7.x → the invokable class form (
#[AsCommand]on the class, an__invoke()method, noextends).
If composer.json is missing or the constraint is ambiguous, check
symfony/console in composer.lock, or ask which version the project targets —
don't guess. The two forms differ enough that picking the wrong one is worse than
a one-line question.
Symfony 8.1+: a single command (method on a service class)
Even one command uses the method-based form on 8.1+. This is the shape to reach for first:
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
final class UserService
{
public function __construct(
private readonly UserRepository $users,
private readonly EntityManagerInterface $em,
) {}
#[AsCommand('app:user:delete', 'delete a user by email')]
public function delete(SymfonyStyle $io, #[Argument('email of the user to delete')] string $email): int
{
$user = $this->users->findOneBy(['email' => $email]);
if (null === $user) {
$io->error(sprintf('No user found with email "%s".', $email));
return Command::FAILURE;
}
if (!$io->confirm(sprintf('Delete user "%s"? This cannot be undone.', $email), false)) {
$io->comment('Aborted.');
return Command::SUCCESS;
}
$this->em->remove($user);
$this->em->flush();
$io->success(sprintf('User "%s" deleted.', $email));
return Command::SUCCESS;
}
}Note what is absent: no extends Command, no configure(), no
execute(), no addArgument(). The argument is declared inline with
#[Argument]; dependencies are constructor-injected. When a second related
command appears (app:user:create, app:user:promote), add it as another
method on this same class — see the family form below.
Symfony 7.x fallback: one command per class
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand('site:scan', 'scan monitored sites for availability')]
final class SiteScanCommand
{
public function __construct(
private readonly SiteRepository $sites,
) {}
public function __invoke(SymfonyStyle $io): int
{
$io->success(sprintf('%d sites scanned', \count($this->sites->findAll())));
return Command::SUCCESS;
}
}- Use the invokable command (
__invoke) — noextends Command, noconfigure()/execute()boilerplate. - Inject dependencies through the constructor.
Symfony 8.1+: a family of commands on one service class
The single-command form above scales directly to a family: keep adding
method-level #[AsCommand] to the same service class. One class exposes a whole
family of related commands that share a single constructor and private helpers.
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\MapInput;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
final class SiteService
{
public function __construct(
private readonly SiteRepository $sites,
private readonly EntityManagerInterface $em,
private readonly HttpClientInterface $http,
) {}
#[AsCommand('site:add', 'add a site to monitor')]
public function add(SymfonyStyle $io, #[Argument] SiteUrl $url): int
{
// ...
return Command::SUCCESS;
}
#[AsCommand('site:scan', 'scan monitored sites for availability')]
public function scan(SymfonyStyle $io, #[MapInput] ScanInput $input): int
{
// ...
return Command::SUCCESS;
}
// Shared by the sibling commands above — the reason they live together.
private function findOrFail(SiteUrl $url): Site
{
return $this->sites->findOneByUrl($url) ?? throw new \RuntimeException('unknown site');
}
}- One class holds a cohesive family of commands (
site:add,site:scan,site:list) that share injected dependencies and private helpers. - The class name reflects its identity as a service, not a command holder:
SiteService, notSiteCommands. The CLI is just another transport into it. - When a method's private helpers stop being shared with its siblings, that's the signal to split it into its own class. Don't build god-classes, and don't go back to one-class-per-command.
Rules
- Never
extends Command. ImportSymfony\Component\Console\Command\Commandonly for the return constants. - Return
Command::SUCCESS/Command::FAILURE/Command::INVALID. #[AsCommand('name', 'description')]— positional description (second argument), not thedescription:named parameter.#[Argument('desc')]and#[Option('desc')]— same convention, positional description string. Neverdescription:.- First parameter is typically
SymfonyStyle $io(when you want its helpers —success,table,progressBar) orOutputInterface $output(leaner). Pick per command. declare(strict_types=1);in every file.- Verbose output via
$io->writeln()gated on$io->isVerbose()/$io->isVeryVerbose(). Nodump()/dd()in committed commands.
Typed inputs — value resolvers and DTOs
Validation, normalization, and parsing belong in value objects and their resolvers, not in command bodies. (Available with attribute-based commands.)
- Single typed atoms (URL, email, ULID, path) → a value object with a
custom
ValueResolverInterface. The value object'sfromString()factory validates; the resolver maps the raw CLI string to the typed object. Used as#[Argument] SiteUrl $url. - Groups of related inputs (several args/options that travel together) →
#[MapInput]DTO classes. Public properties carry#[Argument]/#[Option]. Validate in property hooks (PHP 8.4) or via the Symfony Validator — not in the constructor, because#[MapInput]DTOs are hydrated without calling the constructor. - Composition — DTOs can contain other DTOs
(
ApplyInput { public ScanInput $scan; /* + options */ }); Symfony merges them automatically. - Why this matters — a description like
'url of the site to monitor'lives once on the value object or DTO, not repeated across five command methods. Same for the validation logic.
final class ScanInput
{
#[Argument('url of the site to scan')]
public SiteUrl $url;
#[Option('follow redirects')]
public bool $follow = false {
// validate in a property hook, not the constructor
set => $value;
}
}Use Symfony Validator constraints (#[Assert\Url], #[Assert\Email], …) for
validation — idiomatic and consistent with the rest of Symfony.
Gotchas
- Agent writes
extends Commandwithconfigure()/execute()/addArgument()boilerplate — that's legacy Symfony 6/7 style; use the attribute command on every supported version. - Agent drops to the class-based (or
extends Command) form on Symfony 8.1+ just because it's a single command — a single command on 8.1+ is still one method with#[AsCommand]on a service class. Checkcomposer.jsonfirst. - Agent puts
#[AsCommand]on a class on Symfony 8.1+ when several related commands exist — prefer one service class with method-level#[AsCommand]. - Agent names the class
*Commands— name it*Service; the CLI is a transport into the service. - Agent uses the
description:named parameter — descriptions are positional on#[AsCommand],#[Argument], and#[Option]. - Agent parses/validates CLI strings inside the command body — move it into a
value object +
ValueResolverInterface, or a#[MapInput]DTO. - Agent puts validation in a
#[MapInput]DTO constructor — it's never called; use property hooks (PHP 8.4) or the Validator. - Agent splits a cohesive command family into one-class-per-command — keep them together while they share helpers; split only when they stop sharing.
- Agent returns
0/1integers — useCommand::SUCCESS/Command::FAILURE/Command::INVALID. - Agent leaves
dump()/dd()or ungated verbose output — gate on$io->isVerbose().
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

