Skip to content

feat: implement complete Modern API for Horde/Argv#8

Open
ralflang wants to merge 4 commits intoFRAMEWORK_6_0from
feat/modern-api-complete
Open

feat: implement complete Modern API for Horde/Argv#8
ralflang wants to merge 4 commits intoFRAMEWORK_6_0from
feat/modern-api-complete

Conversation

@ralflang
Copy link
Member

@ralflang ralflang commented Mar 4, 2026

Summary

Complete implementation of Modern API for Horde/Argv following 8 guiding principles established through design analysis. This provides a fully immutable, type-safe, and explicit API for command-line argument parsing in PHP 8.2+.

What's Included

Week 1: Enums and Config Objects

  • 3 enums: OptionAction, OptionType, ConflictHandler
  • 3 config classes: ParserConfig, OptionConfig, OptionGroupConfig
  • 2 exception classes: InvalidOptionException, ValueValidationException
  • Enums as behavior objects with logic encapsulation
  • Readonly classes throughout

Week 2: Result Objects, Exceptions, and Validators

  • Result objects: ParseResult, OptionValues
  • 3 additional exceptions: AmbiguousOptionException, MissingValueException, ConflictingOptionException
  • Validators utility with 12 composable validators
  • Optional infrastructure (not mandated)
  • Explicit access methods (no ArrayAccess)

Week 3: Immutable Builders

  • OptionBuilder: 14 setters, 3 convenience methods (flag, repeatable, counter)
  • GroupBuilder: Group management with title and description
  • ParserBuilder: Complete parser configuration
  • Clone-on-modify pattern (~4μs overhead)
  • Builder branching support for shared base configurations

Week 4: ImmutableParser (Core Engine)

  • Full POSIX-style option parsing
  • Short options: -v, -abc (bundling), -nVALUE (attached)
  • Long options: --verbose, --name VALUE, --name=VALUE
  • Partial long option matching with ambiguity detection
  • Type conversion: String, Int, Float
  • Actions: Store, StoreTrue/False, Append, Count, Callback, StoreConst
  • Custom validators and transformers
  • Choice enforcement
  • Unknown option handling (error/allow/ignore)
  • Use-and-amend pattern via toBuilder()

Week 5: Help Formatting System

  • HelpFormatter: Immutable help text generator
  • Terminal-width-aware text wrapping
  • Auto-detection of terminal width
  • Option alignment with configurable column position
  • Automatic default value and choices display
  • Option groups with headers and descriptions
  • Customizable indentation and layout

Test Coverage

✓ 234 tests (100% passing)
✓ 403 assertions
✓ Comprehensive coverage of all features
✓ All edge cases and error conditions tested

Test Breakdown

  • Week 1: 47 tests (enums, configs, exceptions)
  • Week 2: 39 tests (results, validators)
  • Week 3: 58 tests (builders, immutability)
  • Week 4: 33 tests (parser, parsing scenarios)
  • Week 5: 28 tests (help formatting)
  • Integration: 29 tests (end-to-end scenarios)

Design Principles

All 8 guiding principles fully implemented and verified:

  1. Type Safety Over Convenience - Enums instead of strings
  2. Enums as Behavior Objects - Logic encapsulated in enum methods
  3. Infrastructure Not Mandated - Validators/transformers optional
  4. Explicit Type Contracts - Type specified = type guaranteed
  5. Modern API is Immutable - No mutation after construction
  6. Explicit Access - No ArrayAccess, explicit getter methods
  7. Immutable Throughout - Readonly classes, builder pattern
  8. Layered Evolution - Modern namespace, stable legacy preserved

Example Usage

use Horde\Argv\Modern\Builder\{ParserBuilder, OptionBuilder, GroupBuilder};
use Horde\Argv\Modern\Enum\{OptionType, OptionAction};
use Horde\Argv\Modern\Validator\Validators;

// Build options
$verbose = OptionBuilder::create()
    ->short('-v')
    ->long('--verbose')
    ->counter()
    ->help('Increase verbosity (-v, -vv, -vvv)')
    ->build();

$port = OptionBuilder::create()
    ->short('-p')
    ->long('--port')
    ->type(OptionType::Int)
    ->default(8080)
    ->validator(Validators::range(1, 65535))
    ->help('Server port number')
    ->build();

$format = OptionBuilder::create()
    ->short('-f')
    ->long('--format')
    ->choices(['json', 'xml', 'csv'])
    ->default('json')
    ->help('Output format')
    ->build();

