PHP Language Basics

Type Declarations And Strict Types

Type declarations make a function's accepted inputs and promised result visible in its signature. PHP checks those declarations at runtime, while editors and static-analysis tools can use them before execution.

Types reduce ambiguity, but they do not replace validation. int $quantity says the value is an integer. It does not say the integer is positive, within stock limits, or authorised for the current user.

Declare Parameter And Return Types

PHP example
<?php

declare(strict_types=1);

function applyDiscountCents(int $priceCents, int $discountCents): int
{
    return max(0, $priceCents - $discountCents);
}

echo applyDiscountCents(2500, 400), PHP_EOL;

The signature documents two integer-cent inputs and an integer result. The names carry business meaning and units that int alone cannot express.

Use PHP's real scalar type names: int, float, string, and bool. Aliases such as integer and boolean are not scalar declaration aliases; PHP interprets unsupported names as class names.

Place strict_types Correctly

Enable strict scalar checking with:

PHP example
<?php

declare(strict_types=1);

The declaration belongs at the beginning of the file, after the opening tag. It is a per-file setting rather than a project-wide switch.

Strict typing applies to scalar function calls made from that file. It does not attach permanently to functions declared there. If a non-strict file calls a function declared in a strict file, the caller's coercive mode governs scalar arguments.

This caller rule matters in multi-file applications. Projects commonly place declare(strict_types=1); in every PHP source file so behavior does not depend on which file performs a call.

Compare Coercive And Strict Calls

Without strict mode, PHP may convert compatible scalar values:

PHP example
<?php

function doubleQuantity(int $quantity): int
{
    return $quantity * 2;
}

var_dump(doubleQuantity('4'));

The numeric string can be coerced to integer 4 and the result is 8.

With strict mode, the same string argument causes TypeError:

PHP example
<?php

declare(strict_types=1);

function doubleQuantity(int $quantity): int
{
    return $quantity * 2;
}

try {
    doubleQuantity('4');
} catch (TypeError $exception) {
    echo "Quantity has the wrong PHP type\n";
}

The correct fix is to validate and convert external text before calling the typed function. Removing the declaration only moves the ambiguity deeper into the program.

Strict scalar matching has one deliberate exception: an integer can satisfy a float parameter because every integer value has a corresponding floating-point representation for the call.

Return Types Are Enforced Too

A return declaration checks the value leaving the function:

PHP example
<?php

declare(strict_types=1);

function formatCents(int $cents): string
{
    return 'GBP ' . number_format($cents / 100, 2, '.', ',');
}

If this function returns the raw integer instead, strict mode produces a TypeError because the implementation violated its string promise.

Do not weaken a return type to make a failing implementation pass. Decide what the caller should receive, then make every normal return path honor that contract.

A function can also fail by throwing an exception. An exception leaves the normal return path, so it does not need to match the declared return type.

Use Nullable Types Only When Null Has Meaning

A nullable type accepts one type or null:

PHP example
<?php

declare(strict_types=1);

function customerLabel(?string $name): string
{
    if ($name === null) {
        return 'Guest';
    }

    $name = trim($name);

    return $name === '' ? 'Guest' : $name;
}

?string is shorthand for string|null. Use it when absence is part of the domain, such as an optional middle name or unknown completion date.

Do not add null merely to silence a caller error. If a value is required, keep the non-null type and fix the boundary that failed to supply it.

A nullable parameter with a default can be omitted explicitly:

function customerLabel(?string $name = null): string

The default and the allowed type are separate decisions: the default controls omission, while the type controls supplied values.

Use Union Types For Real Alternatives

PHP supports unions such as int|string:

PHP example
<?php

declare(strict_types=1);

function normaliseReference(int|string $reference): string
{
    return (string) $reference;
}

A union should reflect a genuine API contract. Do not add many alternatives until nearly any value is accepted. Broad unions make callers and implementations handle more branches.

Prefer converting boundary data into one application type where possible. If every downstream function accepts int|string, the conversion decision has not actually been made.

Use void For Intentional Side Effects

A void function does not return a usable value:

PHP example
<?php

declare(strict_types=1);

function printLine(string $message): void
{
    echo $message, PHP_EOL;
}

