How to represent averages and other aggregates in OOP? - php

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.

Related

Laravel Spatie Query Builder - Wrong query when make join

I have a problem with packages spatie/Laravel-query-builder. I used this package for easy way to filter and sort my query but it's not like that :D
I trying to filter result who have two relation - shop and employee. In short, it wants to filter a list of store reports
Look, this is my code. When I use join method then in response receives data with incorrect ID. When comment join methods all it's okey but I need sorting by relation.
return ReportControlResource::collection(
QueryBuilder::for(Report::class)
->with(['employee', 'shop'])
->allowedFilters('shop.name', 'employee.name')
->join('employees', 'employees.id', '=', 'reports.employee_id')
->join('shops', 'shops.id', '=', 'reports.shop_id')
->allowedSorts(['employees.name'])
->get()
);
My relation in Report model:
public function shop(): BelongsTo
{
return $this->belongsTo(Shop::class);
}
public function employee(): BelongsTo
{
return $this->belongsTo(Employee::class);
}
Relation in Shop model:
public function reports()
{
return $this->hasMany(Report::class);
}
And in Employee model
public function reports()
{
return $this->hasMany(Report::class);
}
Do you have any ideas?
I noticed that the ID is being overwritten, but not shop_id and employee_id, but the ID why??
I think the problem is with the library itself. The creators did not take into account the reverse situation of joining tables as in my case.
Look at example from Doc:
$addRelationConstraint = false;
QueryBuilder::for(User::class)
->join('posts', 'posts.user_id', 'users.id')
->allowedFilters(AllowedFilter::exact('posts.title', null, $addRelationConstraint));
And my join
->join('employees', 'employees.id', '=', 'reports.employee_id')
But this join work like this
->join('employees', 'reports.id', '=', 'reports.employee_id')
But why? I checked the order in many ways, even disconnecting filtering and it did not change anything
I found solution for the problem. I had to select a column and now all works fine :-)
return EvidenceControlResource::collection(
QueryBuilder::for(EvidenceControl::class)
->allowedIncludes('employee', 'shop')
->select('evidence_controls.*', DB::raw('employees.id as employee_id'))
->join('employees', 'evidence_controls.employee_id', '=', 'employees.id')
->select('evidence_controls.*', DB::raw('shops.id as shop_id'))
->join('shops', 'evidence_controls.shop_id', '=', 'shops.id')
->allowedFilters('shop.name', 'employee.name')
->allowedSorts(['employees.name', 'shops.name'])
->get()
);
This is not clean code, but when create own class with implements Sort the code might look a lot better.
Thanks Guy's for help! Good luck! :-)

Is it possible to add custom functions on Laravel Models?

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.

Return belonging name with ID form Laravel, check for the type?

sorry for the title of this question but I am not sure how to ask it...
I am working on a project where I have two Models Trains and Cars, to this model I have a belonging Route.
I want to make a query and check if the routeable_type is App\Car than with the selected routeable_id to get the data from the Car. And if the routeable_type is Train then with the ID to get the data from the Tran.
So my models go like this:
Train:
class Train extends Model
{
public function routes()
{
return $this->morphMany('App\Route', 'routeable');
}
}
Car:
class Car extends Model
{
public function routes()
{
return $this->morphMany('App\Route', 'routeable');
}
}
Route:
class Route extends Model
{
public function routeable()
{
return $this->morphTo();
}
}
And the query I have at the moment is:
$data = Route::leftjoin('cars', 'cars.id', '=', 'routes.routeable_id')
->leftjoin('trains', 'trains.id', '=', 'routes.routeable_id')
->select('routes.id', 'cars.model AS carmodel', 'trains.model AS trainmodel', 'routeable_type', 'routes.created_at');
With this query if I have the same ID in cars and trains I get the data from both and all messes up. How do I check if routeable_type is Car ... do this, if routeable_type is Train .. do that?
Will something like this be possible in a 1 single query:
$data = Route::select('routes.id', 'routeable_type', 'routes.created_at');
if(routeable_type == 'Car'){
$data = $data->leftjoin('cars', 'cars.id', '=', 'routes.routeable_id')->select('routes.id', 'cars.model AS carmodel', 'routeable_type', 'routes.created_at');
}else{
$data = $data->leftjoin('trains', 'trains.id', '=', 'routes.routeable_id')->select('routes.id', 'trains.model AS trainmodel', 'routeable_type', 'routes.created_at');
}
Maybe this is what you are looking for?
DB::table('routes')
->leftJoin('cars', function ($join) {
$join->on('cars.id', '=', 'routes.routeable_id')
->where('routes.routeable_type', 'App\Car');
})
->leftJoin('trains', function ($join) {
$join->on('trains.id', '=', 'routes.routeable_id')
->where('routes.routeable_type', 'App\Train');
})
->select('routes.id', 'cars.model AS car_model', 'trains.model AS train_model', 'routes.routeable_type', 'routes.created_at');
->get();
I think you may want to follow the morphedByMany design.
https://laravel.com/docs/5.7/eloquent-relationships#many-to-many-polymorphic-relations
This was also a neat visual for the different relation types.
https://hackernoon.com/eloquent-relationships-cheat-sheet-5155498c209
I was faced with a similar issue though I failed to follow the correct design initially and was forced to query the many possible relations then wrote custom logic after to collect the relation types and ids then do another query and assign them back through iteration. It was ugly but worked... very similar to how Eloquent does things normally.
i don't have enough repo, so i can't comment. that's why i am putting as an answer.
You should use 2 different queries, for each model.
This will be better, code wise as well as performance wise. also if both models have similar fields you should merge them to 1 table and add a 'type' column.
and put non-similar fields in a 'meta' column.
( in my opinion )

Complex joins in Eloquent

I have a table of itineraries. An itinerary belongs to a customer and has multiple days. A package is assigned to each of these days. I want to be able to produce a manifest showing which customers are allocated to a package and on which days.
I'm struggling with Eloquent, because you can't do queries beyond a one-to-Many relationship
What i want to do is this:
return $this->package->where('PackageID, $id)->itineraryDay->itinerary->customer->select('CustomerID', 'Date')
But can only really achieve it using the query builder:
return DB::connection($this->connection)
->table('t_package as PA')
->join('t_itinerary_day_map as IDM', 'IDM.PackageID', '=', 'PA.PackageID')
->join('t_itinerary_day as ID', 'IDM.ItineraryDayID', '=', 'ID.ItineraryDayID')
->join('t_itinerary as IT', 'IT.ItineraryID', '=', 'ID.ItineraryID')
->join('t_customer as CC', 'CC.ItineraryID', '=', 'IT.ItineraryID')
->where('PA.PackageID', $id)
->select('CC.CustomerID', 'ID.Date')
->distinct()
->get();
I really want to use Eloquent as I hate hardcoding table names and i've already created relationships for these models, but can't see any way around it
I believe you could do something like this to find customers that have a package with the given ID:
$packageId = 42;
$customers = $customer->whereHas('packages', function($q) use($packageId){
return $q->where('package_id', $packageId);
})->get();
How would that work for what you want?
I'll have to make few assumptions on your relationship but it seems doable.
If one ItineraryDay belongs to one Itinerary. And one Itinerary belongs to one Customer. And one ItineraryDay may have more than one Package.
$packageID = 111;
$itineraryDays = ItineraryDay::with('itinerary.customer')
->whereHas('package', function($q) use($packageID) {
$q->where('PackageID', $packageID);
})
->get();
foreach($itineraryDays as $itineraryDay) {
var_dump($itineraryDay);
var_dump($itineraryDay->itinerary->customer);
}
I'm not sure if i get your relationship method naming correct, but hopefully this works.

How to 'order_by' on second table when using eloquent one-to-many

Of course I can use order_by with columns in my first table but not with columns on second table because results are partial.
If I use 'join' everything works perfect but I need to achieve this in eloquent. Am I doing something wrong?
This is an example:
//with join
$data = DB::table('odt')
->join('hdt', 'odt.id', '=', 'hdt.odt_id')
->order_by('hdt.servicio')
->get(array('odt.odt as odt','hdt.servicio as servicio'));
foreach($data as $v){
echo $v->odt.' - '.$v->servicio.'<br>';
}
echo '<br><br>';
//with eloquent
$data = Odt::get();
foreach($data as $odt){
foreach($odt->hdt()->order_by('servicio')->get() as $hdt){
echo $odt->odt.' - '.$hdt->servicio.'<br>';
}
}
In your model you will need to explicitly tell the relation to sort by that field.
So in your odt model add this:
public function hdt() {
return $this->has_many('hdt')->order_by('servicio', 'ASC');
}
This will allow the second table to be sorted when using this relation, and you wont need the order_by line in your Fluent join statement.
I would advise against including the order by in the relational method as codivist suggested. The method you had laid is functionally identical to codivist suggestion.
The difference between the two solutions is that in the first, you are ordering odt ( all results ) by hdt.servicio. In the second you are retrieving odt in it's natural order, then ordering each odt's contained hdt by servico.
The second solution is also much less efficient because you are making one query to pull all odt, then an additional query for each odt to pull it's hdts. Check the profiler. Considering your initial query and that you are only retrieving one column, would something like this work?
HDT::where( 'odt_id', '>', 0 )->order_by( 'servico' )->get('servico');
Now I see it was something simple! I have to do the query on the second table and get contents of the first table using the function odt() witch establish the relation "belongs_to"
//solution
$data = Hdt::order_by('servicio')->get();
foreach($data as $hdt){
echo $hdt->odt->odt.' - '.$hdt->servicio.'<br>';
}
The simple answer is:
$data = Odt::join('hdt', 'odt.id', '=', 'hdt.odt_id')
->order_by('hdt.servicio')
->get(array('odt.odt as odt','hdt.servicio as servicio'));
Anything you can do with Fluent you can also do with Eloquent. If your goal is to retrieve hdts with their odts tho, I would recommend the inverse query for improved readability:
$data = Hdt::join('odt', 'odt.id', '=', 'hdt.odt_id')
->order_by('hdt.servicio')
->get(array('hdt.servicio as servicio', 'odt.odt as odt'));
Both of these do exactly the same.
To explain why this works:
Whenever you call static methods like Posts::where(...), Eloquent will return a Fluent query for you, exactly the same as DB::table('posts')->where(...). This gives you flexibility to build whichever queries you like. Here's an example:
// Retrieves last 10 posts by Johnny within Laravel category
$posts = Posts::join('authors', 'authors.id', '=', 'posts.author_id')
->join('categories', 'categories.id', '=', 'posts.category_id')
->where('authors.username', '=', 'johnny')
->where('categories.name', '=', 'laravel')
->order_by('posts.created_at', 'DESC')
->take(10)
->get('posts.*');

Categories