Home » Php » php – Eloquent "Query Builder" with "OR" operator neutralizes my Global Scope – Laravel

php – Eloquent "Query Builder" with "OR" operator neutralizes my Global Scope – Laravel

Posted by: admin July 12, 2020 Leave a comment

Questions:

⛳ What I need:

I am developing an application in Laravel 5.4 and I want a global scope that allows me to filter different elements of the application depending on the user that created them.


🌍 My global scope:

I have a class BaseEloquentModel.php who extends Eloquent and all my models extends from this class. I have a global scope as follow:

protected static function boot()
{
    parent::boot();
    static::addGlobalScope('', function(\Illuminate\Database\Eloquent\Builder $builder) use($userId) {
        /**
        * I get the name of the table with <code>(with(new static))->getTable()</code> 
        * and then filter the query for the <b>user_id</b> field
        */
        $builder->where(
            (with(new static))->getTable() . '.user_id', 
            '=',
            $userId
        );
    });
}

⛔ The problem

When I have a query like this, with or operator, the global scope is “neutralized”:

$qBuilderCars = Car::whereRaw("name like ? or id = ?", [
    '%' . $searchCriteria. '%',
    $searchCriteria
]);

If I call the toSql() method on $qBuilderCars I see that it “correctly” adds the AND operator to the end of the query.

select * from `cars` where name like ? or id = ? and user_id = ?

Maybe you’ve already noticed my problem … If the element’s builder, in this case cars, has used an OR operator, then the global scope will not help, since there is no parenthesis between where name like ? or id = ?. So the resulting query would be something similar to the following:

select * from `cars` where name like ? (or id = ? and user_id = ?)

So this query will return all cars whose name matches or whose ID is the one received and has been created by the user…

When what I need is:

select * from `cars` where (name like ? or id = ?) and user_id = ?

👎 My attempts

I tried to alter my global scope to try to make the AND operator that I add the most restrictive in the query, but without any success.

I can not manually add parentheses to all the application’s queries, so … Is there a way to add global parentheses from the global scope to the builder?


💡 The solution

The solution is to add parentheses to all the raw queries.

  • ✅✅ You can see the @Styx solution which I consider the most successful

  • I will also leave my answer, which acts directly inside the global scope, and which I consider interesting to be able to see how an \Illuminate\Database\Eloquent\Builder object works

How to&Answers:

Well, it seems that your solution to add parentheses is the best workaround, but I have a suggestion how to do that slightly better way.

  1. Create new class QueryBuilder. For example, in \App\Models\ namespace (app/Models/ folder):

    namespace App\Models;
    
    use Illuminate\Database\Query\Builder as EloquentQueryBuilder;
    
    class QueryBuilder extends EloquentQueryBuilder {
    
      public function whereRaw($sql, $bindings = [], $boolean = 'and')
      {
        return parent::whereRaw('('.$sql.')', $bindings, $boolean);
      }
    
    }
    
  2. Add this code to your BaseEloquentModel class:

    use Illuminate\Database\Eloquent\Model;
    use App\Models\QueryBuilder; // <-- addition
    
    class BaseEloquentModel extends Model {
      // ...
      protected function newBaseQueryBuilder()
      {
        $connection = $this->getConnection();
    
        return new QueryBuilder(
            $connection,
            $connection->getQueryGrammar(),
            $connection->getPostProcessor()
        );
      }
      // ...
    }
    

Now, all whereRaw() calls will automatically have parentheses around query.

Answer:

😕 The solution…?

I found a “solution”, before apply my global scope I loop through all where clauses whose type is raw:

protected static function boot()
{
    parent::boot();
    static::addGlobalScope('', function(\Illuminate\Database\Eloquent\Builder $builder) use($userId) {

       /**
        * My workaround to prevent a raw query from neutralizing the global scope.
        * We go through all the queries and, if they are raw, encapsulate them in parentheses.
        */
        $wheres = $builder->getQuery()->wheres;
        foreach ($wheres as $iWhere => $where) {
            // If where clause is "raw" I wrap with parenthesis.
            if ($where['type'] == 'raw') {
                $builder->getQuery()->wheres[$iWhere]["sql"] = "({$where['sql']})";
            }                                                      
        }

       /**
        * I get the name of the table with <code>(with(new static))->getTable()</code> 
        * and then filter the query for the <b>user_id</b> field
        */
        $builder->where(
            (with(new static))->getTable() . '.user_id', 
            '=',
            $userId
        );
    });
}

I do not know if this solution will have any unexpected repercussion or if it will affect in excess the performance of the queries…

Will return the next SQL:

select * from `cars` where (name like ? or id = ?) and `cars`.`user_id` = ?"