Laravel count related data - php

I have in my database:
Campaigns
hasMany Tasks
hasMany Links
hasMany LinkClicks
If I query
Campaign::find(1)->task->count();
It returns the correct number of tasks asigned to that campaign.
But how can I count link clicks from Campaign id?
Campaign Model:
public function task(){
return $this->hasMany('Task','campaign_id');
}
Task Model:
public function campaign(){
return $this->belongsTo('Campaign','campaign_id');
}
public function links(){
return $this->hasMany('Link','task_id');
}
Link Model:
public function task(){
return $this->belongsTo('Task','task_id');
}
public function clicks(){
return $this->hasMany('LinkClick', 'link_id');
}
public function user(){
return $this->belongsTo('User','user_id');
}
LinkClick Model:
public function link(){
return $this->belongsTo('Link','link_id');
}
So, I want to count linkClicks only for links under specific Campaign, no matter which Task or Link, ofcourse that task and link needs to be under specified Campaign.

Use hasManyThrough for the links:
// Campaign model
public function links()
{
return $this->hasManyThrough('Link', 'Task');
}
// then you can fetch the count either querying db:
$campaign->links()->count(); // SELECT COUNT(*) ...
// returns STRING '25'
// or from the collection of already loaded relation:
$campaign->links->count(); // counts collection items
// returns INT 25
This is not going to work further, for the LinkClicks, but there's easy way yuou can achieve that too:
// This is another way for the above:
$campaign = Campaign::with('tasks.links')->find($someId);
$campaign->tasks->fetch('links')->collapse()->count(); // INT 25
// and the LinkClicks:
$campaign = Campaign::with('links.clicks')->find($someId);
$campaign->links->fetch('clicks')->collapse()->count(); // INT 555

Since going down they are all 'hasMany' relations, you will need to iterate over all of the children at each level in order to generate the sum. Something like this ought to work for you:
$campaign = Campaign::find($campaignId);
$count = 0;
foreach($campaign->task as $task) {
foreach($task->link as $link) {
$count += $link->click->count();
}
}
echo 'Clicks for campaign id ' . $campaignId . ': ' . $count . "\n";
Or, you can skip all that and just issue a bare statement against the database:
$results = DB::select(
'SELECT \'a\' FROM campaign c
INNER JOIN task t ON t.campaign_id = c.id
INNER JOIN links l ON t.task_id = t.id
INNER JOIN link_clicks lc ON lc.link_id = l.id'
);
First idea is probably a better idea though.

Since you are using relationships; you can do something like this:
$count = 0;
$campaign_with_tasks = Campaign::with('tasks')->where('id','=',$campaign_id)->get();
foreach($campaign_with_tasks->tasks as $task){
$links = Links::where('task_id','=',$task->id)->get();
foreach($links as $link){
$count += LinkClicks::where('link_id','=',$link->id)->count();
}
}

Related

Fetching has many relationship data Laravel and using avg function

I am using Laravel 5 with vue js. Basically i am fetching data using axios and trying to display on the webpage using vue js v-for directive.
i have tables in database like this:
ratings Table
id review_id rating
Then i have a
reviews table
id review
They have one to many relationship between. so here in my Review Model i have method
public function ratings()
{
return $this->hasMany('App\Rating')
->selectRaw('review_id,AVG(rating) AS average_rating')
->groupBy('review_id');
}
so here i want to fetch list of reviews with their average ratings. so in my controller i am doing this:
public function getAllReviews(Request $request)
{
$reviews = Review::with('ratings')->get();
return $reviews;
}
So i am getting result but the problem is every review doesnt have ratings record so it is returning null? maybe...
when i try to render in vue template it throws an error undefined because in our collection some reviews do not have ratings.
Now my question is: Can i do something like if there is no record in the ratings for a particular review is it possible to add an array with value 0?? so in my frontend it wont see as undefined.
I hope i am successful to explain i am trying.
Thank you.
You may do it this way:
public function getAllReviews(Request $request)
{
$reviews = Review::selectRaw('*, IFNULL((SELECT AVG(rating) FROM ratings where ratings.review_id = reviews.id), 0) as avg_rating')->get();
return $reviews;
}
I would suggest using the basic relationship and a modified withCount():
public function ratings() {
return $this->hasMany('App\Rating');
}
$reviews = Review::withCount(['ratings as average_rating' => function($query) {
$query->select(DB::raw('coalesce(avg(rating),0)'));
}])->get();
public function showProduct($id)
{
$data = Product::where('category_id',$id)
->selectRaw('*, IFNULL((SELECT AVG(value) FROM ratings where ratings.product_id = products.id), 0) as avg_rating')
->get();
return view('ecommerce.web.productsOfcategory',compact('data'));
}
$avgQuery = "IFNULL((SELECT AVG(ratings.rating) FROM ratings WHERE ratings.review_id = reviews.id),'No Ratings') as avg_rating";
$reviews = Review::query()->selectRaw("reviews.*, $avgQuery")->get();
//SQL Query
$sqlQuery = "select reviews.*, IFNULL((SELECT AVG(ratings.rating) FROM ratings where ratings.review_id= ratings.id), 'No ratings') as avg_rating FROM reviews";

Retrieving data with Array values - laravel

My task is to display all contacts for selected groups. When i select two gorups like facebook and instagram, i have to display all contacts belonging to both group.
When i select multiple groups like below and do return $explode_groups, i get the id's of both groups like 1,2.
But my issue is, when i am displaying the contact as return $selected_contacts i get only contacts for group 1.
Why is this happening?
Group
public function customers()
{
return $this->belongsToMany('App\Customer','customer_group','group_id','customer_id')
->withTimestamps();
}
Customer
public function groups()
{
return $this->belongsToMany('App\Group','customer_group','customer_id','group_id')
->withTimestamps();
}
Controller
$get_selected_groups = $request->get('group');
$explode_groups = implode(', ', $get_selected_groups);
$selected_groups = Group::where('id',$explode_groups)->first();
$selected_contacts = $selected_groups->customers()->get();
response
{"id":2,"title":"test","no_of_contacts":0,"user_id":1,"created_at":"2018-04-15 23:55:30","updated_at":"2018-04-15 23:55:30","customers":[{"id":1,"name":"Benson Jones Thomson","phone":"0247878234","group_id":null,"user_id":1,"created_at":"2018-04-16 00:14:20","updated_at":"2018-04-16 05:31:05","pivot":{"group_id":2,"customer_id":1,"created_at":"2018-04-16 05:33:08","updated_at":"2018-04-16 05:33:08"}},{"id":2,"name":"Lawrence Pitcher","phone":"0244371112","group_id":null,"user_id":1,"created_at":"2018-04-16 07:59:15","updated_at":"2018-04-16 07:59:15","pivot":{"group_id":2,"customer_id":2,"created_at":"2018-04-16 07:59:15","updated_at":"2018-04-16 07:59:15"}}]}
You should be using whereIn() to select multiple ids from the given array and to load the relation of each model use with() method.
Like:
$get_selected_groups = $request->get('group');
return Group::whereIn('id', $get_selected_groups)->with('customers')->get();
Try this
$selected_contacts = Customer::whereHas('groups', function($q) use ($explode_groups){
$q->whereIn('id', $explode_groups);
})->get();
You should try this
$selected_groups = Group::whereIn('id',$get_selected_groups)->get();
$selected_contacts = array();
foreach($selected_groups as $selected_group){
$selected_contacts[] = $selected_group->customers()->get();
}

Laravel + Eloquent ORM: HasManyThrough OrderBy TableC.column2

