Skip to content

Adding a Command

This guide walks through adding a new CLI command to dde.

Create a new class in the appropriate namespace:

  • App\Command\Project\ for project-scoped commands
  • App\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']);
}
}

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:example or system:example)
  • description: a brief description shown in dde list
Base ClassWhen to Use
AbstractProjectCommandCommands that operate on a project (need project directory, config)
AbstractSystemCommandCommands that operate on the dde system (no project context needed)

AbstractProjectCommand provides:

  • getProjectDirectory() — finds and returns the project root
  • getResolvedConfig() — loads and merges global + project configuration
  • getProjectConfig() — loads project configuration only
  • resolveDbService() — resolves a database service from config
  • resolveDatabase() — resolves the database name

AbstractBaseCommand (parent of both) provides:

  • resolveFormatter() — returns the configured OutputFormatterInterface (text or JSON)

Keep the command thin. Business logic belongs in managers (App\Manager\) or services (App\Service\). The command should:

  1. Resolve the output formatter
  2. Gather input (options, arguments)
  3. Call a manager method
  4. 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().

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
}
}
Terminal window
make qa

Ensure ECS, PHPStan, Rector, and all tests pass before submitting.

  • Class created in correct namespace (Project\ or System\)
  • #[AsCommand] attribute with name and description
  • Extends AbstractProjectCommand or AbstractSystemCommand
  • 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 qa passes