andydefer / laravel-directive
A flexible CLI command system for Laravel that breaks free from Artisan's constraints. Directives introduces a clean separation between what your command does (business logic) and how it's presented (output/UI).
Requires
- php: >=8.1
- andydefer/php-vo: ^0.8.1
- laravel/framework: ^12.0|^13.0|^14.0|^15.0
Requires (Dev)
- barryvdh/laravel-ide-helper: ^3.6
- composer/composer: ^2.0
- larastan/larastan: ^3.8
- laravel/pint: ^1.26
- orchestra/testbench: ^10.8
- phpunit/phpunit: ^12.5
- rector/rector: *
- symfony/var-dumper: ^7.0
- vimeo/psalm: ^6.14
This package is auto-updated.
Last update: 2026-06-12 11:42:51 UTC
README
Un système de commandes CLI flexible pour Laravel qui se libère des contraintes d'Artisan. Directives introduit une séparation nette entre la logique métier et la présentation.
Table des matières
- Installation
- Configuration
- Premiers pas
- Format des signatures
- Arguments variadiques
- Méthodes de base
- Arguments et options
- Interaction utilisateur
- Charger Laravel optionnellement
- Commandes intégrées
- Codes de sortie
- Tester vos directives
- Exemples complets
- Pourquoi ce package
Installation
composer require andydefer/laravel-directive
Prérequis
- PHP 8.2 ou supérieur
- Laravel 10.x, 11.x ou 12.x
Publication de la configuration (optionnel)
php artisan vendor:publish --tag=directive-config --force
Configuration
// config/directive.php return [ 'path' => getcwd() . '/app/Directives', ];
| Variable d'environnement | Description | Défaut |
|---|---|---|
DIRECTIVE_PATH |
Chemin personnalisé des directives | ./app/Directives |
DIRECTIVE_DEBUG |
Mode debug | false |
Premiers pas
Lister les directives
./vendor/bin/directive --list
Afficher l'aide
./vendor/bin/directive --help
Créer votre première directive
<?php // app/Directives/HelloDirective.php namespace App\Directives; use AndyDefer\Directive\AbstractDirective; use AndyDefer\Directive\Enums\ExitCode; final class HelloDirective extends AbstractDirective { public function getSignature(): string { return 'hello {name?}'; } public function getDescription(): string { return 'Say hello to someone'; } public function execute(): ExitCode { $name = $this->argument('name') ?? 'World'; $this->line("Hello, {$name}!"); return ExitCode::SUCCESS; } }
Exécuter
./vendor/bin/directive hello "John Doe" # Sortie: Hello, John Doe!
Format des signatures
Règles fondamentales
| Règle | Explication |
|---|---|
| Délimiteur autorisé | Uniquement - (tiret) |
| Caractères autorisés | Lettres (a-z, A-Z) et chiffres (0-9) |
| Premier caractère | Doit être une lettre |
| Pas de tirets consécutifs | user--list est interdit |
| Pas de tiret final | user- est interdit |
✅ Exemples valides
| Signature | Classe générée |
|---|---|
user-list |
UserListDirective |
cache-clear |
CacheClearDirective |
api-user-profile |
ApiUserProfileDirective |
❌ Exemples invalides
| Signature | Raison |
|---|---|
user:list |
Caractère : interdit |
create_user |
Underscore _ interdit |
user- |
Tiret final interdit |
user--list |
Tirets consécutifs |
Ordre des paramètres (strict)
| Ordre | Type | Syntaxe | Exemple |
|---|---|---|---|
| 1 | Arguments requis | {name} |
{name} {email} |
| 2 | Arguments avec valeur par défaut | {role=user} |
{role=admin} |
| 3 | Arguments optionnels | {count?} |
{limit?} |
| 4 | Arguments variadiques | {files*} |
{files*} |
| 5 | Options | {--force} ou {-v} |
{--verbose} {-f} |
// ✅ Ordre correct public function getSignature(): string { return 'user-create {name} {email} {role=user} {count?} {files*} {--force} {-v}'; } // ❌ Ordre incorrect public function getSignature(): string { return 'user-create {name?} {email}'; // Requis après optionnel }
Arguments variadiques
Capture tous les arguments restants sous forme de tableau.
public function getSignature(): string { return 'process {name} {files*}'; }
Syntaxe avec crochets (recommandée)
./directive process John [file1.txt, file2.txt, file3.txt] --verbose
Exemple
final class ProcessFilesDirective extends AbstractDirective { public function getSignature(): string { return 'process {name} {files*} {--verbose}'; } public function execute(): ExitCode { $name = $this->argument('name'); $files = $this->getVariadicArguments(); $this->info("Processing files for {$name}"); foreach ($files as $file) { $this->line(" - {$file}"); } return ExitCode::SUCCESS; } }
| Méthode | Description |
|---|---|
getVariadicArguments(): StringTypedCollection |
Retourne tous les arguments variadiques |
hasVariadicArguments(): bool |
Vérifie leur présence |
Les méthodes de base
getSignature()
public function getSignature(): string { return 'user-create {name} {email} {--role=admin}'; }
| Élément | Syntaxe | Description |
|---|---|---|
| Argument requis | {name} |
Paramètre positionnel obligatoire |
| Argument optionnel | {name?} |
Paramètre positionnel optionnel |
| Argument avec défaut | {count=10} |
Valeur par défaut |
| Argument variadique | {files*} |
Capture tous les restants |
| Option avec valeur | {--role=} |
Option attend une valeur |
| Option flag | {--force} |
true/false |
getDescription()
public function getDescription(): string { return 'Create a new user account'; }
execute()
public function execute(): ExitCode { $this->info('User created!'); return ExitCode::SUCCESS; }
getAliases()
use AndyDefer\DomainStructures\Collections\Utility\StringTypedCollection; public function getAliases(): StringTypedCollection { $aliases = new StringTypedCollection(); $aliases->add('user-add'); $aliases->add('create-user'); return $aliases; }
Arguments et options
Accès aux arguments
public function execute(): ExitCode { $name = $this->argument('name'); // string ou null $email = $this->argument('email'); if ($name === null) { $this->error('Name is required'); return ExitCode::INVALID_ARGUMENT; } $this->line("Name: {$name}"); return ExitCode::SUCCESS; }
Accès aux arguments variadiques
public function execute(): ExitCode { $files = $this->getVariadicArguments(); if ($this->hasVariadicArguments()) { foreach ($files as $file) { $this->line("Processing: {$file}"); } } return ExitCode::SUCCESS; }
Vérifier l'existence
if ($this->hasArgument('count')) { $count = $this->argument('count'); } if ($this->hasOption('verbose')) { $this->info('Verbose mode'); }
Accès aux options
public function execute(): ExitCode { $force = $this->option('force'); // bool $role = $this->option('role'); // string ou null if ($force) { $this->warn('Force mode enabled'); } return ExitCode::SUCCESS; }
Interaction utilisateur
Afficher des messages
$this->line('Simple text'); // texte brut $this->info('Success!'); // vert $this->error('Error!'); // rouge $this->warn('Warning!'); // jaune $this->newLine(); // ligne vide $this->separator(); // ligne de séparation (---)
Poser une question
$name = $this->ask('What is your name?');
Demander une confirmation
if ($this->confirm('Continue?')) { $this->info('Continuing...'); }
Afficher un tableau
use AndyDefer\Directive\Collections\RowCollection; use AndyDefer\DomainStructures\Collections\Utility\StringTypedCollection; $headers = new StringTypedCollection(); $headers->add('ID', 'Name', 'Email'); $rows = new RowCollection(); $row = new RowCollection(); $row->add(1, 'John Doe', 'john@example.com'); $rows->add($row); $this->table($headers, $rows);
Sortie :
| ID | Name | Email |
|----|----------|-------------------|
| 1 | John Doe | john@example.com |
Charger Laravel optionnellement
Par défaut, les directives s'exécutent sans Laravel pour des performances optimales.
Activer Laravel
final class UserListDirective extends AbstractDirective { public function shouldBootLaravel(): bool { return true; } public function execute(): ExitCode { $users = User::all(); // Eloquent fonctionne ! foreach ($users as $user) { $this->line("{$user->id}: {$user->name}"); } return ExitCode::SUCCESS; } }
Vérifier la disponibilité
public function execute(): ExitCode { if (!$this->hasLaravel()) { $this->error('Laravel not available!'); return ExitCode::FAILURE; } $this->info('Laravel is available!'); return ExitCode::SUCCESS; }
Accéder à l'instance Laravel
$app = $this->getLaravel(); $version = $app->version();
Le bootstrap de Laravel se fait une seule fois par exécution.
Commandes intégrées
| Commande | Alias | Description |
|---|---|---|
./vendor/bin/directive --list |
-l |
Liste toutes les directives |
./vendor/bin/directive --help |
-h |
Affiche l'aide |
./vendor/bin/directive --version |
-v |
Affiche la version |
Codes de sortie
| Code | Constante | Description |
|---|---|---|
| 0 | ExitCode::SUCCESS |
Succès |
| 1 | ExitCode::FAILURE |
Erreur générale |
| 3 | ExitCode::NOT_FOUND |
Directive non trouvée |
| 4 | ExitCode::INVALID_ARGUMENT |
Argument invalide |
public function execute(): ExitCode { if ($this->argument('name') === null) { $this->error('Name is required'); return ExitCode::INVALID_ARGUMENT; } try { // Logique... return ExitCode::SUCCESS; } catch (\Exception $e) { $this->error($e->getMessage()); return ExitCode::FAILURE; } }
Tester vos directives
Le package fournit DirectiveTestingService pour tester vos directives dans un environnement isolé.
Test basique
<?php namespace Tests\Unit\Directives; use AndyDefer\Directive\Contexts\DirectiveContext; use AndyDefer\Directive\Contexts\LaravelBootstrapperContext; use AndyDefer\Directive\Enums\ExitCode; use AndyDefer\Directive\Records\DirectiveBlueprintRecord; use AndyDefer\Directive\Services\DirectiveTestingService; use AndyDefer\DomainStructures\Collections\Utility\StringTypedCollection; use PHPUnit\Framework\TestCase; use App\Directives\HelloDirective; final class HelloDirectiveTest extends TestCase { private DirectiveTestingService $service; protected function setUp(): void { parent::setUp(); $this->service = new DirectiveTestingService(); } protected function tearDown(): void { $this->service->destroy(); parent::tearDown(); } public function test_directive_returns_success(): void { // Créer le contexte pour la directive $context = new DirectiveContext( laravelBootstrapper: new LaravelBootstrapperContext(), blueprint: new DirectiveBlueprintRecord(HelloDirective::class, 'hello', 'Say hello'), aliases: new StringTypedCollection(), shouldBootLaravel: false ); $directive = new HelloDirective($context, $this->service->getInteraction()); $this->service->registerDirective($directive); $response = $this->service->runDirective('hello', ['John']); $this->assertSame(ExitCode::SUCCESS, $response->exitCode); $this->assertStringContainsString('Hello, John!', $response->output); } }
Directive temporaire avec closure
public function test_temporary_directive(): void { $executed = false; $this->service->createTestDirective('test-closure', function ($d) use (&$executed) { $executed = true; $d->line('Executed!'); return ExitCode::SUCCESS; }); $response = $this->service->runDirective('test-closure'); $this->assertTrue($executed); $this->assertSame(ExitCode::SUCCESS, $response->exitCode); }
Test avec Laravel
protected function setUp(): void { parent::setUp(); $config = new DirectiveTestingConfig(); $context = new DirectiveTestingContext(bootLaravel: true); $context->setConfig($config); $this->service = new DirectiveTestingService($context); }
Méthodes du service
| Méthode | Description |
|---|---|
registerDirective(AbstractDirective $directive) |
Enregistre une directive |
registerDirectives(array $directives) |
Enregistre plusieurs directives |
clearRegisteredDirectives() |
Supprime toutes les directives |
createTestDirective(string $signature, callable $execute) |
Crée une directive temporaire (gère le contexte automatiquement) |
runDirective(string $signature, array $arguments = []) |
Exécute une directive |
createTestDirective()crée automatiquement leDirectiveContextnécessaire, vous n'avez pas à le gérer manuellement.
Exemples complets
Directive de backup avec arguments variadiques
<?php namespace App\Directives; use AndyDefer\Directive\AbstractDirective; use AndyDefer\Directive\Enums\ExitCode; final class BackupDirective extends AbstractDirective { public function getSignature(): string { return 'backup {source} {destination} {excludes*} {--compress} {--format=zip}'; } public function getDescription(): string { return 'Backup files and directories'; } public function execute(): ExitCode { $source = $this->argument('source'); $destination = $this->argument('destination'); $excludes = $this->getVariadicArguments(); $compress = $this->option('compress'); $format = $this->option('format') ?? 'zip'; $this->info("Backup from {$source} to {$destination}"); if ($compress) { $this->info("Compression enabled"); } $this->info("Format: {$format}"); if ($excludes->isNotEmpty()) { $this->info("Excluding: " . implode(', ', $excludes->toArray())); } return ExitCode::SUCCESS; } } // Usage: ./directive backup /var/www /backup [node_modules, .git, cache] --compress
Directive avec base de données (Laravel)
<?php namespace App\Directives; use AndyDefer\Directive\AbstractDirective; use AndyDefer\Directive\Collections\RowCollection; use AndyDefer\Directive\Enums\ExitCode; use AndyDefer\DomainStructures\Collections\Utility\StringTypedCollection; use App\Models\User; final class UserStatsDirective extends AbstractDirective { public function getSignature(): string { return 'user-stats {--active}'; } public function getDescription(): string { return 'Display user statistics'; } public function shouldBootLaravel(): bool { return true; } public function execute(): ExitCode { if (!$this->hasLaravel()) { return ExitCode::FAILURE; } $total = User::count(); $this->info("Total users: {$total}"); $query = User::query(); if ($this->option('active')) { $query->where('is_active', true); } $headers = new StringTypedCollection(); $headers->add('ID', 'Name', 'Email'); $rows = new RowCollection(); foreach ($query->get() as $user) { $row = new RowCollection(); $row->add($user->id, $user->name, $user->email); $rows->add($row); } $this->table($headers, $rows); return ExitCode::SUCCESS; } }
Directive interactive
<?php namespace App\Directives; use AndyDefer\Directive\AbstractDirective; use AndyDefer\Directive\Enums\ExitCode; final class SetupDirective extends AbstractDirective { public function getSignature(): string { return 'app-setup'; } public function getDescription(): string { return 'Interactive setup wizard'; } public function execute(): ExitCode { $this->info('Welcome to the setup wizard!'); $appName = $this->ask('Application name'); $env = $this->ask('Environment (local/production)'); if (!$this->confirm("Create config for {$appName} in {$env}?")) { $this->warn('Cancelled'); return ExitCode::SUCCESS; } $this->info("Configuration created!"); return ExitCode::SUCCESS; } }
Pourquoi ce package ?
Limitations d'Artisan
| Problème | Solution avec Directives |
|---|---|
| Héritage unique obligatoire | Pas de contrainte |
| Logique et présentation mélangées | Séparation claire |
Tests difficiles (ask() impossible à mocker) |
Services mockables |
| Pas d'extensibilité pour les packages | Découverte automatique |
| Arguments non typés | Accès typé |
| Pas d'arguments variadiques | Support des variadiques |
| Couplage fort | Architecture propre |
Avantages
- ✅ Séparation des responsabilités : Logique métier découplée
- ✅ Testabilité exceptionnelle : Chaque directive est mockable
- ✅ Extensibilité : Découverte automatique dans
vendor/*/src/Directives/ - ✅ Laravel à la demande : Bootstrap optionnel
- ✅ Validation stricte : Format et ordre des signatures
- ✅ Typage fort : Arguments et options typés
- ✅ Arguments variadiques : Capture avec
{files*} - ✅ Découverte automatique : Aucune configuration requise