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

sebastian/type

A collection of value objects that model the PHP type system, including simple, union, intersection, and special types. Use it to inspect, compare, and represent type information programmatically in PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Check whether you already have it. In almost every Laravel project sebastian/type arrives transitively via PHPUnit. Before adding it explicitly:

composer show sebastian/type   # shows version + "required by phpunit/phpunit"

If it's already there, just use it — but add an explicit require-dev entry only if you're calling its API directly, so an accidental PHPUnit downgrade can't silently change your type semantics.

Install with the right constraint for your PHP version:

php -v   # 8.4+ ?
composer require --dev sebastian/type:^7.0   # PHP 8.4+
composer require --dev sebastian/type:^6.0   # PHP 8.3 (Laravel 10/11 on 8.3)

v7.0.0 dropped PHP 8.3; v6.0.0 dropped PHP 8.2. Both 6.0.4 and 7.0.1 contain the class-alias assignability fix — pin at least those if you touch aliased type hints.

Where to look first, in order:

  1. SebastianBergmann\Type\Type — the abstract base: isAssignableFrom(), isSatisfiedBy(), allowsNull(), asString(), plus the is*() predicates.
  2. SebastianBergmann\Type\ReflectionMapper — the bridge from PHP reflection to these value objects.
  3. SimpleType, ObjectType, UnionType, IntersectionType, GenericObjectType, UnknownType — the concrete shapes you'll branch on.

First use case — dump a controller signature:

use SebastianBergmann\Type\ReflectionMapper;

$type = (new ReflectionMapper)->fromReflectionReturnType(
    new ReflectionMethod(App\Http\Controllers\UserController::class, 'show')
);

echo $type->asString(); // "App\Models\User|Illuminate\Http\JsonResponse"

For a property (handy with typed Eloquent attributes and promoted constructor properties):

$type = (new ReflectionMapper)->fromPropertyType(
    new ReflectionProperty(App\Models\User::class, 'email')
);

That's the whole on-ramp. Everything else is composition of these two calls.

Implementation Patterns

1. Signature introspection for API contracts / OpenAPI

Walk public methods once, map each parameter and return type, feed a schema builder:

final class SignatureScanner
{
    public function __construct(private ReflectionMapper $mapper = new ReflectionMapper) {}

    public function scan(string $class): array
    {
        $out = [];
        foreach ((new ReflectionClass($class))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            if ($method->isConstructor() || $method->isStatic()) {
                continue;
            }

            $out[$method->getName()] = [
                'params' => array_map(
                    fn (ReflectionParameter $p) => $p->getType()
                        ? $this->mapper->fromType($p->getType())->asString()
                        : 'mixed',
                    $method->getParameters()
                ),
                'returns' => $method->getReturnType()
                    ? $this->mapper->fromReflectionReturnType($method)->asString()
                    : 'mixed',
            ];
        }

        return $out;
    }
}

Because these are value objects, the output is deterministic — safe to snapshot into a generated file and diff in CI.

2. Architecture tests (the highest-value daily use)

Enforce invariants your team argues about in review instead of by hand:

use SebastianBergmann\Type\{ObjectType, UnionType, ReflectionMapper};

$actual = (new ReflectionMapper)->fromReflectionReturnType(
    new ReflectionMethod(App\Http\Controllers\OrderController::class, 'store')
);

$allowed = UnionType::fromTypes([
    new ObjectType(Illuminate\Http\JsonResponse::class),
    new ObjectType(Illuminate\Http\RedirectResponse::class),
    new ObjectType(Illuminate\Http\Response::class),
]);

$this->assertTrue(
    $allowed->isAssignableFrom($actual),
    "store() must return an HTTP response, got {$actual->asString()}"
);

Read isAssignableFrom as: "can a value of the argument be assigned to something declared as $this?" — $expected->isAssignableFrom($actual).

3. Container / binding sanity checks

Verify every constructor dependency is either concrete or has a matching binding:

foreach ((new ReflectionClass($service))->getConstructor()?->getParameters() ?? [] as $param) {
    $type = $param->getType();
    if ($type === null) continue;

    $declared = (new ReflectionMapper)->fromType($type);

    $bound = collect($bindings)->first(
        fn (string $abstract) => $declared->isAssignableFrom(new ObjectType($abstract))
    );

    if ($bound === null && ! $declared->isSatisfiedBy(resolve($param->getName()))) {
        report(new RuntimeException("Unresolvable: {$service}::\${$param->getName()}"));
    }
}

This catches the DateTimeImmutable vs DateTime class-alias class of bugs at boot instead of in production.

4. Type-aware validation rules for DTO hydration

Use isSatisfiedBy() as a post-cast guard, not a raw-input guard:

use SebastianBergmann\Type\{SimpleType, ObjectType};

