The tyhp/core Package

tyhp/core is the runtime that every compiled Tyhp program loads. It contains three kinds of things:

  • APIs you call from your own code. The scalar method catalog ($name->length(), $orders->mapped(...), $total->round(2)), runtime type objects (\Tyhp\Type), typed JSON decoding (\Tyhp\Json), the generics bridge for hand-written PHP (\Tyhp\Generic), contracts such as \Tyhp\Contracts\IsDisposable, attributes such as #[\Tyhp\Php], and the exceptions the runtime throws.
  • Plumbing that the compiler's emitted PHP calls. Property-hook polyfills, per-instance generic bookkeeping, and the traits that host them. You see these names in generated PHP and stack traces, but you do not call them yourself.
  • A Composer plugin. It keeps your root require-dev in sync with the type-definition packages that your dependencies declare in extra.tyhp.require.

This page is the API reference for the package. The language features that sit on top of it have their own pages: Scalar Pseudo-Objects, Extensions, Generics, New and Changed Types, Type Narrowing and Guards, Compile-Time Constructs, Disposables, Operator Overloads, and Composer Runtime Packages. For the other runtime packages, see the package overview.

Installing

tyhp init already adds tyhp/core to a new project. In an existing project, add it to require (not require-dev), because the emitted PHP loads it at runtime, and allow its Composer plugin:

{
    "require": {
        "php": ">=8.2",
        "tyhp/core": "@dev"
    },
    "require-dev": {
        "tyhpdef/php": "@dev",
        "tyhp/compiler": "@dev"
    },
    "config": {
        "allow-plugins": {
            "tyhp/core": true
        }
    }
}

Published versions follow the 80N.X.Y scheme, where 80N is the PHP version that artifact was compiled for (804 is PHP 8.4) and X.Y is the version of tyhp/core itself. An application pins the major that matches its output.phpVersion; a library ORs the majors it supports. See Composer Runtime Packages for the full version map and pinning rules.

The package requires PHP 8.2 or later and Composer's plugin API 2.3 or later. The ext-mbstring extension is optional but recommended: with it, string methods count and slice characters; without it, they fall back to byte-oriented PHP functions (see Unicode and ext-mbstring).

Tip

The first composer require tyhp/core often does not install the ambient type definitions (tyhpdef/php and friends) in that same Composer run. Run composer update or tyhp composer sync afterwards. See CLI: Composer.

Package contents at a glance

Area Names Who uses it
Scalar method catalog StringExtensions, StringExtensionsVersioned, ArrayExtensions, ArrayExtensionsVersioned, IntExtensions, FloatExtensions, BoolExtensions, ClosureExtensions You, as $value->method()
String helper \Tyhp\StringHelper Compiled code; also callable from PHP
Runtime types \Tyhp\Type, \Tyhp\NamedType You and compiled code (typeof, is, default)
Typed JSON \Tyhp\Json You
Generics bridge \Tyhp\Generic Hand-written PHP and compiled code that calls another library's generics
Object with helper \Tyhp\ObjectHelper Compiled with expressions; also callable directly
Contracts \Tyhp\Contracts\IsDisposable, Convertible, IntConvertible, FloatConvertible, BoolConvertible, StringConvertible, ArrayAccessShape<TStruct> You implement them
Attributes #[\Tyhp\Php], #[\Tyhp\PhpType], #[\Tyhp\NativeTypeTest], #[\Tyhp\EraseGeneric], #[\Tyhp\AllowUnset], #[\Tyhp\GenericRuntime], #[\Tyhp\NoEmit], #[\Tyhp\Optimize\Pure], #[\Tyhp\Optimize\Inline] You write most of them; the compiler writes GenericRuntime
Exceptions \Tyhp\Exceptions\IncompatibleTypeException, InvalidTypeException, AggregateException, PropertyNotFoundException, InvalidParametersForOperatorOverloadException Thrown by the runtime and by compiled code
Compiled-code plumbing PropertyAccessor, PropertyAccessorObject, PropertyBackingCell, GenericObject, \Tyhp\Concerns\* traits Compiled code only
Composer plugin Tyhp\Composer\Plugin Composer

Scalar extension methods

tyhp/core attaches methods to string, array, int, float, bool, and \Closure. Its type definition issues global use extension for every catalog, so the methods are available in every file of every project that depends on tyhp/core. You do not write use extension for them. A file-level use extension \Tyhp\StringExtensions; with no adaptations is redundant and warns (TYHP4169); use a local use extension only to hide or rename members in that file. The language rules (how the receiver works, chaining, literals, adaptations) are on Scalar Pseudo-Objects and Extensions.

<?tyhp

function slugify(string $title): string {
    string $lower = $title->trim()->toLower();
    ?string $slug = $lower->regexReplace('/[^a-z0-9]+/', '-');
    return $slug === null ? '' : $slug->trim('-');
}

function percent(int $part, int $whole): string {
    float $ratio = (float) $part / (float) $whole * 100.0;
    return $ratio->round(1)->format(1) . '%';
}

// slugify('  Hello, World! 2026 ') === 'hello-world-2026'
// percent(1, 3) === '33.3%'

How to read the signatures

Each entry below shows the receiver as $s (string), $a (array), $i (int), $f (float), $b (bool), or $fn (\Closure). The receiver is not a parameter: $s->contains('x') passes one argument. Each entry also says what the call emits. Most methods are spliced: the call is replaced by the PHP expression shown, so there is no wrapper object and no extra function call. A few methods need a real PHP body; those emit a static call on the catalog class (for example \Tyhp\ArrayExtensions::sorted($a, $flags)), and hand-written PHP can call them the same way.

Array methods are generic over the receiver's key and value types. In the array section, TKey and TValue are the key and value types of the array you call the method on, so array<string, int> gives TKey = string and TValue = int.

Unicode and ext-mbstring

Methods that count, slice, search, or change the case of characters go through \Tyhp\StringHelper. When ext-mbstring is loaded, those calls use the \mb_* functions and work in characters; otherwise they use the byte functions (\strlen, \substr, \strtoupper, …). Offsets returned by indexOf() and friends are therefore character offsets with mbstring and byte offsets without it. Methods with a byte prefix (byteLength(), byteReverse()) always work in bytes.

trim(), trimLeft(), and trimRight() use PHP's byte-based \trim family in both cases.

PHP-version-dependent methods

A few members differ by output.phpVersion. The signature and the result are the same on every target; only the implementation changes:

Method PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
$s->pad(), padLeft(), padRight() \str_pad (bytes) \mb_str_pad when mbstring is loaded same as 8.3 same as 8.3
$s->ucFirst(), lcFirst() first character via StringHelper same as 8.2 \mb_ucfirst / \mb_lcfirst when mbstring is loaded same as 8.4
$a->find(), findKey(), any(), all() polyfill loop polyfill loop \array_find / \array_find_key / \array_any / \array_all same as 8.4
$a->first(), last() polyfill via \array_key_first / \array_key_last same as 8.2 same as 8.2 \array_first / \array_last

The polyfilled array methods emit a static call on \Tyhp\ArrayExtensionsVersioned; the native ones splice the PHP function. On PHP 8.2 the pad family pads by bytes, so multibyte padding strings can produce a different visible width than on 8.3+.

Chaining and receivers

Calls chain: $title->trim()->toLower(). Integer and float literals need parentheses: (255)->toHex().

A few string methods splice the receiver into a later argument position of the PHP function: split() (\explode($delimiter, $s)), replace(), replaceIgnoreCase(), regexReplace(), regexSplit(), matches(), and hash(). A call can be the receiver of one of those ($s->toLower()->split(' ')) as long as the other arguments are literals, constants, or variables. When the receiver and an earlier argument are both calls ($s->trim()->split(sep())), the compiler cannot keep PHP's evaluation order and reports TYHP4181. Assign one of the values to a variable first, as slugify() does above.

Methods that modify the array in place (sort(), push(), pop(), …) take the receiver by reference, so the receiver must be a variable, property, or array element, not a call result.

String methods

Length and emptiness

Searching

Extracting

Case

Trimming and padding

Replacing, splitting, and joining

Regular expressions

All patterns are PCRE patterns with delimiters, exactly as for PHP's preg_* functions.

<?tyhp

function parseVersion(string $tag): ?string {
    $m = $tag->match('/^v(\d+)\.(\d+)\.(\d+)$/');
    if ($m === null) {
        return null;
    }
    mixed $major = $m[1];
    mixed $minor = $m[2];
    if (!\is_string($major) || !\is_string($minor)) {
        return null;
    }
    return $major . '.' . $minor;
}

// parseVersion('v8.4.12') === '8.4'

Encoding and escaping

Hashing

JSON

Conversion

Array methods

Methods ending in -ed (sorted, filtered, mapped, reversed, …) return a new array and leave the receiver alone. The bare verbs (sort, push, shift, …) modify the receiver in place and return what the PHP function returns.

<?tyhp

type Order = struct {
    int $id = 0;
    string $customer = '';
    float $total = 0.0;
    string $status = 'open';
};

function topCustomers(array<int, Order> $orders): array<int, string> {
    array<int, Order> $open = $orders->filtered(fn(Order $o): bool => $o->status === 'open');
    array<int, Order> $byTotal = $open->sortedBy(fn(Order $a, Order $b): int => $b->total <=> $a->total);
    array<int, string> $names = $byTotal->mapped(fn(Order $o): string => $o->customer);
    return $names->unique()->values()->sliced(0, 3);
}

function revenue(array<int, Order> $orders): float {
    return $orders->reduce(fn(float $sum, Order $o): float => $sum + $o->total, 0.0);
}

function firstLarge(array<int, Order> $orders): ?Order {
    return $orders->find(fn(Order $o, int $i): bool => $o->total > 1000.0);
}

Size and membership

Keys and values

Searching with a predicate

These take a callback that receives the value and then the key.

Transforming

Slicing and combining

Sorting into a new array

Modifying in place

These require a variable (or property, or element) as the receiver.

<?tyhp

function queue(): array<int, string> {
    array<int, string> $jobs = ['email', 'resize'];
    int $n = $jobs->push('cleanup', 'report');   // 4
    ?string $next = $jobs->shift();              // 'email'
    $jobs->sort();                               // ['cleanup', 'report', 'resize']
    return $n > 0 && $next !== null ? $jobs : [];
}

Numeric arrays

Available when the value type is int or float.

Untyped helpers

These are declared on plain array because their result shape depends on the arguments. Annotate the variable you assign the result to.

Int methods

Float methods

Bool methods

Closure methods

These work on any \Closure, including first-class callables such as slowSquare(...). They call static methods on \Tyhp\ClosureExtensions.

<?tyhp

function pipeline(): string {
    \Closure<callable(string): string> $trim = fn(string $s): string => \trim($s);
    \Closure<callable(string): int> $len = fn(string $s): int => \strlen($s);

    $trimThenLen = $trim->then($len);
    int $n = $trimThenLen('  hello  ');            // 5

    \Closure<callable(int): string> $label = fn(int $n): string => "len={$n}";
    $labelOfLen = $label->compose($len);
    return $labelOfLen('abc') . ' / ' . (string) $n;  // 'len=3 / 5'
}

function slowSquare(int $n): int {
    return $n * $n;
}

function cached(): int {
    \Closure<callable(int): int> $square = slowSquare(...);
    $fast = $square->memoize();
    return $fast(12) + $fast(12);                   // second call is served from the cache
}

Runtime types: \Tyhp\Type

A \Tyhp\Type is a runtime description of a Tyhp type. The compiler creates them for you: typeof(T) produces one, $x is ?User and other non-native guards compile to \Tyhp\Type::is($x, …), default(T) compiles to ->defaultValue() once T is known, and tracked generic classes carry the types they were constructed with. You use the class directly when you validate untrusted data, write generic infrastructure, or pass type arguments from hand-written PHP to \Tyhp\Generic::bind. See Compile-Time Constructs for typeof and default.

Type implements \Stringable. Its constructor is protected; create instances with the static factories below. Scalar factories and fromClassName() return a shared instance per name, so Type::int() === Type::int().

<?tyhp

use Tyhp\Type;
use Tyhp\NamedType;

function classify(mixed $value): string {
    Type $idList = Type::generic('array', new NamedType('TKey', Type::int()), new NamedType('TValue', Type::int()));
    Type $maybeName = Type::nullable(Type::string());

    if (Type::is($value, $idList)) {
        return 'ids';
    }
    if (Type::is($value, $maybeName)) {
        return 'name';
    }
    Type::check($value, Type::union(Type::int(), Type::string()));  // throws for anything else
    return (string) Type::of($value);
}

// classify([1, 2]) === 'ids'
// classify(3.5) throws IncompatibleTypeException: "Incompatible type: expected int|string, got float."

Scalar and pseudo-type factories

Composite factories

Checking values

Instance methods

\Tyhp\NamedType

final class NamedType extends \Tyhp\Type pairs a generic parameter name with the type bound to it. It is the argument type of Type::generic(). Matching and display delegate to the underlying type, and its nullability follows the underlying type.

Typed JSON: \Tyhp\Json

\Tyhp\Json decodes JSON straight into a Tyhp struct and verifies the shape, the way JsonSerializer.Deserialize<T> does in C#. JSON objects always decode to associative arrays, which is how structs are represented in PHP. The string methods jsonDecodeAs<T>() and jsonTryDecodeAs<T>() are shorthands for these two methods. \json_decode itself stays an ordinary PHP builtin; see New Functions.

<?tyhp

namespace App;

use Tyhp\Json;
use Tyhp\Exceptions\IncompatibleTypeException;

type User = struct {
    int $id;
    string $name;
    ?string $email;
};

function loadUser(string $json): User {
    try {
        return Json::decode<User>($json);
    } catch (\JsonException $e) {
        throw new \RuntimeException('Malformed user JSON: ' . $e->getMessage(), 0, $e);
    } catch (IncompatibleTypeException $e) {
        throw new \RuntimeException('Unexpected user shape: ' . $e->getMessage(), 0, $e);
    }
}

function tryUser(string $json): ?User {
    User $user = new User() with [id => 0, name => ''];
    if (!Json::tryDecode<User>($json, $user)) {
        return null;
    }
    return $user;
}

// loadUser('{"id": 7, "name": "Ada"}') returns ['id' => 7, 'name' => 'Ada']
// loadUser('{"id": 7, "name": "Ada", "email": null}') returns ['id' => 7, 'name' => 'Ada', 'email' => null]
// loadUser('{"id": "7", "name": "Ada"}') throws RuntimeException:
//     "Unexpected user shape: Incompatible type: expected struct App\User, got array."
// loadUser('{"name": "Ada"}') throws the same exception: `id` is required.
// loadUser('{"id": 7') throws RuntimeException: "Malformed user JSON: Syntax error"
// tryUser('{"id": "x"}') returns null

A struct field is required when it is neither nullable nor given a default, so email may be absent or null while id and name must be present. Keys the struct does not declare are kept in the decoded array.

From hand-written PHP, bind T with \Tyhp\Generic::bind, passing a Type::struct() descriptor whose third argument lists the required keys:

<?php

declare(strict_types=1);

use Tyhp\Generic;
use Tyhp\Json;
use Tyhp\Type;

$userType = Type::struct('App\\User', [
    'id' => Type::int(),
    'name' => Type::string(),
    'email' => Type::nullable(Type::string()),
], ['id', 'name']);

$decodeUser = Generic::bind(Json::decode(...), $userType);

try {
    $user = $decodeUser('{"id": 7, "name": "Ada"}');
} catch (\Throwable $e) {
    \error_log('Could not decode user: ' . $e->getMessage());
}

Generics bridge: \Tyhp\Generic

Tyhp generics are erased from PHP signatures. When a generic class or callable needs its type arguments at runtime, the compiler emits a factory or binder and stamps the declaration with #[\Tyhp\GenericRuntime]. Inside one compilation, Tyhp calls those helpers directly. \Tyhp\Generic::bind is the stable entry point for everyone else: hand-written PHP, and Tyhp code that uses a generic from another compiled library (the compiler emits bind for those call sites). See Generics.

final class Generic cannot be constructed directly.

<?php

declare(strict_types=1);

use Tyhp\Generic;
use Tyhp\Type;

// A generic class from a compiled Tyhp library: class Box<T> { public function __construct(T $value) {} }
$intBox = Generic::bind(\Vendor\Box::class, Type::int())->new(42);

// A generic method: public function identity<T>(T $value): T
$identity = Generic::bind($helper->identity(...), Type::string());
$value = $identity('hello');

Object with helper: \Tyhp\ObjectHelper

A with expression used as a value compiles to \Tyhp\ObjectHelper::with(...) for new Widget() with [color => 'blue'] on a class without readonly overrides (on every PHP version), and for clone $cfg with [...] when the target is PHP 8.2–8.4. On PHP 8.5, clone $cfg with [...] uses PHP's native clone($cfg, [...]). See The with Keyword.

String helper: \Tyhp\StringHelper

final class StringHelper holds the multibyte-or-byte switch behind the string methods. Each method uses the \mb_* function when ext-mbstring is loaded and the byte function otherwise. Compiled code calls it; you can call it from PHP for the same behaviour.

Contracts

\Tyhp\Contracts\IsDisposable

<?tyhp

interface IsDisposable {
    public function dispose(): void;
}

Implement it on classes that own something that must be released: file handles, locks, connections, temporary files. using (...) blocks and := declarations call dispose() for you; see Disposables. Make dispose() safe to call more than once. The asynchronous counterpart, AsyncIsDisposable, and the DisposableScope that := uses live in tyhp/async; see the tyhp/async package.

<?tyhp

use Tyhp\Contracts\IsDisposable;

final class TempFile implements IsDisposable {
    private bool $closed = false;

    public function __construct(public readonly string $path) {}

    public function dispose(): void {
        if ($this->closed) {
            return;
        }
        $this->closed = true;
        if (\is_file($this->path)) {
            \unlink($this->path);
        }
    }
}

function writeReport(): int {
    TempFile $tmp = new TempFile(\sys_get_temp_dir() . '/report.csv');
    try {
        int|false $written = \file_put_contents($tmp->path, "a,b\n");
        return $written === false ? 0 : $written;
    } finally {
        $tmp->dispose();
    }
}

Conversion contracts

Interface Declares Extends
\Tyhp\Contracts\Convertible nothing (marker) —
\Tyhp\Contracts\IntConvertible public function __toInt(): int; Convertible
\Tyhp\Contracts\FloatConvertible public function __toFloat(): float; Convertible
\Tyhp\Contracts\BoolConvertible public function __toBool(): bool; Convertible
\Tyhp\Contracts\StringConvertible __toString() via \Stringable \Stringable, Convertible

These interfaces mark a class that converts itself to a scalar. When a class declares a convert-to operator (operator convert(self $value): int), the compiler emits the matching __toInt() method, adds the matching interface for you, and rewrites explicit casts, arguments, and return values to the call ((int) $celsius, takesInt($celsius), and return $celsius; from an int function all become $celsius->__toInt()). See Operator Overloads. If you implement an interface by hand instead, you get the method and the type, but only operator convert triggers the rewrites: (int) $value stays an ordinary PHP cast, so call the method explicitly. Use Convertible as a parameter type when any conversion is acceptable.

\Tyhp\Contracts\ArrayAccessShape<TStruct extends struct>

Per-key array access over a struct schema: $config['host'] has the host field's type and $config['port'] the port field's type. It extends \ArrayAccess<__IndexKeys<TStruct>, __IndexValueTypes<TStruct>>.

<?tyhp

interface ArrayAccessShape<TStruct extends struct> extends \ArrayAccess<__IndexKeys<TStruct>, __IndexValueTypes<TStruct>> {
    public function offsetExists(#[\Tyhp\PhpType('mixed')] __IndexKeys<TStruct> $offset): bool;

    #[\Tyhp\PhpType('mixed')]
    public function offsetGet<TKey extends __IndexKeys<TStruct>>(#[\Tyhp\PhpType('mixed')] TKey $offset): __IndexValueType<TStruct, TKey>;

    public function offsetSet<TKey extends __IndexKeys<TStruct>>(#[\Tyhp\PhpType('mixed')] TKey $offset, #[\Tyhp\PhpType('mixed')] __IndexValueType<TStruct, TKey> $value): void;

    public function offsetUnset(#[\Tyhp\PhpType('mixed')] __IndexKeys<TStruct> $offset): void;
}

The #[\Tyhp\PhpType('mixed')] attributes keep the emitted PHP signatures compatible with \ArrayAccess; implementors inherit them. Call sites must index with a key literal. The rules for implementing offsetGet and offsetSet (per-key coverage, TYHP4331–TYHP4333) are on New and Changed Types.

Attributes

#[\Tyhp\Php]

<?tyhp

#[\Tyhp\Php(">=8.4")]
function onlyOn84(): string {
    return 'new';
}

#[\Tyhp\PhpType]

#[\Tyhp\NativeTypeTest]

#[\Tyhp\EraseGeneric]

#[\Tyhp\AllowUnset]

<?tyhp

final class Profile {
    #[\Tyhp\AllowUnset]
    public string $cachedName;

    public function __construct(string $name) {
        $this->cachedName = $name;
    }

    public function forget(): void {
        unset($this->cachedName);
    }
}

#[\Tyhp\GenericRuntime]

#[\Tyhp\NoEmit]

#[\Tyhp\Optimize\Pure] and #[\Tyhp\Optimize\Inline]

Exceptions

All exception classes are final and live in \Tyhp\Exceptions.

IncompatibleTypeException

final class IncompatibleTypeException extends \RuntimeException. Thrown when a value does not match a required type at runtime: Type::check(), Json::decode(), generic property writes in tracked classes, and other runtime checks the compiler emits. The message reads Incompatible type for $name: expected X, got Y. (the for $name part only when a name is known).

InvalidTypeException

final class InvalidTypeException extends \LogicException. Signals a programming error in how types are used: a union or intersection with fewer than two distinct members, or a \Tyhp\Generic::bind call on something that is not a compiled Tyhp generic. It has no members beyond \LogicException.

AggregateException

final class AggregateException extends \RuntimeException. Carries several exceptions at once. A using block throws it when more than one resource fails to dispose; see Disposables. Other runtime packages throw it from combinators that collect failures.

<?tyhp

use Tyhp\Contracts\IsDisposable;
use Tyhp\Exceptions\AggregateException;

function closeAll(array<int, IsDisposable> $resources): void {
    array<int, \Throwable> $errors = [];
    foreach ($resources as $r) {
        try {
            $r->dispose();
        } catch (\Throwable $e) {
            $errors[] = $e;
        }
    }
    if ($errors !== []) {
        throw new AggregateException($errors, 'Some resources failed to close');
    }
}

PropertyNotFoundException

final class PropertyNotFoundException extends \RuntimeException. Thrown by ObjectHelper::with() (and therefore by compiled with expressions) when a key does not name a property of the object. Message: Property 'name' does not exist on class 'Class'.

InvalidParametersForOperatorOverloadException

final class InvalidParametersForOperatorOverloadException extends \RuntimeException. Thrown by a compiled operator-overload method when the runtime operand types match none of the declared forms, for example when a value reaches the operator through mixed. Message: Invalid parameters for operator overload Class::method() — received: int, string. See Operator Overloads.

Compiled-code plumbing

The following public types exist so that emitted PHP can implement Tyhp features on every supported PHP version. They are used by compiled code; you don't call them directly, and their members can change with the compiler. You will see them in generated PHP, var_dump output, and stack traces.

Name Purpose
\Tyhp\PropertyAccessor<TValue>, \Tyhp\PropertyAccessorObject, \Tyhp\PropertyBackingCell Polyfill for PHP 8.4 property hooks (get / set, asymmetric visibility, final hooks, parent::$prop::get()) when output.phpVersion is below 8.4.
\Tyhp\GenericObject Per-instance record of the type arguments a tracked generic object was constructed with; backs typeof(T), default(T), and generic property checks inside the class.
\Tyhp\Concerns\HasGenerics Adds the $__tyhpGeneric property that holds a GenericObject.
\Tyhp\Concerns\HasPropertyAccessors, UsesPropertyAccessors Add the $__tyhpPropertyHook property that holds a PropertyAccessorObject.
\Tyhp\Concerns\HandlesGet, HandlesSet, HandlesIsset, HandlesUnset Dispatch __get, __set, __isset, and __unset to registered handlers.
\Tyhp\Concerns\BootsTraits Runs each trait's __bootTrait_* initializer once per instance.
\Tyhp\StringExtensions and the other catalog classes Static methods for catalog members that need a PHP body (see How to read the signatures).

When a class uses property hooks on a PHP target below 8.4, the Handles* traits supply final __get, __set, __isset, and __unset methods, so subclasses cannot override them. If the class defines one of those magic methods itself, its own method is kept and the compiler inserts the hook dispatch at the top of it.

The Composer plugin

tyhp/core is a Composer plugin ("type": "composer-plugin", class Tyhp\Composer\Plugin). Its job is to make the type definitions your dependencies need for type-checking land in your project automatically. A Tyhp library lists those packages in extra.tyhp.require of its composer.json; the plugin collects them across your whole dependency tree and writes them to your root require-dev. The rules for what belongs in extra.tyhp.require are on Composer Runtime Packages.

Composer only runs plugins that you allow:

"config": {
    "allow-plugins": {
        "tyhp/core": true
    }
}

tyhp init, tyhp composer (including tyhp composer sync), and tyhp build --fix add that key when it is missing and never overwrite an explicit false.

What it does on each Composer run

The plugin runs once per Composer command, before Composer resolves dependencies (pre-pool-create):

  1. Skips dev-less installs. Nothing happens when the run excludes dev dependencies: --no-dev, COMPOSER_NO_DEV set to a true value, COMPOSER_DEV_MODE=0, or a lock file installed without dev packages.
  2. Walks the dependency tree. It starts from your root require, require-dev, and extra.tyhp.require, looks each package up in your configured repositories (the highest version that matches the constraint), and follows that package's require and extra.tyhp.require. A dependency's own require-dev is never followed. Platform packages (php, ext-*, lib-*, composer-plugin-api, …) are skipped.
  3. Merges the collected pins onto your root require-dev. Packages already in your root require are left alone, and your own package name is never added. When the same package is pinned twice and the constraints overlap, the existing pin is kept. When they cannot both be satisfied, the Composer command fails with TYHP7700.
  4. Adds the compiler. When tyhp/core is in the graph and tyhp/compiler is not already pinned, it adds "tyhp/compiler": "@dev", unless your root package is tyhp/compiler.
  5. Writes composer.json. When the merged require-dev differs from yours, it rewrites the root composer.json: existing entries keep their order, new entries are appended in alphabetical order, other keys are preserved, and config.allow-plugins.tyhp/core is added if missing. The new requirements are also fed into the running Composer command. If the file cannot be read or written, the command fails with TYHP7703.

For example, if your application requires a library whose composer.json contains:

{
    "name": "acme/invoices",
    "require": {
        "php": ">=8.2",
        "tyhp/core": "804.0.*"
    },
    "extra": {
        "tyhp": {
            "require": {
                "tyhpdef/php": "@dev",
                "tyhpdef/php-ext-intl": "@dev"
            }
        }
    }
}

then after composer update your root require-dev contains tyhpdef/php, tyhpdef/php-ext-intl, and tyhp/compiler, and the type checker sees the intl definitions that acme/invoices exposes in its public API.

Note

A package's extra.tyhp.require is only known once Composer can see that package in a repository. The very first composer require of a Tyhp library therefore often installs the library without its extras. Run composer update or tyhp composer sync once more. tyhp build and tyhp lint report missing extras (TYHP7701) but never change composer.json themselves; see CLI: Composer.

Common mistakes

Danger

Don't add use extension \Tyhp\StringExtensions; (or any other catalog) to a file just to "enable" scalar methods. They are already global; the import only warns (TYHP4169). Use a local use extension only to hide or rename members.

Danger

Don't compare indexOf(), lastIndexOf(), search(), or strpos-style results with != false or treat them as booleans. Position 0 and key 0 are valid results; compare with === false.

Warning

contains() and search() on arrays compare loosely by default, like \in_array. Pass true as the second argument when values can be numeric strings or of mixed types.

Warning

filtered() and unique() keep the original keys. A filtered list such as [0 => 'a', 2 => 'c'] encodes as a JSON object, not an array. Call ->values() when you need a list.

Warning

$flag->__toString() gives 'true' / 'false', but (string) $flag is still PHP's cast and gives '1' / ''. Choose deliberately when building output.

Warning

Type::union() and Type::intersection() throw InvalidTypeException when fewer than two distinct members remain. When you build unions from data, handle the single-type case yourself.

Warning

A \Tyhp\Generic binder constructs one object. Calling new() (or invoking the binder) a second time throws \LogicException; call bind() again for each instance.

Tip

Prefer Json::decode<T>() or $json->jsonDecodeAs<T>() over $json->jsonDecode() whenever the shape is known. You get a typed value and a precise error instead of mixed.