andydefer / php-signature-parser
A flexible signature parser for CLI commands
Requires
- php: ^8.1
- andydefer/domain-structures: ^1.26.0
Requires (Dev)
- laravel/pint: ^1.29
- phpunit/phpunit: ^13.2
- rector/rector: ^2.4
- symfony/var-dumper: ^8.1
README
Un parseur strict et typé pour les commandes CLI qui extrait la source, les arguments requis, les arguments par défaut, les nullables, les variadiques, les énumérations et les flags avec des Value Objects et des collections typées. Support automatique du formatage des espaces via le caractère ^, des commentaires inline, des tokens spéciaux (?, _) et des tags personnalisés.
Table des matières
- Installation
- Concepts fondamentaux
- Commentaires inline
- Formatage des espaces avec
^ - Tokens spéciaux
- Ordre strict des arguments
- Énumérations (Enum)
- Tags personnalisés
- Utilisation du parseur
- Manipulation des collections
- Value Objects
- SignatureDocumentor - Génération de documentation
- QueryBuilder - Construction dynamique
- Les parseurs internes
- Extensibilité
- Cas d'usage avancés
- Exemples complets
- Licence
Installation
composer require andydefer/php-signature-parser
Prérequis
- PHP 8.1 ou supérieur
Concepts fondamentaux
La signature
La signature est une chaîne qui décrit la structure de la commande.
$signature = 'backup {source} {destination} {format=zip} {env=?} ::level->[low,medium,high]=medium {excludes*} {purpose*} {--force} {--verbose}';
| Élément | Syntaxe | Description |
|---|---|---|
| Source | backup |
Nom de la commande (position 0) |
| Requis | {source} |
Argument obligatoire |
| Par défaut | {format=zip} |
Argument avec valeur par défaut |
| Nullable | {env=?} |
Argument pouvant être null |
| Enum | ::level->[low,medium,high]=medium |
Énumération avec valeurs autorisées |
| Variadique | {excludes*} |
Argument qui capture plusieurs valeurs |
| Flag | {--force} |
Flag optionnel (booléen) |
| Tag personnalisé | <key="value"> |
Données supplémentaires (non définies dans la signature) |
| Commentaire | # "comment" |
Documentation inline |
La requête
La requête est la commande réelle exécutée par l'utilisateur.
$query = 'backup /var/www /backup tar.gz staging high [cache, logs, tmp] [home, data, models] --force <user="admin">';
Commentaires inline
Les commentaires permettent de documenter chaque argument directement dans la signature.
Syntaxe
{name}#'comment'
{name=value}#'comment'
{name*>[values]}#'comment'
::name->[values]#'comment'
{--flag}#'comment'
Exemples
$signature = 'backup {source}#"Source directory" {destination}#"Destination" {format=zip}#"Archive format" {--force}#"Force overwrite"';
Utilisation avec les records
Les commentaires sont automatiquement extraits et disponibles dans les records :
$result = $parser->parse($signature, $query); echo $result->requireds->first()->comment; // 'Source directory' echo $result->defaults->first()->comment; // 'Archive format' echo $result->flags->first()->comment; // 'Force overwrite'
Formats supportés
// Guillemets doubles {name}#"The user name" // Guillemets simples {name}#'The user name'
Formatage des espaces avec ^
Le parser remplace automatiquement les caractères ^ par des espaces dans toutes les valeurs extraites.
Règle simple
Pour inclure un espace dans une valeur, utilisez
^à la place.
| Saisie utilisateur | Valeur réelle |
|---|---|
John^Doe |
John Doe |
Hello^World! |
Hello World! |
C:/Program^Files |
C:/Program Files |
Exemples
// Arguments requis $signature = 'user:create {name} {email}'; $query = 'user:create John^Doe john@example.com'; $result = $parser->parse($signature, $query); // $result->requireds->first()->value = 'John Doe' // Valeurs par défaut $signature = 'user:list {format=zip}'; $query = 'user:list tar^gz'; $result = $parser->parse($signature, $query); // $result->defaults->first()->value = 'tar gz'
Tokens spéciaux
Le token ? (null explicite)
Le token ? permet de passer explicitement null comme valeur.
| Cas | Exemple | Résultat |
|---|---|---|
| Argument requis | backup /var/www ? |
destination = null |
| Argument par défaut | deploy staging ? |
env = null (override) |
Le token _ (skip)
Le token _ permet de sauter un argument et d'utiliser la valeur par défaut ou null :
| 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 |
Exemples
// Par défaut → valeur par défaut $signature = 'backup {source} {format=zip}'; $query = 'backup /var/www _'; // format = zip // Nullable → null $signature = 'deploy {env=?} {--force}'; $query = 'deploy _ --force'; // env = null // Enum avec défaut $signature = 'set-level ::level->[low,high]=medium'; $query = 'set-level _'; // level = medium
Ordre strict des arguments
⚠️ 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 $signature = 'backup {source} {destination} {format=zip} {env=?} ::level->[low,high]=medium {excludes*} {--force}'; // ✅ Commentaires à n'importe quelle position $signature = 'backup {source}#"Source" {destination} {--force}#"Force"';
Exemples d'ordre invalide
// ❌ Enum après variadic $signature = 'backup {source} {excludes*} ::level->[low,high]=medium'; // ❌ Required après default $signature = 'backup {format=zip} {source}';
Énumérations (Enum)
Les énumérations permettent de restreindre les valeurs autorisées pour un argument.
Syntaxe
::name->[value1,value2,value3]=state
États possibles
| État | Syntaxe | Description |
|---|---|---|
| Requis | =* |
Doit être fourni |
| Optionnel | =? |
Peut être _ |
| Défaut | =default |
Valeur par défaut |
Exemples
// Avec valeur par défaut $signature = 'set-level ::level->[beginner,middle,master]=middle'; $query = 'set-level master'; // level = 'master' // Requis $signature = 'set-level ::level->[beginner,middle,master]=*'; $query = 'set-level beginner'; // level = 'beginner' // set-level seul échouerait // Optionnel $signature = 'set-level ::level->[beginner,middle,master]=?'; $query = 'set-level _'; // level = null // Avec commentaire $signature = 'set-level ::level->[beginner,middle,master]=medium#"The skill level"';
Accès aux énumérations
$result = $parser->parse($signature, $query); // Valeur $level = $result->enums->get('level'); // 'master' // Valeurs autorisées $allowed = $result->enums->getAllowedValues('level'); // ['beginner', 'middle', 'master'] // Vérifications if ($result->enums->isRequired('level')) { echo "Level est requis"; } if ($result->enums->isAllowed('level', 'master')) { echo "'master' est autorisé"; }
Tags personnalisés
Les tags personnalisés permettent d'ajouter des données supplémentaires à une commande sans modifier la signature.
Syntaxe
<key="value"> <key='value'>
Utilisation
$signature = 'send {recipient} {--verbose}'; $query = 'send John --verbose <greeting="Hello World"> <later="goodby">'; $result = $parser->parse($signature, $query); $customData = $result->custom_data->toArray(); echo $customData['greeting']; // 'Hello World' echo $customData['later']; // 'goodby'
Avec QueryBuilder
$query = QueryBuilder::init('deploy {environment}') ->setRequired('environment', 'staging') ->setCustoms([ 'version' => '1.2.3', 'user' => 'admin' ]) ->build(); // 'deploy staging <version="1.2.3"> <user="admin">'
Utilisation du parseur
Utilisation de base
<?php use AndyDefer\SignatureParser\SignatureParser; $signature = 'backup {source} {destination} {format=zip} {output=dist} {env=?} ::level->[low,high]=medium {excludes*} {purpose*} {--force} {--verbose}'; $query = 'backup /var/www /backup tar.gz dist staging high [cache, logs, tmp] [home, data, models] --force'; $parser = new SignatureParser(); $result = $parser->parse($signature, $query); echo $result->source; // 'backup' echo $result->requireds->first()->value; // '/var/www' echo $result->defaults->first()->value; // 'tar.gz' echo $result->variadics->first()->values->first(); // 'cache' echo $result->flags->first()->value; // true echo $result->enums->get('level'); // 'high'
Validation
// Validation de requête $result = $parser->validate( 'backup {source} {destination}', 'backup /var/www' ); if (!$result->isValid) { foreach ($result->errors as $error) { echo "❌ $error\n"; } } // Validation de signature $result = $parser->validateSignature('backup {source} {format=zip} {--force}'); if ($result->isValid) { echo "✅ Signature valide\n"; }
Manipulation des collections
ArgumentCollection
Collection d'arguments (ArgumentRecord).
$collection = $result->requireds; // Récupérer une valeur par nom $source = $collection->get('source'); // '/var/www' // Vérifier si un argument existe if ($collection->has('destination')) { echo "Destination définie"; } // Récupérer tous les noms $names = $collection->getNames(); // ['source', 'destination'] // Convertir en tableau associatif $assoc = $collection->toAssociativeArray(); // ['source' => '/var/www', 'destination' => '/backup']
FlagCollection
Collection de flags (FlagRecord).
$collection = $result->flags; // Récupérer la valeur d'un flag $force = $collection->get('force'); // true // Vérifier si un flag est actif if ($collection->isActive('force')) { echo "Mode force activé"; } // Récupérer tous les flags actifs $active = $collection->getActiveNames(); // ['force']
EnumCollection
Collection d'énumérations (EnumRecord).
$collection = $result->enums; // Récupérer une valeur $level = $collection->get('level'); // 'high' // Valeurs autorisées $allowed = $collection->getAllowedValues('level'); // ['low', 'medium', 'high'] // Vérifications if ($collection->isRequired('level')) { echo "Level est requis"; } if ($collection->isAllowed('level', 'master')) { echo "'master' est autorisé"; } // Tableau associatif [nom => valeur] $assoc = $collection->toAssociativeArray(); // ['level' => 'high'] // Tableau complet avec toutes les données $full = $collection->toFullArray(); // [ // [ // 'name' => 'level', // 'value' => 'high', // 'allowed_values' => ['low', 'medium', 'high'], // 'default_value' => 'medium', // 'value_state' => 'DEFAULTED' // ] // ]
VariadicArgumentCollection
Collection d'arguments variadiques (VariadicArgumentRecord).
$collection = $result->variadics; // Récupérer les valeurs $excludes = $collection->get('excludes'); // ['cache', 'logs', 'tmp'] // Compter le nombre total de valeurs $total = $collection->countAllValues(); // 5 // Convertir en tableau associatif $assoc = $collection->toAssociativeArray(); // ['excludes' => ['cache', 'logs', 'tmp']]
Value Objects
SignatureStructureVO
Analyse UNIQUEMENT la structure d'une signature (sans requête).
use AndyDefer\SignatureParser\ValueObjects\SignatureStructureVO; $vo = new SignatureStructureVO('backup {source} {destination} {format=zip} ::level->[low,high]=medium {excludes*} {--force}'); // Accès aux informations echo $vo->getSource(); // 'backup' $requireds = $vo->getRequireds(); // ['source', 'destination'] $defaults = $vo->getDefaults(); // ['format' => 'zip'] $enums = $vo->getEnums(); // ['level' => [...]] $variadics = $vo->getVariadics(); // ['excludes'] $flags = $vo->getFlags(); // ['force'] // Vérifications if ($vo->hasEnum('level')) { $allowed = $vo->getEnumAllowedValues('level'); // ['low', 'medium', 'high'] } // Validation if ($vo->isValid()) { echo "✅ Signature valide"; } else { foreach ($vo->getValidationErrors() as $error) { echo "❌ $error\n"; } } // Documentation $markdown = $vo->documentInMarkdown(); $json = $vo->documentInJson(); $array = $vo->documentInArray();
SignatureVO
Analyse complète avec signature ET requête.
use AndyDefer\SignatureParser\ValueObjects\SignatureVO; $vo = new SignatureVO( 'backup {source} {destination} {format=zip} {--force} ::level->[low,high]=medium', 'backup /var/www /backup tar.gz --force high' ); // Accès direct aux valeurs echo $vo->getSource(); // 'backup' echo $vo->getRequired('source'); // '/var/www' echo $vo->getDefault('format'); // 'tar.gz' echo $vo->getFlag('force'); // true echo $vo->getEnum('level'); // 'high' // Vérifications if ($vo->hasFlag('force')) { echo "Force mode activé"; } if ($vo->hasEnum('level')) { echo $vo->getEnum('level'); // 'high' } // Récupération complète $requireds = $vo->getRequireds(); // ['source' => '/var/www', 'destination' => '/backup'] $defaults = $vo->getDefaults(); // ['format' => 'tar.gz'] $flags = $vo->getFlags(); // ['force' => true] $enums = $vo->getEnums(); // ['level' => 'high'] // Validation if (!$vo->isValid()) { foreach ($vo->getValidationErrors() as $error) { echo "❌ $error\n"; } }
SignatureDocumentor - Génération de documentation
Le SignatureDocumentor génère automatiquement une documentation complète pour une signature.
Formats supportés
| Format | Méthode | Description |
|---|---|---|
| Markdown | documentInMarkdown() |
Documentation structurée avec tables |
| Texte | documentInText() |
Documentation en texte brut |
| JSON | documentInJson() |
Export structuré |
| Array | documentInArray() |
Tableau PHP |
Utilisation
use AndyDefer\SignatureParser\ValueObjects\SignatureStructureVO; $vo = new SignatureStructureVO('backup {source}#"Source" {destination} {format=zip}#"Format" {--force}#"Force"'); // Markdown $markdown = $vo->documentInMarkdown(); echo $markdown; // JSON $json = $vo->documentInJson(); file_put_contents('doc.json', $json); // Array $data = $vo->documentInArray(); print_r($data);
Exemple de sortie Markdown
# Commande : backup ## Description ```bash backup <source> <destination> [format=zip] [--force]
Arguments requis
| Nom | Description |
|---|---|
source |
Source |
destination |
— |
Arguments par défaut
| Nom | Défaut | Description |
|---|---|---|
format |
zip |
Format |
Flags
| Nom | Description |
|---|---|
--force |
Force |
---
## QueryBuilder - Construction dynamique
Le `QueryBuilder` permet de construire programmatiquement des requêtes CLI.
### Utilisation de base
```php
use AndyDefer\SignatureParser\QueryBuilder;
$query = QueryBuilder::init('greet {name} {--formal}')
->setRequired('name', 'John')
->setFlag('--formal', true)
->build();
echo $query; // 'greet John --formal'
Avec énumérations
$query = QueryBuilder::init('set-level ::level->[beginner,middle,master]=middle {--verbose}') ->setEnum('level', 'master') ->setFlag('--verbose', true) ->build(); echo $query; // 'set-level master --verbose'
Avec tags personnalisés
$query = QueryBuilder::init('send {recipient} {--verbose}') ->setRequired('recipient', 'John') ->setFlag('--verbose', true) ->setCustom('greeting', 'Hello World') ->setCustoms([ 'later' => 'goodby', 'user' => 'admin' ]) ->build(); echo $query; // 'send John --verbose <greeting="Hello World"> <later="goodby"> <user="admin">'
Parsing d'une requête initiale
$builder = QueryBuilder::init( 'send {recipient} {--verbose}', 'send John --verbose <greeting="Hello">' ); echo $builder->getRequired('recipient'); // 'John' echo $builder->getCustom('greeting'); // 'Hello' $query = $builder->setCustom('greeting', 'Hello World')->build(); // 'send John --verbose <greeting="Hello World">'
Validation
$builder = QueryBuilder::init('greet {name} {--formal}'); if (!$builder->isValid()) { foreach ($builder->getErrors() as $error) { echo "❌ $error\n"; } } try { $query = $builder->build(); } catch (InvalidArgumentException $e) { echo "Erreur: " . $e->getMessage(); }
Les parseurs internes
| Parser | Rôle | Syntaxe | Priorité |
|---|---|---|---|
SourceParser |
Nom de la commande | command |
1 |
RequiredParser |
Arguments requis | {name} |
2 |
DefaultParser |
Par défaut et nullables | {name=value}, {name=?} |
3 |
EnumParser |
Énumérations | ::name->[values]=state |
4 |
VariadicParser |
Arguments variadiques | {name*} |
5 |
FlagParser |
Flags | {--flag} |
6 |
CustomTagParser |
Tags personnalisés | <key="value"> |
7 |
Extensibilité
Ajouter un parseur personnalisé
use AndyDefer\SignatureParser\Contracts\ParserInterface; use AndyDefer\SignatureParser\Records\ParsedResultRecord; use AndyDefer\SignatureParser\Records\ValidationResultRecord; use AndyDefer\SignatureParser\SignatureParser; use AndyDefer\DomainStructures\Collections\Utility\StringTypedCollection; final class CustomParser implements ParserInterface { public function parse(array $signature, array $query): ParsedResultRecord { // Votre logique personnalisée return ParsedResultRecord::from([ 'data' => ['custom' => 'valeur_personnalisee'], 'signature' => $signature, 'query' => $query, ]); } public function validate(array $signature, array $query): ValidationResultRecord { return new ValidationResultRecord( isValid: true, errors: new StringTypedCollection, suggestions: new StringTypedCollection ); } public function getTokenPattern(): string { return '/^@[a-zA-Z_][a-zA-Z0-9_]*$/'; } } $parser = new SignatureParser(); $parser->addParser(new CustomParser()); $result = $parser->parse($signature, $query); echo $result->data->get('custom'); // 'valeur_personnalisee'
Supprimer un parseur
// Supprime le parser de tags personnalisés $parser->removeParser(CustomTagParser::class);
Cas d'usage avancés
Cas 1 : Application CLI complète
<?php use AndyDefer\SignatureParser\SignatureParser; use AndyDefer\SignatureParser\QueryBuilder; use AndyDefer\SignatureParser\ValueObjects\SignatureVO; class CliApplication { private SignatureParser $parser; public function __construct() { $this->parser = new SignatureParser(); } public function run(string $signature, string $query): array { $result = $this->parser->parse($signature, $query); return [ 'source' => $result->source, 'args' => $result->requireds->toAssociativeArray(), 'flags' => $result->flags->toAssociativeArray(), 'enums' => $result->enums->toAssociativeArray(), 'custom' => $result->custom_data->toArray(), ]; } public function build(string $signature, array $params): string { $builder = QueryBuilder::init($signature); if (isset($params['args'])) { foreach ($params['args'] as $name => $value) { $builder->setRequired($name, $value); } } if (isset($params['enums'])) { foreach ($params['enums'] as $name => $value) { $builder->setEnum($name, $value); } } if (isset($params['flags'])) { foreach ($params['flags'] as $name => $active) { $builder->setFlag('--' . $name, $active); } } if (isset($params['custom'])) { $builder->setCustoms($params['custom']); } return $builder->build(); } }
Cas 2 : Documentation automatique
use AndyDefer\SignatureParser\ValueObjects\SignatureStructureVO; function generateHelp(string $signature): string { $vo = new SignatureStructureVO($signature); return $vo->documentInMarkdown(); } // Génération de documentation pour toutes les commandes $commands = [ 'deploy' => 'deploy {environment} {version=?} ::level->[low,high]=medium {--force}', 'backup' => 'backup {source} {destination} {format=zip} {--force}', 'restore' => 'restore {source} {destination} {--force}', ]; foreach ($commands as $name => $signature) { file_put_contents("docs/{$name}.md", generateHelp($signature)); }
Exemples complets
Exemple 1 : Commande avec tous les types
<?php use AndyDefer\SignatureParser\SignatureParser; use AndyDefer\SignatureParser\QueryBuilder; $parser = new SignatureParser(); $signature = 'deploy {environment} {version=?} ::level->[low,medium,high]=medium {excludes*} {--force} {--verbose}'; $query = 'deploy staging 1.2.3 high [cache,logs,tmp] --force'; $result = $parser->parse($signature, $query); echo "Commande: " . $result->source . "\n"; echo "Environnement: " . $result->requireds->get('environment') . "\n"; echo "Version: " . ($result->defaults->get('version') ?? 'null') . "\n"; echo "Niveau: " . $result->enums->get('level') . "\n"; echo "Exclus: " . implode(', ', $result->variadics->get('excludes')) . "\n"; echo "Force: " . ($result->flags->get('force') ? 'true' : 'false') . "\n"; echo "Verbose: " . ($result->flags->get('verbose') ? 'true' : 'false') . "\n"; // Construction avec QueryBuilder $cmd = QueryBuilder::init($signature) ->setRequired('environment', 'production') ->setDefault('version', '2.0.0') ->setEnum('level', 'low') ->setVariadic('excludes', ['temp', 'cache']) ->setFlag('--force', true) ->setFlag('--verbose', false) ->setCustom('user', 'deployer') ->build(); echo $cmd; // 'deploy production 2.0.0 low [temp, cache] --force <user="deployer">'
Exemple 2 : Pipeline de déploiement
<?php use AndyDefer\SignatureParser\ValueObjects\SignatureStructureVO; class DeploymentPipeline { public static function generateDocs(): string { $signatures = [ 'validate' => 'validate {environment} {--strict}', 'backup' => 'backup {environment} {--force}', 'deploy' => 'deploy {environment} {version=?} ::level->[low,medium,high]=medium {--force}', 'verify' => 'verify {environment} {--health-check}' ]; $docs = "# Commandes de déploiement\n\n"; foreach ($signatures as $name => $signature) { $vo = new SignatureStructureVO($signature); $docs .= $vo->documentInMarkdown() . "\n---\n\n"; } return $docs; } } echo DeploymentPipeline::generateDocs();
Licence
MIT © Andy Defer