// Build debugging group
$debugGroup = GroupBuilder::create('Debugging Options')
    ->withDescription('Options for troubleshooting')
    ->addOption(
        OptionBuilder::create()
            ->short('-d')
            ->long('--debug')
            ->flag()
            ->help('Enable debug logging')
            ->build()
    )
    ->build();

// Build parser
$parser = ParserBuilder::create()
    ->withUsage('%prog [options] <input> <output>')
    ->withProg('dataconverter')
    ->withDescription('Convert data files between formats')
    ->withVersion('1.0.0')
    ->withEpilog('For more information, visit https://example.com/docs')
    ->addOption($verbose)
    ->addOption($port)
    ->addOption($format)
    ->addGroup($debugGroup)
    ->build();

// Parse arguments
$result = $parser->parse(['-vv', '--port=3000', '--format', 'xml', 'in.json', 'out.xml']);

// Access results
echo $result->options->get('verbosity');  // 2
echo $result->options->get('port');       // 3000
echo $result->options->get('format');     // "xml"
echo $result->arguments[0];               // "in.json"
echo $result->arguments[1];               // "out.xml"

// Generate help
echo $parser->formatHelp();

Help Output

Usage: dataconverter [options] <input> <output>

Convert data files between formats

Options:
  -v, --verbose         Increase verbosity (-v, -vv, -vvv)
  -p, --port=PORT       Server port number (default: 8080)
  -f, --format=FORMAT   Output format (choices: 'json', 'xml', 'csv')
                        (default: json)

Debugging Options:
  Options for troubleshooting
  -d, --debug           Enable debug logging

For more information, visit https://example.com/docs

Features

Parsing Capabilities

  • POSIX-style short and long options
  • Option bundling (-abc = -a -b -c)
  • Attached values (-nVALUE or --name=VALUE)
  • Partial long option matching (--verb matches --verbose)
  • Ambiguity detection for partial matches
  • Double-dash (--) option terminator
  • Configurable argument interspersing
  • Unknown option handling modes

Type System

  • Built-in types: String, Int, Float
  • Automatic type conversion
  • Type validation before conversion
  • Custom validators (applied after type conversion)
  • Transformers/mappers (applied after validation)
  • Choice whitelists with validation

Actions

  • Store - Store single value
  • StoreTrue/StoreFalse - Boolean flags
  • Append - Collect multiple values
  • Count - Increment counter (e.g., -vvv = 3)
  • StoreConst/AppendConst - Store predefined constants
  • Callback - Custom processing function

Validation & Transformation

  • 12 built-in validators: range, regex, choice, length, email, url, file, directory
  • Composable validators: all(), any()
  • Custom validators (return true or error string)
  • Transformers for value processing
  • Pipeline: parse → type convert → validate → transform

Help Generation

  • Professional formatting
  • Terminal-width-aware wrapping
  • Automatic default value display
  • Automatic choices display
  • Metavar support
  • Option groups with descriptions
  • Configurable layout and indentation

Error Handling

  • Structured exceptions with context
  • Clear error messages
  • Exception types:
    • InvalidOptionException - Unknown/invalid options
    • AmbiguousOptionException - Partial match ambiguity
    • MissingValueException - Required value not provided
    • ConflictingOptionException - Duplicate options
    • ValueValidationException - Type conversion/validation failures

Architecture

Immutability

  • All classes readonly
  • Builders clone on each method call
  • Parsers configured once, never modified
  • Results are immutable snapshots

Performance

  • Option lookup: O(1) via computed maps
  • Builder cloning: ~4μs per call (negligible)
  • No reflection during parsing
  • Readonly class optimizations
  • Efficient text wrapping algorithm

Design Patterns

  • Builder pattern for construction
  • Enum-based behavior
  • Use-and-amend pattern (toBuilder())
  • Strategy pattern for actions
  • Factory methods for validators

Breaking Changes

None - Modern API is in separate namespace:

  • Modern API: Horde\Argv\Modern\
  • Legacy API: Horde_Argv_* (unchanged)

Both APIs can coexist. Legacy API remains stable and supported.

Migration Path

Users can adopt Modern API gradually:

  1. Keep existing code using legacy API
  2. New code uses Modern API
  3. Migrate modules one at a time
  4. No forced migration required

