Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Type Laravel Package

php-standard-library/type

Runtime type validation for PHP built on the Parse, Don't Validate principle: coerce and assert unstructured input into well-typed data, with clear errors when values don't match.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the split package directly, or get it transitively via the umbrella library:

composer require php-standard-library/type
# or, if you already use the metapackage:
composer require php-standard-library/standard-library

Everything lives under the Psl\Type namespace, exposed as namespaced functions that build immutable type specifications:

use Psl\Type;

$spec = Type\shape([
    'name' => Type\string(),
    'age'  => Type\int(),
    'tags' => Type\vec(Type\string()),
]);

$data = $spec->coerce(['name' => 'Ada', 'age' => '36', 'tags' => ['php', 'laravel']]);
// ['name' => 'Ada', 'age' => 36, 'tags' => ['php', 'laravel']]

The two methods you will use on every spec are:

  • coerce(mixed $value) — accepts loosely typed input and attempts to produce the target type; throws Psl\Type\Exception\CoercionException when it cannot.
  • assert(mixed $value) — no coercion; throws Psl\Type\Exception\AssertException if the value is not already exactly the right type.

First place to look: the docs site for the Type chapter, then src/ in the package for the concrete specs (shape, vec, dict, union, nullable, optional, instance_of, literal_scalar, …). Read the signatures of Type\shape() and Type\optional() first — they cover the majority of real Laravel payloads.

First practical use case: parse a JSON request body at the controller/service boundary into a shape you can trust, instead of sprinkling is_array() / is_string() checks through the call stack.

$payload = Type\shape([
    'email'    => Type\string(),
    'password' => Type\string(),
    'profile'  => Type\optional(Type\shape([
        'bio' => Type\string(),
    ])),
])->coerce($request->all());

After this line, downstream code receives a well-shaped array (or a DTO built from it) and should not need to re-check types.

Implementation Patterns

Parse once in a DTO factory. The highest-leverage pattern is a fromArray() that owns its own spec and returns a typed object. Controllers and jobs should call the factory, not re-check types.

final class CreateUser
{
    public function __construct(
        public readonly string $name,
        public readonly int $age,
        public readonly array $tags,
    ) {}

    public static function fromArray(array $raw): self
    {
        $data = self::type()->coerce($raw);

        return new self($data['name'], $data['age'], $data['tags']);
    }

    public static function type(): Type\TypeInterface
    {
        static $type;

        return $type ??= Type\shape([
            'name' => Type\string(),
            'age'  => Type\int(),
            'tags' => Type\vec(Type\string()),
        ]);
    }
}

FormRequest: parse in passedValidation(). Laravel's rules still give you field-level messages and all the required/email/exists checks. Use the type spec afterwards to convert the validated bag into a typed shape.

final class StoreUserRequest extends FormRequest
{
    private array $parsed;

    public function rules(): array
    {
        return [
            'name'    => ['required', 'string', 'max:255'],
            'age'     => ['required', 'integer', 'min:0'],
            'tags'    => ['array'],
            'tags.*'  => ['string'],
        ];
    }

    protected function passedValidation(): void
    {
        $this->parsed = Type\shape([
            'name' => Type\string(),
            'age'  => Type\int(),
            'tags' => Type\vec(Type\string()),
        ])->coerce($this->validated());
    }

    public function payload(): array
    {
        return $this->parsed;
    }
}

Use coerce() here, not assert(): form input arrives as strings even after integer validation, so assert() would throw on '36' while coerce() succeeds.

Bridge to a Laravel validation rule for non-HTTP boundaries. When you have a spec that is hard to express as rules (nested shapes, unions, instance_of), wrap assert() in a rule object so errors surface through the normal validation pipeline.

final class TypeRule implements ValidationRule
{
    public function __construct(private readonly Type\TypeInterface $type) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        try {
            $this->type->assert($value);
        } catch (Type\Exception\AssertException $e) {
            $fail($e->getMessage());
        }
    }
}

Queue jobs and console commands. Jobs receive arrays from serialize() and CLI options arrive as strings. Parse at the top of handle() / execute() and fail fast; do not propagate mixed deeper into the job.

public function handle(): void
{
    $rows = Type\vec(Type\shape([
        'id'   => Type\int(),
        'name' => Type\string(),
    ]))->coerce($this->rows);

    foreach ($rows as $row) { /* $row is typed */ }
}

