andydefer/laravel-directive
Composer 安装命令:
composer require 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).
README 文档
README
Un framework CLI pour Laravel. Orchestration de pipelines, contexte partagé, découverte automatique, appels internes - avec ou sans Laravel.
Table des matières
- Installation
- Philosophie : Créez votre propre CLI
- Première directive
- Signature des commandes
- Accès aux arguments et options
- Contexte partagé
- Appels internes (call)
- Découverte automatique
- Journalisation JSONL
- Suggestions de commandes
- Mode verbose et débogage
- Directives intégrées
- Exécution hors CLI
- Mode autonome (sans Laravel)
- Tests des directives
- Cas d'usage concrets
- Bonnes pratiques
- Référence des codes de sortie
Installation
composer require andydefer/laravel-directive
Prérequis : PHP 8.1+ | Laravel 12.x, 13.x, 14.x ou 15.x
Philosophie : Créez votre propre CLI
Ce package n'est pas un simple binaire préfabriqué. C'est un framework pour construire votre propre système CLI adapté à vos besoins métier.
Le binaire fourni (vendor/bin/directive) est un exemple de démonstration, pas une solution finale.
Pourquoi créer votre propre CLI ?
✅ Contrôle total - Vous décidez des commandes, des providers, des sources ✅ Logique métier - Intégration parfaite avec votre domaine applicatif ✅ Providers personnalisés - Enregistrez vos propres services et dépendances ✅ Performance - Chargez uniquement ce dont vous avez besoin ✅ Sécurité - Contrôle d'accès granulaire ✅ Évolutivité - Facile à étendre avec votre code
Créer votre point d'entrée
#!/usr/bin/env php <?php // bin/my-app require_once __DIR__ . '/vendor/autoload.php'; use AndyDefer\Directive\Bootstrap\ApplicationBuilder; use AndyDefer\Directive\DirectiveKernel; use AndyDefer\Directive\DirectiveServiceProvider; use App\Providers\MyAppServiceProvider; // ✅ Personnalisation complète $app = ApplicationBuilder::internal([ DirectiveServiceProvider::class, MyAppServiceProvider::class, // Vos providers ])->withConfig([ 'app.name' => 'My Application CLI', 'app.debug' => getenv('APP_DEBUG') === 'true', ])->build(); $kernel = $app->make(DirectiveKernel::class); // ✅ Ajouter vos sources $kernel->addSource(__DIR__ . '/src/Directives'); $kernel->addSource(__DIR__ . '/modules/*/Directives'); // ✅ Configuration $kernel->verbose(getenv('VERBOSE') === 'true') ->setLogBasePath('/var/log/my-app'); $exitCode = $kernel->run($argv); exit($exitCode->value);
chmod +x bin/my-app ./bin/my-app deploy staging
Première directive
1. Créer la classe
<?php namespace App\Directives; use AndyDefer\Directive\AbstractDirective; use AndyDefer\Directive\Enums\ExitCode; use AndyDefer\DomainStructures\Collections\Utility\StringTypedCollection; class GreetDirective extends AbstractDirective { public function getSignature(): string { return 'greet {name} {--formal}'; } public function getDescription(): string { return 'Say hello to someone'; } public function getAliases(): StringTypedCollection { return StringTypedCollection::from(['hello', 'hi']); } protected function beforeExecute(): void { if ($this->getFlag('formal')) { $this->info('Formal mode enabled'); } } protected function execute(): ExitCode { $name = $this->getArgument('name'); $formal = $this->getFlag('formal'); $greeting = $formal ? "Good day, $name" : "Hello, $name"; $this->info($greeting); return ExitCode::SUCCESS; } protected function afterExecute(ExitCode $exitCode): void { $this->newLine(); $this->line('Execution completed'); } }
2. Exécuter avec votre CLI
# Avec votre propre CLI ./bin/my-app greet John # Hello, John # Avec option ./bin/my-app greet John --formal # Formal mode enabled # Good day, John # Avec alias ./bin/my-app hello John # Hello, John
Signature des commandes
La signature est la clé de voûte du système. Elle définit la structure complète d'une commande : son nom, ses arguments, ses flags, ses énumérations et plus encore.
Le parseur de signature est basé sur php-signature-parser et supporte une syntaxe riche et expressive.
Structure de la signature
public function getSignature(): string { return 'backup {source} {destination} {format=zip} {env=?} ::level->[low,medium,high]=medium {excludes*} {purpose*} {--force} {--verbose}'; }
Arguments requis
Les arguments requis sont obligatoires. La commande échoue si l'utilisateur ne les fournit pas.
public function getSignature(): string { return 'backup {source} {destination}'; }
Arguments par défaut
Les arguments par défaut fournissent une valeur si l'utilisateur n'en spécifie pas.
public function getSignature(): string { return 'backup {source} {destination} {format=zip} {compression=9} {output=dist}'; }
Arguments nullables
Les arguments nullables peuvent recevoir la valeur null explicitement.
public function getSignature(): string { return 'deploy {environment} {version=?} {config=?}'; }
Arguments variadiques
Les arguments variadiques capturent zéro, une ou plusieurs valeurs.
public function getSignature(): string { return 'delete {files*} {directories*} {--force}'; }
Flags
Les flags sont des options booléennes. Présents = true, absents = false.
public function getSignature(): string { return 'deploy {environment} {--force} {--verbose} {--dry-run}'; }
Énumérations
Les énumérations restreignent les valeurs autorisées.
Syntaxe
::name->[value1,value2,value3]=state
États possibles
| État | Syntaxe | Description |
|---|---|---|
| Requis | =* |
Doit être fourni |
| Optionnel | =? |
Peut être ~ (skip) |
| Défaut | =default |
Valeur par défaut |
Exemples
// Avec valeur par défaut $signature = 'set-level ::level->[beginner,middle,master]=medium'; // Requis $signature = 'set-level ::level->[beginner,middle,master]=*'; // Optionnel $signature = 'set-level ::level->[beginner,middle,master]=?'; // Avec commentaire $signature = 'set-level ::level->[beginner,middle,master]=medium#"The skill level"';
Accès aux énumérations
// Dans une directive $level = $this->getEnum('level'); // Vérifications $allowed = $this->getEnumAllowedValues('level'); $isRequired = $this->isEnumRequired('level'); $isAllowed = $this->isEnumValueAllowed('level', 'master');
Commentaires inline
Les commentaires documentent chaque argument directement dans la signature.
public function getSignature(): string { return 'backup {source}#"Source directory" {destination}#"Destination" {format=zip}#"Archive format" {--force}#"Force overwrite"'; }
Ordre strict des éléments
⚠️ L'ordre des éléments dans la signature est STRICT et IMPÉRATIF.
| Ordre | Type | Syntaxe | Exemple |
|---|---|---|---|
| 1 | Source | command |
backup |
| 2 | Requis | {name} |
{source} {destination} |
| 3 | Par défaut | {name=value} |
{format=zip} {output=dist} |
| 4 | Nullable | {name=?} |
{env=?} {port=?} |
| 5 | Enum | ::name->[values]=state |
::level->[low,high]=medium |
| 6 | Variadique | {name*} |
{excludes*} {purpose*} |
| 7 | Flags | {--flag} |
{--force} {--verbose} |
| 8 | Tags personnalisés | <key="value"> |
<user="admin"> |
Exemples d'ordre valide
// ✅ Ordre correct avec tous les types 'backup {source} {destination} {format=zip} {env=?} ::level->[low,high]=medium {excludes*} {--force}' // ✅ Commentaires à n'importe quelle position 'backup {source}#"Source" {destination} {--force}#"Force"'
Exemples d'ordre invalide
// ❌ Enum après variadic 'backup {source} {excludes*} ::level->[low,high]=medium' // ❌ Required après default 'backup {format=zip} {source}'
Formatage des espaces avec ^
Le parseur remplace automatiquement les caractères ^ par des espaces.
| Saisie utilisateur | Valeur réelle |
|---|---|
John^Doe |
John Doe |
Hello^World! |
Hello World! |
C:/Program^Files |
C:/Program Files |
./bin/my-app user:create John^Doe john@example.com
# name = "John Doe"
Tokens spéciaux
Le token ? (null explicite)
Permet de passer explicitement null comme valeur.
# env = null ./bin/my-app deploy staging ?
Le token ~ (skip)
Permet de sauter un argument.
| Cas | Comportement |
|---|---|
| Argument requis | ~ → null |
| Argument par défaut | ~ → utilise la valeur par défaut |
| Argument nullable | ~ → null |
| Enum avec défaut | ~ → utilise la valeur par défaut |
| Enum optionnel | ~ → null |
# format utilise la valeur par défaut (zip) ./bin/my-app backup /var/www ~ # level utilise la valeur par défaut (medium) ./bin/my-app set-level ~
Tags personnalisés
Les tags permettent d'ajouter des données supplémentaires sans modifier la signature.
./bin/my-app send John --verbose <greeting="Hello World"> <later="goodby">
// Dans la directive $customData = $this->getCustomData(); $greeting = $customData['greeting'] ?? 'Default greeting';
Accès aux arguments et options
Méthodes principales
protected function execute(): ExitCode { // Arguments requis $name = $this->getRequired('name'); $email = $this->getRequired('email'); // Tous les arguments requis $requireds = $this->getRequireds(); // Arguments par défaut $format = $this->getDefault('format'); // Tous les arguments par défaut $defaults = $this->getDefaults(); // Arguments nullables $version = $this->getArgument('version'); // Arguments variadiques $files = $this->getVariadic('files'); // Tous les arguments variadiques $variadics = $this->getVariadics(); // Collection plate de toutes les valeurs variadiques $allValues = $this->getVariadicArguments(); // Flags $force = $this->getFlag('force'); $verbose = $this->getFlag('verbose'); // Tous les flags $flags = $this->getFlags(); // Flags actifs $active = $this->getActiveFlags(); // Énumérations $level = $this->getEnum('level'); // Toutes les énumérations $enums = $this->getEnums(); // Valeurs autorisées pour une enum $allowed = $this->getEnumAllowedValues('level'); // Vérifications if ($this->hasArgument('email')) { /* ... */ } if ($this->hasFlag('force')) { /* ... */ } if ($this->hasRequireds()) { /* ... */ } if ($this->hasDefaults()) { /* ... */ } if ($this->hasEnums()) { /* ... */ } if ($this->hasFlags()) { /* ... */ } if ($this->hasVariadicArguments()) { /* ... */ } return ExitCode::SUCCESS; }
Recherche d'arguments avec getArgument()
getArgument() recherche dans l'ordre de priorité :
- Arguments requis
- Arguments par défaut
- Énumérations
- Arguments variadiques (retourne un tableau)
- Flags (retourne un booléen)
// Peut retourner un string, un booléen ou un array $value = $this->getArgument('key'); if (is_array($value)) { // C'est un variadic } elseif (is_bool($value)) { // C'est un flag } else { // C'est un argument (requis, default, nullable, enum) }
Contexte partagé
Le contexte est un MapCollection mutable accessible par toutes les directives.
Méthodes du contexte
// Définir une valeur $this->contextSet('key', 'value'); // Lire une valeur $value = $this->contextGet('key', 'default'); // Vérifier l'existence if ($this->contextHas('key')) { // ... } // Obtenir tout le contexte $all = $this->contextAll(); // Fusionner des valeurs $this->contextMerge([ 'user_id' => 42, 'user_role' => 'admin', ]); // Supprimer une clé $this->contextRemove('temporary_data'); // Effacer tout le contexte $this->contextClear(); // Incrémenter / Décrémenter $this->contextIncrement('counter', 5); $this->contextDecrement('counter', 2);
Exemple : Pipeline de traitement
class ProcessDataDirective extends AbstractDirective { public function getSignature(): string { return 'data:process {file}'; } protected function execute(): ExitCode { $file = $this->getArgument('file'); $this->contextSet('file', $file); $this->call('data:load'); $this->call('data:clean'); $this->call('data:transform'); $stats = $this->contextGet('processing_stats'); $this->info("📊 Processed {$stats['total']} records"); return ExitCode::SUCCESS; } }
Appels internes (call)
La méthode call() permet d'exécuter d'autres directives depuis une directive.
Syntaxe
// Appel simple $this->call('build --clean'); // Avec argument $this->call('deploy:backup staging'); // Avec options $this->call('deploy:migrate --force'); // Appel dynamique $env = $this->getArgument('environment'); $this->call("deploy:validate $env"); // Multiple appels $this->call('task:one'); $this->call('task:two'); $this->call('task:three'); // Avec parsing du contexte $this->call("greet {$name} --formal");
Détection de circularité
Laravel Directive détecte automatiquement les appels circulaires :
class CircularDirective extends AbstractDirective { public function getSignature(): string { return 'circular'; } protected function execute(): ExitCode { $this->call('circular'); // ⚠️ Détecté automatiquement return ExitCode::SUCCESS; } }
Pipeline de déploiement
class DeployDirective extends AbstractDirective { public function getSignature(): string { return 'deploy {environment} {--skip-tests} {--force}'; } protected function execute(): ExitCode { $env = $this->getArgument('environment'); $skipTests = $this->getFlag('skip-tests'); $force = $this->getFlag('force'); $this->call("deploy:validate $env"); $this->call("deploy:backup $env"); if (!$skipTests) { $this->call('deploy:build --with-tests'); } else { $this->call('deploy:build'); } $forceFlag = $force ? '--force' : ''; $this->call("deploy:migrate $env $forceFlag"); $this->call("deploy:activate $env"); $this->call('deploy:post --notify'); $this->call('deploy:cleanup --keep-last=3'); return ExitCode::SUCCESS; } }
Découverte automatique
Laravel Directive découvre automatiquement les directives via AST (Abstract Syntax Tree).
Sources de découverte
| Source | Description | Dossier par défaut |
|---|---|---|
| Built-in | Directives intégrées | src/BuiltIn/ |
| Workspace | Directives de l'application | src/Directives/, app/Directives/ |
| Vendor | Directives des packages | vendor/*/src/Directives/ |
| Custom | Sources configurées | Configurable |
Filtrer la découverte
$kernel = $app->make(DirectiveKernel::class); // Ignorer une source $kernel->ignoreSource(DiscoverySource::VENDOR); // Ajouter une source $kernel->addSource(__DIR__ . '/src/Commands'); // Filtrer par namespace $kernel->onlyNamespace('App\\Directives\\'); $kernel->excludeNamespace('App\\Directives\\Internal\\'); // Filtrer par préfixe $kernel->onlyPrefix('app:'); $kernel->excludePrefix('admin:'); // Ignorer une directive spécifique $kernel->ignoreDirective('deprecated:command');
Système de problèmes
Le système de découverte collecte automatiquement les problèmes :
$kernel->discover(); $problems = $kernel->getProblems(); foreach ($problems as $problem) { echo $problem->get('key') . ': ' . $problem->get('message'); }
Journalisation JSONL
Chaque exécution est automatiquement journalisée au format JSONL.
Structure du log
{
"time": "2026-07-09T11:45:23+00:00",
"level": "info",
"type": "directive_execution",
"payload": {
"command": "deploy staging",
"directive_class": "App\\Directives\\DeployDirective",
"signature": "deploy {environment} {--skip-tests} {--force}",
"exit_code": 0,
"exit_code_label": "Success",
"success": true,
"duration_seconds": 12.345,
"memory_bytes": 2048,
"memory_human": "2.00 KB",
"peak_memory_bytes": 4096,
"peak_memory_human": "4.00 KB",
"calls_count": 7,
"context": {
"environment": "staging",
"deployment_start": 1700000000
}
}
}
Configuration
$kernel->setLogBasePath('/var/log/directive');
Suggestions de commandes
Laravel Directive utilise un BK-tree (distance de Levenshtein) pour suggérer des commandes similaires.
$ ./bin/my-app depoy Directive not found: depoy 💡 Did you mean: • deploy • deploy:validate • deploy:backup
Les alias sont également pris en compte.
Mode verbose et débogage
Le mode verbose affiche automatiquement les problèmes rencontrés.
Activation
./bin/my-app deploy staging --verbose
$kernel->verbose(true);
Audit du noyau
./bin/my-app kernel:audit ./bin/my-app kernel:audit --verbose
Directives intégrées
| Directive | Description | Alias |
|---|---|---|
help |
Affiche l'aide | -h, --help |
list |
Liste toutes les directives | ls, -l, --list |
version |
Affiche la version | -v, --version |
clean-logs [days] |
Nettoie les logs | - |
kernel:audit |
Audit du noyau | audit |
Exécution hors CLI
Les directives peuvent être exécutées dans n'importe quel contexte PHP.
Dans un contrôleur Laravel
use AndyDefer\Directive\DirectiveKernel; use AndyDefer\Directive\Enums\ExitCode; class TaskController extends Controller { public function run(DirectiveKernel $kernel) { $exitCode = $kernel->runSignature('process:data --format=json'); if ($exitCode === ExitCode::SUCCESS) { return response()->json(['message' => 'Task completed']); } return response()->json(['error' => 'Task failed'], 500); } }
Dans un job Laravel
class ProcessDataJob implements ShouldQueue { public function handle(DirectiveKernel $kernel): void { $kernel->runSignature('process:batch --limit=1000'); } }
Dans un service métier
class ReportService { public function __construct(private DirectiveKernel $kernel) {} public function generateDailyReport(): array { $this->kernel->runSignature('report:fetch'); $this->kernel->runSignature('report:process --format=pdf'); $this->kernel->runSignature('report:send --email=admin@example.com'); return ['status' => 'completed']; } }
Mode autonome (sans Laravel)
Script d'entrée
#!/usr/bin/env php <?php // bin/standalone require_once __DIR__ . '/vendor/autoload.php'; use AndyDefer\Directive\Bootstrap\ApplicationBuilder; use AndyDefer\Directive\DirectiveKernel; use AndyDefer\Directive\DirectiveServiceProvider; $app = ApplicationBuilder::external([ DirectiveServiceProvider::class, ])->withConfig([ 'app.name' => 'Standalone CLI', ])->build(); $kernel = $app->make(DirectiveKernel::class); $kernel->addSource(__DIR__ . '/src/Directives'); exit($kernel->run($argv)->value);
Tests des directives
DirectiveTestingService permet de tester les directives en isolation.
<?php namespace Tests\Directives; use AndyDefer\Directive\Bootstrap\ApplicationBuilder; use AndyDefer\Directive\DirectiveServiceProvider; use AndyDefer\Directive\Services\DirectiveTestingService; use AndyDefer\Directive\Enums\ExitCode; use PHPUnit\Framework\TestCase; class GreetDirectiveTest extends TestCase { private DirectiveTestingService $testing; protected function setUp(): void { parent::setUp(); $app = ApplicationBuilder::internal([ DirectiveServiceProvider::class, ])->build(); $this->testing = new DirectiveTestingService( $app, [__DIR__ . '/../app/Directives'] ); } protected function tearDown(): void { $this->testing->destroy(); parent::tearDown(); } public function test_greet_directive_returns_success(): void { $response = $this->testing->run('greet John'); $this->assertSame(ExitCode::SUCCESS, $response->exit_code); $this->assertStringContainsString('Hello, John', $response->output); } public function test_unknown_command_returns_problems(): void { $response = $this->testing->run('unknown-command'); $this->assertSame(ExitCode::NOT_FOUND, $response->exit_code); $this->assertFalse($response->problems->isEmpty()); } }
Cas d'usage concrets
Pipeline de déploiement
class DeployDirective extends AbstractDirective { public function getSignature(): string { return 'deploy {environment} {--skip-tests} {--force}'; } protected function execute(): ExitCode { $env = $this->getArgument('environment'); $this->call("deploy:validate $env"); $this->call("deploy:backup $env"); $this->call('deploy:build' . ($this->getFlag('skip-tests') ? ' --skip-tests' : '')); $this->call("deploy:migrate $env" . ($this->getFlag('force') ? ' --force' : '')); $this->call("deploy:activate $env"); $this->call('deploy:post --notify'); return ExitCode::SUCCESS; } }
Data processing pipeline
class ProcessDataDirective extends AbstractDirective { public function getSignature(): string { return 'data:process {file} {--format=csv} {--dry-run}'; } protected function execute(): ExitCode { $file = $this->getArgument('file'); $this->call("data:load $file"); $this->call('data:clean'); $this->call('data:transform --aggressive'); $this->call('data:validate'); if (!$this->getFlag('dry-run')) { $format = $this->getArgument('format'); $this->call("data:export --format=$format"); } $stats = $this->contextGet('processing_stats'); $this->info("📊 Processed {$stats['total']} records"); return ExitCode::SUCCESS; } }
Bonnes pratiques
✅ Injection de services
// BON class UserController { public function __construct( private readonly UniqueTaskServiceInterface $taskService ) {} } // ÉVITER (facade) use AndyDefer\Task\Facades\Task; Task::register(...);
✅ Validation des arguments
protected function beforeExecute(): void { $name = $this->getArgument('name'); if (empty($name) || strlen($name) < 3) { $this->error('Name must be at least 3 characters'); throw new \RuntimeException('Invalid name'); } }
✅ Utiliser le contexte pour les données partagées
// BON $this->contextSet('user_id', $user->id); $userId = $this->contextGet('user_id'); // ÉVITER global $userId; $userId = 42;
✅ Hooks before/after
protected function beforeExecute(): void { $this->contextSet('start_time', microtime(true)); $this->info('Starting...'); } protected function afterExecute(ExitCode $exitCode): void { $duration = microtime(true) - $this->contextGet('start_time'); $this->info("Completed in {$duration}s"); }
✅ Créer son propre CLI
#!/usr/bin/env php <?php // bin/my-app require_once __DIR__ . '/vendor/autoload.php'; use AndyDefer\Directive\Bootstrap\ApplicationBuilder; use AndyDefer\Directive\DirectiveKernel; use AndyDefer\Directive\DirectiveServiceProvider; use App\Providers\AppServiceProvider; $app = ApplicationBuilder::internal([ DirectiveServiceProvider::class, AppServiceProvider::class, ])->build(); $kernel = $app->make(DirectiveKernel::class); $kernel->addSource(__DIR__ . '/src/Directives'); exit($kernel->run($argv)->value);
Référence des codes de sortie
| Code | Label | Description |
|---|---|---|
0 |
SUCCESS | Exécution réussie |
1 |
FAILURE | Échec général |
2 |
INVALID_ARGUMENT | Argument invalide |
3 |
NOT_FOUND | Directive non trouvée |
4 |
PERMISSION_DENIED | Permission refusée |
5 |
RUNTIME_ERROR | Erreur d'exécution |
6 |
INVALID_SIGNATURE | Signature invalide |
7 |
CONFLICT | Conflit (circularité) |
8 |
DEPENDENCY_ERROR | Erreur de dépendance |
Licence
MIT © Andy Defer
andydefer/laravel-directive 适用场景与选型建议
andydefer/laravel-directive 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 396 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 05 月 22 日, 在 PHP 生态内属于活跃度较高的组件。
我们在过去多个企业项目中使用过 andydefer/laravel-directive 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 andydefer/laravel-directive 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
统计信息
- 总下载量: 396
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 42
- 依赖项目数: 6
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-22