Documentation
Mixed projects
Introduce ++PHP one file at a time while existing PHP remains part of the source, checks, and build output.Rewriting an application before learning whether a language fits is rarely a sensible migration plan. ++PHP treats gradual adoption as the normal case: .php and .ppphp files share source roots, namespaces, Composer packages, and one project-wide analysis model.
Start with one boundary
Imagine an existing checkout module:
src/
├── Customer.php
├── LegacyTaxClient.php
├── Order.php
└── CheckoutService.php
Rename the service you want to strengthen:
src/
├── Customer.php
├── LegacyTaxClient.php
├── Order.php
└── CheckoutService.ppphp
The renamed file keeps PHP syntax and gains the ++PHP contract. Add missing parameter, return, property, and local types; make nullability explicit; then run a focused check:
vendor/bin/ppphp check src/CheckoutService.ppphp
The compiler uses Customer.php, Order.php, and LegacyTaxClient.php as context. You do not need to convert those files before the service can call them.
One model, two source contracts
Plain PHP and ++PHP play different roles:
.php source |
.ppphp source |
|---|---|
| follows ordinary PHP declaration rules | follows the complete ++PHP contract |
| contributes native types and PHPDoc | contributes native and ++PHP types |
| is copied byte-for-byte when selected | is compiled to corresponding .php |
| remains directly executable as source | executes through emitted PHP |
Project analysis checks calls in both directions. A PHP class with accurate native types or PHPDoc can satisfy a ++PHP caller. The emitted signature and generated PHPDoc from a ++PHP class remain understandable to ordinary PHP callers and tools.
Build a complete application tree
A pathless build mirrors every owned source beneath the output root:
src/CheckoutService.ppphp → build/ppphp/CheckoutService.php
src/Customer.php → build/ppphp/Customer.php
src/Order.php → build/ppphp/Order.php
Plain PHP is not an external dependency that disappears from the build. It is part of the application and is copied byte-for-byte. This means Composer can point at one complete generated tree instead of combining generated and original definitions.
A focused build emits exactly the selected files:
vendor/bin/ppphp build src/CheckoutService.ppphp
Use that for inspection and fast feedback. Use a pathless build for tests, packaging, and deployment so the output contains the whole application.
Improve existing PHP with PHPDoc
Legacy code often uses a broad native type while PHPDoc carries the real relationship:
/**
* @param list<Order> $orders
* @return array<string, Money>
* @throws TaxServiceUnavailable
*/
function calculateTaxes(array $orders): array
{
// Existing PHP implementation.
}
++PHP reads useful @param, @return, generic, and @throws metadata. This lets you improve the contract of a PHP boundary before converting its implementation.
When a stable runtime API has no inspectable declaration, such as framework-generated methods, write a declaration-only stub in the configured stubs directory. Stubs enrich analysis; they are not copied into production output.
Use Composer packages during checks
The compiler reads composer.json, composer.lock, and installed-package declarations to resolve namespaces and public APIs. Dependencies inform checking but are not rewritten or copied into the application build.
Analysis reads declarations and package metadata without booting the application or running Composer scripts. That keeps check predictable and avoids application side effects.
Migrate in a useful order
A practical sequence is:
- Configure source and output roots, then prove that a mixed build runs unchanged.
- Choose a small service or domain boundary with good native types.
- Rename that file to
.ppphpand satisfy strict declarations. - Introduce typed locals so internal state has one meaning.
- Improve adjacent PHPDoc or stubs where legacy APIs are broad.
- Add typed arrays, generics, and checked errors where they clarify real relationships.
- Expand only when the generated output, tests, and team workflow are comfortable.
Do not convert a file merely to increase a migration count. Convert a boundary when the stronger contract makes maintenance, review, or failure handling better.
Keep focused checks honest
A focused command limits which problems it reports and which files it writes. It still reads declarations elsewhere when the selected code uses them. Problems in unrelated files stay out of the way, while project-wide name conflicts that affect the selected code are still reported.
This makes focused checks useful during migration without turning them into single-file linting.
Deploy the same PHP you inspect
Run a complete build, point Composer at the generated tree, refresh the autoloader, and execute the normal test suite. Generated files pass php -l and run as ordinary PHP. If a production incident reaches a generated file, the code remains readable and diagnostics still lead back to the original .ppphp source.
Return to Installation for build and autoload setup, or The CLI for focused selection and JSON diagnostics.