bensedev/type-guard

Type-safe value validation and coercion for PHP with PHPStan support

Installs: 31

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 2

pkg:composer/bensedev/type-guard

v1.0.0 2025-10-06 12:05 UTC

README

Type-safe value validation and coercion for PHP with PHPStan support

TypeGuard provides two simple classes for handling mixed types safely in PHP:

  • Ensure - Returns default values on type mismatch (never throws)
  • Guard - Throws exceptions on type mismatch (strict validation)

Perfect for working with APIs, user input, or any scenario where you need to safely handle mixed types with full PHPStan/Larastan compatibility.

Why TypeGuard?

  • ๐ŸŽฏ PHPStan-friendly - Proper type narrowing that static analysis understands
  • ๐Ÿ›ก๏ธ Two approaches - Choose between safe defaults or strict validation
  • ๐Ÿš€ Zero dependencies - Pure PHP 8.4+
  • ๐Ÿงช Fully tested - Comprehensive test coverage
  • ๐Ÿ“ฆ Lightweight - Minimal footprint, maximum utility

Installation

Install via Composer:

composer require bensedev/type-guard

Usage

Ensure - Safe Defaults

Use Ensure when you want safe fallback values instead of exceptions:

use Bensedev\TypeGuard\Ensure;

// API response with mixed types
$data = json_decode($apiResponse, true);

// Safely extract values with defaults
$name = Ensure::string($data['name'] ?? null);  // '' if not string
$age = Ensure::int($data['age'] ?? null);        // 0 if not int
$price = Ensure::float($data['price'] ?? null);  // 0.0 if not float
$active = Ensure::bool($data['active'] ?? null); // false if not bool
$tags = Ensure::array($data['tags'] ?? null);    // [] if not array

Custom Defaults

use Bensedev\TypeGuard\Ensure;

$name = Ensure::string($value, 'Unknown');
$age = Ensure::int($value, 18);
$price = Ensure::float($value, 9.99);
$active = Ensure::bool($value, true);
$tags = Ensure::array($value, ['default']);

Guard - Strict Validation

Use Guard when you want to enforce types and throw exceptions:

use Bensedev\TypeGuard\Guard;
use Bensedev\TypeGuard\Exceptions\TypeMismatchException;

try {
    $userId = Guard::int($request->input('user_id'));
    $email = Guard::string($request->input('email'));
    $settings = Guard::array($request->input('settings'));

    // Use the validated values
    processUser($userId, $email, $settings);
} catch (TypeMismatchException $e) {
    // Handle type mismatch
    // e.g., "Expected value of type "int", but got "string""
}

Real-World Examples

API Response Handling

use Bensedev\TypeGuard\Ensure;

class UserApiResponse
{
    public function __construct(array $data)
    {
        // Safely extract with defaults - no null checks needed!
        $this->id = Ensure::int($data['id'] ?? null);
        $this->name = Ensure::string($data['name'] ?? null, 'Anonymous');
        $this->email = Ensure::string($data['email'] ?? null);
        $this->age = Ensure::int($data['age'] ?? null, 0);
        $this->isActive = Ensure::bool($data['is_active'] ?? null);
        $this->roles = Ensure::array($data['roles'] ?? null);
    }
}

Form Validation with Guards

use Bensedev\TypeGuard\Guard;
use Bensedev\TypeGuard\Exceptions\TypeMismatchException;

class CreateUserRequest
{
    public function validate(array $input): array
    {
        try {
            return [
                'name' => Guard::string($input['name'] ?? null),
                'age' => Guard::int($input['age'] ?? null),
                'email' => Guard::string($input['email'] ?? null),
                'settings' => Guard::array($input['settings'] ?? null),
            ];
        } catch (TypeMismatchException $e) {
            throw new ValidationException('Invalid input: ' . $e->getMessage());
        }
    }
}

Laravel Controller Example

use Bensedev\TypeGuard\Ensure;
use Bensedev\TypeGuard\Guard;

class ProductController extends Controller
{
    public function store(Request $request)
    {
        // Strict validation for required fields
        $name = Guard::string($request->input('name'));
        $price = Guard::float($request->input('price'));

        // Safe defaults for optional fields
        $description = Ensure::string($request->input('description'), 'No description');
        $stock = Ensure::int($request->input('stock'), 0);
        $tags = Ensure::array($request->input('tags'));

        Product::create([
            'name' => $name,
            'price' => $price,
            'description' => $description,
            'stock' => $stock,
            'tags' => $tags,
        ]);
    }
}

PHPStan Integration

TypeGuard works perfectly with PHPStan/Larastan for proper type narrowing:

use Bensedev\TypeGuard\Ensure;
use Bensedev\TypeGuard\Guard;

function processData(mixed $input): void
{
    // PHPStan knows $value is string after this
    $value = Ensure::string($input);
    strlen($value); // โœ… PHPStan happy

    // PHPStan knows $count is int after this
    $count = Guard::int($input);
    $count + 10; // โœ… PHPStan happy
}

Working with JSON

use Bensedev\TypeGuard\Ensure;

$json = '{"name":"John","age":"not a number","tags":["php","laravel"]}';
$data = json_decode($json, true);

$name = Ensure::string($data['name']);      // 'John'
$age = Ensure::int($data['age'], 25);       // 25 (default, because 'not a number' isn't int)
$tags = Ensure::array($data['tags']);       // ['php', 'laravel']
$active = Ensure::bool($data['active']);    // false (key doesn't exist)

API Reference

Ensure Methods

All methods accept a mixed value and return the requested type (never null):

Ensure::string(mixed $value, string $default = ''): string
Ensure::int(mixed $value, int $default = 0): int
Ensure::float(mixed $value, float $default = 0.0): float
Ensure::bool(mixed $value, bool $default = false): bool
Ensure::array(mixed $value, array $default = []): array

Guard Methods

All methods accept a mixed value and return the requested type or throw:

Guard::string(mixed $value): string
Guard::int(mixed $value): int
Guard::float(mixed $value): float
Guard::bool(mixed $value): bool
Guard::array(mixed $value): array

Note: Guard::float() accepts both float and int values (converts int to float).

Exceptions

TypeMismatchException extends InvalidArgumentException

Thrown by Guard methods when type validation fails. Message format:

Expected value of type "int", but got "string"

When to Use Which?

Use Case Use Ensure Use Guard
Optional API fields โœ… โŒ
Required API fields โŒ โœ…
User input (optional) โœ… โŒ
User input (required) โŒ โœ…
Config with defaults โœ… โŒ
Strict validation โŒ โœ…
Working with legacy code โœ… โŒ

Testing

Run the test suite:

composer test

Run PHPStan:

composer analyse

Run Laravel Pint:

composer format

Requirements

  • PHP 8.4 or higher

License

The MIT License (MIT). Please see License File for more information.

Credits

Support

If you discover any issues, please open an issue on GitHub.