Builders, factories and collections¶
Three places where Laravel resolves a class at runtime that static analysis cannot guess. In each case naming the class on the model, and documenting the generic, is what makes the chain type check.
Custom model builders¶
A custom builder gives better analysis than model scopes do, and keeps the model class smaller. Point the model at it with Laravel's attribute:
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/** @extends Builder<User> */
class UserBuilder extends Builder
{
/** @return $this */
public function active(): static
{
$this->where('active', true);
return $this;
}
}
#[UseEloquentBuilder(UserBuilder::class)]
class User extends Model
{
}
The @extends Builder<User> on the builder is what ties it to the model, and it
is the one annotation you do need. The attribute carries the rest, so there is no
trait to add and no generic to repeat.
The HasBuilder trait works too, if you prefer it or are already using it. It
needs its generic documented, since that is where the builder type comes from:
class User extends Model
{
/** @use HasBuilder<UserBuilder> */
use HasBuilder;
protected static string $builder = UserBuilder::class;
}
Model factories¶
#[UseFactory] is resolved the same way, so a factory needs no repeated
generic either. The HasFactory trait is still required, since that is what
puts factory() on the model:
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/** @extends Factory<User> */
class UserFactory extends Factory
{
/** @return array<string, mixed> */
public function definition(): array
{
return [];
}
}
#[UseFactory(UserFactory::class)]
class User extends Model
{
use HasFactory;
}
The @extends Factory<User> on the factory is what says which model it builds,
so a factory pointed at the wrong model is caught rather than assumed.
At level 6 and above, document the trait's generic too
HasFactory is generic, so missingType.generics asks for its type
parameter whether or not the attribute is present. Adding it costs one line
and silences that:
The $factory property and a newFactory() override are both understood as
well, and take precedence in that order.
Custom model collections¶
Same shape. #[CollectedBy] is resolved, so a custom collection needs no trait:
use Illuminate\Database\Eloquent\Attributes\CollectedBy;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/** @extends Collection<array-key, User> */
final class UserCollection extends Collection
{
}
#[CollectedBy(UserCollection::class)]
class User extends Model
{
}
The HasCollection trait is equally understood, and is what you want when one
collection serves several models, since the generic can then say which:
/**
* @template TKey of array-key
* @template TModel of \Illuminate\Database\Eloquent\Model
* @extends Collection<TKey, TModel>
*/
class GeneralCollection extends Collection
{
}
class User extends Model
{
/** @use HasCollection<GeneralCollection<int, static>> */
use HasCollection;
protected static string $collectionClass = GeneralCollection::class;
}