Open source · Apache 2.0

Typed. Compiled.
Still PHP.

++PHP is a typed superset of PHP, giving you compile-time guarantees without changing how you deploy.

  • Zero runtime cost
  • PHPStan and IDE friendly
  • Adopt file by file

Why ++PHP

Types the compiler enforces, in PHP your tools already read

Real syntax, not annotations

Generics, typed arrays, and throws clauses are part of the language. The compiler rejects code that breaks them before it runs, instead of relying on PHPDoc that a linter may or may not read.

Nothing changes at runtime

Everything compiles down to plain PHP with conventional PHPDoc. No extension, no bootstrap, and no ++PHP dependency in production. PHPStan, Psalm, and your IDE keep working on the output.

Adopt at your own pace

.php and .ppphp files live side by side in one project. Convert a file when it pays off and leave the rest exactly as it is.

The language

What you can write

Express the types, collections, and failure cases your application works with. Build them into ordinary PHP.

string $name = "Ada";

Typed locals

Every local has an explicit type and an initializer. Mark a binding readonly when it must never change.

class Box<T> { }

Erased generics

Write reusable classes and functions that keep the type you give them. A Box<User> returns a User, with generated PHPDoc carrying that relationship into your PHP tools.

array<string, User>

Typed arrays

array<T> lists and array<K, V> maps checked at compile time. At runtime they are ordinary PHP arrays.

when ($x) { ... }

when expressions

A conditional that yields a value. Branches are full blocks, so you can validate, loop, and compute before returning, and every branch must produce a result.

throws NotFound

Checked errors

Make recoverable failures part of your function signature. Callers catch them or declare them for their own callers, so a forgotten handler is caught before deployment.

*.php + *.ppphp

Mixed projects

Adopt file by file. Plain .php passes through the build unchanged, so a mixed project still builds to a complete PHP project.

The compiler

What you write. What ships.

Express delivery charges by weight; standard delivery charges per parcel. Each when branch calculates its price and returns it to initialize one readonly variable. The build turns that decision into ordinary PHP.

src/shipping.ppphpyou write
<?php

// Parcel weights in kg; prices in cents.
array<int> $weights = [2, 5, 3];
bool $express = true;

readonly int $shipping = when ($express) {
    int $weight = 0;
    foreach ($weights as int $parcel) {
        $weight += $parcel;
    }
    return 1200 + $weight * 50;
} else {
    int $parcels = count($weights);
    return 500 + $parcels * 100;
};

echo $shipping;
build/ppphp/shipping.phpppphp emits
<?php
declare(strict_types=1);

// Parcel weights in kg; prices in cents.
/** @var list<int> $weights */ $weights = [2, 5, 3];
/** @var bool $express */ $express = true;

if ($express) {
    /** @var int $weight */
    $weight = 0;
    /**
     * @var int $parcel
     */
    foreach ($weights as $parcel) {
        $weight += $parcel;
    }
    $shipping = 1200 + $weight * 50;
} else {
    $parcels = count($weights);
    $shipping = 500 + $parcels * 100;
}

echo $shipping;

Diagnostics

Caught before it runs

Every diagnostic points at your .ppphp source with a stable code, the exact location, and what to do about it. ppphp check exits non-zero, so it slots into CI like any other test.

See how the language works
src/Checkout.ppphpyou write
<?php

final class Receipt
{
    public function __construct(
        public readonly Money $total,
    ) {}
}

function charge(array<Order> $items): Receipt
{
    Money $total = sum($items);

    return new Receipt($total);
}
ppphp checkreports
Error[P4003]: Checked Error Is Not Handled

EmptyCart is not caught and is not covered by the enclosing callable throws clause.

  --> src/Checkout.ppphp:12:20
11 | {
12 |     Money $total = sum($items);
   |                    ^^^^^^^^^^^ EmptyCart is not caught and is not covered by the enclosing callable throws clause.
13 |

Related: The called error contract is declared here.
  --> src/cart.ppphp:4:12
3 | function sum<T : Priced>(array<T> $items): Money
4 |     throws EmptyCart
  |            ^^^^^^^^^
5 | {

Related: The enclosing callable is declared here.
  --> src/Checkout.ppphp:10:1
 9 |
10 | function charge(array<Order> $items): Receipt
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 | {
   | ^
   ...
14 |     return new Receipt($total);
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15 | }
   | ^
16 |

Help: Catch the checked error or add it to the enclosing callable throws clause.

Get started

Up and running in a minute

Install the compiler with Composer, initialize your project, then check and build as you work. Focus checks and builds on one file or run them across the whole project.

  1. init
    Set up the projectCreates ppphp.json and the output, cache, and stub directories.
  2. check
    Check itChecks every .ppphp and .php file in the project, or just the path you name, and exits non-zero on errors.
  3. build
    Build itRuns the same checks, then writes a complete, readable PHP tree in one atomic step.

Questions

Common questions

How is this different from PHPStan or Psalm?

Those tools check annotations on plain PHP. ++PHP gives the same ideas real syntax that a compiler validates, then emits the PHPDoc they understand. You keep the tools; you stop maintaining the comments by hand.

Is there a runtime cost?

No. Types are checked at compile time and erased. The output is ordinary PHP with no extension, bootstrap, or ++PHP package required in production.

Can I adopt it gradually?

Yes. .php and .ppphp files share the same source roots. Plain PHP takes part in checking and is copied into the build unchanged, so the result is always a complete PHP project.

What happens to generics at runtime?

They are erased. The compiler checks every relationship, then emits deterministic @template, @param, and @return PHPDoc that PHPStan and your IDE already read.

Does it work with Laravel?

Not out of the box yet. Today ++PHP fits a Composer project that owns its own layout: a service, a package, an application built directly on Composer. Laravel integration is in the pipeline, with other frameworks behind it; the introduction post on the blog says where that stands.

Does it understand Composer projects?

Yes. It follows Composer autoloading, installed packages, native PHP types, and PHPDoc while checking your project, and can point Composer at the compiled output for you.

What is the license?

Apache 2.0. The compiler is open source and developed in the open on GitHub.

Your codebase already speaks PHP.
Give it a type system.

Start with one .ppphp file, keep every dependency, and deploy the PHP you already know.