Rule::make(function (string $attribute, mixed $value, Closure $fail) {
    $type = new SimpleType(SimpleType::TYPE_INT);

    if (! $type->isSatisfiedBy($value)) {
        $fail("The {$attribute} field must be an integer.");
    }
});

isSatisfiedBy() is strict — no juggling. '42' does not satisfy int. That's a feature for validating already-cast DTOs, and a trap for FormRequest input.

5. Wrapping it in one service, not scattered calls

Reflection is the expensive part, not the mapper. Centralize with a per-request keyed cache:

final class TypeMap
{
    private array $cache = [];

    public function __construct(private ReflectionMapper $mapper =	new ReflectionMapper) {}

    public function method(string $class, string $method): SebastianBergmann\Type\Type
    {
        return $this->cache["{$class}::{$method}"] ??= $this->mapper
            ->fromReflectionReturnType(new ReflectionMethod($class, $method));
    }
}

Bind this as a singleton and inject it — you now have one seam to swap if you later move to PHPStan's own type model.

6. Recursive normalization for codegen

Branch once on the concrete class, then recurse:

function toSchema(SebastianBergmann\Type\Type $type): array
{
    return match (true) {
        $type instanceof UnionType => [
            'anyOf' => array_map('toSchema', $type->types()),
        ],
        $type instanceof IntersectionType => [
            'allOf' => array_map('toSchema', $type->types()),
        ],
        $type instanceof ObjectType => [
            'type' => 'object',
            'class' => $type->className(),
            'nullable' => $type->allowsNull(),
        ],
        default => [
            'type' => $type->name(),
            'nullable' => $type->allowsNull(),
        ],
    };
}

7. Testing assertions on mocks and fakes

$expected = new ObjectType(Illuminate\Contracts\Cache\Repository::class);

$this->assertTrue($expected->isAssignableFrom(new ObjectType(get_class($fakeRepository))));

Cheaper and clearer than assertInstanceOf when the declared type is a union or an interface hierarchy.

Gotchas and Tips

isAssignableFrom() argument direction is the #1 mistake. SimpleType('iterable')->isAssignableFrom(SimpleType('array')) is true; the reverse is false. Test it with a literal true/false assertion once and never think about it again.

Nullable is not a union. PHP reflection collapses ?int and int|null into a single named type with allowsNull() === true. So UnionType::fromTypes() is something you'll construct manually for your own models; it rarely shows up from ReflectionMapper. Don't write if ($type instanceof UnionType) and assume it caught nullability.

ReflectionMapper throws on anything it can't model. Unsupported/odd named types raise SebastianBergmann\Type\RuntimeException. Wrap codegen loops:

try {
    $type = $mapper->fromType($reflectionType);
} catch (SebastianBergmann\Type\RuntimeException) {
    $type = new UnknownType($reflectionType->getName());
}

One unmodelled self/parent reference in a legacy class should not take down your whole docs build.

iterable is modelled as a SimpleType, not array|Traversable. This was the subject of the iterable-in-unions fixes (6.0.1/6.0.2/5.1.1/5.1.2). If your code branches on array or Traversable and you feed it iterable, you'll fall through to the default arm. Handle it explicitly.

void and never have no satisfying value. isSatisfiedBy() is meaningless for them. Branch on $type->isVoid() / $type->isNever() before doing anything value-shaped, or your generators will emit nonsense schemas.

Object types resolve classes by name, which triggers autoloading. ObjectType::className() gives you a string; isSatisfiedBy() instantiates instanceof against it. If you're scanning classes that aren't loadable in the current process (separate package, generated code), guard with class_exists() first or you'll get confusing false negatives.

Pin ^6.0.4 / ^7.0.1 minimum if you use class aliases. Earlier versions returned incorrect isAssignableFrom() results for use DateTime as DT style aliases — a silent, data-dependent bug. There is no runtime error to warn you.

Don't use it for raw HTTP input validation. JSON gives you strings; these types are strict. Cast with a FormRequest or a DTO constructor first, then validate.

Keep the composer constraint loose. Because PHPUnit depends on this package, a hard pin can cause dependency conflicts on composer update. Use ^7.0, not 7.0.1.

Prefer composition over extending Type. The abstract class is small and the constructors are not a stable extension point across majors (v5 → v6 → v7 each dropped PHP versions and moved internals). Wrap it in your own DomainType/normalizer class instead of subclassing, and you won't be rewriting at the next major.

name() vs value() vs asString() differ in nullability rendering. Pick one for anything you snapshot or generate, otherwise a version bump will produce a noisy diff across hundreds of generated files.

Debugging in Tinker: dump($type) prints nested value objects legibly; echo $type->asString() gives the source-like form. Reach for asString() first when a type assertion fails — the message tells you whether you got a union, a nullable, or an UnknownType fallback.

It is not a static analyzer. This models types; it does not infer them. If you need flow analysis, use PHPStan/Psalm and consume this package through their rule APIs rather than reimplementing inference.

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.
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode