We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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:
class Customer extends Model
{
protected $table = 'customers';
// Link the model to the custom builder
public function newEloquentBuilder($query): CustomerBuilder
{
return new CustomerBuilder($query);
}
}
Now, let's create a custom CustomerBuilder
class where you can define custom query methods:
use Illuminate\Database\Eloquent\Builder;
class CustomerBuilder extends Builder
{
public function active()
{
return $this->where('status', 'active');
}
public function hasRole(string $role)
{
return $this->where('role', $role);
}
}
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:
use Illuminate\Database\Eloquent\Builder;
/**
* @template-extends Builder<Customer>
*/
class CustomerBuilder extends Builder
{
/**
* @return static
*/
public function active()
{
return $this->where('status', 'active');
}
/**
* @param string $role
* @return static
*/
public function hasRole(string $role)
{
return $this->where('role', $role);
}
}
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 thatCustomerBuilder
extends Laravel'sBuilder
class but is tied to theCustomer
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:
use Eloquent;
/**
* @method static CustomerBuilder|static query()
* @method CustomerBuilder newQuery()
* @mixin Eloquent
*/
class Customer extends Model
{
protected $table = 'customers';
public function newEloquentBuilder($query): CustomerBuilder
{
return new CustomerBuilder($query);
}
}
- The
@method static CustomerBuilder|static query()
annotation informs static analyzers that when we call thequery()
method, we should expect aCustomerBuilder
. - 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:
-
Better IDE Support: Your IDE can now provide autocompletion for methods like
active()
andhasRole()
when you're working withCustomer::query()
or$customer->newQuery()
. -
Static Analysis Improvements: Tools like PHPStan or Psalm can perform better type checks, catching potential bugs early in the development process.
-
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:
// Fetch all active company users with a specific role
$activeManagers = Customer::query()->active()->hasRole('manager')->get();
// Or using the model instance:
$customer = new Customer();
$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.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.