Write ++PHP. Ship PHP.
Today we are releasing ++PHP 2026.3.1, the first public stable release of ++PHP, pronounced “plus plus PHP.”
PHP already has the runtime, package ecosystem, hosting support, frameworks, and deployment model behind an enormous range of applications. Those are strengths worth preserving. ++PHP starts from that foundation and adds stronger ways to express relationships, state, decisions, and recoverable failures directly in source code.
The compiler checks those relationships across the project, then produces a complete PHP 8.4+ application. Production runs PHP. Composer remains the package manager. Existing .php files can stay exactly where they are.
Stronger contracts while you write. PHP when you deploy.
Why ++PHP exists
PHP’s native type system has grown considerably, and tools such as PHPStan have made deep static analysis practical for real applications. Even so, several important ideas often remain divided between native declarations, PHPDoc, analyzer configuration, and team convention.
A reusable repository may return object while PHPDoc explains the actual entity relationship. An array parameter may say array while its keys and values are known throughout the application. A method may document expected exceptions in prose without requiring its callers to make a recovery decision. A local variable may acquire a different meaning several assignments after it first appears.
++PHP brings a focused set of those relationships into the language:
- Generic parameters preserve the type flowing through reusable APIs.
- Typed arrays describe lists and maps directly in source.
- Typed locals give each variable one declared meaning.
readonlyprotects local storage that should not change.- Checked errors make recoverable failures part of a callable declaration.
whenexpressions let conditional branches prepare data before producing one typed value.
These are not isolated syntax additions. The value comes from checking them together across the whole application.
See the language
The following example is recognizably PHP, but it makes more of the program’s intent available to the compiler and the next person reading it:
<?php
function loadUser(string $id): User
throws UserNotFound, StorageUnavailable
{
return UserRepository::find($id);
}
function summarize(array<string, User> $users): string
{
readonly int $count = count($users);
return when ($count === 0) {
return 'No users';
} else {
array<string> $names = [];
foreach ($users as User $user) {
$names[] = $user->displayName();
}
string $preview = implode(', ', $names);
return $count . ' users: ' . $preview;
};
}
Each declaration answers a question a caller or maintainer would otherwise have to reconstruct:
throws UserNotFound, StorageUnavailableidentifies the recoverable failures every caller must handle or declare.array<string, User>says that user identifiers are the keys and every value is aUser.readonly int $countintroduces a local integer whose storage cannot be reassigned or structurally mutated.whenlets the non-empty branch collect and format user names before returning the final summary.
The compiler follows those facts through calls, assignments, branches, returns, PHPDoc, Composer packages, and the .php files around them. A contradiction becomes a source-located diagnostic before the application is deployed.
What ships
++PHP syntax is checked, erased, or translated into standard PHP during the build. Type relationships are retained as compatible PHPDoc, and value-producing constructs become readable PHP control flow.
This ++PHP source:
function labelFor(array<string, User> $users): string
{
readonly int $count = count($users);
return when ($count === 0) {
return 'No users';
} else {
array<string> $names = [];
foreach ($users as User $user) {
$names[] = $user->displayName();
}
string $preview = implode(', ', $names);
return $count . ' users: ' . $preview;
};
}
produces ordinary PHP with the same behavior. Conceptually, the generated code looks like this:
/**
* @param array<string, User> $users
*/
function labelFor(array $users): string
{
/** @var int $count */
$count = count($users);
if ($count === 0) {
$label = 'No users';
} else {
/** @var list<string> $names */
$names = [];
foreach ($users as $user) {
$names[] = $user->displayName();
}
/** @var string $preview */
$preview = implode(', ', $names);
$label = $count . ' users: ' . $preview;
}
return $label;
}
The generated application runs on the official PHP runtime and uses normal PHP classes, arrays, exceptions, extensions, and functions. The output is designed to be read during review, inspected during debugging, and deployed with existing PHP infrastructure.
++PHP adds a build step to development and CI. It does not add a production service or require a PHP extension.
Keep the PHP ecosystem
Composer packages remain PHP packages. The compiler reads Composer autoloading and installed declarations while checking the project. After a successful build, Composer loads the generated PHP tree in the same way it loads any other application source.
Generated PHPDoc preserves relationships for PHPStan and compatible IDE tooling. Existing PHP callers can use compiled ++PHP classes, and ++PHP code can call existing PHP declarations whose native types or PHPDoc describe their behavior.
PHP remains authoritative at runtime. Array behavior, exception propagation, object behavior, truthiness, extensions, and memory management continue to follow PHP.
Why this is more than PHPDoc
PHPStan and PHPDoc remain valuable parts of the PHP ecosystem. ++PHP works with them rather than asking teams to discard them.
The difference is where selected contracts live and when they are enforced. In ++PHP, generics, typed arrays, local types, readonly local storage, checked errors, and value-producing conditionals are first-class source syntax. The compiler enforces them as one language and emits compatible PHPDoc so the rest of the ecosystem can retain the information.
This gives source code one clear place to state its intent while preserving the tools that already understand PHP.
Adopt one file at a time
++PHP is designed for existing applications as well as new ones. A project can contain .php and .ppphp files in the same source directory:
src/
├── LegacyController.php
├── UserService.ppphp
└── ReportGenerator.ppphp
│
│ vendor/bin/ppphp build
▼
build/ppphp/
├── LegacyController.php
├── UserService.php
└── ReportGenerator.php
Plain .php source contributes types and declarations during checking, then passes into the output unchanged. Each .ppphp file receives the complete ++PHP language rules and compiles to a corresponding .php file.
The result is one complete PHP application. A team can begin with a service whose contracts matter, learn from the generated output, and expand at the pace that makes sense for the codebase.
Stronger contracts with practical outcomes
Refactor reusable APIs with confidence
Generics preserve a caller-selected type through classes, interfaces, traits, functions, and methods:
interface Repository<T>
{
public function find(string $id): ?T;
public function save(T $entity): void;
}
A Repository<Customer> returns ?Customer and accepts Customer. Rename a member, change a return type, or reshape the repository and the compiler can show every affected use across the project.
Make collection contents visible
Typed arrays distinguish a list from a map and carry element types into indexing and iteration:
array<Order> $orders = [];
array<string, Order> $ordersById = [];
The runtime value remains a PHP array. The source now tells readers and tools what belongs inside it.
Make recovery part of the API
Checked errors turn an expected failure from documentation into a caller obligation:
function charge(Money $amount): Receipt
throws CardDeclined, GatewayUnavailable
{
// ...
}
Every caller catches those errors or declares that they can continue outward. At runtime, they remain ordinary PHP exceptions handled with try and catch.
Give local state one meaning
Typed local declarations make variables visible at the point they begin:
int $attempts = 0;
?User $currentUser = null;
readonly string $requestId = createRequestId();
Later assignments must remain compatible with the declared type. Readonly local storage protects decisions and identifiers that should remain stable through the rest of the function.
Let complete branches produce a value
when gives multi-step conditional branches enough room to work while requiring one compatible result:
ShippingQuote $quote = when ($customer->hasPriorityDelivery()) {
Money $fee = $rates->priorityFeeFor($basket);
$audit->recordPriorityQuote($customer, $fee);
return new ShippingQuote('priority', $fee);
} else {
Money $fee = $rates->standardFeeFor($basket);
$audit->recordStandardQuote($customer, $fee);
return new ShippingQuote('standard', $fee);
};
The compiler checks every branch and translates the expression into normal PHP control flow.
Use PHP's match when every arm is already one direct expression. Use when when a branch needs to validate input, calculate intermediate values, record an event, iterate over data, or perform other work before returning its result.
The development workflow
A .ppphp file follows stricter rules than a .php file. Parameters, properties, return values, and local variables are explicit. Nullability is written. Recoverable checked errors are handled or declared. Dynamic constructs that prevent reliable project analysis stay in ordinary PHP at the edges where they are needed.
That stricter source contract is the point. It lets ppphp check find incompatible assignments, invalid arguments, missing returns, unhandled checked errors, unknown members, and incomplete when expressions before ppphp build creates the deployable PHP tree.
Included in 2026.3.1
The stable 2026.3.1 release includes:
- Explicit typed and
readonlylocal declarations - Generics for classes, interfaces, traits, functions, and methods
array<T>list types andarray<K, V>map types- Checked-error declarations with
throws - Value-producing
whenexpressions - Strict project-wide validation
- Mixed
.phpand.ppphpproject support - Readable PHP 8.4+ build output
- Compatible PHPDoc for PHPStan and supporting IDE tooling
- Focused and complete project checks with machine-readable diagnostics
Get started
Add the compiler to an existing Composer project as a development dependency:
composer require --dev atatusoft-ltd/ppphp-src
vendor/bin/ppphp init
vendor/bin/ppphp check
vendor/bin/ppphp build
The Playground is the fastest way to experience the language. Edit the examples, run the compiler, inspect diagnostics, and compare ++PHP source with the generated PHP before installing anything.
++PHP 2026.3.1 targets PHP 8.4 or newer and is available under the Apache License 2.0.
This is a stronger language for writing PHP applications while keeping PHP at the center of how those applications run.