Skip to content
16

Primitives & Scalars ​

TypePHP provides runtime enforcement for all native PHP primitives, literal scalar values, integer refinements, string refinements, class-string subtypes, float constraints, resources, and special return control types.


Strict Non-Coercion Policy (Zero Type Juggling) ​

In standard PHP execution without declare(strict_types=1);, the Zend Engine aggressively juggles scalar types (converting numeric strings like '42' to int, or truthy integers like 1 to bool).

TypePHP enforces a strict Zero-Coercion Policy at runtime:

  • Strings are NEVER coerced to numbers: Passing '42' into int or positive-int triggers an immediate TypeError.
  • Numbers are NEVER coerced to booleans: Passing 1 or 0 into bool, true, or false triggers an immediate TypeError.
  • Booleans are NEVER coerced to strings: Passing true into string or non-empty-string triggers an immediate TypeError.

The Single Standard Exception: Whole Integer to Float Widening ​

To remain 100% faithful to the native PHP language specification, TypePHP allows whole integers to satisfy float contracts.

Even under native PHP's strictest mode (declare(strict_types=1);), the Zend Engine allows passing an int into a parameter expecting float (e.g. passing 10 into float $rate widens losslessly to 10.0 in C memory) [1]. TypePHP adopts this exact widening behavior while strictly rejecting reverse coercion (float into int is always rejected).

Because TypePHP's exceptions extend PHP's native \TypeError [1], you can catch all contract failures using standard native PHP error handling without importing any custom classes:

Strict Non-Coercion & Int-to-Float Widening
php
<?php

declare(strict_types=1);

/**
 * @param int $id
 * @param float $rate
 * @param bool $active
 */
function configureMetric($id, $rate, $active): void
{
    echo "✓ Success: ID={$id}, Rate={$rate}, Active=" . ($active ? 'true' : 'false') . "\n";
}

// 1. Valid Call: Whole int 10 is allowed for float (PHP standard widening)
configureMetric(101, 10, true);

// 2. Coercion Attempt: Passing numeric string '101' into int
try {
    configureMetric('101', 10.5, true);
} catch (\TypeError $e) {
    echo "✗ Rejected string -> int coercion:\n   " . $e->getMessage() . "\n";
}

// 3. Coercion Attempt: Passing integer 1 into bool
try {
    configureMetric(101, 10.5, 1);
} catch (\TypeError $e) {
    echo "✗ Rejected int -> bool coercion:\n   " . $e->getMessage() . "\n";
}

// 4. Coercion Attempt: Passing float 10.5 into int (no lossy truncation)
try {
    configureMetric(10.5, 10.5, true);
} catch (\TypeError $e) {
    echo "✗ Rejected float -> int coercion:\n   " . $e->getMessage() . "\n";
}

Exact Scalar Literals & Float Epsilon Tolerance ​

TypePHP supports enforcing exact literal scalar values in DocBlock types ('active', 42, 3.14, true, false).

Furthermore, because floating-point calculations like 0.1 + 0.2 compute to 0.30000000000000004 under IEEE 754 representation, TypePHP compares float literals using an Epsilon Tolerance (1e-9) to prevent false-positive crashes on microscopic rounding errors:

Exact Literals & Float Epsilon Tolerance
php
<?php

declare(strict_types=1);

/**
 * @param 'active'|'pending' $status
 * @param 200|201 $statusCode
 * @param true $requireTls
 * @param 0.3 $threshold
 */
function establishConnection(string $status, int $statusCode, bool $requireTls, float $threshold): void
{
    echo "✓ Connected: Status='{$status}', Code={$statusCode}, Threshold={$threshold}\n";
}

// 1. Valid Call (0.1 + 0.2 satisfies 0.3 via 1e-9 epsilon tolerance)
establishConnection('active', 200, true, 0.1 + 0.2);

// 2. Invalid String Literal
try {
    establishConnection('archived', 200, true, 0.3);
} catch (\TypeError $e) {
    echo "✗ Caught invalid string literal:\n   " . $e->getMessage() . "\n";
}

// 3. Invalid Integer Literal
try {
    establishConnection('active', 500, true, 0.3);
} catch (\TypeError $e) {
    echo "✗ Caught invalid integer literal:\n   " . $e->getMessage() . "\n";
}

// 4. Invalid Boolean Literal (false passed where true is required)
try {
    establishConnection('active', 200, false, 0.3);
} catch (\TypeError $e) {
    echo "✗ Caught invalid boolean literal:\n   " . $e->getMessage() . "\n";
}