Table A = Inventory | Table B = ItemAssociation | Table C = ItemValue
I have Table A, B and C. A and B have a one-to-one relationship, B and C have a one to one relationship. I'm currently using the HasManyThrough relationship to arrive at this:
public function item(){
return $this->hasManyThrough('App\ItemValue','App\ItemAssociation','id','id');
}
And in my controller:
public function orm(){
$inventory = Inventory::getAssocBySteamID(76561198124900864)->get();
$i = 0;
foreach($inventory as $inv){
$this->data[$i] = $inv->item()->get();
$i++;
}
return $this->data;
}
Where Inventory::getAssocBySteamID:
public static function getAssocBySteamID($id){
return SELF::where('steamid64','=',$id);
}
This returns all the data I need, however, I need to order this by a column in Table C, the ItemValue model.
Any help would be greatly appreciated.
You can add ->orderBy('TableName', 'desc') to the getAssocBySteamID function.
Or to the query in the Orm() function before the ->get()
Also for Where clauses in Eloquent's QB you don't need the "="sign. You can just do where('steamid',$id)
Do not use static function use scopes. Try using join query like this:
// Inventory model method
public function scopeAssocBySteamID($query, $id) {
$query->where('steamid64', $id)
->join('ItemValue'. 'ItemValue.id', '=', 'Inventory.ItemValue_id')
->select('Inventory.*')
->orderBy('ItemValue.item_price')
}
And then:
public function orm(){
$inventory = Inventory::assocBySteamID(76561198124900864)->get();
$i = 0;
foreach($inventory as $inv){
$this->data[$i] = $inv->item()->get();
$i++;
}
return $this->data;
}
Check all table and field names befor testing this example.

Laravel, Datatables, column with relations count

I have two models, User and Training, with Many to many relationship between them. I'm using the Laravel Datatables package to display a table of all the users. This is how the data controller method (which retrieves the query results and creates a Datatables table) looks like:
public function getData()
{
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
How can I add a column to the created table which displays the total number of relations for each user (that is, how many Trainings does each User have)?
The brute force way would be to try a User::selectRaw(...) which has a built in subquery to get the count of trainings for the user and expose it as a field.
However, there is a more built-in way to do this. You can eager load the relationship (to avoid the n+1 queries), and use the DataTables add_column method to add in the count. Assuming your relationship is named trainings:
public function getData() {
$users = User::with('trainings')->select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->add_column('trainings', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->make();
}
The name of the column in add_column should be the same name as the loaded relationship. If you use a different name for some reason, then you need to make sure to remove the relationship column so it is removed from the data array. For example:
return \Datatables::of($users)
->add_column('trainings_count', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->remove_column('trainings')
->make();
Edit
Unfortunately, if you want to order on the count field, you will need the brute force method. The package does its ordering by calling ->orderBy() on the Builder object passed to the of() method, so the query itself needs the field on which to order.
However, even though you'll need to do some raw SQL, it can be made a little cleaner. You can add a model scope that will add in the count of the relations. For example, add the following method to your User model:
Note: the following function only works for hasOne/hasMany relationships. Please refer to Edit 2 below for an updated function to work on all relationships.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$parentKey = $relation->getQualifiedParentKeyName(); // ex: users.id
$relatedKey = $relation->getForeignKey(); // ex: trainings.user_id
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $related->select(DB::raw('count(*)'))->whereRaw($relatedKey . ' = ' . $parentKey);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
With that scope added to your User model, your getData function becomes:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings')
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
If you wanted the count field to have a different name, you can pass the name of the field in as the second parameter to the selectRelatedCount scope (e.g. selectRelatedCount('trainings', 'training_count')).
Edit 2
There are a couple issues with the scopeSelectRelatedCount() method described above.
First, the call to $relation->getQualifiedParentKeyName() will only work on hasOne/hasMany relations. This is the only relationship where that method is defined as public. All the other relationships define this method as protected. Therefore, using this scope with a relationship that is not hasOne/hasMany throws an Illuminate\Database\Query\Builder::getQualifiedParentKeyName() exception.
Second, the count SQL generated is not correct for all relationships. Again, it would work fine for hasOne/hasMany, but the manual SQL generated would not work at all for a many to many relationship (belongsToMany).
I did, however, find a solution to both issues. After looking through the relationship code to determine the reason for the exception, I found Laravel already provides a public method to generate the count SQL for a relationship: getRelationCountQuery(). The updated scope method that should work for all relationships is:
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($related->newQuery(), $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
Edit 3
This update allows you to pass a closure to the scope that will modify the count subquery that is added to the select fields.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null, $callback = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// start a new query for the count statement
$countQuery = $related->newQuery();
// if a callback closure was given, call it with the count query and relationship
if ($callback instanceof Closure) {
call_user_func($callback, $countQuery, $relation);
}
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($countQuery, $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
$queryBindings = $query->getBindings();
$countBindings = $countQuery->getBindings();
// if the new count query has parameter bindings, they need to be spliced
// into the existing query bindings in the correct spot
if (!empty($countBindings)) {
// if the current query has no bindings, just set the current bindings
// to the bindings for the count query
if (empty($queryBindings)) {
$queryBindings = $countBindings;
} else {
// the new count query bindings must be placed directly after any
// existing bindings for the select fields
$fields = implode(',', $query->getQuery()->columns);
$numFieldParams = 0;
// shortcut the regex if no ? at all in fields
if (strpos($fields, '?') !== false) {
// count the number of unquoted parameters (?) in the field list
$paramRegex = '/(?:(["\'])(?:\\\.|[^\1])*\1|\\\.|[^\?])+/';
$numFieldParams = preg_match_all($paramRegex, $fields) - 1;
}
// splice into the current query bindings the bindings needed for the count subquery
array_splice($queryBindings, $numFieldParams, 0, $countBindings);
}
}
// add the select to the query and update the bindings
return $query->addSelect(DB::raw($select))->setBindings($queryBindings);
}
With the updated scope, you can use the closure to modify the count query:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings', 'trainings', function($query, $relation) {
return $query
->where($relation->getTable().'.is_creator', false)
->where($relation->getTable().'.is_speaker', false)
->where($relation->getTable().'.was_absent', false);
})
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
Note: as of this writing, the bllim/laravel4-datatables-package datatables package has an issue with parameter bindings in subqueries in the select fields. The data will be returned correctly, but the counts will not ("Showing 0 to 0 of 0 entries"). I have detailed the issue here. The two options are to manually update the datatables package with the code provided in that issue, or to not use parameter binding inside the count subquery. Use whereRaw to avoid parameter binding.
I would setup your DB tables and Eloquent models using the conventions provided at http://laravel.com/docs/4.2/eloquent. In your example you would have three tables.
trainings
training_user
users
Your models would look something like this.
class Training {
public function users() {
return $this->belongsToMany('User');
}
}
class User {
public function trainings() {
return $this->belongsToMany('Training');
}
}
You can then use Eloquent to get a list of users and eager load their trainings.
// Get all users and eager load their trainings
$users = User::with('trainings')->get();
If you want to count the number of trainings per user you can simply iterate over $users and count the size of the trainings array.
foreach ( $users as $v ) {
$numberOfTrainings = sizeof($v->trainings);
}
Or you can simply do it in pure SQL. Note that my example below assumes you follow Laravel's conventions for naming tables and columns.
SELECT
u.*, COUNT(p.user_id) AS number_of_trainings
FROM
users u
JOIN
training_user p ON u.id = p.user_id
GROUP BY
u.id
Now that you have a couple of ways to count the number of relations, you can use whatever method you like to store that value somewhere. Just remember that if you store that number as a value in the user table you'll need to update it every time a user creates/updates/deletes a training (and vice versa!).

Laravel Eloquent: Count items model relationship

I'm using Laravel and the Eloquent class. I have three models.
City.php:
public function itineraries() {
return $this->has_many('Itinerary', 'city_id');
}
Itinerary.php:
public function city()
return $this->belongs_to('City');
}
public function type()
{
return $this->belongs_to('Itinerarytype');
}
Itinerarytype.php:
public function itineraries()
{
return $this->has_many('Itinerary');
}
As you can see a city has many itineraries and an itinerary belongs to a city and an itinerary type. The itinerarytype model has many itineraries.
Using the with() method is it possible to get a count of itineraries grouped into itinerary type?
For instance here is what I have so far:
$city = City::with(array('itineraries'))->where_slug($city_slug)->first();
This gets the city which has that slug and all of it's itineraries.
I'd like to get a list like: (where the text is the itinerary type and the number is the count)
History: 10
Entertainment: 5
Outdoor: 6
...
You'd have to use join and a bit of raw sql to achieve that on a performant way:
$city = City::where_slug($slug)->first();
$types = ItineraryType::join('itineraries', 'itineraries.type_id', '=', 'itenerary_types.id')
->where('itineraries.city_id', '=', $city->id)
->group_by('itinerary_types.id')
->order_by('itinerary_count')
->get(array('itinerary_types.*', DB::raw('COUNT(itineraries.id) as itinerary_count')));
foreach ($types as $type) {
print($type->label . ': ' . $type->itinerary_count);
}

Categories