xphp-lang/xphp-symfony-bundle

Symfony bundle to write XPHP (PHP with native generics) inside a Symfony app and deploy the auto-generated PHP.

Maintainers

Package info

github.com/xphp-lang/xphp-symfony-bundle

Type:symfony-bundle

pkg:composer/xphp-lang/xphp-symfony-bundle

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-06-03 21:47 UTC

This package is auto-updated.

Last update: 2026-06-03 22:09:33 UTC


README

Write XPHP — PHP 8.4 with native generics via compile-time monomorphization — inside a Symfony application, and have the auto-generated vanilla PHP produced as part of your normal build/deploy.

The bundle wires xphp's transpiler into the Symfony lifecycle:

  • a xphp:compile console command to compile on demand during development;
  • a cache warmer so bin/console cache:warmup (run by every production build) emits the generated PHP as a deterministic deploy artifact;
  • a runtime autoloader for the generated XPHP\Generated\* classes, so the app boots against fresh output without a composer dump-autoload.

Requirements

Installation

composer require xphp-lang/xphp-symfony-bundle

Register the bundle (Symfony Flex does this for you):

// config/bundles.php
return [
    // ...
    Xphp\SymfonyBundle\XphpBundle::class => ['all' => true],
];

Configuration

Defaults (override only what you need):

# config/packages/xphp.yaml
xphp:
    # Where your .xphp sources live.
    source: '%kernel.project_dir%/xphp'
    # Rewritten, vanilla .php files (generic call sites replaced).
    target: '%kernel.project_dir%/var/xphp/dist'
    # Generated specialized classes (XPHP\Generated\*).
    cache: '%kernel.project_dir%/var/xphp/generated'
    # Hex length (16-64) of the hash in specialized class names. null = xphp default.
    hash_length: null
    # Compile automatically during cache:warmup (recommended for deploys).
    compile_on_warmup: true
    # Register an SPL autoloader for XPHP\Generated\ at boot.
    register_runtime_autoloader: true

These map directly to the upstream CLI: xphp compile <source> <target> <cache>.

Layout & autoloading

A .xphp source uses standard PSR-4 namespace-to-directory mapping. Compilation produces two outputs:

Output Namespace Goes to
Rewritten user code original (e.g. App\…) target dir
Specialized classes XPHP\Generated\… <cache>/Generated/

The bundle autoloads XPHP\Generated\* at runtime out of the box. For the rewritten code you choose where the application loads App\ from. For a production deploy, point composer at the compiled output and dump an optimized classmap:

// composer.json
"autoload": {
    "psr-4": {
        "App\\": ["src/", "var/xphp/dist/App/"],
        "XPHP\\Generated\\": "var/xphp/generated/Generated/"
    }
}
composer dump-autoload --optimize --classmap-authoritative

When composer owns the classmap, you can set register_runtime_autoloader: false.

Usage

  1. Put your generics under the source directory, e.g.:

    xphp/
    └── App/
        └── Collections/
            └── Collection.xphp     # final class Collection<T> { ... }
    
  2. Compile during development:

    bin/console xphp:compile

    …or just warm the cache (also runs the compiler):

    bin/console cache:warmup
  3. Reference the generated classes from your app as usual; instantiations like Collection<User> are monomorphized to concrete classes with native type hints and zero runtime penalty.

Generic services (dependency injection)

Service-like generics — Repository<User>, CachedFinder<User>, Handler<Cmd> — can be registered in the container and autowired. This is opt-in: declare each instantiation under xphp.services.bindings. Value-type generics (Box<T>, Collection<T>) are not meant for DI — keep new-ing those.

# config/packages/xphp.yaml
xphp:
  services:
    _defaults:                 # inherited by every binding below
      autowire: true
      autoconfigure: true
      public: false
      shared: true
    bindings:
      - template: 'App\Generic\CachedFinder'
        args: ['App\Entity\User']          # FQN string, or nested { template, args }
        # any of autowire/autoconfigure/public/shared/tags/alias may be set per binding

The bundle resolves each binding to the class the transpiler monomorphizes it into (XPHP\Generated\…\T_<hash>, via the transpiler's own Registry::generatedFqn) and registers it as a service under that class name.

The class that consumes the generic must itself be .xphp — only .xphp can name CachedFinder<User> as a type. Write that consumer (a service, a command, a controller) in .xphp, register it as a service, and inject the generic directly; its compiled constructor type is the hash class, so autowiring matches with no seam needed:

// src/Command/FindCommand.xphp  (a .xphp class — register it as a service)
#[AsCommand('app:find')]
final class FindCommand extends Command
{
    public function __construct(private readonly CachedFinder<User> $finder) {   // -> the specialization
        parent::__construct();
    }
}

(If a specialization does implement a stable interface, set alias: on the binding to that interface so handwritten .php can inject it — but a generic implementing a type-specific interface is usually a smell.)

Declaring a binding makes compilation run at container-build time (so the generated classes exist for autowiring/autoconfigure), and the container is rebuilt when any .xphp source changes. No separate xphp:compile step is needed.

Two rules for DI-bound generics

  • Write collaborator types fully-qualified in the template (no use): monomorphization fully-qualifies type parameters but copies other type references verbatim, so a short use-imported name would resolve into the generated XPHP\Generated\… namespace. Write \App\Finder\SourceInterface, not a used SourceInterface.
  • You don't need a call site. A generic that's only injected is never written as new Foo<Bar>() anywhere, yet monomorphization is usage-driven. The bundle bridges this by synthesizing a throwaway instantiation for each declared binding at compile time (transparent; cleaned up afterwards). (Stopgap: a future xphp release may accept specialization roots directly.)

Autowire the specialization (or its alias), not the bare template name — App\Generic\CachedFinder compiles to an empty marker interface that every specialization implements, so it is ambiguous across type arguments.

Deploy

A typical build step needs nothing xphp-specific beyond what you already run:

composer install --no-dev --optimize-autoloader
bin/console cache:warmup --env=prod   # compiles .xphp -> var/xphp/*

Commit .xphp sources; treat var/xphp/ as generated (gitignored), regenerated on each build. If you prefer to ship precompiled PHP without xphp on the target, run xphp:compile in CI and include the target/cache dirs in the artifact.

The cache warmer is mandatory and fail-loud: if compilation fails during cache:warmup, the command exits non-zero and the build aborts, so a broken artifact (missing or stale XPHP\Generated\* classes) never ships.

Compiling in a separate step (compile_on_warmup: false)

If you'd rather drive compilation explicitly — e.g. a dedicated CI stage, or to keep cache:warmup free of transpilation — turn the warmer off:

# config/packages/xphp.yaml
xphp:
    compile_on_warmup: false

The bundle then never compiles on its own; you own the step and must run it before the app is deployed (and before composer dump-autoload, since the optimized classmap is built from the generated output):

bin/console xphp:compile --env=prod   # explicit, fails the pipeline on error
composer dump-autoload --optimize --classmap-authoritative

xphp:compile is itself fail-loud — it exits non-zero on a compilation error — so a CI stage running it will stop the pipeline just like the warmer would.

With the warmer disabled, cache:warmup no longer regenerates var/xphp/, so make sure your build runs xphp:compile on every deploy; otherwise the app boots against whatever output happens to be on disk.

Try the demo

A runnable, console-only example app lives in demo/. It writes .xphp generics alongside plain PHP in one src/, compiles them through this bundle, and calls the generated code from a Symfony command:

cd demo && composer install
bin/console xphp:compile
bin/console app:demo

Development

make test            # PHPUnit
make test/mutation   # Infection

License

MIT