Documentation

  • Comprehensive PHPDoc on all public methods
  • Clear type declarations throughout
  • Example usage in class headers
  • 234 test cases serve as documentation
  • Design principles documented

Commits

  1. chore: add test autoload configuration and upgrade guide
  2. docs: mark under-development stub classes as internal
  3. feat(modern): implement Week 1 - Enums and Config objects
  4. feat(modern): implement Week 2 - Result objects, exceptions, and validators
  5. test(modern): add comprehensive unit tests for Weeks 1-2
  6. feat(Modern): add immutable builders (Week 3)
  7. feat(Modern): implement ImmutableParser with full parsing engine (Week 4)
  8. feat(Modern): implement help formatting system (Week 5)

Checklist

  • All tests passing (234/234)
  • 100% type coverage
  • PHPDoc complete
  • Conventional commits
  • No breaking changes
  • Legacy API untouched
  • All principles implemented
  • Performance validated
  • Immutability verified
  • Help formatting working

Future Enhancements

Possible future additions (not in this PR):

  • Shell completion generation
  • Subcommand parsing
  • Additional validators
  • Color output support
  • Man page generation

Status

Modern API is production-ready and feature-complete for command-line argument parsing in PHP 8.2+ applications!

ralflang added 4 commits March 4, 2026 15:16
Implement immutable help formatter for Modern API following Principle #5 and #7.
Provides professional, terminal-width-aware help text formatting with full
support for options, groups, and descriptions.

Core Components:
- HelpFormatter: Immutable help text generator
- Integration with ImmutableParser via formatHelp()
- Builder pattern for customization
- Terminal width auto-detection

Features:
- Usage line formatting with %prog substitution
- Description and epilog support
- Option formatting with alignment and wrapping
- Option groups with descriptions
- Default value display (e.g., "(default: 8080)")
- Choice list display (e.g., "(choices: 'json', 'xml', 'csv')")
- Metavar support for argument placeholders
- Short/long option ordering control
- Text wrapping for terminal width
- Configurable indentation

Formatter Configuration:
- width: Terminal width (0 = auto-detect, default: 80)
- indent: Indentation increment (default: 2)
- maxHelpPosition: Column before wrapping help text (default: 24)
- shortFirst: Show short option first in listings (default: true)

Formatting Rules:
- Options aligned at configurable column position
- Help text wraps to terminal width
- Default values and choices automatically appended
- Groups separated with headers
- Consistent spacing and indentation

Parser Integration:
- formatHelp(): Generate help text for parser
- Accepts optional custom formatter
- Uses builder-provided formatter if available
- Falls back to default formatter

Example Usage:
  $formatter = HelpFormatter::create()
      ->withWidth(100)
      ->withIndent(4)
      ->build();

  $parser = ParserBuilder::create()
      ->withUsage('%prog [options] <file>')
      ->withDescription('Process files')
      ->addOption($verboseOption)
      ->build();

  echo $parser->formatHelp($formatter);

Output Example:
  Usage: myapp [options] <file>

  Process files

  Options:
    -v, --verbose         Increase verbosity
    -p, --port=PORT       Server port (default: 8080)
    -f, --format=FORMAT   Output format (choices: 'json', 'xml', 'csv')
                          (default: json)

Test Coverage:
- 28 tests added (234 total Modern API tests)
- 73 assertions added (403 total assertions)
- All tests passing (100%)
- Tests cover all formatting scenarios
- Integration tests with ImmutableParser

