xphp-lang / xphp-symfony-bundle
Symfony bundle to write XPHP (PHP with native generics) inside a Symfony app and deploy the auto-generated PHP.
Package info
github.com/xphp-lang/xphp-symfony-bundle
Type:symfony-bundle
pkg:composer/xphp-lang/xphp-symfony-bundle
Requires
- php: ^8.4.0
- symfony/config: ^8.0
- symfony/console: ^8.0
- symfony/dependency-injection: ^8.0
- symfony/http-kernel: ^8.0
- xphp-lang/xphp: ^0.1
Requires (Dev)
- infection/infection: ^0.33
- phpunit/phpunit: ^13.0
- symfony/framework-bundle: ^8.0
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:compileconsole 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 acomposer dump-autoload.
Requirements
- PHP
^8.4 - Symfony
^8.0 xphp-lang/xphp^0.1(pulled in automatically)
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
-
Put your generics under the
sourcedirectory, e.g.:xphp/ └── App/ └── Collections/ └── Collection.xphp # final class Collection<T> { ... } -
Compile during development:
bin/console xphp:compile
…or just warm the cache (also runs the compiler):
bin/console cache:warmup
-
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 shortuse-imported name would resolve into the generatedXPHP\Generated\…namespace. Write\App\Finder\SourceInterface, not ausedSourceInterface. - 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\CachedFindercompiles 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