Remove a model’s Global Scopes in Laravel Nova

I am working on a Laravel project that has moderated reviews. Most of the time we only deal with reviews that have been approved, so the Review model has an ApprovedScope Global Scope to only show approved review, but staff members need to see un-approved reviews in our Nova admin so that we can approve, (or reject), reviews. Removing a Global Scope from a model for all of Nova is trickier than it appears at first glance, but there’s at least one way to do it, and maybe more.

Things that do not work

Call withoutGlobalScopes() in the Nova::serving() callback

Googling “remove global scope nova” turns up a post on Medium called Add or remove global scopes in Nova. Sounds promising! It claims we can remove global scopes by adding this to the boot method of the `NovaServiceProvider:

class NovaServiceProvider extends NovaApplicationServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Nova::serving(function () {
            \App\Rating::withoutGlobalScopes();
        });
    }
}

Cool! But it doesn’t work. When adding a global scope to a model using the model’s boot method like the Laravel documentation shows, the model’s boot method is called after the Nova::serving() callback. So in this situation we remove nothing from the Rating, then add a global scope later when the model is booted.

Call withoutGlobalScopes() in the model’s booted event

If a model has a global scope added during its boot then we should be able to remove it during the ‘booted’ event – or at least that’s what I thought, so I tried this in NovaServiceProvider::boot :

Nova::serving(function() {
    Event::listen('eloquent.booted: App\Rating', function($rating) {
        Rating::withoutGlobalScopes();
    });
}); 

No dice. I’m not certain why this doesn’t work, but some XDebug spelunking shows me that:

  1. There’s no withoutGlobalScopes() method on Laravel models, but when the model gets a method call that it doesn’t recognize it passes it to the model’s Builder instance.
  2. The Builder does have a withoutGlobalScopes method, so we don’t get an InvalidMethodException. In the Builder’s withoutGlobalScopes call the ApprovedScope is removed from the Builder’s list of global scopes.
  3. But the Global Scope is still applied to the query. My best guess is that a different query builder is used to actually generate results, or that later, when the models are about to be retrieved from the DB, the model re-passes the list of Global Scopes to the builder – and since we haven’t removed the Global Scope from the model it gets re-added to the Query Builder. If anyone knows what’s actually going on here I would love to know in the comments or on Twitter.

Something that does work

We need to remove the Global Scope after the model has booted and prevent it from being passed or re-passed to the Query Builder. How about a Model::withoutGlobalScopes method? The model’s addGlobalScope method comes from the Illuminate\Database\Eloquent\Concerns\HasGlobalScopes trait, and stores the global scopes in a static::$globalScopes[static::class] array. Creating our own HasRemovableGlobalScopes trait, with withoutGlobalScope and withoutGlobalScopes methods that mirror the signature of the Illuminate\Database\Eloquent\Builder withoutGlobalScope and withoutGlobalScopes methods can solve the problem, (also available as a Gist):

<?php

namespace App\Concerns;

use Closure;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;

trait HasRemovableGlobalScopes {

	/**
	 * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
	 */
	public static function withoutGlobalScope( $scope )
	{
		if (is_string($scope) && is_array(static::$globalScopes[static::class])) {
			Arr::forget(static::$globalScopes[static::class], $scope);
		} elseif ($scope instanceof Closure) {
			Arr::forget(static::$globalScopes[static::class], spl_object_hash($scope));
		} elseif ($scope instanceof Scope) {
			Arr::forget(static::$globalScopes[static::class], get_class($scope));
		}
	}

	/**
	 * @param \Illuminate\Database\Eloquent\Scope[]|string[] $scopes
	 */
	public static function withoutGlobalScopes( array $scopes = [])
	{
		if(empty($scopes)) {
			static::$globalScopes = [];
		} else {
			foreach($scopes as $scope) {
				static::withoutGlobalScope($scope);
			}
		}
	}
}

The withoutGlobalScope method mirrors the HasGlobalScopes::addGlobalScope method to remove a single global scope, and the withoutGlobalScopes method can accept an array of global scopes to remove or be called with no parameters to remove all global scopes, (the same as Builder::withoutGlobalScopes).

A drawback?

The fact that we have to do this to remove global scopes for Nova seems to be an oversight. I’m hopeful that withoutGlobalScope/withoutGlobalScopes methods will be added to future versions of Laravel. If that happens there will be a method name collision between the HasRemovableGlobalScopes trait’s methods and the first-party ones, so read the release notes if you’re going to use this method.

Something else that might work

After writing the HasRemovableGlobalScopes trait I realized it should be possible to create an additional global scope that undoes my original global scope. In this case something like this might work:

Nova::serving(function() {
    Event::listen('eloquent.booted: App\Rating', function($rating) {
        Rating::addGlobalScope(new AllRatingsScope);
    });
}); 

Where AllRatingsScope says to include all ratings. I haven’t tried this so don’t know what happens when there are two global scopes that specify opposite conditions. You might get no results, or the scope that’s applied last might win. Assuming that the scope applied last wins it’s still important to use the eloquent.booted event to make sure the “undo” scope is added after the original scope.

All together now

I’m sticking with the HasRemovableGlobalScopes trait. I feel like actually removing the Global Scope is more logical than adding another scope to undo what the first one does. Using the HasRemovableGlobalScopes trait, this is what my whole system looks like:

Apply the original global scope to the Rating, (or wherever you need a global scope), and use the HasRemovableGlobalScopes trait:

class Rating extends Model
{
	use HasRemovableGlobalScopes;

	// ... 

	protected static function boot()
	{
		parent::boot();
		static::addGlobalScope(new ApprovedScope);
	}

	// ...
}

Set the event listener in the NovaServiceProvider’s boot method to remove the global scope for Nova:

class NovaServiceProvider extends NovaApplicationServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
	    Nova::serving(function() {

		    Event::listen('eloquent.booted: App\Rating', function($rating) {
			    Log::info('rating booted ' . request()->url());
		    	Rating::withoutGlobalScopes();
	        });
	    });

	    parent::boot();
    }
}

It works well for me. A possible enhancement would be to have my own App\Model class that extends Laravel’s Model and uses HasRemovableGlobalScopes, then extend my other models from that. At the moment I don’t have my own App\Model, and don’t have a lot of places where I need to share between all my models, but if I find I’m sprinkling too many use HasRemovableGlobalScopes lines around my code base I’ll make the change.

Using CSS attribute selectors to simulate legacy HTML layout

Have you ever created new clean XHTML template, applied it to a CMS with years of back content, and discovered that the years of legacy HTML looks terrible?  Because you’re using a new XHTML doctype many, if not all, of the presentational attributes in the old HTML no longer work, however, CSS can be used to fix this.

You may say that the whole point of building a new template with XHTML & CSS is so that we won’t have any of the old <p align="right"> in our code, and you are right, but in situations when there can be thousands of pages of content it is often not practical to re-code them all using CSS, that’s where the attribute selectors come in.

Here is a very basic stylesheet that will help get you started:

img[align="right"], table[align="right"] {float:right;}
img[align="left"], table[align="left"] {float:left;}
img[align="center"], table[align="center"] {display:block;margin-left:auto;margin-right:auto;}

p[align="center"] {text-align:center;}
p[align="left"] {text-align:left;}
p[align="right"] {text-align:right;}

table[border="2"], table[border="2"] td {border:1px solid #000;}
table[border="1"], table[border="1"] td {border:1px solid #000;}
table[border="0"], table[border="0"] td {border:0;}

Although it would be nice to have one master stylesheet that we could drop in to a document and automatically have the old HTML look great, I believe that stylesheet would have to be extremely large, so I recommend using a base set of styles, (like the one above), and adding any extra styles to it that may be needed, (for example, img[border="5"] {border:5px solid #000} would not be needed every time).

What about browser compatibility? I’ve tested in FireFox 3, Safari 3, Internet Explorer 7, and Internet Explorer 6, and, if I remember correctly, Opera 9.  The only browser that didn’t render the styles as I would have liked to see them was IE6, which is no surprise, but also has a rapidly diminishing market share.

If you are looking for a way to keep your legacy HTML from looking terrible that doesn’t involve recoding it all, this may be the way for you to go.