andydefer/laravel-directive
| Install | |
|---|---|
composer require andydefer/laravel-directive |
|
| Latest Version: | v3.67.10 |
| PHP: | >=8.1 |
| Laravel: | 12 · 13 |
| License: | MIT |
| Last Updated: | Oct 16, 2026 |
| Links: | GitHub · Packagist |
Laravel Directive
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
Publier la config
php artisan vendor:publish --tag=directive-config
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
Related Packages
Powerful PHP database abstraction layer (DBAL) with many features for database s...
Laravel Serializable Closure provides an easy and secure way to serialize closur...
Cli error handling for console/command-line PHP applications.
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v3.67.10 | >=8.1 | ^12.0| | MIT | |
| v3.67.9 | >=8.1 | ^12.0| | MIT | |
| v3.67.8 | >=8.1 | ^12.0| | MIT | |
| v3.67.7 | >=8.1 | ^12.0| | MIT | |
| v3.67.6 | >=8.1 | ^12.0| | MIT | |
| v3.67.5 | >=8.1 | ^12.0| | MIT | |
| v3.67.4 | >=8.1 | ^12.0| | MIT | |
| v3.67.3 | >=8.1 | ^12.0| | MIT | |
| v3.67.2 | >=8.1 | ^12.0| | MIT | |
| v3.67.1 | >=8.1 | ^12.0| | MIT | |
| v3.67.0 | >=8.1 | ^12.0| | MIT | |
| v3.66.62 | >=8.1 | ^12.0| | MIT | |
| v3.66.61 | >=8.1 | ^12.0| | MIT | |
| v3.66.60 | >=8.1 | ^12.0| | MIT | |
| v3.66.59 | >=8.1 | ^12.0| | MIT | |
| v3.66.58 | >=8.1 | ^12.0| | MIT | |
| v3.66.57 | >=8.1 | ^12.0| | MIT | |
| v3.66.56 | >=8.1 | ^12.0| | MIT | |
| v3.66.55 | >=8.1 | ^12.0| | MIT | |
| v3.66.54 | >=8.1 | ^12.0| | MIT | |
| v3.66.53 | >=8.1 | ^12.0| | MIT | |
| v3.66.52 | >=8.1 | ^12.0| | MIT | |
| v3.66.51 | >=8.1 | ^12.0| | MIT | |
| v3.66.50 | >=8.1 | ^12.0| | MIT | |
| v3.66.49 | >=8.1 | ^12.0| | MIT | |
| v3.66.48 | >=8.1 | ^12.0| | MIT | |
| v3.66.47 | >=8.1 | ^12.0| | MIT | |
| v3.66.46 | >=8.1 | ^12.0| | MIT | |
| v3.66.45 | >=8.1 | ^12.0| | MIT | |
| v3.66.44 | >=8.1 | ^12.0| | MIT |
Showing the latest 30 of 236. See every release on Packagist