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

⛳ 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

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.
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);
}
}
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.

😕 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` = ?"

Related

Laravel Eloquent - Model extends other model

I have a question about extending my own Models eloquent.
In the project I am currently working on is table called modules and it contains list of project modules, number of elements of that module, add date etc.
For example:
id = 1; name = 'users'; count = 120; date_add = '2007-05-05';
and this entity called users corresponds to model User (Table - users) so that "count" it's number of Users
and to update count we use script running every day (I know that it's not good way but... u know).
In that script is loop and inside that loop a lot of if statement (1 per module) and inside the if a single query with count. According to example it's similar to:
foreach($modules as $module) {
if($module['name'] == 'users') {
$count = old_and_bad_method_to_count('users', "state = 'on'");
}
}
function old_and_bad_method_to_count($table, $sql_cond) {}
So its look terrible.
I need to refactor that code a little bit, because it's use a dangerous function instead of Query/Builder or Eloquent/Model and looks bad.
I came up with an idea that I will use a Models and create Interface ElementsCountable and all models that do not have an interface will use the Model::all()->count(), and those with an interface will use the interface method:
foreach ($modules as $module) {
$className = $module->getModelName();
if($className) {
$modelInterfaces = class_implements($className);
if(isset($modelInterfaces[ElementsCountable::class])) {
/** #var ElementsCountable $className */
$count = $className::countModuleElements();
} else {
/** #var Model $className */
$count = $className::all()->count();
}
}
}
in method getModelName() i use a const map array (table -> model) which I created, because a lot of models have custom table name.
But then I realize that will be a good way, but there is a few records in Modules that use the same table, for example users_off which use the same table as users, but use other condition - state = 'off'
So it complicated things a little bit, and there is a right question: There is a good way to extends User and add scope with condition on boot?
class UserOff extends User
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(function (Builder $builder) {
$builder->where('state', '=', 'off');
});
}
}
Because I have some concerns if this is a good solution. Because all method of that class NEED always that scope and how to prevent from method withoutGlobalScope() and what about other complications?
I think it's a good solution to create the UserOff model with the additional global scope for this purpose.
I also think the solution I would want to implement would allow me to do something like
$count = $modules->sum(function ($module) {
$className = $module->getModelName();
return $className::modulesCount();
}
I would create an interface ModulesCountable that mandates a modulesCount() method on each of the models. The modulesCount() method would return either the default count or whatever current implementation you have in countModuleElements().
If there are a lot of models I would probably use a trait DefaultModulesCount for the default count, and maybe the custom version too eg. ElementsModuleCount if that is consistent.

how to reset default scope in yii2

I am using default scope in my project and it works fine.
public static function find()
{
return parent::find()->where(['is_deleted' => 0]);
}
But now, I want to show all the deleted records in the report section.
How can I skip default scope for particular query only?
Use this to clear or redefine your condition:
$model = Model::find()->where('');
If you want to make sure that you're using fresh query (without any params or conditions), you need to create new ActiveQuery object for given model.
$query = Yii::createObject(ActiveQuery::className(), [Post::class]);
Or add a helper method in model itself:
public static function freshFind()
{
return parent::find();
}
and use it instead of Post::find().
You could avoid the use of find() ..using a findBySql
$sql = 'SELECT * FROM product';
$product= Product::findBySql($sql,)->all();
in this way you all the models of product ..
and you could also use
$sql = 'SELECT * FROM ' . Product::tableName() ;
for avoid explici table name for Products

Laravel Custom Model Methods

Whenever I add additional logic to Eloquent models, I end up having to make it a static method (i.e. less than ideal) in order to call it from the model's facade. I've tried searching a lot on how to do this the proper way and pretty much all results talk about creating methods that return portions of a Query Builder interface. I'm trying to figure out how to add methods that can return anything and be called using the model's facade.
For example, lets say I have a model called Car and want to get them all:
$cars = Car::all();
Great, except for now, let's say I want to sort the result into a multidimensional array by make so my result may look like this:
$cars = array(
'Ford' => array(
'F-150' => '...',
'Escape' => '...',
),
'Honda' => array(
'Accord' => '...',
'Civic' => '...',
),
);
Taking that theoretical example, I am tempted to create a method that can be called like:
$cars = Car::getAllSortedByMake();
For a moment, lets forget the terrible method name and the fact that it is tightly coupled to the data structure. If I make a method like this in the model:
public function getAllSortedByMake()
{
// Process and return resulting array
return array('...');
}
And finally call it in my controller, I will get this Exception thrown:
Non-static method Car::getAllSortedByMake() should not be called statically, assuming $this from incompatible context
TL;DR: How can I add custom functionality that makes sense to be in the model without making it a static method and call it using the model's facade?
Edit:
This is a theoretical example. Perhaps a rephrase of the question would make more sense. Why are certain non-static methods such as all() or which() available on the facade of an Eloquent model, but not additional methods added into the model? This means that the __call magic method is being used, but how can I make it recognize my own functions in the model?
Probably a better example over the "sorting" is if I needed to run an calculation or algorithm on a piece of data:
$validSPG = Chemical::isValidSpecificGravity(-1.43);
To me, it makes sense for something like that to be in the model as it is domain specific.
My question is at more of a fundamental level such as why is all()
accessible via the facade?
If you look at the Laravel Core - all() is actually a static function
public static function all($columns = array('*'))
You have two options:
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
or
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Both will allow you to do
Car::getAllSortedByMake();
Actually you can extend Eloquent Builder and put custom methods there.
Steps to extend builder :
1.Create custom builder
<?php
namespace App;
class CustomBuilder extends \Illuminate\Database\Eloquent\Builder
{
public function test()
{
$this->where(['id' => 1]);
return $this;
}
}
2.Add this method to your base model :
public function newEloquentBuilder($query)
{
return new CustomBuilder($query);
}
3.Run query with methods inside your custom builder :
User::where('first_name', 'like', 'a')
->test()
->get();
for above code generated mysql query will be :
select * from `users` where `first_name` like ? and (`id` = ?) and `users`.`deleted_at` is null
PS:
First Laurence example is code more suitable for you repository not for model, but also you can't pipe more methods with this approach :
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
Second Laurence example is event worst.
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Many people suggest using scopes for extend laravel builder but that is actually bad solution because scopes are isolated by eloquent builder and you won't get the same query with same commands inside vs outside scope. I proposed PR for change whether scopes should be isolated but Taylor ignored me.
More explanation :
For example if you have scopes like this one :
public function scopeWhereTest($builder, $column, $operator = null, $value = null, $boolean = 'and')
{
$builder->where($column, $operator, $value, $boolean);
}
and two eloquent queries :
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->where('first_name', 'like', 'b');
})->get();
vs
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->whereTest('first_name', 'like', 'b');
})->get();
Generated queries would be :
select * from `users` where (`first_name` like ? and `first_name` like ?) and `users`.`deleted_at` is null
vs
select * from `users` where (`first_name` like ? and (`id` = ?)) and `users`.`deleted_at` is null
on first sight queries look the same but there are not. For this simple query maybe it does not matter but for complicated queries it does, so please don't use scopes for extending builder :)
for better dynamic code, rather than using Model class name "Car",
just use "static" or "self"
public static function getAllSortedByMake()
{
//to return "Illuminate\Database\Query\Builder" class object you can add another where as you want
return static::where('...');
//or return already as collection object
return static::where('...')->get();
}
Laravel model custom methods -> best way is using traits
Step #1: Create a trait
Step #2: Add the trait to model
Step #3: Use the method
User::first()->confirmEmailNow()
app/Model/User.php
use App\Traits\EmailConfirmation;
class User extends Authenticatable
{
use EmailConfirmation;
//...
}
app/Traits/EmailConfirmation.php
<?php
namespace App\Traits;
trait EmailConfirmation
{
/**
* Set email_verified_at to now and save.
*
*/
public function confirmEmailNow()
{
$this->email_verified_at = now();
$this->save();
return $this;
}
}

select random row for each category in array [duplicate]

Here is how I query my database for some words
$query = $qb->select('w')
->from('DbEntities\Entity\Word', 'w')
->where('w.indictionary = 0 AND w.frequency > 3')
->orderBy('w.frequency', 'DESC')
->getQuery()
->setMaxResults(100);
I'm using mysql and I'd like to get random rows that match the criteria, I would use order by rand() in my query.
I found this similar question which basically suggests since ORDER BY RAND is not supported in doctrine, you can randomize the primary key instead. However, this can't be done in my case because I have a search criteria and a where clause so that not every primary key will satisfy that condition.
I also found a code snippet that suggests you use the OFFSET to randomize the rows like this:
$userCount = Doctrine::getTable('User')
->createQuery()
->select('count(*)')
->fetchOne(array(), Doctrine::HYDRATE_NONE);
$user = Doctrine::getTable('User')
->createQuery()
->limit(1)
->offset(rand(0, $userCount[0] - 1))
->fetchOne();
I'm a little confused as to whether this will help me work around the lack of support for order by random in my case or not. I was not able to add offset after setMaxResult.
Any idea how this can be accomplished?
The Doctrine team is not willing to implement this feature.
There are several solutions to your problem, each having its own drawbacks:
Add a custom numeric function: see this DQL RAND() function
(might be slow if you have lots of matching rows)
Use a native query
(I personally try to avoid this solution, which I found hard to maintain)
Issue a raw SQL query first to get some IDs randomly, then use the DQL WHERE x.id IN(?) to load the associated objects, by passing the array of IDs as a parameter.
This solution involves two separate queries, but might give better performance than the first solution (other raw SQL techniques than ORDER BY RAND() exist, I won't detail them here, you'll find some good resources on this website).
Follow these steps:
Define a new class at your project as:
namespace My\Custom\Doctrine2\Function;
use Doctrine\ORM\Query\Lexer;
class Rand extends \Doctrine\ORM\Query\AST\Functions\FunctionNode
{
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'RAND()';
}
}
Register the class config.yml:
doctrine:
orm:
dql:
numeric_functions:
Rand: My\Custom\Doctrine2\Function\Rand
Use it directly as:
$qb->addSelect('RAND() as HIDDEN rand')->orderBy('rand()'); //Missing curly brackets
In line with what Hassan Magdy Saad suggested, you can use the popular DoctrineExtensions library:
See mysql implementation here: https://github.com/beberlei/DoctrineExtensions/blob/master/src/Query/Mysql/Rand.php
# config.yml
doctrine:
orm:
dql:
numeric_functions:
rand: DoctrineExtensions\Query\Mysql\Rand
Tested in Doctrine ORM 2.6.x-dev, you can then actually do:
->orderBy('RAND()')
Or you could do this -->
$words = $em->getRepository('Entity\Word')->findAll();
shuffle($words);
Of course this would be very inefficient if you have many records so use with caution.
Why not to use repository?
<?php
namespace Project\ProductsBundle\Entity;
use Doctrine\ORM;
class ProductRepository extends ORM\EntityRepository
{
/**
* #param int $amount
* #return Product[]
*/
public function getRandomProducts($amount = 7)
{
return $this->getRandomProductsNativeQuery($amount)->getResult();
}
/**
* #param int $amount
* #return ORM\NativeQuery
*/
public function getRandomProductsNativeQuery($amount = 7)
{
# set entity name
$table = $this->getClassMetadata()
->getTableName();
# create rsm object
$rsm = new ORM\Query\ResultSetMapping();
$rsm->addEntityResult($this->getEntityName(), 'p');
$rsm->addFieldResult('p', 'id', 'id');
# make query
return $this->getEntityManager()->createNativeQuery("
SELECT p.id FROM {$table} p ORDER BY RAND() LIMIT 0, {$amount}
", $rsm);
}
}
For me, the most useful way was to create two arrays where i say order type and different properties of the Entity. For example:
$order = array_rand(array(
'DESC' => 'DESC',
'ASC' => 'ASC'
));
$column = array_rand(array(
'w.id' => 'w.id',
'w.date' => 'w.date',
'w.name' => 'w.name'
));
You could add more entries to array $column like criteria.
Afterwards, you can build your query with Doctrine adding $column and $order inside ->orderBy. For example:
$query = $qb->select('w')
->from('DbEntities\Entity\Word', 'w')
->where('w.indictionary = 0 AND w.frequency > 3')
->orderBy($column, $order)
->getQuery()
->setMaxResults(100);
This way improved the performance of my application. I hope this helps someone.
Shuffling can be done on the query (array) result, but shuffling does not pick randomly.
In order to pick randomly from an entity I prefer to do this in PHP, which might slow the random picking, but it allows me to keep control of testing what I am doing and makes eventual debugging easier.
The example below puts all IDs from the entity into an array, which I can then use to "random-treat" in php.
public function getRandomArt($nbSlotsOnPage)
{
$qbList=$this->createQueryBuilder('a');
// get all the relevant id's from the entity
$qbList ->select('a.id')
->where('a.publicate=true')
;
// $list is not a simple list of values, but an nested associative array
$list=$qbList->getQuery()->getScalarResult();
// get rid of the nested array from ScalarResult
$rawlist=array();
foreach ($list as $keyword=>$value)
{
// entity id's have to figure as keyword as array_rand() will pick only keywords - not values
$id=$value['id'];
$rawlist[$id]=null;
}
$total=min($nbSlotsOnPage,count($rawlist));
// pick only a few (i.e.$total)
$keylist=array_rand($rawlist,$total);
$qb=$this->createQueryBuilder('aw');
foreach ($keylist as $keyword=>$value)
{
$qb ->setParameter('keyword'.$keyword,$value)
->orWhere('aw.id = :keyword'.$keyword)
;
}
$result=$qb->getQuery()->getResult();
// if mixing the results is also required (could also be done by orderby rand();
shuffle($result);
return $result;
}
#Krzysztof's solution is IMHO best here, but RAND() is very slow on large queries, so i updated #Krysztof's solution to gives less "random" results, but they are still random enough. Inspired by this answer https://stackoverflow.com/a/4329492/839434.
namespace Project\ProductsBundle\Entity;
use Doctrine\ORM;
class ProductRepository extends ORM\EntityRepository
{
/**
* #param int $amount
* #return Product[]
*/
public function getRandomProducts($amount = 7)
{
return $this->getRandomProductsNativeQuery($amount)->getResult();
}
/**
* #param int $amount
* #return ORM\NativeQuery
*/
public function getRandomProductsNativeQuery($amount = 7)
{
# set entity name
$table = $this->getClassMetadata()
->getTableName();
# create rsm object
$rsm = new ORM\Query\ResultSetMapping();
$rsm->addEntityResult($this->getEntityName(), 'p');
$rsm->addFieldResult('p', 'id', 'id');
# sql query
$sql = "
SELECT * FROM {$table}
WHERE id >= FLOOR(1 + RAND()*(
SELECT MAX(id) FROM {$table})
)
LIMIT ?
";
# make query
return $this->getEntityManager()
->createNativeQuery($sql, $rsm)
->setParameter(1, $amount);
}
}
I hope this would help others:
$limit = $editForm->get('numberOfQuestions')->getData();
$sql = "Select * from question order by RAND() limit $limit";
$statement = $em->getConnection()->prepare($sql);
$statement->execute();
$questions = $statement->fetchAll();
Note here the table question is an AppBundle:Question Entity. Change the details accordingly. The number of questions is taken from the edit form, make sure to check the variable for the form builder and use accordingly.
First get the MAX value from DB table & then use this as offset in PHP i.e
$offset = mt_rand(1, $maxId)
I know this is an old question. But I used the following solution to get the random row.
Using an EntityRepository method:
public function findOneRandom()
{
$id_limits = $this->createQueryBuilder('entity')
->select('MIN(entity.id)', 'MAX(entity.id)')
->getQuery()
->getOneOrNullResult();
$random_possible_id = rand($id_limits[1], $id_limits[2]);
return $this->createQueryBuilder('entity')
->where('entity.id >= :random_id')
->setParameter('random_id', $random_possible_id)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
Probably the easiest (but not necessarily the smartest) way to get a single object result ASAP would be implementing this in your Repository class:
public function findOneRandom()
{
$className = $this->getClassMetadata()->getName();
$counter = (int) $this->getEntityManager()->createQuery("SELECT COUNT(c) FROM {$className} c")->getSingleScalarResult();
return $this->getEntityManager()
->createQuery("SELECT ent FROM {$className} ent ORDER BY ent.id ASC")
->setMaxResults(1)
->setFirstResult(mt_rand(0, $counter - 1))
->getSingleResult()
;
}
Just add the following:
->orderBy('RAND()')

Escape raw SQL queries in Laravel 4

How does one go about escaping parameters passed to a raw query in Laravel 4? I expected something like DB::escape() (which rings a bell from Laravel 3) and also attempted DB::quote() (which I thought could be available through the PDO object)
$query = DB::select("SELECT * FROM users WHERE users.id = " . DB::escape($userId));
We can't use the select method with placeholders as the above is just a simplified example of what we are trying to achieve. We have a large custom query with a few nested select queries that cannot be adapted to the query builder.
What is the best approach to escaping something prior to inserting in Laravel 4?
EDIT:
I've just discovered that you can access the PDO object and use the quote function on it this way. Is this still the best approach, or is there an easier way to access this function?
DB::connection()->getPdo()->quote("string to quote");
You can quote your strings this way, through the DB facade.
DB::connection()->getPdo()->quote("string to quote");
I did put this answer in my question when I discovered it, however I've now put it in as an actual answer to make it easier for others to find.
$value = Input::get("userID");
$results = DB::select( DB::raw("SELECT * FROM users WHERE users.id = :value"), array(
'value' => $value,
));
More Details HERE
You may also try this, (Read Documentation)
$results = DB::select('SELECT * FROM users WHERE users.id = ?', array($userId));
Two answers here, that I use, have less verbose solutions built into the DB facade.
First, value quoting:
// From linked answer
DB::connection()->getPdo()->quote("string to quote");
// In the DB facade
DB::getPdo()->quote('string to quote');
Second, identifier quoting (table and column names):
// From linked answer
DB::table('x')->getGrammar()->wrap('table.column');
// In the DB facade
DB::getQueryGrammar()->wrap('table.column');
I found this question when looking for generic sql escaping in Laravel. What I actually needed though was table/column name escaping. So, for future reference:
/**
* Quotes database identifier, e.g. table name or column name.
* For instance:
* tablename -> `tablename`
* #param string $field
* #return string
*/
function db_quote_identifier($field) {
static $grammar = false;
if (!$grammar) {
$grammar = DB::table('x')->getGrammar(); // The table name doesn't matter.
}
return $grammar->wrap($field);
}
I'm using this in my helpers.php at Laravel 5:
if ( ! function_exists('esc_sql'))
{
function esc_sql($string)
{
return app('db')->getPdo()->quote($string);
}
}
Then I can use esc_sql function where I need to pergorm escaping for raw SQL queries.
Here's a fuller example, showing how to escape both values and columns and extend Laravel's querybuilder:
<?php
namespace App\Providers;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\ServiceProvider;
class DatabaseQueryBuilderMacroProvider extends ServiceProvider {
public function register() {
Builder::macro('whereInSet', function($columnName, $value) {
/** #var \Illuminate\Database\Query\Grammars\Grammar $grammar */
$grammar = $this->getGrammar();
return $this->whereRaw('FIND_IN_SET(?,' . $grammar->wrap($columnName) . ')', [$value]);
});
}
}
PHP Heredoc
<?php
use Illuminate\Support\Facades\DB;
$sql = <<<SQL
WITH table1 AS(SELECT...
SQL;
$parameters = [1,2,3,...]
$table = DB::select($sql, $parameters);

Categories