Is it possible to conditionally chain methods in PHP / Laravel, or to pass method chains through functions?
I'd like to control a query in Laravel's query builder by setting up a base query first, then passing that to a function to have other methods chained on to the query. As a quick example:
$query = Model::whereFoo('bar');
if ($needFoo) {
$query = $query->has('foo');
}
$query = queryMethod($query);
function queryMethod($query) {
return $query->where('something', '<', 10);
}
$items = $query->get();
This seems to ignore everything between $query = Model::whereFoo('bar'); and $items = $query->get(); -- it doesn't seem to make any difference what happens to $query in between the two.
Is it possible / advisable to achieve this?
Edit: I've accepted the answer below as it answers the question I asked. My problem was actually unrelated. If it helps anyone: I had a rogue orWhere() call in the base query that was of course including unwanted results regardless of the other chained methods. That needed nesting inside an advanced where method as described in Laravel docs.
What you have should work, but you can simplify it:
$query = Model::whereFoo('bar');
if ($needFoo) $query->has('foo');
queryMethod($query);
function queryMethod($query) {
$query->where('something', '<', 10);
}
$items = $query->get();
Since objects in PHP are passed by reference, you'll always be dealing with the same instance.
Related
I have an Orders table that has relations to a Movements table, and im constantly doing things like this to calculate several common values for each order:
$warehouse = 7;
$order = Order::find(16111);
$entries = Movement::selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('order_id', $order->id)
->where('to_id', $warehouse)
->first();
$exits = Movement::selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('order_id', $order->id)
->where('from_id', $warehouse)
->first();
is it possible to create a custom function to just query the DB doing something like this:
$warehouse = 7;
$entries = Order::find(16111)->entries($warehouse);
$exits = Order::find(16111)->exits($warehouse);
If so how can it be done?
Thanks for your help...
Absolutely. What you are looking for is called local Query Scopes; it allows you to avoid repeating complexe queries in your code.
Local scopes allow you to define common sets of query constraints that you may easily re-use throughout your application.
Write your local query scope in your model and you'll never have to repeat this code again (DRY principle).
Here's an example to give you an idea, you'll need to tweak it to your needs.
In your Order model:
public function scopeEntries($query)
{
$warehouse = $this->warehouse; // Take advantage of Eloquent wherever you can
return $query->movements()->selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('to_id', $warehouse->id);
}
public function scopeExits($query)
{
$warehouse = $this->warehouse; // Take advantage of Eloquent wherever you can
return $query->movements()->selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('from_id', $warehouse->id)
->where('to_id', $warehouse->id);
}
Now in your code, you will be able to simply call $order->entries()->first() to retrieve the first entry but you can also call $order->exits()->get() to retrieve all exits.
So I'm coming across an issue with QueryBuilder cloning. He's my situation, I need to run 3 separate queries, all based off a single "BaseQuery":
$baseQuery = $model->selectRaw("column")->whereNull("deleted_at");
$query1 = clone($baseQuery);
$query1Results = $query1->where("condition", "=", 0)->get();
$query2 = clone($baseQuery);
$query2Results = $query2->where("condition2", "=", 1)->get();
$query3 = clone($baseQuery);
$query3Results = $query3->where("condition3", "=", 0)->get();
By the time I get to $query3, it has 3 where statements. Substituting $query3 ... get() with $query3 ... ->toSql(); shows these results:
SELECT `column` FROM `models` WHERE `deleted_at` IS NULL AND `condition` = ? AND `condition2` = ? AND `condition3` = ?;
It seems that even though I am defining each $queryX as a clone of $baseQuery, the additional ->where() clause is being applied to each of the queries. I've tried doing the clone() before appending any of the ->where() clauses, but that doesn't have any affect.
Is there a way to clone() something and ensure that it creates a new instance? It looks like clone() isn't separating $baseQuery from $queryX, so the ->where()s are being applied to every subsequent copy, even though each should be a new query with only the ->whereNull() clause.
Another interesting point is that running
\Log::info($baseQuery == $queryX);
After each modification returns true (=== is false though)
You should use scopes here.
Local scopes allow you to define common sets of constraints that you may easily re-use throughout your application. For example, you may need to frequently retrieve all users that are considered "popular". To define a scope, simply prefix an Eloquent model method with scope
public function scopeBase($q)
{
return $q->selectRaw('column')->whereNull('deleted_at');
}
Then just do something like this:
$query1Results = $model->where('condition', 0)->base()->get();
$query2Results = $model->where('condition2', 1)->base()->get();
$query3Results = $model->where('condition3', 0)->base()->get();
I have a website where I populate the first page with objects of different nature (last posts, last recipes, last ingredients published). I have currently one querybuilder and then one query for each of them because I call ->getQuery()->getResult() on each of them.
Is there not a way to merge all those querybuilders before executing the query so as to retrieve an array of results made of the results of each of those querybuilders ?
Would that be a best practice ? How would we do it ?
EDIT: what I hoped we could do:
$recipesQueryBuilder = $this->getDoctrine->getRepository('Recipe')->createQueryBuilder('r');
$postsQueryBuilder = $this->getDoctrine->getRepository('Post')->createQueryBuilder('p');
$results = mergeQueryBuilder($recipesQueryBuilder, $postQueryBuilder)->getQuery()->getResult();
$recipes = $results['r'];
$posts = $results['p'];
I do this with many of our queries. I doubt there is a formal "best practice" for this kind of thing, however I can vouch for the fact that re-using builders does simplify the code. For example:
public function getListBuilder(User $user)
{
return $this->createQueryBuilder('l')->where('l.user = :user')->setParameter('user', $user)->orderBy('l.name');
}
I have a number of queries that re-use this base builder. For example:
public function countLists(User $user = null)
{
$qb = $this->getListBuilder($user);
return $qb->select('COUNT(l)')->getQuery()->getSingleScalarResult();
}
Likewise another method findActiveLists() changes the order to createdAt and generates a query with setMaxResults() specified.
I'm trying to do a mass update on an eloquent collection.
So I have my query, which looks a bit like this:
\Responder::with('details')
->where('job_number', $project->job_number)
->where('batch_id', ((int) $batch_id) - 1)
->where('updated_at', '<=', $target_time)
->whereHas('transactions', function($q) {
$q->where('status', 'success');
}, '<', 1)
->whereHas('details', function($q) {
$q->where('email', '<>', '');
});
This query object is stored as $query (because I'm re-using it - the same reason I dont want to switch how I'm doing the query), I am then performing an update on the collection, e.g.
$query->update(array('batch_id' => $batch_id));
This works great except it updates all the 'updated_at' timestamps. Now i like the timestamps, they are used extensively elsewhere, so i cant turn them off all together but I thought I could disable them temporarily but I've tried the following:
$query->timestamps = false;
$query->update(array('email_drop_off_index' => $batch_id));
and I can confirm that doesn't work, is there a way to do this?
Any help much appreciated
timestamps = false should be made on your model, but what you are doing is setting the value on the query builder. That's why it is not being picked up.
timestamps is an instance variable so you can't set it statically, and I don't think there is a built-in way to do it from the query builder. So I suggest try instantiating the model first, then create a new query from it, like this:
$responder = new \Responder;
$responder->timestamps = false;
$query = $responder->newQuery()
->with('details')
->where('job_number', $project->job_number)
...; // the rest of your wheres
$query->update(array('email_drop_off_index' => $batch_id));
Here's a possible solution: subclass your Responder model and turn off timestamps in the subclass.
class MassUpdateResponder extends Responder
{
public $timestamps = false;
}
Then use your new class to do the updates. This seems like a bit of a hack, but it should work.
BTW, doing an update like the following worked for me:
$query->timestamps = false;
$query->value = "new value";
$query->save();
The update() method may be doing something different that's causing it to ignore the value of $timestamps.
I have a search query that needs to be done. However, a search doesn't always have all values set, like in this case.
$aEvents = DB::table('events')
->where('client_id', '=', $client_id);
The question is, how can I make this where statement depend on the value of $client_id. So if the value is empty I don't want the Where statement to occur.
Also, I do not want to write several complete queries with if statements in PHP. To many variables. Ideally I'd like something like this:
$aEvents = DB::table('events')
->(($client_id != "") ? where('client_id', '=', $client_id) : "");
Using eloquent is (really!) nice and save, but I'm not yet up to speed with if statements in std Class objects I guess. Any help is appreciated.
You may try something like this:
$query = DB::table('events');
if(!empty($client_id)) {
$query->where('client_id', $client_id);
}
$aEvents = $query->get(); // Call this at last to get the result
If you are passing client_id to the server via a form/query string(user input) then you may try something like this:
if($client_id = Input::get('client_id')) {
$query->where('client_id', $client_id);
}
Update: For pagination try this:
$aEvents = $query->paginate(10); // For 10 per page
So you may call links() method in your view if you pass it like this:
return View::make('viewName')->with('aEvents', $aEvents);
In the view for pagination links:
$aEvents->links()
You can also use query scopes in the model for this purpose. Scopes allow you to easily re-use query logic in your models. In the model Event, you can add the following query scope:
public function scopeClientID($query, $client_id)
{
if ($client_id != '') {
return $query->where('client_id', '=', $client_id);
} else {
return $query;
}
}
Then from your controller or wherever you're calling it from, you can do the following:
$aEvents = Event::clientID($client_id);
If you want to get all the results, then you can do:
$aEvents = Event::clientID($client_id)->get();
Or if you want pagination, you can do:
$aEvents = Event::clientID($client_id)->paginate();
You can also chain it with other methods like you'd do in a eloquent query.
You can read more about model query scopes at http://laravel.com/docs/eloquent#query-scopes