Language

Strict project-wide types

Declare the types your functions and classes accept, and catch incompatible uses across your 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

Pass a value of the declared type rather than relying on PHP to convert it. For example, an int parameter accepts 3, but not the string '3'. ++PHP checks these arguments and return values before your application runs:

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.

Check nullable values before using them

Use ?Customer when a lookup can return no customer. Check for null before passing the result to code that requires a Customer:

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.

Working with dynamic PHP code

++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.

Use static include paths, declared properties, and direct variable access in .ppphp files. If an existing PHP module depends on dynamic behavior, leave it as .php and expose a typed API for your ++PHP code to call. See Mixed projects for PHPDoc and stub guidance.

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.