Config binding in a service provider. Parse config once and bind a typed config object into the container, so the rest of the app never sees the raw config() array.

$this->app->singleton(StripeConfig::class, fn () =>
    StripeConfig::fromArray(config('services.stripe'))
);

Compose rather than branch. Type\union(), Type\nullable(), Type\optional() and Type\dict() replace chains of try/catch or nested ifs. For enum-like strings, Type\literal_scalar('pending') gives you a checked literal; if your installed PSL version provides a backed-enum spec, prefer it over hand-rolled string unions.

Testing. Treat the spec as part of the public surface: test successful coercion with realistic wire values ("42", JSON-decoded payloads) and test failures with expectException(Type\Exception\CoercionException::class). Because specs are cheap to build and immutable, constructing a fresh one per test is fine.

Gotchas and Tips

  • This repo is a read-only split. The releases here are one-line pointers to the main php-standard-library/php-standard-library release notes. Read the changelog in the main repo before upgrading, and pin the version in composer.json.
  • coerce() vs assert() is the main footgun. HTTP and queue payloads are almost always the wrong scalar type ("1", "true"), so assert() will throw where coerce() succeeds. Use coerce() at the boundary, assert() only for data you produced yourself.
  • Coercion is type-level, not semantic. Type\string() will happily pass any string; it does not validate email format, URL shape, or length. Keep Laravel's email, url, max rules for that and use the type spec as an additional layer.
  • ConvertEmptyStringsToNull turns empty fields into null. After that middleware, Type\string() throws on a submitted-but-empty field. Wrap with Type\nullable(Type\string()) or use Type\optional() inside a shape when the key may be absent.
  • optional() only makes sense inside shape(). Using it as a standalone spec is a misuse; it is a key marker, not a nullable value spec. Use nullable() for values.
  • Shapes check required keys but do not sanitize extra ones. If you need a closed output, build the DTO explicitly from named keys (as in the factory above) rather than passing $data around wholesale.
  • vec() is a list, dict() is a keyed map. A JSON object decoded with json_decode($json, true) is a dict; a JSON array is a vec. vec() requires keys 0..n-1, so a sparse or re-keyed array will fail even though it "looks like an array".
  • Exception types are not Laravel's. Catch Psl\Type\Exception\CoercionException and AssertException (or their common parent), then convert to ValidationException::withMessages(). Without this, a malformed payload becomes a 500 instead of a 422. If your version exposes a path on the exception, use it to map failures to field names.
  • Annotate DTO factories for static analysis. PHPStan/Psalm cannot infer a shape() from the runtime spec. Add @return array{name: string, age: int, tags: list<string>} on the spec-adjacent factory so the typed shape survives into analysis.
  • Cache specs; don't rebuild them per iteration. Specs are immutable and shareable, but constructing them inside a loop or per request handler is wasteful. A static local or a container-bound spec is enough.
  • Do not replace Eloquent casts with this. $casts is the right tool for attributes coming out of the database. This package is for unstructured input crossing a boundary (HTTP, queue, CLI, third-party API responses).
  • Wrap it. The split package has minimal adoption and the repository is a mirror. Keep a thin App\Support\Type adapter around the handful of specs and exception conversions you use, so a future swap to webmozart/assert or hand-rolled checks is a one-file change.
  • Import functions deliberately. use function Psl\Type\{shape, string, int, vec, optional}; reads better than Type\int() everywhere, but make sure the team convention is consistent — mixed styles make it hard to tell PSL functions from local helpers.
  • Custom specs are an extension point. Implement Type\TypeInterface for domain primitives (money, country code) and compose them with shape/union. Give toString() a useful implementation, because it appears in coercion error messages.
  • Watch large JSON numbers. Values beyond PHP_INT_MAX or decoded as floats can make integer coercion lossy. Validate ranges with Laravel rules in addition to the type spec when the field is an identifier or amount.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules
loco/loco
starfederation/datastar-php
yupmin/modern-phystrix
stephenjude/extended-artisan-commands
lumphp/laravel-macroable
lumphp/laravel-collections
lumphp/laravel-contracts
sermepa/sermepa
arkecosystem/fortify