printLine('Ready');

A void function may use bare return; to leave early, but it must not return a value. Use void when the action is the contract, such as writing output or recording a log entry.

Business calculations and formatters are usually easier to reuse and test when they return values instead of printing directly.

Type External Input At The Boundary

Query parameters, form fields, environment variables, CLI arguments, and CSV cells commonly arrive as strings. Strict types do not parse them for you.

PHP example
<?php

declare(strict_types=1);

function parsePositiveQuantity(string $input): int
{
    if (!ctype_digit($input)) {
        throw new InvalidArgumentException('Quantity must contain digits only.');
    }

    $quantity = (int) $input;

    if ($quantity < 1) {
        throw new InvalidArgumentException('Quantity must be positive.');
    }

    return $quantity;
}

function calculateLineTotal(int $unitPriceCents, int $quantity): int
{
    return $unitPriceCents * $quantity;
}

$quantity = parsePositiveQuantity('3');
echo calculateLineTotal(1299, $quantity), PHP_EOL;

The parser owns validation and conversion. The calculation receives application-ready integers and does not need to understand raw text formats.

Prove The Calling-File Rule

Suppose functions.php declares strict types and defines an integer function:

PHP example
<?php

declare(strict_types=1);

function increment(int $value): int
{
    return $value + 1;
}

A different file without strict mode can require it and call increment('4'); the calling file permits scalar coercion. If that caller also begins with declare(strict_types=1);, the same string call throws TypeError.

This is why adding strict mode only to helper files is insufficient. Apply the project convention consistently to entry points, tests, scripts, and included source files. The declaration does not propagate through require into another file's calls.

Parse Booleans Instead Of Casting Text

Casting external text to bool is often wrong because every non-empty string except '0' becomes true. The string 'false' is therefore true when cast.

Use a parser whose accepted spellings are explicit:

PHP example
<?php

declare(strict_types=1);

function parseBoolean(string $input): bool
{
    return match (strtolower(trim($input))) {
        '1', 'true', 'yes' => true,
        '0', 'false', 'no' => false,
        default => throw new InvalidArgumentException('Invalid boolean value.'),
    };
}

After parsing, pass the real boolean into typed application functions. Type declarations protect the internal boundary; the parser defines which external representations are valid.

Declarations Do Not Describe Array Shapes

An array declaration accepts any PHP array:

function orderTotal(array $order): int

PHP does not enforce that the array contains items, that every item has price_cents, or that those values are integers. Runtime checks, value objects, tests, PHPDoc array shapes, and static analysis can provide stronger guarantees.

Do not assume array $order makes nested reads safe. Validate external structures before passing them deeper into the application.

Likewise, callable says a value can be called but does not declare its parameter and return signature. Static-analysis annotations can add that precision.

Read TypeError As A Contract Failure

A TypeError usually means code crossed a typed boundary with the wrong PHP value or returned the wrong type. Catching one can be useful in a small demonstration or at a top-level failure boundary, but routine application logic should not use TypeError as user-input validation.

Validate raw input before the typed call and handle expected validation exceptions. If an internal TypeError occurs, fix the caller or implementation rather than broadly catching and continuing with uncertain state.

Strict Types Do Not Make PHP Statically Typed

PHP still determines many values at runtime, arrays can contain mixed shapes, and variables can be reassigned to another type unless another declaration constrains the boundary.

Strict types improve scalar function-call and return behavior. They do not validate database rows, infer business units, prevent every coercion in operators, or replace static analysis. Treat them as one layer in a broader correctness strategy.

Common Type Mistakes

Symptom Likely cause
Numeric CLI value causes TypeError Raw string was not parsed at the boundary
Behavior differs between files One caller lacks declare(strict_types=1)
Function returns wrong type Implementation violates its return declaration
Negative quantity is accepted PHP type is correct but business validation is missing
Missing nested key warning array declaration was mistaken for an array-shape guarantee
Nullable type spreads everywhere null was allowed instead of resolving absence at the boundary
Function accepts too many unrelated values Union type is hiding an unclear contract
Internal bug is treated as bad user input TypeError was caught too broadly
Editor cannot verify callback arguments callable lacks a declared signature

What You Should Be Able To Do

