Decrement function, decrement all the rows in the tables - php

Im using laravel v3.2.12-4, and I have an problem with the decrement function. Instead of update only one row, this method affects all the rows in the column. Im using Eloquent, and I have a many_to_many relationship.
The code that constains the decrement method is:
foreach ($ids as $id) {
$indicator = Indicator::find($id);
$tags = $indicator->tags()->get();
foreach ($tags as $tag) {
$indicator->tags()->detach($tag->id);
if ($tag->frequency == 1) {
$tag->delete();
} else {
// I have to made this code to fix the problem with decrement function
// But i want to use decrement
$tag->frequency = $tag->frequency - 1;
$tag->save();
// This dosnt work for me.
// $tag->decrement('frequency');
}
}
$indicator->delete();
}
In the model class Indicator i made the relation with this function:
public function tags()
{
return $this->has_many_and_belongs_to('Tag');
}
In the model class Tag i made the relation with this function:
public function indicators()
{
return $this->has_many_and_belongs_to('Indicator');
}
Well, if I made an update to the column this result OK for me, but when If I use the decrement function this affect all the rows and I don't know if this a bug or something with this method.
Thanks.

This is how it's designed. The decrement() method is actually defined on the Query Builder and not on Eloquents builder. What this means is that when you call $tag->decrement('frequency') it's actually falling through to the QB and simply running a query like UPDATE tag SET frequency=frequency - 1. Notice that there's no WHERE clause?
You could still use the decrement() method but you'd have to do it like this.
$tag->where_id($tag->id)->decrement('frequency');
Now you've set the WHERE clause and only that tag will be decremented. Of course, the cleaner solution is what you've got. Or perhaps this.
$tag->frequency--;
Not tested, may throw an error of some sort.

Related

Dynamic model filter in Laravel's Eloquent

I'm looking for a way to make a dynamic & global model filter in Laravel.
I'm imagining a function like the following in my User.php model:
public function filter() {
return ($someVariable === true);
}
Whenever I do a query using Eloquent's query builder, I only want users to show up in the collection when the filter above returns true. I would have thought a feature like that existed, but a quick look at the documentation suggests otherwise. Or did I miss it?
I believe what you're looking for is Query Scopes.
They are methods that may be defined in a global or local context, that mutate the current query for a given model.
https://laravel.com/docs/5.5/eloquent#query-scopes
For example:
Lets say I have a database table called "Teams" and it has a column on it called "Wins." If I wanted to retrieve all Teams that had a number of Wins between Aand B I could write the following Local scope method on the teams model:
public function scopeWinsBetween($query, int $min, int $max)
{
return $query->whereBetween('wins', $min, $max);
}
And it could be invoked as such:
$teams = Teams::winsBetween(50, 100)->get();
I think you could use Collection macro but you will need to suffix all your eloquent get(); to get()->userDynamicFilter();
Collection::macro('userDynamicFilter', function () {
//$expected = ...
return $this->filter(function ($value) use($expected) {
return $value == $expected;
});
});
Thanks. For now I've simply added a post filter option to the models using the following code:
// Apply a post filter on the model collection
$data = $data->filter(function($modelObject) {
return (method_exists($modelObject, 'postFilter')) ? $modelObject->postFilter($modelObject) : true;
});
in Illuminate/Database/Eloquent/Builder.php's get() function, after creating the collection. This allows me to add a function postFilter($model) into my model which returns either true or false.
Probably not the cleanest solution but a working one for now.

Object returning three value instead of one. Laravel

I have used following code:
public function verify($id,$sid)
{
$sfees = sfee::where('student_id', $sid)->first(['mfee_id']);//mfee_id is the column I'm trying to recover
foreach ($sfees as $sfee) {
echo $sfee;
}
The problem is that it is returning three 1. While it was supposed to return only one.
If i echo $sfees before foreach it returns only one value: {"mfee_id":1}.
What is the problem? Can anyone help me?
You are asking for one model, you don't need to try and iterate it, its a single model (object). (first() can return null though, so you should check)
$sfees->mfee_id;
If you just want the value of that column for that one record:
$mfee_id = sfee::where('student_id', $sid)->value('mfee_id');
Laravel 5.2 Docs - Query Builder - Retrieving Results - Retrieving A Single Row / Column From A Table
You should use return statement for returning from function, not echo.
public function verify($id,$sid)
{
$sfees = sfee::where('student_id', $sid)->first(['mfee_id']);//mfee_id is the column I'm trying to recover
return $some_variable;
}
Your problem is that that your trying to itterate over a single model. That's cause that you itterate over attributes and you get odd result the only think that you need is to do like this:
public function verify($id,$sid)
{
$sfees = sfee::where('student_id', $sid)->first(['mfee_id']);//mfee_id
echo $sfees->mfee_id;
}
$sfees in this example is a single model not a collection of models becasuse you've used first to get it.

Set Query Builder conditions to Model in Laravel 5.1

First I have to say that I tried to find solution, and i didn't.
Basic question:
$Br = new BrandTop;
dd( $Br->limit(10)->get() ); // Will return 10 rows
and
$Br = new BrandTop;
$Br->limit(10);
dd( $Br->get() ); // Will return all rows.
So, the basic question - why? How can I set some limit for Model, but still work with it, for example set (or not set) some where or order depends on other variables.
Advanced question:
I want to use Model like this:
class BrandTop extends Model
{
public function withBrand() {
return $this->leftJoin('brand', 'brand.id' , '=', 'brandtop.brand_id');
}
public function forType($type) // there is much more conditions for type
{
return $this->where(['type' => $type]);
}
// main function
public function forSunglasses($limit = 0, $logo = false)
{
if ($logo)
$this->where(['menu_logo' => 1])->orderBy('total_sales', 'desc');
if ($limit)
$this->limit($limit);
return $this->forType('sunglasses')->withBrand();
// But there goes Error, because forType() return Builder object, and it has no withBrand() method
}
}
So, there is much more conditions, and it's much easier to set all conditions in separate methods. But how?
Model vs Builder
The thing to understand here is the difference between the Model object and the underlying Builder (query builder) object.
The statement $Br = new BrandTop; will create a new instance of a Model, and assign it to the $Br variable. Next, the $Br->limit(10) statement will create a new instance of a Builder object for the brand_tops table, with a limit of 10 applied.
In your first example, by doing $Br->limit(10)->get(), you're calling get() on the Builder that has your limit applied.
In your second example, your individual $Br->limit(10) creates the new Builder instance, but never uses it for anything. The next statement, $Br->get(), creates another new Builder instance without any constraints, so it retrieves all the records.
To be able to build up your query, you need to assign your Builder instance to a variable, and continue to modify that instance before finally calling get(). For example, to get your second example to work:
$query = BrandTop::query();
$query->limit(10);
$query->where(/*conditions*/);
dd($query->get());
Query Scopes
In relation to the second part of your question, you probably want to look into query scopes.
class BrandTop extends Model
{
// renamed to "JoinBrand" instead of "WithBrand", as "with" would imply
// an eager loaded relationship vs a joined table
public function scopeJoinBrand($query)
{
return $query->leftJoin('brand', 'brand.id' , '=', 'brandtop.brand_id');
}
// got rid of "for" prefix
public function scopeType($query, $type)
{
return $query->where('type', $type);
}
// got rid of "for" prefix
public function scopeSunglasses($query, $limit = 0, $logo = false)
{
if ($logo)
$query->where(['menu_logo' => 1])->orderBy('total_sales', 'desc');
if ($limit)
$query->limit($limit);
return $query->type('sunglasses')->joinBrand();
}
}
With the above model, your code would look something like:
dd(BrandTop::sunglasses()->get());
// or, more verbosely:
$query = BrandTop::query();
$query->sunglasses(); // $query already an object, no need to reassign it to itself
dd($query->get());

Exceeded maximum time error when overriding the newQuery on Laravel 4.0

So, I was trying to implement this answer for my other question on the same subject... and it keeps givin me the exceeded time error. Any clues?
This is on my product model. It inherits from Eloquent.
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if( $user_permission->master )
return parent::newQuery();
else if( $user_permission->web_service )
{
$allowed_ids = array();
foreach( $user_permission->allowed_products()->get() as $allowed)
$allowed_ids[] = $allowed->id;
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
If the user is master there is no need to query scope on the request. But, if it isn't then I need to filter by the logged user's permissions.
UPDATE:
I tried the following code in a controller and it works alright:
$user_permission = Auth::user()->permissions;
echo "<PRE>"; print_r($user_permission->allowed_products()->get()); exit;
UPDATE 2:
Guys, I just found out that the problem was in this peace of code:
$allowed = Auth::user()->permissions()->first()->allowed_products()->get()->list('id');
It somehow give me an Maximum execution time of 30 seconds exceeded. If I put the exact same code in a controller, works like a charm, though! I also tried to put it in a scope, also worked. This it's really grinding my gears!
Elloquent has a function called newQuery. Controller does not. When you implement this function in a Model you are overriding the one in Elloquent. If you then invoke Elloquent methods that need a new query for your model before they can return, like ->allowed_products()->get(). Then you are calling your own newQuery() method recursively. Since the user permissions have not changed, this results in infinite recursion. The only outcome can be a timeout because it will keep on trying to determine a filtered product list which causes your newQuery() method to be called, which tries to determine the filtered product list before returning the query, and so on.
When you put the method into a Controller, it is not overriding the Elloquent newQuery method so there is no infinite recursion when trying to get the allowed_product list.
It would be more efficient to apply the filter to the product query based on whether the id is in the user's allowed_products() list using ->whereExists() and build up the same query as allowed_products() except now add condition that id from the query you are filtering is equal to the product id in the allowed products query. That way the filtering is done in the database instead of PHP and all is done in the same query so there is no recursion.
I don't see how your update code works. Illuminate\Database\Eloquent\Collection does not have any magic methods to call the relation functions, you should get an undefined method error trying to do that.
Can you try something like
public function newQuery($excludeDeleted = true)
{
// Returns `Illuminate\Database\Eloquent\Collection`
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
// If here you was to call $user_permission->allowed_products()->get() not much is going to happen, besides probably getting an undefined method error.
$allowed_ids = Auth::user()->permissions()->allowed_products()->get()->lists('id');
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
Update: as per comments below I believe the problem is due to newQuery() being called multiple times as the code works just fine when called once in a controller. When this is applied to every query there is no need to collect all the IDs over and over again (assuming they're not going to change each time you call for them). Something such as the below will allow you to store these and only process them once per request rather than every time a query is run.
private $allowed_ids_cache = null;
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
if ($this->allowed_ids_cache === null)
{
$this->allowed_ids_cache = Auth::user()->permissions()->allowed_products()->get()->lists('id');
}
return parent::newQuery()->whereIn('id', $this->allowed_ids_cache);
}
return parent::newQuery();
}

Laravel 4: How to apply a WHERE condition to all queries of an Eloquent class?

I'm trying to implement an "approved' state for a table I have, it's pretty straightforward, basically, if the row's approve column equals 1; that row should be retrieved, otherwise it shouldn't.
The problem is, now I have to go through the whole codebase and add a WHERE statement(i.e., function call) which is not only time consuming but also inefficient(if I ever want to remove that feature, etc.)
How can I do that? Is it as easy as adding $this->where(..) inside the Eloquent child class' constructor? Wouldn't that affect other CRUD operations? such as not updating an unapproved row?
The answer was given when there was no query scope feature available.
You can override the main query, only for the Post model, like
class Post extends Eloquent
{
protected static $_allowUnapprovedPosts = false;
public function newQuery()
{
$query = parent::newQuery();
if (!static::$_allowUnapprovedPosts) {
$query->where('approved', '=', 1);
} else {
static::$_allowUnapprovedPosts = false;
}
return $query;
}
// call this if you need unapproved posts as well
public static function allowUnapprovedPosts()
{
static::$_allowUnapprovedPosts = true;
return new static;
}
}
Now, simply use anything, but unapproved users won't appear in the result.
$approvedPosts = Post::where('title', 'like', '%Hello%');
Now, if you need to retrieve all posts even unapproved ones then you can use
$approvedPosts = Post::allowUnapprovedPosts()->where('title', 'like', '%Hello%');
Update (Using the query scope):
Since, Laravel now provides Global Query Scopes, leverage that instead of this hacky solution, notice the date of this answer, it's too old and so much things changed by now.
// Using a local query scope
class Post extends Eloquent
{
public function scopeApproved($query)
{
return $query->where('approved', 1);
}
}
You can use it like:
$approvedPosts = Post::approved()->get();
The closest thing I found is Eloquent query scope.
Even though it requires a minor change in my code(prefixing queries) it still gives me what I'm looking with great flexibility.
Here's an example:
Create a function within the Eloquent child class:
class Post extends Eloquent {
public function scopeApproved($query)
{
return $query->where('approved', '=', 1/*true*/);
}
}
Then simply use it like this:
$approvedPosts = Post::approved()-><whatever_queries_you_have_here>;
Works perfectly. No ugly repeated WHERE function calls. easy to modify. Much easier to read(approved() makes much more sense than where('approved', '=', 1) )
You can use global scope for your need, docs for that are here : https://laravel.com/docs/5.6/eloquent#query-scopes
Good example is SoftDeletingScope which is applied to all queries by default on models which use SoftDeletes trait.

Categories