larastan/larastan · error · InvalidArgumentException
is not a Model.
Error message
is not a Model.
What it means
determineBuilderName() in BuilderHelper resolves the Eloquent builder class used for a model, and it only accepts class names whose ClassReflection is exactly Illuminate\Database\Eloquent\Model (checked via ->is(Model::class)). Any other class name passed in makes Larastan throw an InvalidArgumentException ('X is not a Model.'). This is an internal invariant: callers such as determineBuilderClass must only pass model class names.
Solutions
- Check the calling code/PHPDoc and pass the model class name (a subclass of Illuminate\Database\Eloquent\Model) instead of the builder or other class.
- If you hit it on your own code, inspect @template TModel annotations on the custom builder/relation so generics resolve to the model, not the builder.
- If it reproduces on plain Laravel models, file a Larastan issue with a minimal reproducer — it indicates a wrong class reached determineBuilderClass.
Example fix
// before (custom builder annotated wrong)
/** @template TModel of \App\Builders\PostBuilder */
class Post extends Model {}
// after
class Post extends Model {}
/** @extends Builder<Post> */
class PostBuilder extends Builder {} Defensive patterns
Strategy: type-guard
Validate before calling
// before relying on builder resolution
if (! is_subclass_of($modelClassName, \Illuminate\Database\Eloquent\Model::class) && $modelClassName !== \Illuminate\Database\Eloquent\Model::class) {
throw new \InvalidArgumentException("$modelClassName must be an Eloquent Model.");
} Type guard
function isEloquentModel(string $class): bool
{
return class_exists($class) && (is_subclass_of($class, \Illuminate\Database\Eloquent\Model::class) || $class === \Illuminate\Database\Eloquent\Model::class);
} Prevention
- Only pass model class names (not builders, relations, or tables) into model-based analysis APIs.
- Keep @template TModel annotations on custom builders bound to the model class, not the builder itself.
- When writing custom relations/builders, mirror an existing Larastan-tested pattern.
When it happens
Trigger: A code path calls determineBuilderName()/determineBuilderClass() with a class name that is not (a subclass resolution of) Eloquent\Model — e.g. a Builder or query class name was passed where a model class was expected.
Common situations: Custom relation or builder code paths feeding a non-model type into model-based builder resolution; misconfigured generic template parameters where TModel was substituted with a builder or plain class instead of a model; internal extension bugs after Laravel/PPHPStan upgrades.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Class :: does not exist
- Function :: does not exist
- If this scenario happens, the "provides" method is removed…
AI-assisted analysis of larastan/larastan@79091ad281 (2026-09-15).
Data as JSON: /api/errors/490b4c4e758e6fd2.
Report an issue: GitHub.
Appendix: source
Thrown at src/Methods/BuilderHelper.php:294
public function getBuilderType(string $builderClassName, Type $modelType): ObjectType
{
if (! $this->reflectionProvider->getClass($builderClassName)->isGeneric()) {
return new ObjectType($builderClassName);
}
return new GenericObjectType($builderClassName, [$modelType]);
}
/**
* @throws MissingMethodFromReflectionException
* @throws InvalidArgumentException
*/
public function determineBuilderName(string $modelClassName): string
{
$modelReflection = $this->reflectionProvider->getClass($modelClassName);
if (! $modelReflection->is(Model::class)) {
throw new InvalidArgumentException($modelClassName . ' is not a Model.');
}
$method = $modelReflection->getNativeMethod('newEloquentBuilder');
if ($method->getDeclaringClass()->getName() === Model::class) {
$attrs = $modelReflection->getNativeReflection()->getAttributes('Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder'); //@phpstan-ignore argument.type (Attribute class might not exist)
if ($attrs !== []) {
$expr = $attrs[0]->getArgumentsExpressions()[0];
if ($expr instanceof ClassConstFetch && $expr->class instanceof Name) {
return $expr->class->toString();
}
}
}
$returnType = $method->getVariants()[0]->getReturnType();
View on GitHub (pinned to 79091ad281)