Tutorials Logic, IN info@tutorialslogic.com

PHP Type Declarations and strict_types

PHP Type Contracts

PHP declarations constrain parameters, returns, properties, and supported class constants with atomic, nullable, union, intersection, and DNF types. Strict scalar behavior is selected per calling file and does not replace boundary parsing.

Strong contracts use narrow meaningful types, initialize state, preserve variance and named arguments, convert external data once, supplement native types with runtime validation and static analysis, and label every version-specific feature.

Function Types

  • Use int, float, string, and bool rather than aliases such as integer or boolean.
  • A return declaration checks the value leaving the function.
  • A void function returns no value; never describes a path that cannot return normally.

Typed Price Calculation

Typed Price Calculation
<?php
declare(strict_types=1);

function total(float $price, int $quantity): float
{
    return $price * $quantity;
}

echo total(49.5, 2);
Output
99

Strict Scalar Calls

Without strict_types, PHP may coerce a scalar argument when conversion is possible. With declare(strict_types=1), incompatible scalar arguments throw TypeError, except that an int is accepted for a float declaration.

Strict typing is enabled per file and follows the calling file for scalar calls. Place the declaration immediately after the opening tag.

Nullable and Union Types

?string means string|null. A union such as int|string accepts either listed type. Use a union only when both shapes are meaningful to callers, not to avoid normalizing input.

Nullable Lookup Result

Nullable Lookup Result
<?php
declare(strict_types=1);

function findLabel(int $id): ?string
{
    return $id === 7 ? 'PHP' : null;
}

echo findLabel(7) ?? 'Not found';
Output
PHP

Typed Properties

A typed property must receive a compatible value before it is read. Constructor promotion can declare and assign simple dependencies and value fields in one signature.

Promoted Properties

Promoted Properties
<?php
declare(strict_types=1);

final class Lesson
{
    public function __construct(
        public string $title,
        public int $durationMinutes
    ) {
    }
}

$lesson = new Lesson('PHP Types', 20);
echo $lesson->title;
Output
PHP Types

TypeError Diagnosis

  • Read the expected and actual types in the exception message.
  • Inspect the caller where the incompatible value entered the contract.
  • Validate external strings before calling typed domain functions.
  • Do not catch TypeError merely to ignore a programming mistake.

Declaration Sites

PHP type declarations can constrain parameters, return values, properties, and, on supported versions, class constants. A mismatch produces TypeError, while an uninitialized typed property has its own state distinct from null.

Declare the narrowest useful contract at public boundaries. Types improve local guarantees and tool feedback but do not validate string format, numeric range, authorization, collection element types, or external data shape.

Initialize typed properties through constructors or controlled factories before reading them. Nullable does not mean optional initialization; it means null is a permitted value after initialization.

The resource type cannot be used as a normal userland type declaration. Wrap resource ownership behind a class or validate with runtime APIs when a boundary accepts a stream-like handle.

  • Type parameters, returns, properties, and supported constants.
  • Add domain validation beyond language types.
  • Initialize every typed property before access.
  • Wrap resource ownership behind explicit abstractions.

Scalar Coercion

Without strict typing, PHP may coerce compatible scalar arguments and returns among int, float, string, and bool according to language rules. Coercion can turn input into a surprising but valid value, so external boundaries should parse explicitly.

`declare(strict_types=1)` applies per file and affects scalar function calls made from that file. The caller preference governs argument coercion even when the function was declared elsewhere. An integer is still accepted for a float declaration.

Strict mode also checks scalar return behavior in the declaring execution context, but it does not transform PHP into a fully static language. Array contents, object state, dynamic properties, and decoded data still need design and analysis.

Place the strict declaration at the beginning of project source files and test calls from both strict and legacy callers during migration. Do not assume enabling it in one bootstrap automatically changes every included file call site.

  • Parse external scalars instead of relying on coercion.
  • Remember strict arguments follow the calling file.
  • Treat int-to-float acceptance as the strict exception.
  • Migrate and test strictness file by file.

Atomic Types

Built-in declarations include scalar types, array, object, callable, iterable, mixed, void, never, null, true, false, and class types subject to version support and position rules. Use the exact PHP spelling such as `bool`, not aliases such as `boolean`.

`mixed` accepts every PHP value and communicates an intentionally unresolved boundary; it should trigger immediate narrowing. `object` accepts any object but says nothing about behavior, so prefer an interface when the caller needs a capability.

`void` means the caller cannot use a meaningful returned value, while `never` means the function does not return normally because it throws, exits, or otherwise terminates. Use never only when every path satisfies that contract.

`iterable` accepts arrays and Traversable objects. It does not promise rewindability, countability, element type, or repeated traversal. Document those properties or use a narrower interface where consumers depend on them.

  • Use exact built-in type names.
  • Narrow mixed and object at the boundary.
  • Distinguish void completion from never returning.
  • Document iterable lifetime and element expectations.

Nullable Types and Union Design

`?T` and `T|null` express the same two-value type for one base type. Use null only when absence is a real domain state, and distinguish it from an omitted argument by the function signature and default.

A union such as `int|string` accepts any listed type. Before adding a union, ask whether callers truly use two meaningful representations or whether parsing should convert both into one domain value at the boundary.

Literal true and false types support APIs with specific sentinel contracts on suitable PHP versions, but a result object or exception is often clearer than mixing a useful value with false. Preserve existing extension contracts when wrapping them.

Redundant unions are rejected when PHP can determine redundancy without loading classes. Keep unions small and behaviorally coherent; a long union often signals that one function owns unrelated responsibilities.

  • Use null for a meaningful absence state.
  • Normalize multiple transport forms before domain logic.
  • Avoid sentinel unions in new APIs when clearer results exist.
  • Refactor broad unions into cohesive operations.

Intersections and DNF

An intersection such as `Countable&Iterator` requires one object that satisfies every listed class-type contract. It describes combined capabilities without inventing a new named interface, but a domain interface is clearer when the combination has business meaning.

Intersection types require PHP 8.1 or later and use class types rather than scalar types. Compatibility and redundancy rules apply, so test the syntax on the project minimum runtime.

PHP 8.2 added disjunctive normal form types, allowing a union whose member can be a parenthesized intersection, such as `(A&B)|C`. Parentheses are part of the required grammar and should be used only when callers genuinely supply those alternatives.

Complex composite signatures can be precise yet difficult to explain and mock. Introduce a named adapter or interface when the type expression appears repeatedly or when additional invariants go beyond capability membership.

  • Use intersections for one object with several capabilities.
  • Label intersection syntax as PHP 8.1 or later.
  • Label DNF syntax as PHP 8.2 or later.
  • Name repeated or domain-significant combinations.

Variance and Inheritance

Implementations and overrides must remain compatible with inherited method contracts. Parameter types may become broader under contravariance, while return types may become narrower under covariance, subject to the language rules.

Property type compatibility is stricter because callers may both read and write, though modern property capabilities can add version-specific variance rules. Preserve visibility, readonly behavior, and hook contracts across supported runtimes.

Named arguments make public parameter names observable even though type compatibility focuses on types and positions. Keep inherited names stable so an override does not break callers that use the parent contract by name.

A subtype must preserve behavioral promises beyond the signature: valid states, exceptions, side effects, ordering, and result meaning. Run one contract suite against every implementation rather than trusting successful compilation.

  • Broaden accepted parameters only compatibly.
  • Narrow returned values only compatibly.
  • Preserve named-argument parameter names.
  • Test behavioral substitution, not only signatures.

Boundary Narrowing

Request data, JSON, database rows, cache values, environment strings, and unserialized data enter PHP without trustworthy domain types. Validate shape and convert them into typed value objects or commands before core logic.

Use `is_*`, `instanceof`, enum parsing, and domain factories to narrow uncertain values. Assertions may help static analysis inside proven internal paths but should not be the sole validation for hostile input or essential production invariants.

Array type declarations do not specify keys or element types. Use typed records, iterators, and supported static-analysis annotations for internal precision, plus runtime validation wherever data crosses a trust boundary.

Catch TypeError only at a boundary that can translate a programming or input error responsibly. Catching it around arbitrary application code can misclassify a defect as a client mistake and leave partial effects.

  • Convert untyped boundary data once.
  • Narrow values through explicit runtime checks.
  • Combine static collection types with runtime validation.
  • Translate TypeError only at an accountable boundary.

Type Verification

Test accepted and rejected values at every public function boundary, including null, falsy scalars, subclasses, anonymous implementations, generators, and uninitialized properties where relevant. Run compile checks on all supported PHP versions.

Static analysis can infer generics, array shapes, callable signatures, unreachable branches, and nullable flow beyond native declarations. Configure a meaningful level and baseline existing debt rather than suppressing new findings globally.

Reflection can inspect declared types for frameworks and diagnostics, but avoid making business behavior depend on fragile string formatting of composite types. Work with ReflectionType subclasses and named components.

During migration, add return types and internal declarations before narrowing widely called inputs, observe deprecations, and update callers in small steps. Public libraries should treat every signature change as a compatibility decision.

  • Test both accepted and rejected boundary values.
  • Run syntax checks on minimum and current runtimes.
  • Use static analysis for collection and flow precision.
  • Migrate public contracts through reviewed compatibility steps.
Before you move on

Mastery Check

5 checks
  • Declare the narrowest useful native contract.
  • Parse external values instead of relying on coercion.
  • Use null, unions, intersections, and DNF only for real domain states.
  • Preserve variance, property, and named-argument compatibility.
  • Test minimum runtime syntax and rejected boundary values.

Type Contract Check

0 of 2 checked

Q1. Where does strict_types apply?

Q2. What does ?string accept?

Try this next

Tighten a Contract

0 of 2 completed

  1. Create a function that accepts two integers, then call it with a numeric string before and after strict_types.
  2. Write a lookup that returns a string or null and handle the null result explicitly.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.