Documentation

Strict project-wide types

Make callable, state, and nullability boundaries explicit, and prove them across the whole application.

A native PHP type declaration is useful, but it does not by itself describe an entire program. A caller can still opt into coercion, an untyped local can quietly change shape, PHPDoc can disagree with a declaration, and a mistake in one file may not become visible until a distant code path runs.

++PHP checks the project as a connected whole. It follows calls, returns, properties, PHPDoc, Composer packages, and configured stubs together, then reports failures against the original .ppphp source.

Make the service boundary readable

Consider an application service that reserves inventory:

<?php

namespace App\Inventory;

final class ReservationService
{
    public function __construct(
        private StockRepository $stock,
        private Clock $clock,
    ) {
    }

    public function reserve(
        ProductId $productId,
        int $quantity,
    ): Reservation {
        StockItem $item = $this->stock->get($productId);

        if ($quantity < 1) {
            throw new InvalidQuantity($quantity);
        }

        return new Reservation(
            $item,
            $quantity,
            $this->clock->now(),
        );
    }
}

A reader can see every input, stored dependency, local value, and result without reconstructing types from later use. The compiler checks that StockRepository::get accepts ProductId, that Clock::now produces the expected value, and that every successful path returns Reservation.

The rule is consistent throughout a .ppphp file:

  • every parameter has a native type;
  • every property has a native type;
  • every function and method has a native return type;
  • constructors and destructors omit return declarations, as in PHP;
  • every ordinary local uses an explicit typed declaration;
  • nullable values write ?T or a union that admits null.

Exact scalar behavior

PHP's strict_types declaration is chosen by the calling file. ++PHP cannot base a project-wide guarantee on whether each caller remembered that directive, so it checks scalar arguments and returns before runtime:

function repeat(string $text, int $count): string
{
    return str_repeat($text, $count);
}

string $message = repeat('Ready', 3);
string $invalid = repeat('Ready', '3'); // rejected

The generated file also contains declare(strict_types=1). Project checks catch incompatible calls before the application reaches that PHP runtime check.

Nullability must be part of the model

Absence is not inferred from a default value or a branch you happen to remember:

function findCustomer(string $email): ?Customer
{
    // ...
}

?Customer $customer = findCustomer($email);

if ($customer !== null) {
    sendWelcomeMessage($customer);
}

Calling a Customer method before narrowing the nullable value is rejected. Returning null from a function declared as Customer is rejected. The same facts flow through assignments and control flow, so a check in one branch can make later access safe without a cast.

Broad types are explicit tools

Strict does not mean “every value must be narrow.” External JSON, plugin callbacks, legacy arrays, and reflection-adjacent APIs sometimes are genuinely broad:

function normalizeWebhook(mixed $payload): array
{
    array $normalized = [];

    // Validate and narrow the boundary deliberately.
    return $normalized;
}

mixed, bare array, object, callable, and iterable are available when you choose them. What ++PHP rejects is accidental mixed created by missing information. A broad boundary should be visible at the declaration where validation belongs.

Use typed arrays once keys and values are known, and generics when an API must preserve a caller-selected type.

Keep project checks predictable

++PHP checks declarations and data flow without executing the application. It rejects PHP constructs that hide the code or variable being used until runtime:

  • eval;
  • variable variables such as $$name;
  • dynamic include or require paths;
  • assignment and iteration by reference;
  • return-by-reference declarations;
  • dynamic property creation.

Static include paths and ordinary package APIs remain available. The distinction is whether the compiler can identify the declaration and data flow without executing application code.

Use types from existing PHP

A strict ++PHP service can call a legacy PHP repository, and legacy PHP can call emitted ++PHP. Native PHP types and accurate PHPDoc contribute real information in both directions:

<?php

namespace App\Legacy;

/** @return list<string> */
function activeCustomerIds(): array
{
    // Existing PHP remains ordinary PHP.
}

When a call cannot be resolved safely, the compiler reports what information is missing. Add a precise PHPDoc declaration or a stub when the API is stable but its declaration is not visible in source.

What a successful check means

A clean project check establishes that known arguments and returns are compatible, required returns exist, locals are initialized before use, nullability is respected, referenced members exist, and declared state is not assigned an incompatible value.

Use checked errors for recoverable failures that callers must acknowledge. Databases, extensions, and infrastructure can also fail at runtime, so continue to monitor the generated PHP application as usual.

Next, learn how typed locals make method bodies as explicit as their public signatures.