Install xphp into a Composer project, set up the PSR-4 autoload so the generated classes resolve, write your first generic, and run it. Should take under ten minutes.
- PHP 8.4+.
- Composer.
- A project with a working
composer.json(or runcomposer initfirst).
composer require --dev xphp-lang/xphpThis puts the compiler at vendor/bin/xphp and pulls in the runtime
dependencies (nikic/php-parser, symfony/console, symfony/process).
xphp emits two kinds of files: rewritten user code (your .xphp with
the generic call sites resolved, now valid .php) and specialized
classes (each unique Box<int> / Box<string> / ... becomes its
own real class under the XPHP\Generated\ namespace).
Both are picked up by Composer's standard PSR-4 autoloader once you
register them. Add to your project's composer.json:
{
"autoload": {
"psr-4": {
// Specialized classes live in their own namespace
// mirroring your template's namespace.
"XPHP\\Generated\\": ".xphp-cache/Generated/",
"App\\": [
// Path where your normal php code lives.
"src",
// Path where xphp drops the rewritten .php files.
// Same App\ namespace; PSR-4 finds the class in either.
"dist"
]
}
}
}Then rebuild the autoloader:
composer dump-autoloadReplace
App\\with your project's actual top-level namespace.
// src/Container.xphp
<?php
declare(strict_types=1);
namespace App;
class Container<T> {
public function __construct(public T $item) {}
public function get(): T {
return $this->item;
}
}// src/Use.xphp
<?php
declare(strict_types=1);
namespace App;
$intContainer = new Container::<int>(42);
$strContainer = new Container::<string>('hello');
echo $intContainer->get(), PHP_EOL;
echo $strContainer->get(), PHP_EOL;vendor/bin/xphp compile src dist .xphp-cache| Argument | Required | Default | Purpose |
|---|---|---|---|
<source> |
yes | -- | Directory of .xphp files (PSR-4 layout). |
<target> |
no | dist |
Where rewritten .php files land — your user code with generic args resolved. |
<cache> |
no | .xphp-cache |
Where specialized classes live. |
After the compile completes you'll have:
dist/Container.php— the template stripped down to a marker interface.dist/Use.php— your script withnew Container::<int>(42)rewritten to point at the specialized class..xphp-cache/Generated/App/Container/T_<hash>.php— one specialized class file per unique instantiation.
Both dist/ and .xphp-cache/ can be gitignored — they're
generated artifacts your CI/CD pipeline rebuilds on every deploy.
Safe by default: compile runs the same validation gate as
check — the generic validators plus PHPStan over the compiled
output — before emitting, and fails the build (writing nothing) if it
finds an error, so a typo'd or undeclared type never reaches runtime. For a
fast iteration build — when you've already run check and just want to
re-emit — skip the gate with --no-check:
vendor/bin/xphp compile src dist .xphp-cache --no-check # transpile only, no gateThe single-directory form above is the quickest way to compile one
self-contained tree (and it keeps working unchanged). But for any real
project — and required the moment you consume another package's
templates or ship your own — the recommended setup is an xphp.json
manifest at the project root. It's the project config (like composer.json):
it records your source roots and dependencies once, so compile/check take
no positional arguments, and it's what lets the compiler pull several source
roots together instead of staging them into one tree:
{
"sources": ["src"],
"include": ["vendor/**"],
"target": "dist",
"cache": ".xphp-cache"
}sources— this package's own.xphproots (relative to the manifest). Omitted ⇒["."].include— other packages to pull in, transitively. Each entry is a directory or a glob:*/?/[…]match within one path segment, and**(globstar) matches recursively. A glob auto-discovers: any matched directory that has its ownxphp.jsonis compiled in, others are skipped — so"vendor/**"picks up every installed xphp package at any depth and needs no edit when you add another. ("vendor/*/*"also works for Composer's flatvendor/<org>/<pkg>layout.) An explicit (non-glob) entry without anxphp.jsonis an error.target/cache— optional output dirs (CLI--target/--cacheoverride).
Then compile (or check) against the manifest — --config, or just run where the
xphp.json is auto-detected:
vendor/bin/xphp compile --config xphp.json # or: vendor/bin/xphp compile (auto-detect)
vendor/bin/xphp check --config xphp.jsonDistribution model. A library ships its .xphp sources plus its xphp.json
(via Composer). A downstream build pulls those sources and compiles the whole
union into its own output — so the upstream's marker interfaces and the
specializations your call sites need all get emitted, and everything runs without
the library being pre-compiled. The single-directory compile src dist cache form
above keeps working unchanged.
Any normal PHP runtime that loads Composer's autoload will pick up the generated classes via PSR-4. For example:
// public/index.php (or wherever your entry point lives)
<?php
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../dist/Use.php';Expected output:
42
hello
The compiler:
- Read the
.xphpfiles and parsed the generic syntax. - Found two distinct instantiations:
Container<int>andContainer<string>. - Emitted two specialized classes under
.xphp-cache/Generated/App/Container/T_<hash>.php, each withintorstringbaked into every signature. - Rewrote
src/Use.xphpintodist/Use.php, replacingnew Container::<int>(...)with a reference to the right specialized class.
PSR-4 routes the autoload, so neither you nor any downstream library ever has to know the generated class names.
- Syntax tour — every shipped feature, one page each.
- Caveats — what's NOT supported and what to do instead.
- Errors — verbatim catalog of every compile-time error message.
- How it works — the compile pipeline, end to end.
- Roadmap — what's shipped, what's in discovery.