Stop duplicating your Eloquent query scopes and constraints. Re-use them as select statements with a new Laravel package.

In the Laravel framework, you've undoubtedly come across the Eloquent ORM. It makes working with your database a breeze as its query builder is both simple and incredibly powerful. It also has this concept of scopes, which you can apply globally to a model, or locally throughout your app. An example of a global scope is the built-in Soft Deletes feature.

You can chain local scopes to precisely define your query. This allows you to retrieve a specific set of records from your database. Take a look at this Post Model. It can have one or more comments, and it has scopes to determine if it has a subtitle and whether someone published it this year.

class Post extends Model
{
protected $casts = [
'published_at' => 'datetime',
];
 
public function comments()
{
return $this->hasMany(Comment::class);
}
 
public function scopeHasSubtitle($query)
{
$query->whereNotNull('subtitle');
}
 
public function scopeFromCurrentYear($query)
{
$query->whereYear('published_at', date('Y'));
}
 
public function scopeHasTenOrMoreComments($query)
{
$query->has('comments', '>=', 10);
}
}

With these scopes, we can filter down the records to our needs:

$allPosts = Post::query()->get();
$postsWithSubtitles = Post::query()->hasSubtitle()->get();
$postsFromCurrentYear = Post::query()->fromCurrentYear()->get();
$popularPostsFromCurrentYear = Post::query()->fromCurrentYear()->hasTenOrMoreComments()->get();

You get the idea: scopes make your code extremely readable, and you can do fascinating stuff like querying relationships, advanced subqueries and iterating using cursors. But what if you want to retrieve all records and then want to know whether a post is popular? You can iterate over the collection and determine the popularity, but then you would be recreating the logic from your local scopes. In the example above, this would be something like this:

Post::query()
->withCount('comments')
->get()
->each(function (Post $post) {
$recentlyPopularWithSubtitle = $post->subtitle
&& $post->published_at->isCurrentYear()
&& $post->comments_count >= 10;
});

While this is technically correct and maybe fine for smaller projects, this can become cumbersome in larger projects. That's where this new package comes in!

You only have to install the package using composer and add the macro to the query builder with the addMacro method.

composer require protonemedia/laravel-eloquent-scope-as-select

In the boot method of your AppServiceProvider:

ScopeAsSelect::addMacro();

This package allows you to re-use your scopes as if it was a select statement. With the addScopeAsSelect method, you can dynamically add an attribute to your Model, and call the scopes with a Closure. Let's take a look!

Post::query()
->addScopeAsSelect('recently_populair_with_subtitle', function ($query) {
$query->fromCurrentYear()->hasTenOrMoreComments()->hasSubtitle();
})
->get()
->each(function (Post $post) {
$post->recently_populair_with_subtitle;
});

Instead of rewriting all that logic, you can re-use your scopes in the Closure, and each Post Model will have a recently_populair_with_subtitle boolean attribute. You can add multiple selects, and you can use inline queries as well.

array:2 [
0 => array:6 [
"id" => 1
"title" => "foo",
"subtitle" => null,
"published_at" => "2019-06-01T12:00:00.000000Z"
"recently_populair_with_subtitle" => false
]
1 => array:6 [
"id" => 2
"title" => "foo"
"subtitle" => "bar",
"published_at" => "2020-12-01T12:00:00.000000Z"
"recently_populair_with_subtitle" => true
]
]

The documentation contains more examples, which you can find along with the package on GitHub. I'll do a live demonstration of this package on YouTube, scheduled for December 3 at 14:00 CET.

Update 8 dec: I'm currently working on another method that can flip/invert scopes. It'll probably end up in a new package/repository, but you can already check out the PR on GitHub.