// 5. Invalid Float Literal (Genuine deviation beyond epsilon)
try {
    establishConnection('active', 200, true, 0.35);
} catch (\TypeError $e) {
    echo "✗ Caught deviating float literal:\n   " . $e->getMessage() . "\n";
}

Integer Refinements & Bounded Ranges ​

TypePHP provides strict enforcement for all integer refinement subtypes and range expressions:

Keyword / SyntaxMathematical RuleValid ExamplesInvalid Examples
positive-intn>01, 42, 1000, -5
negative-intn<0-1, -420, 5
non-positive-intn≤00, -1, -101, 5
non-negative-intn≥00, 1, 100-1, -5
non-zero-intn≠01, -1, 1000
unsigned-intn≥00, 10, 50-10
int<min, max>min≤n≤maxint<1, 100> (e.g. 50)0, 101
int<0, max>n≥00, 500-1
Integer Refinements & Bounded Ranges
php
<?php

declare(strict_types=1);

class IntegerChecker
{
    /**
     * @param positive-int $pos
     * @param negative-int $neg
     * @param non-positive-int $nonPos
     * @param non-negative-int $nonNeg
     * @param non-zero-int $nonZero
     * @param unsigned-int $unsigned
     * @param int<1, 100> $range
     */
    public function verifyAll(
        int $pos,
        int $neg,
        int $nonPos,
        int $nonNeg,
        int $nonZero,
        int $unsigned,
        int $range
    ): void {
        echo "✓ All 7 integer refinements satisfied!\n";
    }
}

$checker = new IntegerChecker();

// 1. Happy Path: All integer bounds satisfied
$checker->verifyAll(
    pos: 42,
    neg: -10,
    nonPos: 0,
    nonNeg: 0,
    nonZero: 5,
    unsigned: 100,
    range: 50
);

// 2. Test positive-int boundary (0 is not positive)
try {
    $checker->verifyAll(0, -10, 0, 0, 5, 100, 50);
} catch (\TypeError $e) {
    echo "✗ positive-int check:\n   " . $e->getMessage() . "\n";
}

// 3. Test negative-int boundary (0 is not negative)
try {
    $checker->verifyAll(42, 0, 0, 0, 5, 100, 50);
} catch (\TypeError $e) {
    echo "✗ negative-int check:\n   " . $e->getMessage() . "\n";
}

// 4. Test non-positive-int boundary (5 is positive)
try {
    $checker->verifyAll(42, -10, 5, 0, 5, 100, 50);
} catch (\TypeError $e) {
    echo "✗ non-positive-int check:\n   " . $e->getMessage() . "\n";
}

// 5. Test non-negative-int boundary (-1 is negative)
try {
    $checker->verifyAll(42, -10, 0, -1, 5, 100, 50);
} catch (\TypeError $e) {
    echo "✗ non-negative-int check:\n   " . $e->getMessage() . "\n";
}

// 6. Test non-zero-int boundary (0 given)
try {
    $checker->verifyAll(42, -10, 0, 0, 0, 100, 50);
} catch (\TypeError $e) {
    echo "✗ non-zero-int check:\n   " . $e->getMessage() . "\n";
}

// 7. Test unsigned-int boundary (-50 is signed negative)
try {
    $checker->verifyAll(42, -10, 0, 0, 5, -50, 50);
} catch (\TypeError $e) {
    echo "✗ unsigned-int check:\n   " . $e->getMessage() . "\n";
}

// 8. Test int<1, 100> range boundaries (150 > 100)
try {
    $checker->verifyAll(42, -10, 0, 0, 5, 100, 150);
} catch (\TypeError $e) {
    echo "✗ int<1, 100> range check:\n   " . $e->getMessage() . "\n";
}

Integer Bitmasks (int-mask<...> & int-mask-of<...>) ​

TypePHP validates bitwise flag combinations created by bitwise OR (|) operations on integers:

  • int-mask<1, 2, 4>: Value must be a valid bitwise combination of the explicitly specified integers (or 0).
  • int-mask-of<self::FLAG_*>: Value must be a valid bitwise combination of class constants or enum cases matching the wildcard prefix.
Integer Bitmasks (int-mask & int-mask-of)
php
<?php

declare(strict_types=1);

class FilePermission
{
    public const PERM_READ = 1;    // 0001
    public const PERM_WRITE = 2;   // 0010
    public const PERM_EXEC = 4;    // 0100

    /**
     * @param int-mask<1, 2, 4> $numericMask
     * @param int-mask-of<self::PERM_*> $constantMask
     */
    public static function apply(int $numericMask, int $constantMask): void
    {
        echo "✓ Permissions Applied: Numeric={$numericMask}, Constant={$constantMask}\n";
    }
}