After this lesson, you should be able to add parameter and return declarations, place declare(strict_types=1); correctly, explain why the calling file controls strict scalar arguments, predict coercive versus strict behavior, recognise the integer-to-float exception, use nullable and union types only for meaningful alternatives, choose void for an intentional side effect, parse external strings before typed calls, distinguish PHP types from business validation, and explain why array does not enforce a record shape.

Official references: PHP's manual pages for type declarations and declare.

Practice

Task: Typed Discount

Task

Create a strict PHP file and implement:

function applyDiscountCents(int $priceCents, int $discountCents): int

Assume both inputs were already validated as non-negative. Return the price after discount, but never below 0.

Run these calls and print the results:

PHP example
applyDiscountCents(2500, 400);
applyDiscountCents(2500, 2500);
applyDiscountCents(2500, 3000);

Expected output:

2100
0
0

Hints

  • Put declare(strict_types=1); immediately after <?php.
  • max(0, ...) expresses the lower boundary.
  • Test below, exactly at, and beyond the boundary.
Show solution

Solution

PHP example
<?php

declare(strict_types=1);

function applyDiscountCents(int $priceCents, int $discountCents): int
{
    return max(0, $priceCents - $discountCents);
}

echo applyDiscountCents(2500, 400), PHP_EOL;
echo applyDiscountCents(2500, 2500), PHP_EOL;
echo applyDiscountCents(2500, 3000), PHP_EOL;

Explanation

The signature requires integer-cent arguments and promises an integer result. max() clamps negative subtraction results to zero.

The three calls verify the ordinary path and both sides of the lower boundary. The type declarations do not enforce non-negative inputs; that remains the stated validation responsibility of the caller.

Task: Fix Return Type

Task

This strict function promises a string but returns an integer:

PHP example
<?php

declare(strict_types=1);

function formatCents(int $cents): string
{
    return $cents;
}

Fix it so it returns a GBP label with two decimal places, a decimal point, and comma thousands separators.

Verify these calls:

formatCents(99)     -> GBP 0.99
formatCents(1299)   -> GBP 12.99
formatCents(123456) -> GBP 1,234.56

Hints

  • Divide cents by 100 only for presentation.
  • Use all four number_format() arguments so separators are explicit.
  • Keep the numeric parameter and string return declaration.
Show solution

Solution

PHP example
<?php

declare(strict_types=1);

function formatCents(int $cents): string
{
    return 'GBP ' . number_format($cents / 100, 2, '.', ',');
}

echo formatCents(99), PHP_EOL;
echo formatCents(1299), PHP_EOL;
echo formatCents(123456), PHP_EOL;

Explanation

The implementation now fulfills its declared string result. Integer cents remain the input unit, while conversion and separators belong to presentation.

The three cases verify a value below one major currency unit, an ordinary value, and a value that requires a thousands separator. Removing the return declaration would hide the original contract error rather than fixing it.

Task: Write Typed Normalizer

Task

Create a strict PHP file and implement:

function normaliseRequiredText(string $value): string

Trim the value, throw InvalidArgumentException('Value is required.') when the cleaned string is empty, and otherwise return it.

Loop over [' PHP ', ' ', '0']. Catch the expected exception for each value and print:

Accepted: PHP
Rejected
Accepted: 0

Hints

  • A string can satisfy the PHP type while still failing a business rule.
  • Compare the trimmed value with === ''.
  • Do not use empty(), because the valid string '0' would be rejected.
Show solution

Solution

PHP example
<?php

declare(strict_types=1);

function normaliseRequiredText(string $value): string
{
    $value = trim($value);

    if ($value === '') {
        throw new InvalidArgumentException('Value is required.');
    }

    return $value;
}

foreach (['  PHP  ', '   ', '0'] as $value) {
    try {
        echo 'Accepted: ' . normaliseRequiredText($value) . "\n";
    } catch (InvalidArgumentException $exception) {
        echo "Rejected\n";
    }
}

Explanation

All three inputs have the declared PHP type string. The whitespace-only value still violates the function's required-text rule after trimming.

The exact empty-string comparison preserves '0' as valid text. This demonstrates why type declarations and business validation are complementary rather than interchangeable.