#development #eloquent #laravel #php

When working with Eloquent models in Laravel, customizing the query builder can greatly improve code readability and flexibility. However, maintaining type safety and providing accurate autocompletion in IDEs (like PHPStorm or VSCode) can be challenging, especially when dealing with custom builders. Fortunately, the @template-extends annotation can help provide better static analysis and type hinting for your code. In this post, we'll discuss how to properly annotate a custom Eloquent Builder in Laravel using the @template-extends docstring.

What is a custom Eloquent Builder?

In Laravel, the Eloquent query builder is the core tool for interacting with the database through your models. Sometimes, you may want to extend its functionality by creating custom methods that apply to specific models. For example, you might have a Customer model with a custom builder that includes specific query scopes or filters.

Let's say you have the following Customer model:

 1class Customer extends Model
 2{
 3    protected $table = 'customers';
 4
 5    // Link the model to the custom builder
 6    public function newEloquentBuilder($query): CustomerBuilder
 7    {
 8        return new CustomerBuilder($query);
 9    }
10}

Now, let's create a custom CustomerBuilder class where you can define custom query methods:

 1use Illuminate\Database\Eloquent\Builder;
 2
 3class CustomerBuilder extends Builder
 4{
 5    public function active()
 6    {
 7        return $this->where('status', 'active');
 8    }
 9
10    public function hasRole(string $role)
11    {
12        return $this->where('role', $role);
13    }
14}

Problem: proper type hinting and autocompletion

When using this custom builder, your IDE might not be able to infer the correct type for CustomerBuilder and will default to the generic Builder. This leads to a lack of autocompletion for custom methods like active() or hasRole(). Additionally, static analysis tools (like PHPStan or Psalm) may struggle with type inference when chaining Eloquent queries.

To solve this, we can use PHP's @template and @extends annotations.

The Role of @template-extends

To improve type inference, we use the @template and @extends annotations to specify that our custom builder extends the base Builder but is tied to the Customer model.

Here's how you can annotate the CustomerBuilder class:

 1use Illuminate\Database\Eloquent\Builder;
 2
 3/**
 4 * @template-extends Builder<Customer>
 5 */
 6class CustomerBuilder extends Builder
 7{
 8    /**
 9     * @return static
10     */
11    public function active()
12    {
13        return $this->where('status', 'active');
14    }
15
16    /**
17     * @param string $role
18     * @return static
19     */
20    public function hasRole(string $role)
21    {
22        return $this->where('role', $role);
23    }
24}

In this example:

  • @template-extends Builder<Customer> tells the static analyzer that this builder operates on a model type (in our case, Customer) and makes it clear that CustomerBuilder extends Laravel's Builder class but is tied to the Customer model.

Setting @template-extends in the model

Now that we've annotated our builder, we also need to ensure the model is correctly type-hinted when using this builder. Here's how we annotate the Customer model:

 1use Eloquent;
 2
 3/**
 4 * @method static CustomerBuilder|static query()
 5 * @method CustomerBuilder newQuery()
 6 * @mixin Eloquent
 7 */
 8class Customer extends Model
 9{
10    protected $table = 'customers';
11
12    public function newEloquentBuilder($query): CustomerBuilder
13    {
14        return new CustomerBuilder($query);
15    }
16}
  • The @method static CustomerBuilder|static query() annotation informs static analyzers that when we call the query() method, we should expect a CustomerBuilder.
  • The newEloquentBuilder() method is overridden to return our custom builder, making the connection between the model and builder.

Why Use @template-extends Builder<Customer>?

Using @template-extends Builder<Customer> ensures that:

  1. Better IDE Support: Your IDE can now provide autocompletion for methods like active() and hasRole() when you're working with Customer::query() or $customer->newQuery().

  2. Static Analysis Improvements: Tools like PHPStan or Psalm can perform better type checks, catching potential bugs early in the development process.

  3. Cleaner Code: By correctly type-hinting your builder, you avoid needing to manually cast or guess at types in your code, making it more maintainable.

Example Usage

Here's how you can now use the custom builder with type hints:

1// Fetch all active company users with a specific role
2$activeManagers = Customer::query()->active()->hasRole('manager')->get();
3
4// Or using the model instance:
5$customer = new Customer();
6$activeAdmins = $customer->newQuery()->active()->hasRole('admin')->get();

In both cases, your IDE will provide autocompletion for active() and hasRole().

Conclusion

By leveraging the @template-extends Builder<Customer> docstring in your custom Laravel Eloquent builder, you can drastically improve the static analysis and type inference in your code. This makes it easier to work with custom query builders, providing more robust autocompletion and type checking in your development environment. With these annotations, your code will be cleaner, safer, and more maintainable.