// 1. Valid Combinations:
// 3 = READ | WRITE (1 | 2)
// 7 = READ | WRITE | EXEC (1 | 2 | 4)
// 0 is always allowed as an empty mask
FilePermission::apply(3, FilePermission::PERM_READ | FilePermission::PERM_EXEC);
FilePermission::apply(0, 0);

// 2. Invalid numeric bitmask: 8 (01000) is outside allowed flags (1 | 2 | 4 = 7)
try {
    FilePermission::apply(8, 3);
} catch (\TypeError $e) {
    echo "✗ Invalid numeric bitmask check:\n   " . $e->getMessage() . "\n";
}

// 3. Invalid constant bitmask: 16 is outside self::PERM_* combinations
try {
    FilePermission::apply(3, 16);
} catch (\TypeError $e) {
    echo "✗ Invalid constant bitmask check:\n   " . $e->getMessage() . "\n";
}

String Refinements ​

Validate string lengths, casing, formatting, and boolean truthiness:

Refinement KeywordConstraint RuleValid ExamplesInvalid Examples
non-empty-stringString length > 0'hello', '1''' (empty string)
numeric-stringis_numeric($val) === true'123', '45.67', '-10''abc', ''
lowercase-stringstrtolower($val) === $val'hello', 'user_100''Hello', 'ADMIN'
non-empty-lowercase-stringNon-empty & lowercase'hello', 'abc''', 'Hello'
uppercase-stringstrtoupper($val) === $val'USD', 'HELLO_100''Hello', 'admin'
non-empty-uppercase-stringNon-empty & uppercase'EUR', 'ABC''', 'Eur'
array-keyis_int($val) || is_string($val)100, 'user_100'true, [], null
literal-stringNative string scalar'active', 'user'Numbers, Objects
truthy-stringString evaluates to true'hello', '1''', '0'
String Refinements Validation
php
<?php

declare(strict_types=1);

class StringValidator
{
    /**
     * @param non-empty-string $nonEmpty
     * @param numeric-string $numeric
     * @param lowercase-string $lower
     * @param non-empty-lowercase-string $nonEmptyLower
     * @param uppercase-string $upper
     * @param non-empty-uppercase-string $nonEmptyUpper
     * @param array-key $key
     * @param truthy-string $truthy
     */
    public function verifyStrings(
        string $nonEmpty,
        string $numeric,
        string $lower,
        string $nonEmptyLower,
        string $upper,
        string $nonEmptyUpper,
        int|string $key,
        string $truthy
    ): void {
        echo "✓ All string refinements satisfied!\n";
    }
}

$validator = new StringValidator();

// 1. Happy Path: All string contracts satisfied
$validator->verifyStrings(
    nonEmpty: 'Title',
    numeric: '149.99',
    lower: 'slug_title',
    nonEmptyLower: 'active',
    upper: 'USD',
    nonEmptyUpper: 'EUR',
    key: 'user_101',
    truthy: 'yes'
);

// 2. Test non-empty-string (empty string given)
try {
    $validator->verifyStrings('', '100', 'a', 'a', 'A', 'A', 'k', '1');
} catch (\TypeError $e) {
    echo "✗ non-empty-string check:\n   " . $e->getMessage() . "\n";
}

// 3. Test numeric-string ('abc' is non-numeric)
try {
    $validator->verifyStrings('Title', 'abc', 'a', 'a', 'A', 'A', 'k', '1');
} catch (\TypeError $e) {
    echo "✗ numeric-string check:\n   " . $e->getMessage() . "\n";
}

// 4. Test non-empty-lowercase-string ('User' has uppercase)
try {
    $validator->verifyStrings('Title', '100', 'a', 'User', 'A', 'A', 'k', '1');
} catch (\TypeError $e) {
    echo "✗ non-empty-lowercase-string check:\n   " . $e->getMessage() . "\n";
}

// 5. Test non-empty-uppercase-string ('usd' has lowercase)
try {
    $validator->verifyStrings('Title', '100', 'a', 'a', 'A', 'usd', 'k', '1');
} catch (\TypeError $e) {
    echo "✗ non-empty-uppercase-string check:\n   " . $e->getMessage() . "\n";
}

// 6. Test truthy-string ('0' evaluates to false in PHP boolean context)
try {
    $validator->verifyStrings('Title', '100', 'a', 'a', 'A', 'A', 'k', '0');
} catch (\TypeError $e) {
    echo "✗ truthy-string check:\n   " . $e->getMessage() . "\n";
}

Class-String Subtypes & Generics (class-string<T>) ​

Enforce that string parameters contain valid class, interface, trait, or enum references, with optional upper bounds (class-string<T>):

KeywordValidation RuleValid ExamplesInvalid Examples
class-stringSyntactically valid class that exists in runtimeUser::class, stdClass::class'NonExistentClass'
class-string<T>Valid class-string that extends or implements TDog::class for class-string<Animal>Car::class for class-string<Animal>
interface-stringinterface_exists($val) === trueDateTimeInterface::classstdClass::class
trait-stringtrait_exists($val) === trueLoggerTrait::classstdClass::class
enum-stringenum_exists($val) === trueStatusEnum::classstdClass::class
Class-String Subtypes & Generics
php
<?php

declare(strict_types=1);

interface RepositoryInterface {}
class UserRepository implements RepositoryInterface {}
class OrderRepository implements RepositoryInterface {}
class UnrelatedHelper {}

trait IdentifiableTrait {}
enum UserRole { case ADMIN; }

class ClassStringTester
{
    /**
     * @param class-string $anyClass
     * @param interface-string $iface
     * @param trait-string $trait
     * @param enum-string $enum
     * @param class-string<RepositoryInterface> $repo
     */
    public function verifyClassStrings(
        string $anyClass,
        string $iface,
        string $trait,
        string $enum,
        string $repo
    ): void {
        echo "✓ All class-string specifications verified!\n";
    }
}

$tester = new ClassStringTester();

// 1. Happy Path: All class-string types satisfied
$tester->verifyClassStrings(
    anyClass: stdClass::class,
    iface: RepositoryInterface::class,
    trait: IdentifiableTrait::class,
    enum: UserRole::class,
    repo: UserRepository::class
);

// 2. Test class-string with non-existent class
try {
    $tester->verifyClassStrings('App\\NonExistentClass', RepositoryInterface::class, IdentifiableTrait::class, UserRole::class, UserRepository::class);
} catch (\TypeError $e) {
    echo "✗ class-string existence check:\n   " . $e->getMessage() . "\n";
}

// 3. Test interface-string with class name
try {
    $tester->verifyClassStrings(stdClass::class, stdClass::class, IdentifiableTrait::class, UserRole::class, UserRepository::class);
} catch (\TypeError $e) {
    echo "✗ interface-string check:\n   " . $e->getMessage() . "\n";
}

// 4. Test trait-string with interface name
try {
    $tester->verifyClassStrings(stdClass::class, RepositoryInterface::class, RepositoryInterface::class, UserRole::class, UserRepository::class);
} catch (\TypeError $e) {
    echo "✗ trait-string check:\n   " . $e->getMessage() . "\n";
}

// 5. Test enum-string with class name
try {
    $tester->verifyClassStrings(stdClass::class, RepositoryInterface::class, IdentifiableTrait::class, stdClass::class, UserRepository::class);
} catch (\TypeError $e) {
    echo "✗ enum-string check:\n   " . $e->getMessage() . "\n";
}

// 6. Test class-string<T> bound (UnrelatedHelper does not implement RepositoryInterface)
try {
    $tester->verifyClassStrings(stdClass::class, RepositoryInterface::class, IdentifiableTrait::class, UserRole::class, UnrelatedHelper::class);
} catch (\TypeError $e) {
    echo "✗ class-string<T> upper bound check:\n   " . $e->getMessage() . "\n";
}

Float Refinements ​

TypePHP enforces signs and bounds on floating-point parameters (and note that whole integers widen losslessly to float):

Refinement KeywordConstraint RuleValid ExamplesInvalid Examples
positive-floatFloat > 0.012.34, 0.01, 50.0, -5.5
negative-floatFloat < 0.0-12.34, -0.01, -50.0, 5.5
non-positive-floatFloat <= 0.00.0, -1.51.5, 10
non-negative-floatFloat >= 0.00.0, 1.5, 10-1.5, -5
non-zero-floatFloat != 0.01.5, -1.5, 50.0, 0
Float Refinements & Boundaries
php
<?php

declare(strict_types=1);

class FloatChecker
{
    /**
     * @param positive-float $pos
     * @param negative-float $neg
     * @param non-positive-float $nonPos
     * @param non-negative-float $nonNeg
     * @param non-zero-float $nonZero
     */
    public function verifyFloats(
        float $pos,
        float $neg,
        float $nonPos,
        float $nonNeg,
        float $nonZero
    ): void {
        echo "✓ All 5 float refinements satisfied!\n";
    }
}

$checker = new FloatChecker();

// 1. Happy Path: All float refinements satisfied (including whole int widening)
$checker->verifyFloats(
    pos: 0.0825,
    neg: -15.50,
    nonPos: 0.0,
    nonNeg: 10, // whole integer widens cleanly
    nonZero: -1.2
);

// 2. Test positive-float (0.0 is not strictly positive)
try {
    $checker->verifyFloats(0.0, -15.5, 0.0, 1.0, 1.0);
} catch (\TypeError $e) {
    echo "✗ positive-float check:\n   " . $e->getMessage() . "\n";
}

// 3. Test negative-float (0.0 is not strictly negative)
try {
    $checker->verifyFloats(1.0, 0.0, 0.0, 1.0, 1.0);
} catch (\TypeError $e) {
    echo "✗ negative-float check:\n   " . $e->getMessage() . "\n";
}

// 4. Test non-positive-float (1.5 is positive)
try {
    $checker->verifyFloats(1.0, -1.0, 1.5, 1.0, 1.0);
} catch (\TypeError $e) {
    echo "✗ non-positive-float check:\n   " . $e->getMessage() . "\n";
}

// 5. Test non-negative-float (-1.5 is negative)
try {
    $checker->verifyFloats(1.0, -1.0, 0.0, -1.5, 1.0);
} catch (\TypeError $e) {
    echo "✗ non-negative-float check:\n   " . $e->getMessage() . "\n";
}

// 6. Test non-zero-float (0.0 is zero)
try {
    $checker->verifyFloats(1.0, -1.0, 0.0, 1.0, 0.0);
} catch (\TypeError $e) {
    echo "✗ non-zero-float check:\n   " . $e->getMessage() . "\n";
}

Truthiness, Numeric Types, Resources & Control Returns ​

Validate boolean truthiness, generic numeric types, open/closed stream resources, and exit control types (void and never):

KeywordValidation RuleValid ExamplesInvalid Examples
truthyEvaluates to true in boolean context'hello', 1, [1], true0, '', null, false
falsy, falseyEvaluates to false in boolean context0, '', null, false, []'hello', 1, true
numeric, numberInteger, Float, or Numeric String10, 12.34, '99.9''abc', null
open-resource, resourceis_resource($val) === truefopen('php://memory', 'r')Closed stream, String
closed-resourceHandle closed via fclose($fp)Closed stream handleOpen stream, String
neverFunction never returns normallyThrows exception or exit()Returns any value
Truthiness, Numeric, Resources & never
php
<?php

declare(strict_types=1);

class SpecialTypeTester
{
    /**
     * @param truthy $truthy
     * @param falsy $falsy
     * @param numeric $num
     * @param open-resource $openRes
     * @param closed-resource $closedRes
     */
    public function verifySpecials(mixed $truthy, mixed $falsy, mixed $num, $openRes, $closedRes): void
    {
        echo "✓ Special types verified!\n";
    }

    /**
     * @return never
     */
    public function haltService(): string
    {
        return 'unexpected_return'; // Violates never!
    }
}

$tester = new SpecialTypeTester();

$openFp = fopen('php://memory', 'r+');
$closedFp = fopen('php://memory', 'r+');
fclose($closedFp);

// 1. Happy Path: All special types satisfied
$tester->verifySpecials(
    truthy: 'enabled',
    falsy: '',
    num: '199.95',
    openRes: $openFp,
    closedRes: $closedFp
);
fclose($openFp);

// 2. Test truthy (empty string is falsy)
try {
    $dummy = fopen('php://memory', 'r+');
    $closed = fopen('php://memory', 'r+');
    fclose($closed);
    $tester->verifySpecials('', '', '10', $dummy, $closed);
} catch (\TypeError $e) {
    echo "✗ truthy check:\n   " . $e->getMessage() . "\n";
} finally {
    if (isset($dummy) && is_resource($dummy)) fclose($dummy);
}

// 3. Test numeric ('abc' is not numeric)
try {
    $dummy = fopen('php://memory', 'r+');
    $closed = fopen('php://memory', 'r+');
    fclose($closed);
    $tester->verifySpecials('ok', '', 'abc', $dummy, $closed);
} catch (\TypeError $e) {
    echo "✗ numeric check:\n   " . $e->getMessage() . "\n";
} finally {
    if (isset($dummy) && is_resource($dummy)) fclose($dummy);
}

// 4. Test open-resource (passing closed handle)
try {
    $closed = fopen('php://memory', 'r+');
    fclose($closed);
    $tester->verifySpecials('ok', '', '10', $closed, $closed);
} catch (\TypeError $e) {
    echo "✗ open-resource check:\n   " . $e->getMessage() . "\n";
}

// 5. Test @return never contract failure (method returned instead of throwing)
try {
    $tester->haltService();
} catch (\TypeError $e) {
    echo "✗ never return contract check:\n   " . $e->getMessage() . "\n";
}