Search by

alex-kassel / stub-engine

Alexander Macenko

Hierarchical template and stub scaffolding engine with token interpolation and host overrides for PHP and Laravel

Package info

github.com/alex-kassel/stub-engine

pkg:composer/alex-kassel/stub-engine

Statistics

Installs: 1

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-18 19:39 UTC

This package is auto-updated.

Last update: 2026-09-18 19:44:12 UTC


README

Hierarchical template and stub scaffolding engine with token interpolation and host overrides for PHP and Laravel.

Why This ExistsKey FeaturesRequirementsInstallationQuickstartUsage & RecipesDocumentationAPI ReferenceTestingChangelogLicense

Audit Verified Latest Version Laravel Support PHP Support License

Why This Exists

Generators, module builders, manifest installers, and CLI scaffolding tools repeatedly reinvent low-level filesystem operations:

  1. Interpolating token placeholders in file contents.
  2. Interpolating token placeholders in directory and file names (e.g. src/{{ ClassName }}.php.stub).
  3. Stripping template extensions (.stub).
  4. Enabling consumer applications to override default stubs without modifying vendor packages.
  5. Handling single-file stubs (e.g. compiling a standalone script or configuration) alongside multi-file directory trees.

StubEngine extracts this entire lifecycle into a lightweight, zero-bloat service. Whether you need to render an in-memory string, scaffold an individual file with host overrides, or generate an entire directory hierarchy, StubEngine provides a clean, unified API.

Key Features

  • Dual Override Strategies (Overlay vs Replace):
    • OverrideStrategy::Overlay (Default): Cascading file-by-file overlay. If a host project customizes 1 file out of 10, the other 9 package defaults are preserved.
    • OverrideStrategy::Replace: Complete directory substitution ("all-or-nothing"). Perfect for document bundles, template suites, or thematic assets where a custom template completely replaces the default directory layout.
  • Extensible Token Modifiers (Open-Closed Principle):
    • Built-in string casing: studly, camel, kebab, snake, lower, upper, title, plural, and singular.
    • Register custom modifiers via StubEngine::registerModifier('custom', fn ($val) => ...).
  • High-Performance Single-Pass Compilation: Token dictionaries are pre-compiled and sorted once upfront per tree operation, eliminating $O(N \times M)$ overhead.
  • Configurable Delimiters & Zero-Trust Fallbacks:
    • Globally customize placeholder delimiters (e.g. <% %> or [[ ]]) via config/stub-engine.php to eliminate syntax collisions with Blade ({{ $var }}), Vue, Jinja, or bash.
    • Override delimiters on a per-call basis at runtime.
    • Zero-trust resilience: falls back gracefully to default delimiters ({{ and }}) even if config is absent or empty.
  • Global & Dynamic Tokens:
    • Define application-wide global tokens (e.g. company_name, year, author) in config/stub-engine.php.
    • Runtime tokens seamlessly merge and take precedence over global tokens.
  • Dual-Axis Token Interpolation: Replace tokens in both file contents AND file/directory pathnames simultaneously (e.g. src/<% Module|studly %>.php.stub).
  • Path Normalization: Cross-platform path handling for seamless generation on Windows, macOS, and Linux.
  • Binary & Raw Asset Protection: Files without .stub extension are safely copied without string interpolation, and system junk files (.DS_Store, Thumbs.db, .gitkeep) are automatically filtered out.
  • Strict Diagnostics Mode: Discover required tokens in templates via extractTokens(), or pass strict: true to fail fast before deploying broken code.
  • Safe Overwrite & Dry-Run Modes: Prevent accidental file overwrites (force: false) and simulate execution non-destructively for CLI commands (dryRun: true).
  • Decoupled Architecture: Pure constructor DI with Illuminate\Filesystem\Filesystem and array config. Usable across Laravel console commands, service providers, background jobs, or standalone pure PHP CLI tools.
  • Rich DTO Architecture: Returns a typed ScaffoldResult object with granular file status arrays (createdFiles, overwrittenFiles, skippedFiles, overrideFiles, rawCopiedFiles, unresolvedTokens) and combined $renderedFiles for clear inspection.

Requirements

  • PHP: ^8.2 | ^8.3 | ^8.4
  • Laravel Framework (or Components): ^11.0 | ^12.0 | ^13.0
    • illuminate/filesystem
    • illuminate/support

Installation

Install via Composer:

composer require alex-kassel/stub-engine

If you are using Laravel, the service provider and StubEngine facade are automatically registered via package discovery.

Quickstart

1. Scaffold a Single File (e.g. CLI Runner or Config)

use AlexKassel\StubEngine\Facades\StubEngine;

$result = StubEngine::from(__DIR__ . '/../stubs/runner.stub')
    ->to(base_path('bin/my-tool'))
    ->withTokens([
        '{{ runnerName }}' => 'my-tool',
        '{{ manifestPath }}' => 'tool.json',
    ])
    ->override(base_path('stubs/runner.stub')) // Optional host override
    ->force(false) // Skip if target file already exists
    ->scaffold();

if ($result->renderedFiles !== []) {
    echo "Standalone runner created at bin/my-tool!";
}

2. Render In-Memory Content from a Stub

use AlexKassel\StubEngine\DTOs\ScaffoldRequest;
use AlexKassel\StubEngine\Facades\StubEngine;

$compiled = StubEngine::renderFile(new ScaffoldRequest(
    source: __DIR__ . '/../stubs/config.stub',
    tokens: [
        '{{ appName }}' => 'My Application',
    ],
    override: base_path('stubs/config.stub'),
));

3. Scaffold a Complete Directory Tree

Organize your stubs directory keeping the natural folder hierarchy:

my-package/stubs/
├── composer.json.stub
├── src/
│   └── {{ ClassName }}.php.stub
└── tests/
    └── {{ ClassName }}Test.php.stub

Execute scaffolding:

use AlexKassel\StubEngine\Facades\StubEngine;

$result = StubEngine::from(__DIR__ . '/../stubs')
    ->to(base_path('packages/acme/my-tool'))
    ->withTokens([
        '{{ vendor }}' => 'acme',
        '{{ package }}' => 'my-tool',
        '{{ ClassName }}' => 'MyTool',
    ])
    ->override(base_path('stubs/my-generator'))
    ->scaffold();

echo "Rendered " . count($result->renderedFiles) . " files into {$result->request->target}!";

Usage & Recipes

1. Dependency Injection in Console Commands

In clean architecture, inject AlexKassel\StubEngine\StubEngine directly into your console commands:

namespace Acme\Generator\Console;

use AlexKassel\StubEngine\StubEngine;
use Illuminate\Console\Command;

class MakeModuleCommand extends Command
{
    protected $signature = 'make:module {name}';

    public function __construct(
        protected readonly StubEngine $engine,
    ) {
        parent::__construct();
    }

    public function handle(): int
    {
        $name = trim((string) $this->argument('name'));

        $result = $this->engine->from(dirname(__DIR__, 2) . '/stubs')
            ->to(app_path("Modules/{$name}"))
            ->withTokens([
                '{{ moduleName }}' => $name,
                '{{ namespace }}' => "App\\Modules\\{$name}",
            ])
            ->override(base_path('stubs/modules'))
            ->scaffold();

        $source = $result->overrideFiles !== [] ? 'custom host stubs' : 'default stubs';
        $count = count($result->renderedFiles);
        $this->info("Module [{$name}] scaffolded successfully using {$source} ({$count} files).");

        return self::SUCCESS;
    }
}

2. Configuration & Global Tokens

Publish the package configuration file to customize delimiters and register application-wide tokens:

php artisan vendor:publish --tag=stub-engine-config

The published config/stub-engine.php file:

return [
    /*
    |--------------------------------------------------------------------------
    | Token Delimiters
    |--------------------------------------------------------------------------
    | Customize delimiters to avoid syntax collisions with Blade ({{ $var }}),
    | Vue, Jinja, or bash scripts.
    */
    'delimiters' => [
        'open' => env('STUB_ENGINE_OPEN_DELIMITER', '{{'),
        'close' => env('STUB_ENGINE_CLOSE_DELIMITER', '}}'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Global Tokens
    |--------------------------------------------------------------------------
    | Shared tokens merged automatically into every scaffolding operation.
    */
    'global_tokens' => [
        'company' => env('STUB_ENGINE_COMPANY_NAME', 'Acme Corp'),
        'year' => date('Y'),
    ],
];

3. Dual Override Strategies: Overlay vs Replace

When consumer applications provide custom stubs, choose between two distinct strategies using OverrideStrategy:

use AlexKassel\StubEngine\Enums\OverrideStrategy;
use AlexKassel\StubEngine\Facades\StubEngine;

// Strategy A: Overlay (Default cascading merge)
// If the override directory contains 1 file out of 10, the other 9 package defaults are preserved.
$result = StubEngine::from(__DIR__ . '/../stubs')
    ->to(base_path('app/Modules/Billing'))
    ->withTokens(['name' => 'Billing'])
    ->override(base_path('stubs/modules'))
    ->scaffold();

// Strategy B: Replace ("All-or-Nothing" complete substitution)
// Ideal for document packages or custom suites where the consumer directory completely replaces the default layout.
$docResult = StubEngine::from(__DIR__ . '/../sample_docs')
    ->to(storage_path('app/client_docs'))
    ->withTokens(['client' => 'Globex'])
    ->override(base_path('stubs/client_docs'), OverrideStrategy::Replace)
    ->scaffold();

4. Custom Delimiters (Preventing Syntax Collisions)

When scaffolding templates that already contain Blade, Vue, or bash syntax, specify custom delimiters at runtime or via config:

$result = StubEngine::from(__DIR__ . '/../blade_stubs')
    ->to(resource_path('views/modules/billing'))
    ->withTokens(['entity' => 'user profile'])
    ->delimiters('<%', '%>')
    ->scaffold();

In your stubs and file paths, use <% entity|studly %> or <% entity|kebab %>, while preserving native Blade syntax like {{ $user->name }} without interference.

5. Built-in Token Case Modifiers

Tokens can be automatically transformed using built-in pipe modifiers in both file contents and file paths:

$result = StubEngine::from(__DIR__ . '/../stubs')
    ->to(base_path('app/Modules/Billing'))
    ->withTokens([
        'entity' => 'user profile',
    ])
    ->scaffold();

In any stub file or file path, you can use:

  • {{ entity|studly }}UserProfile
  • {{ entity|camel }}userProfile
  • {{ entity|kebab }}user-profile
  • {{ entity|snake }}user_profile
  • {{ entity|lower }}user profile
  • {{ entity|upper }}USER PROFILE
  • {{ entity|title }}User Profile
  • {{ entity|plural }}user profiles
  • {{ entity|singular }}user profile
  • {{ entity|trim }}user profile (whitespace stripped)

Registering Custom Modifiers

Extend the engine at runtime with custom domain modifiers:

use AlexKassel\StubEngine\Facades\StubEngine;

StubEngine::registerModifier('slug', fn (string $val): string => \Illuminate\Support\Str::slug($val));
StubEngine::registerModifier('shout', fn (string $val): string => strtoupper($val) . '!!!');

// In stubs: {{ title|slug }} or {{ alert|shout }}

6. Strict Mode Diagnostics

Prevent broken PHP code caused by forgotten placeholder variables:

// 1. Strict scaffolding (fails fast if any placeholder is missing)
try {
    StubEngine::from(__DIR__ . '/../stubs')
        ->to(app_path('Modules/Billing'))
        ->withTokens(['name' => 'Billing'])
        ->strict() // Throws InvalidArgumentException on unresolved tokens
        ->scaffold();
} catch (\InvalidArgumentException $e) {
    // Gracefully report missing inputs to CLI user
}

// 2. Direct token diagnostics on raw strings using StubEngine or Interpolator
$tokens = StubEngine::extractTokens($content);
// Returns: ['missing_token', 'other']

7. Inspecting Scaffold Results & Dry-Run Mode

The ScaffoldResult DTO provides fine-grained visibility into file operations through strongly-typed, public readonly properties:

$result = StubEngine::from(__DIR__ . '/../stubs')
    ->to(base_path('packages/acme/my-tool'))
    ->withTokens(['name' => 'MyTool'])
    ->override(base_path('stubs/custom'))
    ->force(false) // Skip existing files
    ->dryRun(true) // Preview changes without writing to disk
    ->scaffold();

// Count of rendered files
echo count($result->renderedFiles);

// Detailed file categorizations
$created     = $result->createdFiles;     // ['src/MyTool.php']
$overwritten = $result->overwrittenFiles; // []
$skipped     = $result->skippedFiles;     // ['composer.json']
$overridden  = $result->overrideFiles;    // ['src/MyTool.php']
$rendered    = $result->renderedFiles;    // ['src/MyTool.php']

// Direct status checks
if ($result->overrideFiles !== []) {
    echo "Custom host stubs were utilized!";
}

if ($result->skippedFiles !== []) {
    echo "Some files already existed and were protected from overwriting.";
}

Documentation

Comprehensive deep-dive guides are available in the docs/ directory:

  • Fluent ScaffoldBuilder Guide: Complete guide to the chainable builder API (from(), to(), withTokens(), override()), conditional steps (when(), unless()), macro extensions, and detailed operational flags (force(), dryRun(), strict(), delimiters(), ignore(), onProgress()).
  • Token Modifiers Guide: Exhaustive reference for built-in string transformations (studly, camel, kebab, snake, lower, upper, title, plural, singular, trim), modifier chaining ({{ model|snake|plural }}), custom modifier registration, and Blade @{{ ... }} escaping.
  • Architecture & Guide: Architecture overview, DI singleton lifecycle, custom macros on the engine, and raw token diagnostics via extractTokens().

API Reference

StubEngine::from

public function from(string $source): ScaffoldBuilder

Starts a fluent scaffolding pipeline for a source stub file or directory.

StubEngine::renderFile

public function renderFile(ScaffoldRequest $request): string

Renders a single stub file into a string with token replacements using a ScaffoldRequest DTO. Throws InvalidArgumentException if the source is a directory.

StubEngine::scaffold

public function scaffold(ScaffoldRequest $request): ScaffoldResult

Executes scaffolding for either a single file or a complete directory tree (automatically determined from $request->source). Returns a ScaffoldResult object detailing created, overwritten, and skipped files.

StubEngine::registerModifier

public function registerModifier(string $name, callable $callback): self

Registers a custom runtime token modifier function (e.g. {{ var|name }}) delegated to the Interpolator service.

StubEngine::extractTokens

public function extractTokens(
    string $content,
    ?string $open = null,
    ?string $close = null,
): array

Scans raw template or generated content and returns an array of unique unescaped token names (before any modifier pipes) found within delimiters.

Testing

Run the test suite using PHPUnit:

composer test

Or via direct PHPUnit binary:

vendor/bin/phpunit

Changelog

Please see CHANGELOG.md for more information on recent changes.

Contributing

Contributions are welcome! Please review CONTRIBUTING.md for details.

License

The MIT License (MIT). Please see License File for more information.