Adding a Command
This guide walks through adding a new CLI command to dde.
Step 1: Create the Command Class
Section titled “Step 1: Create the Command Class”Create a new class in the appropriate namespace:
App\Command\Project\for project-scoped commandsApp\Command\System\for system-wide commands
<?php
declare(strict_types=1);
namespace App\Command\Project;
use App\Command\AbstractProjectCommand;use App\Manager\ConfigManager;use App\Output\FormatterResolver;use Symfony\Component\Console\Attribute\AsCommand;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand( name: 'project:example', description: 'An example project command',)]final class ProjectExampleCommand extends AbstractProjectCommand{ public function __construct( ConfigManager $configManager, FormatterResolver $formatterResolver, ) { parent::__construct($configManager, $formatterResolver); }
protected function execute(InputInterface $input, OutputInterface $output): int { $formatter = $this->resolveFormatter($output, $input); $config = $this->getResolvedConfig(); $projectDir = $this->getProjectDirectory();
// Your logic here -- delegate to a manager or service
return $formatter->success(['message' => 'Done']); }}Step 2: Use the #[AsCommand] Attribute
Section titled “Step 2: Use the #[AsCommand] Attribute”The #[AsCommand] attribute registers the command with Symfony. No additional YAML or service configuration is needed thanks to autowiring.
Required properties:
name: the command name (e.g.project:exampleorsystem:example)description: a brief description shown indde list
Step 3: Extend the Right Base Class
Section titled “Step 3: Extend the Right Base Class”| Base Class | When to Use |
|---|---|
AbstractProjectCommand | Commands that operate on a project (need project directory, config) |
AbstractSystemCommand | Commands that operate on the dde system (no project context needed) |
AbstractProjectCommand provides:
getProjectDirectory()— finds and returns the project rootgetResolvedConfig()— loads and merges global + project configurationgetProjectConfig()— loads project configuration onlyresolveDbService()— resolves a database service from configresolveDatabase()— resolves the database name
AbstractBaseCommand (parent of both) provides:
resolveFormatter()— returns the configuredOutputFormatterInterface(text or JSON)
Step 4: Implement execute()
Section titled “Step 4: Implement execute()”Keep the command thin. Business logic belongs in managers (App\Manager\) or services (App\Service\). The command should:
- Resolve the output formatter
- Gather input (options, arguments)
- Call a manager method
- Return results via the formatter
Use $formatter->success() for successful results and $formatter->error() for errors. Both return an integer exit code suitable for returning from execute().
Step 5: Write a Unit Test
Section titled “Step 5: Write a Unit Test”Create a test at tests/Unit/Command/Project/ProjectExampleCommandTest.php:
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Command\Project;
use PHPUnit\Framework\TestCase;
final class ProjectExampleCommandTest extends TestCase{ public function testExecuteReturnsSuccess(): void { // Test your command's behavior with mocked dependencies }}Step 6: Run QA
Section titled “Step 6: Run QA”make qaEnsure ECS, PHPStan, Rector, and all tests pass before submitting.
Checklist
Section titled “Checklist”- Class created in correct namespace (
Project\orSystem\) -
#[AsCommand]attribute with name and description - Extends
AbstractProjectCommandorAbstractSystemCommand -
declare(strict_types=1)at the top - Business logic delegated to a manager (not in the command)
- Uses
resolveFormatter()for output - Unit test written
-
make qapasses