Paulund

Laravel Eloquent Builder Vs Scopes

Laravel eloquent provides a simple way of making database queries in your application. It uses Models as an object to interact with a table. To fetch all records from a table you can simple use.

Post::get();

There are helpful methods to fetch data based on conditions.

// Fetch all published posts
Post::where('status', 'published')->get();

Eloquent Scopes

Eloquent scopes allow you to move the query logic into the models for ease of reusing the same conditions around your application. If we take the example above of querying for published posts and want to reuse this elsewhere in the application we can move this into a local scope.

class Post extends Model
{
    public function scopePublished($query)
    {
        return $query->where('status', 'published');
    }
}

Now whenever we want to query only the published posts then we can use the following code.

Post::published()->get();

This will then call the scopePublished method on the model and return all posts that are published.

Global Scopes

Local scopes are methods placed within the local model, Laravel also has functionality to create global scopes these have 2 benefits, first it allows the model to always use these query constraints, secondly it allows you to reuse the scope over multiple models.

Global scopes will always add these constraints to the queries on the models. A good example of this is Laravel's soft deletes, that will add a global scope to return where deleted_at is NULL.

public function apply(Builder $builder, Model $model)
{
    $builder->whereNull($model->getQualifiedDeletedAtColumn());
}

If we were to add this to the Post model, whenever we query for all posts Post::get() it will also query those where deleted_at is NULL. We can override this global scope to return all posts whether they are deleted or not by using the withTrashed method.

Post::withTrashed->get();

To allow you to reuse this constraint over multiple models you can boot the model with a global scope, allowing you to do this on multiple models to reuse the same constraints.

class Post extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new PublishedScope);
    }
}

Create Published Global Scope

To create a global scope you need to create a class that implements the Illuminate\Database\Eloquent\Scope interface which will add an apply method.

The apply method is where you will put your logic for the query constraints.

class PublishedScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('status', 'published');
    }
}

Then we can add this to the model by using the addGlobalScope method.

static::addGlobalScope(new PublishedScope);

The addGlobalScope can take 2 parameters, the first is the name of the scope, the second is the class of the scope. If you want the global scope to be accessed by the scope class name you can just pass in the scope class into the addGlobalScope method. If you want to change the name of the scope you can use then as the first parameter.

static::addGlobalScope('publishedPosts', new PublishedScope);

Now you can use this method when querying the published posts.

Post::publishedPosts()->get();

Anonymous Global Scopes

Instead of creating a new class for global scopes you can use a closure to create anonymous global scopes.

/**
 * The "booting" method of the model.
 *
 * @return void
 */
protected static function boot()
{
    parent::boot();

    static::addGlobalScope('published', function (Builder $builder) {
        $builder->where('status', 'published');
    });
}

These are good for adding constraints to your queries but is not good for query reusability.

Removing Global Scopes

If you would want to remove the global scopes of a model you can use the method withoutGlobalScope.

Post::withoutGlobalScope()->get();

If you want to remove a specific global scope then you can pass this in as a parameter to the method.

Post::withoutGlobalScope(PublishedScope::class)->get();

Extend Scopes

Global scopes are instaniated inside the Eloquent Builder withGlobalScope method, \Illuminate\Database\Eloquent\Builder::withGlobalScope.

Inside this method the scopes are querying an extend method.

if (method_exists($scope, 'extend')) {
    $scope->extend($this);
}

This extend method is used by SoftDelete scope to add additional methods to the eloquent builder by using marcos. Using the SoftDelete functionality if you want to return posts without the scope you will use the method withoutTrashed, this is how it's defined.

/**
 * Add the without-trashed extension to the builder.
 *
 * @param  \Illuminate\Database\Eloquent\Builder  $builder
 * @return void
 */
protected function addWithoutTrashed(Builder $builder)
{
    $builder->macro('withoutTrashed', function (Builder $builder) {
        $model = $builder->getModel();

        $builder->withoutGlobalScope($this)->whereNull(
            $model->getQualifiedDeletedAtColumn()
        );

        return $builder;
    });
}

This means that if you have a global scope but then want easy queries to turn off the global scope or to query the posts in a different way when the global scope is applied you can use the extend method to add new marco method to the builder.

Custom Eloquent Builder

Laravel has a nice way of overriding the eloquent query builder functionality and can also change the Collection object.

The base eloquent model has a newEloquentBuilder method that returns the same query builder class for each type of model in your application.

/**
 * Create a new Eloquent query builder for the model.
 *
 * @param  \Illuminate\Database\Query\Builder  $query
 * @return \Illuminate\Database\Eloquent\Builder|static
 */
public function newEloquentBuilder($query)
{
    return new Builder($query);
}

As this method is located on the model we can override this on a per model basis to create our own eloquent builder for each model.

We will create our own builder by extending the default eloquent builder.

<?php

namespace App\Builders;

use Illuminate\Database\Eloquent\Builder;

class PostBuilder extends Builder
{
    public function published()
    {
        
    }
}

Then you override the newEloquentBuilder in your model and return the new builder and remove the scopePublished method in your model.

Your scope will go from this.

/**
 * @param $query
 * @return
 */
public function scopePublished($query)
{
    return $query->whereStatus(self::STATUS_PUBLISHED);
}

To a method on your PostBuilder.

/**
 * @return PostBuilder
 */
public function published()
{
    $this->whereStatus(Post::STATUS_PUBLISHED);
    return $this;
}

You can still query the model in the same way as using scopes.

Post::published()->get();

This has allowed us to refactor our scopes outside of the model reducing the size of the model classes.

Custom Collection

Along with adding custom eloquent builders with your models you can also replace the collection that the eloquent builder returns.

When you do a get query to return multiple records the eloquent builder will return an eloquent collection Illuminate\Database\Eloquent\Collection. On the base model there's a method called newCollection.

/**
 * Create a new Eloquent Collection instance.
 *
 * @param  array  $models
 * @return \Illuminate\Database\Eloquent\Collection
 */
public function newCollection(array $models = [])
{
    return new Collection($models);
}

When Laravel needs to return multiple models it will run the data through this query to return a collection of models. As this is on the base model we can override this on the model to create our own collection.

/**
 * Create a new Eloquent Collection instance.
 *
 * @param  array  $models
 * @return \Illuminate\Database\Eloquent\Collection
 */
public function newCollection(array $models = [])
{
    return new PostCollection($models);
}

Now we can create our new PostCollection class where we can store common where or filter methods we would use on a collection of posts.