Laravel 4.2 introduced a new way of handling soft deletes
by making use of traits
and query builder macros
wrapped in as a feature, that many people asked for for a very long time – Global Scopes.
This might be very useful for everyone, however the docs are not very eloquent in this case. So keep on reading to learn how you can implement global scopes in your project – it’s really easy and lets you write even more expressive code for your Eloquent models.
Check the ready-to-use demo at …
https://github.com/jarektkaczyk/laravel4-global-scope-example
Below example applies to Laravel 4.2. For Laravel 5 solution available click here.
Applying global scope requires just 2 simple steps:
- Create a class
AbcScope
that implementsScopeInterface
- Boot that class in your Eloquent model calling
static::addGlobalScope(new AbcScope)
So basically you could just create the scope and then boot it in the model, which should use the scope.
However this is not going to be very reusable, so instead let’s take a bit longer path (and this is how SoftDeleting
is implemented in the Eloquent core):
- Create the scope
- Create a trait that will boot the scope and implement some handy methods
- Add single
use AbcTrait
line to all the models that will use the scope
NOTE: Below example assumes no namespaces for the Scope and/or Trait so you can place both in app/models
folder which is autoloaded. Linked demo uses namespaces in order to show both ways.
As an example let’s use Post
with draft/published feature. By default we would like to get only published posts, but when we are logged in to the admin area, we need to see also the drafts:
(In real life I would use whereNull('published_at')
as a constraint, however this would be exactly the same as SoftDeletingTrait
, so instead I’m gonna check for published=1
condition, for this is a bit trickier)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
// app/models/PublishedScope.php <?php use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\ScopeInterface; class PublishedScope implements ScopeInterface { /** * Apply scope on the query. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return void */ public function apply(Builder $builder) { $column = $builder->getModel()->getQualifiedPublishedColumn(); $builder->where($column, '=', 1); $this->addWithDrafts($builder); } /** * Remove scope from the query. * * @param Builder $builder * @return void */ public function remove(Builder $builder) { $query = $builder->getQuery(); $column = $builder->getModel()->getQualifiedPublishedColumn(); $bindingKey = 0; foreach ((array) $query->wheres as $key => $where) { if ($this->isPublishedConstraint($where, $column)) { $this->removeWhere($query, $key); // Here SoftDeletingScope simply removes the where // but since we use Basic where (not Null type) // we need to get rid of the binding as well $this->removeBinding($query, $bindingKey); } // Check if where is either NULL or NOT NULL type, // if that's the case, don't increment the key // since there is no binding for these types if ( ! in_array($where['type'], ['Null', 'NotNull'])) $bindingKey++; } } /** * Remove scope constraint from the query. * * @param \Illuminate\Database\Query\Builder $builder * @param int $key * @return void */ protected function removeWhere(BaseBuilder $query, $key) { unset($query->wheres[$key]); $query->wheres = array_values($query->wheres); } /** * Remove scope constraint from the query. * * @param \Illuminate\Database\Query\Builder $builder * @param int $key * @return void */ protected function removeBinding(BaseBuilder $query, $key) { $bindings = $query->getRawBindings()['where']; unset($bindings[$key]); $query->setBindings($bindings); } /** * Check if given where is the scope constraint. * * @param array $where * @param string $column * @return boolean */ protected function isPublishedConstraint(array $where, $column) { return ($where['type'] == 'Basic' && $where['column'] == $column && $where['value'] == 1); } /** * Extend Builder with custom method. * * @param \Illuminate\Database\Eloquent\Builder $builder */ protected function addWithDrafts(Builder $builder) { $builder->macro('withDrafts', function(Builder $builder) { $this->remove($builder); return $builder; }); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// app/models/PublishedTrait.php <?php trait PublishedTrait { /** * Boot the scope. * * @return void */ public static function bootPublishedTrait() { static::addGlobalScope(new PublishedScope); } /** * Get the name of the column for applying the scope. * * @return string */ public function getPublishedColumn() { return defined('static::PUBLISHED_COLUMN') ? static::PUBLISHED_COLUMN : 'published'; } /** * Get the fully qualified column name for applying the scope. * * @return string */ public function getQualifiedPublishedColumn() { return $this->getTable().'.'.$this->getPublishedColumn(); } /** * Get the query builder without the scope applied. * * @return \Illuminate\Database\Eloquent\Builder */ public static function withDrafts() { return with(new static)->newQueryWithoutScope(new PublishedScope); } } |
Usage
1. Apply the scope for the Post
model:
1 2 3 4 5 6 7 8 9 10 11 |
<?php use Illuminate\Database\Eloquent\Model as Eloquent; class Post extends Eloquent { use PublishedTrait; } |
2. Use it:
1 2 3 4 5 6 7 8 9 10 |
Post::all(); // only published Post::first(); // only published Post::withDrafts()->get(); // all the posts, using withDrafts static method Post::where('some_field', 'some_value')->withDrafts()->get(); // all the posts with some_field=some_value, using Builder extension method |
And that’s all!
Be sure to check the SoftDeletingTrait
too and play with the global scopes. I’m sure you will love it!