Laravel 5 relationship not working - mysql

I have courses and subscription types.
A subscribed user has a subscription type, i want all the courses that a users subscription type matches.
My attempt (which returns all the courses, not just the right ones):
$courses = Course::has('maintask')->with(['subscriptionType' => function ($q) use ($user)
{
return $q->where('subscription_type_id',$user->subscription[0]->subscription_type_id)->where('status','1');
}])->get();
Any tips?
Thanks!

Use whereHas():
$courses = Course::has('maintask')
->whereHas('subscriptionType', function ($q) use ($user) {
$q->where('id', $user->subscription[0]->subscription_type_id)
->where('status', 1);
})->get();

Related

How to do in Laravel, subquery with calculated fields on SELECT?

How can I make this query in Laravel eloquent. Please, no DB Record solution.
SELECT slo.batch,
slo.type,
(SELECT Count(sl1.id)
FROM sync_log sl1
WHERE sl1.status = 1
AND sl1.batch = slo.batch) AS success,
(SELECT Count(sl2.id)
FROM sync_log sl2
WHERE sl2.status = 0
AND sl2.batch = slo.batch) AS failed,
slo.created_at
FROM sync_log slo
GROUP BY batch
ORDER BY slo.created_at
Below is the database table.
Try something like :
$result=DB::table('sync_log as slo')
->select('slo.batch','slo.type', 'slo.created_at', DB::raw(('SELECT Count(sl1.id) FROM sync_log sl1 WHERE sl1.status=1 AND sl1.batch = slo.batch) AS success'), DB::raw(('SELECT Count(sl2.id) FROM sync_log sl2 WHERE sl2.status = 0 AND sl2.batch = slo.batch) AS failed')
->groupBy('batch')
->orderBy('slo.created_at')
->get();
Without any idea on your structure or models. guessing a manyToMany relation wetween batch and type where sync_log is the pivot table between them.
$batchs = Batch::withCount([
'types as success' => function ($query) {
$query->where('status', 1);
},
'types as failed' => function ($query) {
$query->where('status', 0);
}])
->get();
Using Eloquent ORM it can be tricky but I guess will work, you can define hasMany relation(s) in same model which will relate to same model using batch attribute/key like
class SyncLog extends Model
{
public function success_batches()
{
return $this->hasMany(SyncLog::class, 'batch', 'batch')->where('status',1);
}
public function failed_batches()
{
return $this->hasMany(SyncLog::class, 'batch', 'batch')->where('status',0);
}
}
Then using your model you can get count for these relations using withCount
$bacthes = SyncLog::withCount(['success_batches','failed_batches'])
->select(['batch','type'])
->distinct()
->orderBy('created_at')
->get();
If you don't want to define it twice based on where clause then you can follow the approach explained in #N69S's answer like
class SyncLog extends Model
{
public function batches()
{
return $this->hasMany(SyncLog::class, 'batch', 'batch');
}
}
$bacthes = SyncLog::withCount([
'batches as success' => function ($query) {
$query->where('status', 1);
},
'batches as failed' => function ($query) {
$query->where('status', 0);
}])
->select(['batch','type'])
->distinct()
->orderBy('created_at')
->get();

Is it possible to combine whereHas with 'or' queries?

I'm trying to implement a filter system where, among other attributes and relationships, items are categorized. However, the challenge appears when combining OR queries with other filters using the regular and clause. The result grabs rows which I do not want and adds the or when the condition for that fails, thus polluting the final results with unwanted data.
<?php
class ProductSearch {
public $builder;
private $smartBuild; // this is the property I'm using to disable the alternation when other search parameters are present to avoid polluting their result
function __construct( Builder $builder) {
$this->builder = $builder;
}
public function applyFilterToQuery(array $filters) {
$pollutants = ['subcategory', 'subcategory2', 'category'];
$this->smartBuild = empty(array_diff( array_keys($filters), $pollutants)); // [ui=>9, mm=>4], [mm]
foreach ($filters as $filterName => $value) {
// dd($filters, $filterName );
if (method_exists($this, $filterName) && !empty($value) )
$this->$filterName( $value);
}
return $this;
}
public function location( $value) {
$this->builder = $this->builder
->whereHas('store2', function($store) use ($value) {
$store->where('state', $value);
});
}
public function subcategory( $value) {
$name = Subcategories::where('id', $value)->pluck('name');
$this->builder = $this->builder->where('subcat_id', $value);
if ($name->isNotEmpty() && $this->smartBuild) {
$names = preg_split('/\W\s+/', $name[0]);
if (!$names) $names = $name;
foreach ($names as $value)
$this->builder = $this->builder->orWhere('name', 'like', "%$value%");
}
}
}
You may observe from the above that making a request for categories searches products matching the category name. But on attempting to combine that alternate match with legitimate AND queries (in location for instance, the result tends to include matching locations OR matching names.
The desired result is ((matching name OR matching category) AND matching location). Is this possible?
I had similar situation like this few days ago about User and Posts.
Needed a list of posts which user has commented or participated in and which user owns.
So I did following on User model
//Get user created post or if user has participated in the post
$queryString->where(function ($query) use ($user_id) {
return $query->whereHas('participants', function ($q) use ($user_id) {
$q->where('user_id', $user_id);
})->orWhere('id', $user_id);
});
Hope this helps.

Filter eloquent

Im using laravel for a project i am wanting to create an endpoint which filters and searches a Film model and returns a collection of films that match the chosen filter options which will be Options, Location and Age Rating picked by the user.
I have created a query with how i would initially join the tables in symfony(hopefully its correct) but im unsure on how i adjust this to be more Laravel or eloquent.
Film Model
public function location(): BelongsTo
{
return $this->belongsTo(Location::class);
}
public function options(): BelongsToMany
{
return $this->belongsToMany(
Option::class,
'film_options',
'film_id',
'option_id'
);
}
public function ageRatings(): BelongsToMany
{
return $this->belongsToMany(
AgeRating::class,
'film_age_ratings',
'film_id',
'age_rating_id'
);
}
Query in Doctrine
Select *
From films f
Join film_options fo on f.id = fo.film_id
Join options o on o.id = fo.option_id
Join film_age_ratings fa on f.id = fa.film_id
Join age_ratings a on a.id = fa.age_rating_id
Join location l on l.id = p.location_id
How would i write this query within laravel?
To achieve what you're after you can use a mixture of when() and whereHas():
$films = Film
::when($request->input('first'), function ($query, $first) {
$query->whereHas('options', function ($query) use ($first) {
$query->where('first', $first);
});
})
->when($request->input('second'), function ($query, $second) {
$query->whereHas('options', function ($query) use ($second) {
$query->where('second', $second);
});
})
->when($request->input('age'), function ($query, $age) {
$query->whereHas('ageRatings', function ($query) use ($age) {
$query->where('age', $age);
});
})
->when($request->input('country'), function ($query, $country) {
$query->whereHas('locations', function ($query) use ($country) {
$query->where('country', $country);
});
})
->when($request->input('city'), function ($query, $city) {
$query->whereHas('locations', function ($query) use ($city) {
$query->where('city', $city);
});
})
->orderBy('updated_at', 'desc')
->get();
Essentially, what the above is doing is saying when you have a non-empty value submitted it will add a where clause to the query for that relationship
e.g. is main is submitted it will limit the films to only ones that have categories when main is the submitted value.
Also, just an FYI, you don't need to add the from(...) method when using Eloquent.
A simple way to limit the number of lines would be to add scopes to your Film model e.g.
In your Film model:
public function scopeHasMain($query, $main)
{
$query->whereHas('options', function ($query) use($first) {
$query->where('first', $first);
});
}
Then your when() method would be:
when($request->input('first'), function ($query, $first) {
$query->hasFirst($first);
})
You can use the whereHas method as shown in the docs: https://laravel.com/docs/5.8/eloquent-relationships#querying-relationship-existence
Example:
use Illuminate\Database\Eloquent\Builder;
//...
$optionIds = [/* Option ids to filter by */];
$films = Film::whereHas('options', function (Builder $query) use ($optionIds) {
$query->whereIn('options.id', $optionIds);
})->get();

How to select all rows except (A and B) in Eloquent ORM using Scope?

I'm trying to figure out how to get all rows except few (A and B) in Eloquent ORM modal.
User Model
public function notifications()
{
return $this->hasMany('notification','listener_id','id');
}
Model Notification
public function scopeFriendship($query)
{
return $query->where('object_type', '=', 'Friendship Request');
}
public function scopeSent($query)
{
return $query->where('verb', '=', 'sent');
}
Here how can I get all notifications of a user except other than (Friendship and Sent) scope.
Something like:- all rows except !(Friendship AND Sent)
It is possible to use scopes in combination with eager loading. Like that:
User::with(['notifications' => function($q){
$q->friendship();
}])->get();
However we now need to invert the scope somehow. I can think of two ways to solve this.
1. Add negative scopes
public function scopeNotFriendship($query){
return $query->where('object_type', '!=', 'Friendship Request');
}
public function scopeNotSent($query){
return $query->where('verb', '!=', 'sent');
}
User::with(['notifications' => function($q){
$q->notFriendship();
$q->notSent();
}])->get();
2. Optional parameter
Or you could introduce an optional parameter to your current scopes. Something like this:
public function scopeFriendship($query, $is = true)
{
return $query->where('object_type', ($is ? '=' : '!='), 'Friendship Request');
}
public function scopeSent($query, $is = true)
{
return $query->where('verb', ($is ? '=' : '!='), 'sent');
}
This way you would only have to pass in false:
User::with(['notifications' => function($q){
$q->friendship(false);
$q->sent(false);
}])->get();
Edit
You can even gain more control by adding a second parameter for the boolean (AND or OR of the where:
public function scopeFriendship($query, $is = true, $boolean = 'and')
{
return $query->where('object_type', ($is ? '=' : '!='), 'Friendship Request', $boolean);
}
And if you wanted either scope to be true:
$q->friendship(true, 'or');
$q->sent(true, 'or');
2nd Edit
This one finally worked (from the chat)
Notification::where('listener_id', $user_id)
->where(function($q){
$q->friendship(false)
$q->sent(false, 'or')
})
->get();

Laravel 4 Method Improvement

I have this index method:
public function index()
{
// In the view, there are several multiselect boxes (account managers, company names and account types). This code retrives the values from the POST method of the form/session.
$company_names_value = Input::get('company_names_value');
$account_managers_value = Input::get('account_managers_value');
$account_types_value = Input::get('account_types_value');
// If there has been no form submission, check if the values are empty and if they are assign a default.
// Essentially, all of the records in the table column required.
if (is_null($company_names_value))
{
$company_names_value = DB::table('accounts')
->orderBy('company_name')
->lists('company_name');
}
if (is_null($account_managers_value))
{
$account_managers_value = DB::table('users')
->orderBy(DB::raw('CONCAT(first_name," ",last_name)'))
->select(DB::raw('CONCAT(first_name," ",last_name) as amname'))
->lists('amname');
}
if (is_null($account_types_value))
{
$account_types_value = DB::table('account_types')
->orderBy('type')
->lists('type');
}
// In the view, there is a dropdown box, that allows the user to select the amount of records to show per page. Retrieve that value or set a default.
$perPage = Input::get('perPage', 10);
// This code retrieves the order from the session that has been selected by the user by clicking on a table column title. The value is placed in the session via the getOrder() method and is used later in the Eloquent query and joins.
$order = Session::get('account.order', 'company_name.asc');
$order = explode('.', $order);
// Here we perform the joins required and order the records, then select everything from accounts and select their id's as aid. Then whereIn is used to select records where company name, account manager name and account type matches the values of the multiselect boxes or the default set above.
$accounts_query = Account::leftJoin('users', 'users.id', '=', 'accounts.user_id')
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->orderBy($order[0], $order[1])
->select(array('accounts.*', DB::raw('accounts.id as aid')));
if (!empty($company_names_value)) $accounts_query = $accounts_query->whereIn('accounts.company_name', $company_names_value);
$accounts = $accounts_query->whereIn(DB::raw('CONCAT(users.first_name," ",users.last_name)'), $account_managers_value)
->whereIn('account_types.type', $account_types_value)
->paginate($perPage)->appends(array('company_names_value' => Input::get('company_names_value'), 'account_managers_value' => Input::get('account_managers_value'), 'account_types_value' => Input::get('account_types_value')));
$accounts_trash = Account::onlyTrashed()
->leftJoin('users', 'users.id', '=', 'accounts.user_id')
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->orderBy($order[0], $order[1])
->select(array('accounts.*', DB::raw('accounts.id as aid')))
->get();
$message = Session::get('message');
$default = ($perPage === null ? 10 : $perPage);
$this->layout->content = View::make('admin.accounts.index', array(
'accounts' => $accounts,
'accounts_trash' => $accounts_trash,
'company_names' => DB::table('accounts')->orderBy('company_name')->lists('company_name', 'company_name'),
'account_managers' => DB::table('users')->orderBy(DB::raw('CONCAT(first_name," ",last_name)'))->select(DB::raw('CONCAT(first_name," ",last_name) as amname'))->lists('amname', 'amname'),
'account_types' => DB::table('account_types')->orderBy('type')->lists('type', 'type'),
'perPage' => $perPage,
'message' => $message,
'default' => $default
));
}
Basically, I am building a query that searches several tables (hence the joins). In the view a user has the ability to select multiple values from various multi-select boxes and then submit a form which will then populate the $company_names_value, $account_managers_value and $account_types_value variables.
Initially, when there is no form submission, I'm using Query Builder to select all records for each type, and then using them in the query.
It works but it is slow and messy. I was wondering if any of you Laravel 4 gurus could help me improve it further, so that the queries are faster and the code is lighter.
Thanks in advance.
This has now been refactored significantly, and it is very fast now. I've moved most of my code in to my models, as well as refactoring that code.
Here's the new index method:
public function index()
{
$account = explode(',', Input::get('account'));
$account_manager = explode(',', Input::get('account_manager'));
$account_type = explode(',', Input::get('account_type'));
$perPage = Input::get('perPage', 10);
$order = Session::get('account.order', 'company_name.asc');
$order = explode('.', $order);
$accounts = Account::accounts($order, $account, $account_manager, $account_type)->paginate($perPage)->appends(array(
'account' => Input::get('account'),
'account_manager' => Input::get('account_manager'),
'account_type' => Input::get('account_type'),
'perPage' => Input::get('perPage')
));
$accounts_trash = Account::accountsTrash($order)->get();
$message = Session::get('message');
$default = ($perPage === null ? 10 : $perPage);
$this->layout->content = View::make('admin.accounts.index', compact('accounts', 'accounts_trash', 'message', 'default'));
}
And the new getAccountByName() method in my controller that is used during my AJAX call. This should probably go in the model:
public function getAccountByName()
{
$name = Input::get('account');
return Account::select(array('id', DB::raw('company_name as text')))->where('company_name', 'like', "%$name%")->get();
}
And finally two new methods in my model for retrieving accounts and accounts trash:
public function scopeAccounts($query, $order, $account, $account_manager, $account_type)
{
$query->leftJoin('users', 'users.id', '=', 'accounts.user_id')
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->orderBy($order[0], $order[1])
->select(array('accounts.*', DB::raw('accounts.id as aid')));
if (!empty($account[0])) {
$query = $query->whereIn('accounts.id', $account);
}
if (!empty($account_manager[0])) {
$query = $query->whereIn('users.id', $account_manager);
}
if (!empty($account_type[0])) {
$query = $query->whereIn('account_types.id', $account_type);
}
}
public function scopeAccountsTrash($query, $order)
{
$query->onlyTrashed()
->leftJoin('users', 'users.id', '=', 'accounts.user_id')
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->orderBy($order[0], $order[1])
->select(array('accounts.*', DB::raw('accounts.id as aid')));
}
Again, there's probably a ton of things to get tidied up here but I'm certainly closer to a much faster and cleaner solution. Doing it like this has reduced the loading times from 12 seconds to 234ms.