Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<h1 align="center">TypePHP</h1>

<p align="center">
<b>No transpilation. No build steps. No C-extensions.<br>
Drop TypePHP into your existing codebase and let your DocBlocks scream when types fail.</b>
</p>

<p align="center">
<a href="https://github.com/typephp-php/typephp/actions"><img src="https://github.com/typephp-php/typephp/actions/workflows/ci.yml/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/typephp/typephp"><img src="https://img.shields.io/packagist/v/typephp/typephp.svg?style=flat&color=blue" alt="Latest Stable Version"></a>
Expand All @@ -11,7 +16,7 @@

------

TypePHP is the first pure-PHP library that transparently enforces extended PHPDoc type contracts (generics, array shapes, scalar refinements, and callables) at runtime during execution, without introducing any new syntax or requiring C-extensions.
TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, setup complex build toolchains, or compile C-extensions and simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime.


**[Read the full TypePHP documentation »](https://typephp-php.github.io/typephp/)**
Expand Down
40 changes: 23 additions & 17 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export default defineConfig({
nav: [
{ text: 'Home', link: '/' },
{ text: 'Documentation', link: '/getting-started/installation' },
{ text: 'Architecture', link: '/architecture/how-it-works' },
{ text: 'CLI', link: '/production/cache-commands' },
{ text: 'Generics', link: '/generics/generics-and-bounds' },
{ text: 'CLI', link: '/getting-started/cli-commands' },
{ text: 'FAQ', link: '/troubleshooting' },
{ text: 'GitHub', link: 'https://github.com/typephp-php/typephp' }
],
sidebar: [
Expand All @@ -20,52 +21,57 @@ export default defineConfig({
{ text: 'Installation', link: '/getting-started/installation' },
{ text: 'Quick Start', link: '/getting-started/quick-start' },
{ text: 'Configuration', link: '/getting-started/configuration' },
{ text: 'CLI Commands', link: '/getting-started/cli-commands' },
]
},
{
text: 'Architecture',
items: [
{ text: 'How It Works', link: '/architecture/how-it-works' },
]
},
{
text: 'Core Concepts',
text: 'Enforcement Boundaries',
items: [
{ text: 'Function Contracts', link: '/core-concepts/function-contracts' },
{ text: 'Inline Variables', link: '/core-concepts/inline-variables' },
{ text: 'Property Validation', link: '/core-concepts/property-validation' },
{ text: 'Generics & Bounds', link: '/core-concepts/generics-and-bounds' },
{ text: 'Type Aliases', link: '/core-concepts/type-aliases' },
{ text: 'Inline Variables', link: '/core-concepts/inline-variables' },
]
},
{
text: 'Supported Types',
text: 'Type Reference',
items: [
{ text: 'Primitives & Scalars', link: '/supported-types/primitives-and-scalars' },
{ text: 'Arrays & Shapes', link: '/supported-types/arrays-and-shapes' },
{ text: 'Callables & Closures', link: '/supported-types/callables-and-closures' },
{ text: 'Iterators & Generators', link: '/supported-types/iterators-and-generators' },
{ text: 'Unions, Intersections & Conditionals', link: '/supported-types/unions-intersections-and-conditionals' },
{ text: 'Type Aliases', link: '/supported-types/type-aliases' },
]
},
{
text: 'Runtime Generics',
items: [
{ text: 'Generics & Bounds', link: '/generics/generics-and-bounds' },
]
},
{
text: 'Advanced Features',
text: 'Advanced & Architecture',
items: [
{ text: 'How It Works', link: '/advanced/how-it-works' },
{ text: 'Liskov & Inheritance', link: '/advanced/liskov-and-inheritance' },
{ text: 'Vendor Isolation', link: '/advanced/vendor-and-path-filtering' },
{ text: 'Ignore Annotations', link: '/advanced/ignore-annotations' },
{ text: 'Extensions', link: '/advanced/extensions' },
{ text: 'Exception Handling', link: '/advanced/exception-handling' },
{ text: 'Troubleshooting & FAQ', link: '/advanced/troubleshooting' }
]
},
{
text: 'Production & Performance',
text: 'Production & Operations',
items: [
{ text: 'Production Readiness', link: '/production/production-readiness' },
{ text: 'Cache CLI Commands', link: '/production/cache-commands' },
{ text: 'Performance Considerations', link: '/production/performance-considerations' },
]
},
{
text: 'Help & Support',
items: [
{ text: 'Troubleshooting & FAQ', link: '/troubleshooting' },
]
}
],
socialLinks: [
Expand Down
File renamed without changes.
39 changes: 23 additions & 16 deletions docs/advanced/liskov-and-inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,35 +292,42 @@ $service->update(10, 'Charlie');

---

## Parameter Renaming ($id $\rightarrow$ $userId$)
## Parameter Renaming ($id $userId) & Position Shifts

PHP permits child classes to rename parameters when implementing an interface or extending a class. TypePHP maps inherited parameter contracts by **index position** (0, 1, 2...) rather than parameter name:
When a child class or attribute constructor overrides a parent method, parameter positions or parameter names may shift. TypePHP resolves parameter contract inheritance using **Name-First Resolution**:

1. **Name Matching:** If a parameter name in the child method matches a parameter name in the parent class (e.g. `$api`), the parent's contract is inherited by that parameter regardless of its position index in the child.
2. **Position Fallback:** If a parameter is renamed in the child class (e.g., `$id` $\rightarrow$ `$userId`), TypePHP falls back to matching by position index.

```php
interface UserApiInterface
class BaseField
{
/**
* Interface uses parameter name $id
* Parent constructor has $api at position #1
*
* @param positive-int $id
* @param string $type
* @param bool|array{admin-api: bool} $api
*/
public function find(int $id): bool;
public function __construct(string $type, bool|array $api = false) {}
}

class UserApi implements UserApiInterface
class OneToManyRelation extends BaseField
{
// Child renames parameter $id to $userId
public function find(int $userId): bool
{
return true;
/**
* Child inserts $entity, $ref, $onDelete BEFORE $api (position shift!)
*/
public function __construct(
string $entity,
string $ref,
OnDeleteOption $onDelete = OnDeleteOption::NO_ACTION,
bool|array $api = false
) {
parent::__construct('one-to-many', $api);
}
}

$api = new UserApi();

// $userId = -50 is checked at index 0 against interface's @param positive-int $id!
$api->find(-50);
// Throws: TypeError: UserApi::find(): Argument $userId must be of type positive-int
// $onDelete (position #2 in child) is NOT overwritten by $api's type (position #1 in parent)!
$attr = new OneToManyRelation('unit', 'unit_id', OnDeleteOption::CASCADE, true);
```

---
Expand Down
File renamed without changes.
27 changes: 27 additions & 0 deletions docs/core-concepts/function-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ registerUser(-5, 'Alice', 'admin');

> **Execution Order Note:** Native PHP type hints (e.g., `int $id`, `string $username`) are evaluated by PHP's C-engine *before* function execution begins. TypePHP's extended PHPDoc contracts (e.g., `positive-int`, `non-empty-string`) execute at the very start of the function/method body. If a native type hint fails, PHP throws its native `TypeError` before TypePHP's guard rails run.

---
## PHP 8.0+ Named Arguments

TypePHP natively supports PHP 8.0+ Named Arguments. Because parameter contracts are mapped by parameter name rather than argument position index, you can pass named arguments in any order, and TypePHP will accurately validate each parameter:

```php
<?php

declare(strict_types=1);

/**
* @param positive-int $id
* @param non-empty-string $username
* @param int<1, 100> $age
*/
function registerUser(int $id, string $username, int $age): void
{
// ...
}

// Valid Call: Arguments passed in completely reversed/swapped order
registerUser(age: 25, username: 'Alice', id: 42);

// Invalid Call: $id (-5) passed as 3rd named argument
registerUser(age: 25, username: 'Alice', id: -5);
// Throws: TypeError: registerUser(): Argument $id must be of type positive-int, negative int (-5) given
```
---

## Class Methods (Instance & Static)
Expand Down
File renamed without changes.
130 changes: 75 additions & 55 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ layout: home
hero:
name: "TypePHP"
text: "Transparent Runtime Type Enforcement"
tagline: "The first pure PHP library to enforce DocBlock types at runtime transparently without introducing any new syntax. Validates generics, array shapes, and advanced type contracts during execution."
tagline: "No transpilation. No build steps. No C-extensions. Just 100% pure PHP that makes your existing DocBlocks scream the moment types fail."
actions:
- theme: brand
text: "Get Started →"
Expand All @@ -16,17 +16,52 @@ hero:
features:
- title: "Zero Production Overhead"
details: "Install as a development dependency to enforce strict types during local testing and CI/CD pipelines, guaranteeing absolute zero performance cost in live production environments."
- title: "No Transpilation or C-Extensions"
details: "Operates 100% in pure PHP user-land using native stream wrappers and AST transformations. No build scripts, Node.js tools, or C-extensions required."
- title: "True Runtime Generics"
details: "Binds generic template types to specific object instances dynamically using native WeakMap memory tracking."
- title: "Typed Arrays & Shapes"
details: "Deeply validates sequential lists, typed class arrays, and strict associative array shape structures right out of the box."
- title: "PHP 8.4 Support"
details: "Native support for intercepting and validating PHP 8.4 Property Hooks (get/set) and Asymmetric Visibility (public private(set))."
- title: "Arrays, Shapes & Extractions"
details: "Deeply validates sequential lists, typed arrays, array shapes, and key-of / value-of constant extractions out of the box."
---

::: tip Pure PHP • Zero Transpilation • Zero Build Steps
**You don't have to change a single line of code, and you don't need a compilation build toolchain.** TypePHP operates entirely in native PHP user-land and no custom PHP binaries, C-extensions, or Node.js transpilers needed. Drop TypePHP into your existing project, run your code, and your DocBlocks will instantly start screaming at runtime when dynamic data violates a type contract.
:::

## See It In Action

TypePHP is the first pure PHP library that operates entirely in user-land using native stream wrappers and AST transformations. Because it requires no C-extensions or FFI, you can drop it into any PHP 8.1+ project effortlessly. It parses your standard PHPDoc annotations and enforces them the moment your code runs.
TypePHP operates entirely in user-land using native stream wrappers and AST transformations. Because it requires no C-extensions or FFI, you can drop it into any PHP 8.1+ project or web framework effortlessly. It reads your existing PHPDoc annotations and enforces them the moment your code runs.

### Real-World Framework Guard Rails (Laravel / Symfony)
Prevent dynamic data bugs from leaking into database queries or API responses:

```php
namespace App\Models;

use App\Enums\Role;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* @return list<int>
*/
public function assignableRoles(): array
{
if ($this->isSuperAdmin()) {
// Bug! Returns an array of Role Enum instances instead of integers:
return Role::cases();
}

return [Role::STAFF->value];
}
}

// Executing $user->assignableRoles() throws:
// TypePHP\Exception\TypeError: User::assignableRoles(): Return value[0] must be of type int, App\Enums\Role returned
```

---

### True Runtime Generics
Define generic templates and TypePHP will track their state in memory per object instance:
Expand All @@ -51,68 +86,53 @@ $users->add(new Product('SKU-100'));
// Throws TypeError: Argument $item (template T = User) must be of type User, Product given
```

### Array Shapes & Typed Arrays
Enforce strict associative array structures and collections of specific objects:

```php
/**
* @param array{status: 'active'|'pending', tags: list<non-empty-string>} $options
* @param User[] $collaborators
*/
function processBatch(array $options, array $collaborators): void
{
// ...
}
---

processBatch(
options: ['status' => 'active', 'tags' => ['php', 'types']],
collaborators: [new User(), new User()]
); // Valid
### Array Shapes & Key/Value Extractions
Enforce strict associative array structures and constant extractions:

processBatch(
options: ['status' => 'archived', 'tags' => ['php']],
collaborators: []
);
// Throws TypeError: Argument $options['status'] must be of type ('active' | 'pending')
```
```php
namespace App\Services;

### Scalar Refinements & Function Boundaries
Catch invalid parameters before your function executes, and invalid return values before they leak out:
use App\Database\DriverManager;

```php
/**
* @param positive-int $id
* @return non-empty-string
* @phpstan-type ConnectionParams array{
* driver: key-of<DriverManager::DRIVER_MAP>,
* driverClass?: value-of<DriverManager::DRIVER_MAP>
* }
*/
function generateUserToken(int $id): string
class DatabaseService
{
return ""; // Throws TypeError: Return value must be of type non-empty-string
/**
* @param ConnectionParams $params
*/
public function connect(array $params): void
{
// ...
}
}

generateUserToken(-5);
// Throws TypeError: Argument $id must be of type positive-int, negative int (-5) given
$service = new DatabaseService();

$service->connect(['driver' => 'pdo_mysql']); // Valid

$service->connect(['driver' => 'pdo_invalid']);
// Throws TypeError: Argument $params['driver'] must be a key of DriverManager::DRIVER_MAP
```

---

## Precise Stack Trace & Error Reporting
## Precise Call-Site Trace Attribution

TypePHP injects single-line guard rails without shifting your source file line numbers.
A common problem with AST code injection is that adding new statements pushes subsequent code down, causing line numbers in stack traces to drift out of sync.

When an inline variable or type contract fails, framework error handlers and test runners (like Pest, PHPUnit, and Whoops) point **directly to the exact line number** where the invalid assignment or argument occurred in your application code:
TypePHP solves this with **Zero Line-Drift Formatting**. Injected guard rails are squashed onto single lines and appended directly to existing code blocks. **Line numbers in your source files remain 100% identical before and after transformation.**

```
FAILED Tests\SomeTest > test

TypeError: Variable $typeArray[3] must be of type int, string '1' given

at tests/SomeTest.php:7
3| declare(strict_types=1);
4|
5| test('test', function () {
6| /** @var array<int> */
➜ 7| $typeArray = [1, 2, 3, '1'];
8|
9| expect($typeArray)->toBeArray();
10| });
```
When a type contract fails, web exception handlers (**Laravel Ignition, Whoops, Symfony ErrorHandler**) and CLI test runners (**Pest, PHPUnit**) point **directly to the exact line number** where the invalid assignment or return value occurred in your application code:

### Web Framework Trace (Laravel Ignition)
![Laravel Ignition Exception Trace](/laravel-error-screen.png)

### CLI Test Runner Trace (Pest PHP)
![Pest CLI Exception Trace](/pest-error-screen.png)
Binary file added docs/public/laravel-error-screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/pest-error-screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading