xphp-lang/xphp
XPHP extends PHP with static typing and modern language features, compiling back to plain PHP for seamless interoperability with existing code, Composer packages, and Laravel projects.
XPHP is a young, single-purpose package (14 stars, no dependents at the time of writing), so the first hour should be spent confirming what it actually is before you design around it.
Install and pin.
composer require xphp-lang/xphp --dev
Install it as a dev dependency first. Nothing in a 0-dependent package should land in require until you've proven it works on your PHP version.
Read the repo before the README. With a package this small, the source is the documentation. Look in this order:
composer.json → require.php, autoload, bin, extra.laravelbin/ or src/Console/ → the actual entry points you'll be callingexamples/ and tests/ → the only reliable source of working, current API usageCHANGELOG.md / tags → the 2026-07-20 release is recent; check how much churn preceded itVerify it runs at all. The fastest smoke test is a throwaway script that exercises the package's primary public class:
// smoke.php
require 'vendor/autoload.php';
// instantiate the main compiler/parser/transpiler class from src/ and dump output
If there's a binary, run vendor/bin/xphp --help (or whatever bin declares) and confirm the version matches the tag you installed.
First real use case: pick one small, boring, leaf-node piece of your app — a single value object, a formatter, a config loader — and run it through XPHP end to end. Do not start with a controller or a model. You want a target where a wrong output is obvious and cheap to debug.
Decide the integration surface early. Because XPHP is not Laravel-aware, you're choosing between three shapes: a CLI/build step (safest), a dev-only service provider (convenient), or runtime library calls in production (highest risk). Pick the build step unless you have a strong reason not to.
Compile in a Composer script, commit the output. The pattern that ages best with a low-adoption tool is to treat it as a build artifact generator and never let it run in production:
{
"scripts": {
"xphp:build": "xphp build src/X/ --out=generated/",
"post-autoload-dump": ["@xphp:build"]
}
}
Generated files go in generated/ and are either committed (reviewable diffs, no CI dependency) or .gitignored and rebuilt in CI. Committed output means a broken XPHP release can't take down a deploy — you just don't upgrade.
Wrap the library in a thin adapter. Don't sprinkle XPHP\... calls through your app. Give it one seam:
// app/Support/Xphp.php
final class Xphp
{
public static function compile(string $source): string
{
// single place that touches the package API
}
}
When the package inevitably renames or reshapes a method, you change one file. This also makes it trivially mockable in tests.
Expose it as an Artisan command in local only. A dev-only command is the right ergonomic layer for a Laravel team:
// app/Console/Commands/XphpCompile.php — registered only when $this->app->environment('local')
protected $signature = 'xphp:compile {path?} {--watch}';
Guard registration with $this->app->environment('local', 'testing') so it never becomes an accidental production entry point.
Golden-file tests are non-negotiable. Snapshot the generated output for every input you care about:
public function test_compiles_value_object(): void
{
$this->assertSame(
file_get_contents(base_path('tests/fixtures/vo.expected.php')),
Xphp::compile(file_get_contents(base_path('tests/fixtures/vo.xphp')))
);
}
With no dependents upstream, you are the compatibility test suite. These fixtures are also your upgrade safety net — a bump that changes output shows up as a red test, not a runtime bug.
CI: install pinned, build, diff, fail on drift. Run the build in CI and fail if git diff --exit-code generated/ is non-empty. That catches both "someone forgot to rebuild" and "a dependency update silently changed codegen."
Pipeline shape if you go the CLI route. xphp build in a Makefile/Composer target → linter and static analysis over the generated PHP (not the source) → PHPUnit → deploy. The generated code is what runs, so it's what must pass phpstan and Pint.
"xphp-lang/xphp": "1.2.3", not ^1.2) so a minor release can't change your codegen overnight.require.php constraint matches every environment you deploy to. A package releasing in 2026 may target syntax your production PHP doesn't have.composer dump-autoload after a build, or they need an explicit require. A stale classmap will produce "class not found" errors that look like XPHP bugs and aren't.Overall: use it in a build step, behind an adapter, with golden-file tests and an exact version pin. That posture gets you the productivity upside while capping the downside at "we wrote some PHP by hand again."
How can I help you explore Laravel packages today?