A minimal, console-only Symfony app that uses the
xphp-lang/xphp-symfony-bundle (consumed locally via a Composer path
repository). It shows the whole loop: write .xphp generics → compile them with
the bundle → call the generated PHP from a handwritten Symfony command.
.xphp and plain .php files live side by side under src/:
src/
├── Command/DemoCommand.php # handwritten PHP — calls into the compiled Catalog
├── Models/Plastic.php # handwritten PHP — left untouched by the build
├── Containers/Box.xphp # generic template class Box<T>
├── Demo/Catalog.xphp # uses new Box::<Plastic>() (so it must be .xphp)
└── Kernel.php
The bundle's compiler is pointed at src/ (see config/packages/xphp.yaml) but
only ever processes *.xphp; your *.php files are ignored and stay byte-for-byte
the same. Compiled output goes to var/xphp/ and App\ is mapped (in
composer.json) to both src/ and var/xphp/dist/, so each class resolves to
exactly one real .php:
| Class | Loaded from | Kind |
|---|---|---|
App\Command\DemoCommand |
src/Command/DemoCommand.php |
handwritten |
App\Models\Plastic |
src/Models/Plastic.php |
handwritten, untouched |
App\Containers\Box |
var/xphp/dist/Containers/Box.php |
compiled (supertype) |
App\Demo\Catalog |
var/xphp/dist/Demo/Catalog.php |
compiled |
XPHP\Generated\…\T_<hash> |
var/xphp/generated/Generated/… |
generated (specialized) |
The generated XPHP\Generated\* classes are autoloaded by the bundle's runtime
autoloader — no extra composer config needed.
Requires PHP 8.4+.
cd demo
composer install
bin/console xphp:compile # src/*.xphp -> var/xphp/dist + var/xphp/generated
bin/console app:demoExpected output: the colors red, blue produced by the compiled Catalog, plus
the concrete XPHP\Generated\App\Containers\Box\T_<hash> class that Box<Plastic>
was monomorphized into — proving it's the generated PHP that actually runs.
In a real deploy you'd skip the explicit xphp:compile: bin/console cache:warmup
runs the compiler as part of the standard build (see the bundle README).
The demo shows both ways to use a generic:
1. Value type — app:demo (Box<Plastic> / Catalog). You new the generic
inside .xphp; handwritten code calls the compiled class. No DI involved.
2. Service via DI — app:find (CachedFinder<User>). The generic is registered
as a service and constructor-autowired, with real service dependencies:
bin/console app:find lookup #1: Ada
lookup #1 again: Ada
source queried: 1 time(s) for 2 lookups (cache-aside works)
[OK] Autowired concrete type: XPHP\Generated\App\Generic\CachedFinder\T_<hash>
FindCommand is itself an .xphp file — that's what lets it name
CachedFinder<User> as a constructor type. Its compiled form injects the
monomorphized class directly (no interface, no alias), and that class's own
CacheInterface + SourceInterface deps were autowired. No xphp:compile needed
first — declaring the binding in config/packages/xphp.yaml makes compilation run at
container build.
Inspect the wiring:
bin/console debug:container 'App\Command\FindCommand'
# -> ctor arg autowired to XPHP\Generated\App\Generic\CachedFinder\T_<hash>src/Demo/Catalog.xphp— the only place that writesBox<Plastic>(value-type path).src/Generic/CachedFinder.xphp— the generic service. It implements no interface (aT-generic class can't honestly implement aUser-specific one); note its collaborator types are written fully-qualified (\App\Finder\SourceInterface), notused.src/Command/FindCommand.xphp— a.xphpcommand whose constructor takesCachedFinder<User>. Compare it withvar/xphp/dist/Command/FindCommand.php: the#[AsCommand]attribute andusestatements survive; only the generic type-hint is rewritten to the hash class.config/packages/xphp.yaml— theservices.bindingsentry that turnsCachedFinder<User>into an autowireable service.src/Models/Plastic.php— compare before/after a compile; it never changes.