Formatter Characteristics:
- Readonly class (Principle #7)
- Immutable builder pattern (Principle #5)
- No side effects, pure output generation
- Thread-safe and cacheable
- Fast formatting (no reflection)

Text Wrapping Algorithm:
- Word-based wrapping (no mid-word breaks)
- Respects indentation for wrapped lines
- Handles long option strings gracefully
- Falls back to next-line help for long options

Terminal Width Detection:
- Auto-detects via tput cols if available
- Falls back to 80 columns default
- Can be explicitly set via withWidth()
- Zero width disables wrapping

Principles Demonstrated:
- Principle #5: Modern API is Immutable - formatter fully immutable
- Principle #7: Immutable Throughout - readonly class, builder for config
- Principle #6: Explicit Access - clear method calls, no magic
- Clean separation between formatting logic and parser logic

Next Steps: Modern API is now feature-complete with parsing and help formatting
Implement core context infrastructure for subcommand-style parsing.
Contexts allow command-specific option sets that don't conflict.

New Components:
- ContextConfig: Immutable context configuration
  - Name, aliases, description, usage, help
  - Context-specific options and groups
  - Argument count validation (min/max)
  - Sub-context support for nested commands

- ContextBuilder: Immutable builder for contexts
  - Fluent API: withDescription(), withUsage(), withAliases()
  - Add options with addOption() and addOptions()
  - Argument requirements: requiresArguments(), acceptsArguments()
  - Convenience methods: requiresExactly(), requiresAtLeast(), requiresAtMost()
  - Sub-context nesting with addContext()

- ParseResult: Extended for context support
  - globalOptions: Options available across all contexts
  - contextOptions: Options specific to activated context
  - context: Name of activated context (null if no context)
  - Backward compatible: existing code works unchanged
  - Convenience methods: hasContext(), getContext()
  - Unified access: getOption() checks context then global

- ParserBuilder: Context support
  - addContext() method for adding contexts
  - addContexts() for bulk addition
  - Contexts passed to ImmutableParser constructor

- ImmutableParser: Context infrastructure
  - Accept contexts array in constructor
  - Build context map for O(1) lookup
  - toBuilder() includes contexts
  - Ready for Phase 2 parsing logic

Tests:
- ContextConfigTest: 16 tests, 51 assertions
- ContextBuilderTest: 18 tests, 40 assertions
- ParseResultContextTest: 8 tests, 39 assertions
- All 421 existing tests passing (100% backward compatible)

Total: 42 new tests, 130 new assertions

Example Usage:
```php
$deploy = ContextBuilder::create('deploy')
    ->withDescription('Deploy application')
    ->withAliases(['dep'])
    ->addOption($forceOption)
    ->requiresArguments(1, 'environment')
    ->build();

$parser = ParserBuilder::create()
    ->addOption($globalVerbose)
    ->addContext($deploy)
    ->build();

// Phase 2 will implement actual parsing logic
```

Design Decisions:
- Follows all 8 guiding principles from argv-modernization-design-decisions.md
- Immutable configs (readonly classes)
- Builders are NOT readonly (internal mutability for clone-on-modify)
- Explicit access methods (no ArrayAccess)
- Type-safe with PHP 8.2+ features
- Backward compatible: no breaking changes to existing Modern API
- Context is optional: parsers work with or without contexts

Phase Status:
✅ Phase 1: Core Context Infrastructure (Complete)
⏳ Phase 2: Context Parsing Logic (Next)
⏳ Phase 3: Help Generation
⏳ Phase 4: Advanced Features
⏳ Phase 5: Polish and Documentation
Implement full context-aware parsing with global and context-specific options.

Context Detection:
- First positional argument checked against registered contexts
- Context name or any alias triggers context activation
- Global options parsed before context detected
- Context-specific options parsed after context activated

Parsing Modes:
1. Legacy Mode (no contexts):
   - Standard POSIX parsing like before
   - All options in result->options
   - 100% backward compatible

2. Context Mode (contexts registered, none active):
   - Parses global options
   - No context detected in arguments
   - Options in result->globalOptions
   - result->context is null

3. Context Mode (context active):
   - Parses global options first
   - Detects context from positional arg
   - Parses context-specific options
   - result->globalOptions + result->contextOptions
   - result->context contains context name

Context Option Lookup:
- Context options checked first when in context
- Falls back to global options if not found in context
- Allows same option name in different contexts without conflict

Argument Validation:
- Validates argument count against context requirements
- Throws InvalidArgumentCountException if count invalid
- Provides clear error messages (exact, at least, at most)

New Exceptions:
- UnknownContextException: Unknown context name provided
- InvalidArgumentCountException: Wrong number of arguments for context

ImmutableParser Changes:
- parse() delegates to parseLegacy() or parseWithContexts()
- parseLegacy(): Original parsing logic (no contexts)
- parseWithContexts(): Two-phase context-aware parsing
- processContextOption(): Parse context-specific options
- findContextOption(): O(1) lookup with partial matching

ParseResult Enhancements:
- getOption() checks context then global (unified access)
- hasOption() checks both context and global
- hasContext(), getContext() for context info
- globalOptions, contextOptions properties

Tests:
- ContextParsingTest: 15 comprehensive tests
  - Basic context detection
  - Context with arguments (single and multiple)
  - Global options parsing
  - Context-specific options parsing
  - Combined global and context options
  - getOption() fallback behavior
  - Context aliases
  - Multiple contexts
  - No context detected (fallback to global)
  - Argument count validation (exact, range)
  - Context option overrides global
  - Backward compatibility (no contexts)

Total: 436 tests, 887 assertions (100% passing)

Example Usage:
```php
$globalVerbose = OptionBuilder::create()
    ->long('--verbose')
    ->flag()
    ->build();

$contextForce = OptionBuilder::create()
    ->long('--force')
    ->flag()
    ->build();

$deployContext = ContextBuilder::create('deploy')
    ->withDescription('Deploy application')
    ->withAliases(['dep'])
    ->addOption($contextForce)
    ->requiresArguments(1, 'environment')
    ->build();

$parser = ParserBuilder::create()
    ->addOption($globalVerbose)
    ->addContext($deployContext)
    ->build();

// Parse: app --verbose deploy --force production
$result = $parser->parse(['--verbose', 'deploy', '--force', 'production']);

// Access results:
$result->getContext();                          // 'deploy'
$result->globalOptions->get('verbose');         // true
$result->contextOptions->get('force');          // true
$result->arguments[0];                          // 'production'
$result->getOption('force');                    // true (checks context)
$result->getOption('verbose');                  // true (fallback to global)
```

Phase Status:
✅ Phase 1: Core Context Infrastructure (Complete)
✅ Phase 2: Context Parsing Logic (Complete)
⏳ Phase 3: Help Generation (Next)
⏳ Phase 4: Advanced Features
⏳ Phase 5: Polish and Documentation
Add comprehensive help generation for contexts with proper separation
between data structure (Argv) and formatting (HelpFormatter).

HelpFormatter Enhancements:
- formatContexts(): Lists all available contexts with descriptions
- formatContext(): Formats single context name and description
- formatContextHelp(): Complete help for specific context
- formatArgumentRequirements(): Shows argument count requirements
- Handle null prog parameter gracefully (defaults to 'program')

Format Structure:
1. Main Help (parser->formatHelp()):
   - Usage line
   - Description
   - Commands: (contexts list)
   - Global Options:
   - Epilog

2. Context Help (parser->formatContextHelp('context')):
   - Usage: program context [options]
   - Context description
   - Detailed help text
   - Context Options: (context-specific)
   - Global Options: (available in context)
   - Arguments: (requirements)

ImmutableParser API:
- formatHelp(): Generate main help with contexts
- formatContextHelp(name): Generate help for specific context
- getContexts(): Get all registered contexts
- getContext(name): Get specific context by name or alias
- hasContexts(): Check if contexts are registered

Help Content:
- Context names with aliases shown as: deploy (dep, depl)
- Context descriptions from ContextConfig
- Global vs context-specific options clearly separated
- Argument requirements formatted: "Requires exactly 2 arguments: source destination"
- Terminal width-aware text wrapping

Design Philosophy:
- Argv provides data structure and help data
- HelpFormatter handles text formatting
- horde/cli can add colors, interactive features later
- Clean separation of concerns maintained

Tests:
- ContextHelpTest: 11 comprehensive tests
  - Format help with contexts
  - Context aliases in help
  - Context-specific help
  - Invalid context exception
  - Get contexts/context methods
  - hasContexts() method
  - Global and context options separation
  - Argument requirements formatting (exact, range, at least)
  - Backward compatibility (no contexts)
  - Custom formatter

Total: 447 tests, 932 assertions (100% passing)

Example Output:
```
Usage: myapp [options] <command>

Application deployment tool

Commands:
  deploy (dep)      Deploy application to environment
  rollback (roll)   Rollback to previous version

Options:
  -v, --verbose     Verbose output

# Context-specific help:
$ myapp help deploy

Usage: myapp deploy [options] <environment>

Deploy application to environment

Deploys the application to the specified environment with health
checks and rollback capabilities.

Context Options:
  --force           Force deployment
  --timeout INT     Deployment timeout in seconds (default: 60)

Global Options:
  -v, --verbose     Verbose output

Arguments:
  Requires exactly 1 argument: environment name
```

Phase Status:
✅ Phase 1: Core Context Infrastructure (Complete)
✅ Phase 2: Context Parsing Logic (Complete)
✅ Phase 3: Help Generation (Complete)
⏳ Phase 4: Advanced Features (Optional)
⏳ Phase 5: Polish and Documentation (Optional)

Core feature complete and production-ready!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant