Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: [rcalicdan]
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
test:
name: PHP ${{ matrix.php }} (${{ matrix.os }})
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
php: ['8.1', '8.2', '8.3', '8.4', '8.5']

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, mbstring, zip, libxml, json, tokenizer, fileinfo
coverage: none

- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Composer Dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-

- name: Install Dependencies
run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress

- name: Check Code Style (Pint)
run: ./vendor/bin/pint --test
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3'

- name: Run Static Analysis (PHPStan)
run: ./vendor/bin/phpstan analyse --no-progress
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3'

- name: Run Test Suite (Pest)
run: ./vendor/bin/pest --ci
49 changes: 49 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Deploy Documentation

on:
push:
branches:
- main
paths:
- 'docs/**'
- '.github/workflows/deploy-docs.yml'

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: 'pages'
cancel-in-progress: true

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Build with VitePress
run: npm run docs:build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
composer.lock
/vendor
/.vscode
/var
/var
/node_modules
package-lock.json
docs/.vitepress/cache
docs/.vitepress/dist
77 changes: 77 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { defineConfig } from 'vitepress'

export default defineConfig({
title: "TypePHP",
description: "Runtime Type Enforcement for PHP.",
themeConfig: {
siteTitle: "TypePHP",
nav: [
{ text: 'Home', link: '/' },
{ text: 'Documentation', link: '/getting-started/installation' },
{ text: 'Architecture', link: '/architecture/how-it-works' },
{ text: 'CLI', link: '/production/cache-commands' },
{ text: 'GitHub', link: 'https://github.com/typephp-php/typephp' }
],
sidebar: [
{
text: 'Getting Started',
items: [
{ text: 'Installation', link: '/getting-started/installation' },
{ text: 'Quick Start', link: '/getting-started/quick-start' },
{ text: 'Configuration', link: '/getting-started/configuration' },
]
},
{
text: 'Architecture',
items: [
{ text: 'How It Works', link: '/architecture/how-it-works' },
]
},
{
text: 'Core Concepts',
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: 'Supported Types',
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: 'Advanced Features',
items: [
{ 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',
items: [
{ text: 'Production Readiness', link: '/production/production-readiness' },
{ text: 'Cache CLI Commands', link: '/production/cache-commands' },
{ text: 'Performance Considerations', link: '/production/performance-considerations' },
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/typephp-php/typephp' }
],
search: {
provider: 'local'
}
}
})
114 changes: 114 additions & 0 deletions docs/advanced/exception-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Exception Handling

If you run TypePHP in live production applications—especially for validating external HTTP API payloads, webhooks, or dynamic database records—catching `TypePHP\Exception\TypeError` gives you a clean way to intercept data validation failures gracefully without letting exceptions crash your application.

TypePHP provides a custom exception class, `TypePHP\Exception\TypeError`, designed for both polymorphic compatibility with PHP's native type system and surgical error handling at application boundaries.

---

## The Exception Class Hierarchy

All contract violations in TypePHP instantiate `TypePHP\Exception\TypeError`, which extends PHP's native `\TypeError`:

```
\Throwable
└── \Error
└── \TypeError
└── TypePHP\Exception\TypeError
```

---

## Dual Catching Modes

Because `TypePHP\Exception\TypeError` extends native `\TypeError`, you can choose how broadly or narrowly to catch type failures:

### 1. Polymorphic Catching (`catch (\TypeError $e)`)

Catches both native PHP engine type errors (such as passing a string into a native `int` type hint) and TypePHP contract failures:

```php
try {
processUser(-50);
} catch (\TypeError $e) {
// Catches both native PHP TypeErrors and TypePHP contract failures!
}
```

### 2. Specific Contract Catching (`catch (\TypePHP\Exception\TypeError $e)`)

Specifically catches TypePHP contract violations while allowing native PHP engine errors to bubble up separately:

```php
use TypePHP\Exception\TypeError as TypePHPTypeError;

try {
processUser(-50);
} catch (TypePHPTypeError $e) {
// Catches ONLY TypePHP contract violations!
} catch (\TypeError $e) {
// Catches native PHP engine type errors!
}
```

---

## Call-Site Trace Attribution

When a function parameter or callback argument fails type validation, TypePHP automatically rewrites the exception's file and line attributes.

Instead of blaming internal library files, `ErrorFactory` filters out internal frames and attributes `$e->file` and `$e->line` directly to **the exact line of code in the caller file where the invalid argument was passed**, matching native PHP engine behavior.

---

## HTTP API Payload Validation (Framework-Agnostic 422 Responses)

`TypePHP\Exception\TypeError` is especially useful when validating external HTTP request payloads at application boundaries.

When dynamic request data fails contract validation, you can catch `TypePHPTypeError` and return a clean HTTP `422 Unprocessable Entity` response:

```php
namespace App\Http;

use App\Services\UserService;
use TypePHP\Exception\TypeError as TypePHPTypeError;

class UserApiController
{
public function __construct(private UserService $userService) {}

public function handleRequest(array $requestData): array
{
try {
// UserService enforces @param array{id: positive-int, email: non-empty-string}
$this->userService->registerUser($requestData);

return [
'status' => 200,
'body' => ['message' => 'User registered successfully'],
];
} catch (TypePHPTypeError $e) {
// Convert TypePHP contract failure into an HTTP 422 response
return [
'status' => 422,
'body' => [
'error' => 'Unprocessable Entity',
'message' => $e->getMessage(),
],
];
}
}
}
```

---

## Human-Readable Error Formatting

TypePHP's `TypeFormatter` formats invalid values into descriptive human-readable strings inside exception messages:

* **Integers:** `negative int (-50)`, `zero int (0)`, `int (42)`
* **Strings:** `empty string ('')`, `string 'this_is_a_very_lo...'`
* **Booleans:** `bool (true)`, `bool (false)`
* **Arrays:** `empty array ([])`, `list (3 items)`, `associative array (key 'id')`
* **Objects:** Class FQCN (e.g., `App\Models\Product`)
Loading
Loading