Is it possible to add custom functions on Laravel Models? - php

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.

Related

Refactoring Laravel function to reduce number of database queries

I wondered if anyone could offer me some advice on refactoring the below function to reduce the number of database queries? Alternatively, there may be a completely different way of achieving this using Laravel.
I am trying to calculate the P&L for a Job, which is made up of Products, which are made up of Components:
public function jobProfitAndLoss($id)
{
$products_in_job = DB::table('job_product')
->where('job_id', $id)
->get();
$total_price = 0.0000;
$total_cost = 0.0000;
foreach ($products_in_job as $row) {
$total_price = $total_price + ($row->quantity)*($row->price);
$product_id = $row->product_id;
$components_in_product = DB::table('components')
->where('product_id', $product_id)
->get();
foreach ($components_in_product as $component) {
$total_cost = $total_cost + ($component->cost)*($row->quantity);
}
}
return $total_price-$total_cost;
}
Products have Components -
https://www.dropbox.com/s/ncnij8dnh99sb9v/Screenshot%202016-04-09%2015.22.26.png?dl=0
Components belong to Products -
https://www.dropbox.com/s/3dx6u30gbod2rv4/Screenshot%202016-04-09%2015.23.43.png?dl=0
Jobs have many Products -
https://www.dropbox.com/s/q179t0knd7y8z4k/Screenshot%202016-04-09%2015.24.11.png?dl=0
You will see here that there are some of the same queries being executed multiple times, which I am not sure how to avoid in this situation -
https://www.dropbox.com/s/xonbtx9cdqvq1wd/Screenshot%202016-04-09%2015.33.07.png?dl=0
Any help is much appreciated.
Edit: It seems that you aren't using models. If you have not done so, create models for your database entries. You will need to use the protected $table attribute for job_product as Eloquent may not be able to automatically convert your class name into the correct table name.
First of all, set up relations if you have not done so. For example, under Job.php, include the Products relation:
public function products() {
return $this->hasMany(App\Products::class); // Assuming App is the namespace
}
Now, instead of using a Fluent query for $components_in_product, you are able to directly do $components_in_product = $products_in_job->products;. However, this still leads to N+1 queries.
As a result, take a look at this: https://laravel.com/docs/5.2/eloquent-relationships#eager-loading
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
For this operation, only two queries will be executed:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
Therefore, change $products_in_job to an Eloquent query and add ->with('products') to the query.

How to get different queries (models) results with one statement? - laravel

Sorry if my title is confusing, not sure how to explain this within a line. Let's say I have a table with some columns and I have this
$model = Document::where('systemName', '=', $systemName)->where('ticketNumber', '=', ($nextTicketNumber))->get(); ticketNumber is unique where as there are quite a few systemNames
The above will get exactly what I want but I want more. I want an another array which will store all the rows under the same systemName. I know I can do this by doing
$allSystemNameModel = Document::where('systemName', '=', $systemName)
But is there a possible way to not having two variables and be easier?
No, you can't get both collections into one variable with one statement, however, you can create an array and store your results there:
$both = [];
$both['model'] = ...
$both['all'] = ...
UPDATE:
To avoid querying the database twice, you can use a first method that laravel provides us with.
$allSystemNameModel = Document::where('systemName', '=', $systemName);
$model = $allSystemNameModel->first(function ($doc) use ($nextTicketNumber) {
return $doc->ticketNumber == $nextTicketNumber;
});
$both['model'] = $model;
$both['all'] = $allSystemNameModel->all();
Note: Be sure to use use when working with php closures since $nextTicketNumber will be undefined otherwise.

Can we merge doctrine querybuilders and retrieve each ones' results ? is that a best practice?

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.

How to represent averages and other aggregates in OOP?

I have a Model called Product. Product has among other things a price, a category and a created date.
I want to show a table showing the average price and average age by category in many places. Sometimes I want just one category, sometimes everything, sometimes only products that have been in stock for more than a certain time etc.
At the moment I'm using Laravel's Query Builder to generate those numbers in my controller, then passing that to a view.
To help try to reuse it, I have this in the methods before I need it:
$product_averages_base_query = DB::table('products')
->leftJoin('categories', 'products.MakeDescription', '=', 'categories.id')
->select(
DB::raw('count(products.id) as TotalNumber'),
DB::raw('AVG(Datediff("'.date('Y-m-d').'",products.created)) as AvgDaysInStock'),
DB::raw('AVG(Price) as AvgPrice')
);
Then when for the specific use case I use:
$averages = $product_averages_base_query->where('categories.name', '=', 'Example 1')->get();
or whatever the variant is.
This feels really "wrong" because I of course end up copying this code all over the place.
How do I represent this data in a way that will let me reuse it more easily? Should I have a class? What should it be called, and what's in it?
Should I have a Model somehow?
Any advice is welcome!
As for the appropriate place, you could very easily use query scopes and drop everything in your model. You'd probably want to start with your base query...
public function scopeBaseQuery($query)
{
return $query->leftJoin('categories', 'products.MakeDescription', '=', 'categories.id')
->select(
DB::raw('count(products.id) as TotalNumber'),
DB::raw('AVG(Datediff("'.date('Y-m-d').'",products.created)) as AvgDaysInStock'),
DB::raw('AVG(Price) as AvgPrice')
);
}
And then continue with the category name scope...
public function scopeOfName($query, $name)
{
return $query->where('categories.name', $name);
}
Add additional scopes as you need.
Then to use this, it would be quite easy...
$averages = Product::baseQuery()->ofName('Example 1')->get();
One solution would be to create an Eloquent class for the model and then put the functionality into a scope.
class Product extends Eloquent {
public function scopeAveragesByCategoryName($q, $catName) {
return $q->leftJoin('categories', 'products.MakeDescription', '=', 'categories.id')
->select(DB::raw('count(products.id) as TotalNumber'),
DB::raw('AVG(Datediff("'.date('Y-m-d').'",products.created)) as AvgDaysInStock'),
DB::raw('AVG(Price) as AvgPrice'))
->where('categories.name', '=', $catName);
}
}
$products = Product::averagesByCategoryName('Example 1')->get();
var_dump($products);
Or you could just as easily put the whole thing into a function.

Only query "Where" if there is a value in Laravel eloquent

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

Categories