Eloquent Specify Relation Columns of belongsToMany relationship - json

I have tried all kinds of methods of limiting the columns which are returned in my many-to-many relationship, and none seem to work.
Background - Not really necessary, but to give the big picture
Essentially, in my app, I want to build a list of contacts for the currently logged in user. Administrator and Billing users should be able to contact everybody including users of group Customer.
Customer should only be able to contact Administrator and Billing.
So my way to tackle this is firstly to determine the groups that the user is in.
$userGroups = Sentry::getUser()->getGroups()->lists('name', 'id');
Then iterate over the groups, to see if the user is in the group Administrator or Billing or Customer and build the contact groups for that user.
foreach($userGroups as $group)
{
if ($group === 'Administrator' || $group === 'Billing')
{
$contactGroups = \Group::with('users')->get(['id', 'name']);
}
else if ($group === 'Customer')
{
$contactGroups = \Group::where('name', 'Administrator')
->orWhere('name', 'Billing')
->with('users')
->get(['id', 'name']);
}
else
{
return Response::json('No Contacts found', 404);
}
}
The problem - It appears that I am unable to select specific columns to select on belongsToMany relations.
I have tried:
$contactGroups = \Group::where('name', 'Administrator')
->orWhere('name', 'Billing')
->with(['users', function($q){
$q->select('id', 'first_name', 'last_name');
}])
->get(['id', 'name']);
I have also tried limiting the select within the Group model
class Group extends Eloquent
{
protected $table = 'groups';
public function users()
{
return $this->belongsToMany('User', 'users_groups')
->select('id', 'first_name', 'last_name', 'email', 'telephone');
}
}
Either way, the query runs, but it returns the entire user object and completely ignores my selects.
As such, when I return a json response, everything that I do not want is included.
So what I have done as a temporary fix is iterate over each of the users in each of the groups, and unset all the attributes which I do not want.
foreach ($contactGroups as $group)
{
foreach($group->users as $user)
{
unset($user->persist_code);
unset($user->created_at);
unset($user->updated_at);
unset($user->deleted_at);
unset($user->last_login);
unset($user->permissions);
unset($user->activated_at);
unset($user->activated);
unset($user->reset_password_code);
unset($user->pivot);
}
}
return Response::json($contactGroups, 200);
This is really clunky, inefficient and seems like a waste of time. Is there a better way of achieving the above?

For some reason selecting specific columns with belongsToMany is not working.
But i have found an alternate solution.
There is a provision in laravel, Converting to Arrays or json that allows you to whitelist/blacklist specific columns when using toArray() or toJson.
To prevent specific fields from appearing in the relation :
class User extends Eloquent{
protected $hidden = array("persist_code","created_at","updated_at","deleted_at","last_login");
}
Instead if you wish to allow specific fields :
protected $visible = array("Visibile fields");

try this
$contactGroups = \Group::where('name', 'Administrator')
->orWhere('name', 'Billing')
->with(['users', function($q){
$q->get(['id', 'first_name', 'last_name']);
}])
->get(['id', 'name']);
or this
$contactGroups = \Group::where('name', 'Administrator')
->orWhere('name', 'Billing')
->with(['users:id,first_name,last_name'])
->get(['id', 'name']);

Related

Optimization of Laravel pivot table relationship

I have a pivot table called invite_riskarea which is designed as follows:
This table handles the permissions that have a specific user (through an invite id) to access to specific riskfields. Each riskfield is associated with a riskarea which acts as the main container of specific riskfields.
Within the model Invite I have this relationship:
public function riskareas()
{
return $this->belongsToMany(Riskarea::class)->withPivot('riskfield_id', 'insert', 'edit', 'view');
}
In this way I can return all the riskareas associated with a specific invite, and I should be able to return all the riskfields associated with a specific riskarea in the same invite model.
As you can see from the table invite_riskarea, I have three columns called insert, edit, and delete. These columns manage the types of permissions assigned to a specific user (via an invite id) for a specific riskfield belonging to a riskarea.
I'm trying to retrieve the riskarea permission in the following way:
$invite = Invite::where('id', 58)->first();
$riskarea = $invite->riskareas[0];
$riskfield = $riskareas->riskfields[0];
echo 'view permission => ' . $riskfield->insert;
The problem's that I'm not able to setup a correct relationship in the Invite model that returns me the pivot data of the permissions columns only for the riskfield associated with the riskarea.
So I have manage to handle this situation in this way:
$riskareas = Riskarea::all();
foreach ($riskareas as &$riskarea) {
foreach ($riskarea->riskfields as &$riskfield) {
$result = DB::table('invite_riskarea')
->select('insert', 'edit', 'view')
->where([
'riskarea_id' => $riskarea->id,
'riskfield_id' => $riskfield->id
])
->first();
if ($result) {
$riskfield->insert = $result->insert;
$riskfield->edit = $result->edit;
$riskfield->view = $result->view;
}
}
}
Essentially, I get all the riskareas, and then I iterate over the riskfields associated. For each riskfield, I get the permissions in the invite_riskarea table and then I have the correct structure that I want.
So to summarize:
Is it actually possible create a model relationship that returns the permissions for riskfield and not for riskarea?
Is my table implementation good enough to handle that situation?
I suggest you define back the many-to-many relation for the Riskfield model with the Invite model.
You can also define a direct many-to-many relationship with riskfield in the Invite model. This is how convenient it is for you personally.
And so the inverse many-to-many relationship
public function invites()
{
return $this->belongsToMany(Invite::class)->withPivot('insert', 'edit', 'view');
}
Then get all objects' Riskfields that are associated with the specified invite:
$riskfields = Riskfields::wherehas('invites' . function (Builder $query) use ($invite_id) {
$query->where('invites.id', $invite_id);
})->with('invites')->get();
Then you can access the desired fields of the pivot table in the specified way:
foreach ($riskfields as $riskfield) {
foreach ($riskfield->invites as $invite) {
$insertRiskField = $invite->pivot->insert;
$editRiskField = $invite->pivot->edit;
$viewRiskField = $invite->pivot->view;
}
}
Eager loading executes one query to the database
Yes
Documentation Laravel

Getting all users except admins in many-to-many relationship

I have a users table and roles table, connecting them in many-to-many relationship in role_user table.
I want to get all users except the users that have admin role, I want to include the users that do not have any roles.
Basically all users except admins.
Expecting that the relationships are setup properly, this can be achieved rather easily with whereDoesntHave():
$roleToExclude = 1;
$users = User::query()
->whereDoesntHave('roles', function (Builder $query) use ($roleToExclude) {
$query->where('id', $roleToExclude);
})
->get();
Regarding the comment: if you want to retrieve all users that have at least one role, but their roles may not contain the admin role, then you can use this query:
$roleToExclude = 1;
$users = User::query()
->has('roles')
->whereDoesntHave('roles', function (Builder $query) use ($roleToExclude) {
$query->where('id', $roleToExclude);
})
->get();
has('roles') will ensure there EXISTS one role for the user, while whereDoesntHave('roles', fn()) will ensure it is not an admin role.
A note about the suggested edit of #Jino Antony:
When dealing with many-to-many relations, all the whereX($col, $val) methods of the query builder operate on the other table (roles in this case), not the pivot table (role_user). To query a column on the pivot table, you'd need to use wherePivot('role_id', $roleToExclude) in my example.
Add a relation in the User model.
User.php
public function roles(){
return $this->belongsToMany(Role::class);
}
For Retrieving
$user = User::whereHas('roles', function($query){
$query->where('name', '<>', 'admin') // role with no admin
});
For plain MYSQL
SELECT u.* FROM users u
INNER JOIN role_user ru ON ru.user_id = u.id
INNER JOIN roles r ON r.id = ru.role_id WHERE r.name <> 'admin';
Since Above Answers are missing the Reverse Method i have added that Relation
public function roles()
{
return $this->belongsToMany(Role::class);
}
//roles that need to be excuded
//it also accepts the array
$rolesExcept = 'admin';
//roles that need to be included
//it also accepts the array
$rolesOnly = 'admin';
//closure that filter the the rolesOnly
$withSpecificRolesClosure = function ($query) use ( $rolesOnly)
{
$query-> whereIn( 'name', (array) $rolesOnly); // role with only admin
};
//closure that filter the the rolesExcept
$withOutSpecificRolesClosure = function ($query) use ( $rolesExcept)
{
$query->whereNotIn('name', (array)$rolesExcept); // role with no admin
};
//get all the users with the role with admim
$userWithRoleAdmin = App\Models\User::whereHas('roles', $withSpecificRolesClosure)->get();
//get all the users with the role without admim
$userWithOutRoleAdmin = App\Models\User::whereHas('roles',$withOutSpecificRolesClosure)->get();
can you try this one. if you're not using a model relationship this would work
$users = DB::table(role_user as ru')
->join('users as u', 'ru.user_id', '=', 'u.id')
->join('roles as r', 'r.id', '=', 'ru.id')
->where('r.name', '<>', 'admin')->get()
try below code:
$users = User::whereDoesntHave('roles', function ($query) {
$query->where('name', Role::ROLE_ADMIN);
})->get();
add relationship code in User.php file.
public function roles()
{
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
}

Laravel: hasMany relationship + where condition fails

I have a Customer Eloquent model. Customer can have multiple WishLists where he / she can add some products. Typical ecommerce functionality.
The point is that Customer can belong to many Users models.
This was easy:
public function users()
{
return $this->belongsToMany(User::class, 'users_sync_customers', 'customer_uuid', 'user_id')
->withTimestamps()
->orderBy('last_name', 'asc');
}
So I can get all Customers assigned for logged in user by
auth()->user()->customers 🎉
As I mentioned, Customer can have multiple Wishlists:
public function wishLists()
{
return $this
->hasMany(WishList::class, 'customer_uuid', 'uuid')
->where('user_id', '=', auth()->user()->id); // <----- this will fail when I log out
}
but WishList is scoped to both Customer UUID and User ID.
Above relationship works but only when I'm logged in obviously.
As soon as I log out the auth()->user()->is is NULL and I get:
ErrorException {#1483 #message: "Trying to get property 'id' of
non-object"
Question: How can I reference in wishLists() the user_id value?
WishList model has this:
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
So can I use something like $this->user->id?
edit:
Nope, this also doesn't work.
you must check that the user is logged in?
Auth::check() ? Auth::user()->id : null

Yii2: how to perfect separate validate model and save model?

In which case is it better to use the Validate model? Example: I have two model
AgreementForm
Agreement
When I create data - I use
$model = new AgreementForm();
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
Code of AgreemtnForm
AgreementForm extemds Model {
......
public function save() {
if(!$this->validate()) {
return null;
}
$model = new Agreement();
$model->content = $this->content;
if( $model->save() ) {
return true;
}
}
But when I update data, I use only that code:
public function actionUpdate( $id ) {
$model = Agreement::findOne( $id );
if( $model->load( \Yii::$app->request->post() ) && $model->save() ){
$this->refresh();
}
else {
return $this->render('update', [
'model' => $model,
]);
}
}
What is the validation model in this case, if I duplicate the validation in another model? Can I only use 1 model?
I will try to explain why you should almost always use a Form model to validate your data, providing some cases:
1) Doing
if( $model->load( \Yii::$app->request->post() ) && $model->save() )
could be dangerous, the user can send post data that he was not supposed to.
eg. Agreement has a userid column that gets id of the user that created it. If a user, sends this field in the post request, he could potentially change its value.
Your form model should define the properties that you expect to be sent.
2) You want to define additional validation for your model based on eg. the user role, the time the Agreement is valid etc.
Lets say you have 2 user roles:
Retailer
Merchant
Retailer can create an Agreement that is 300 characters long and max price 1000, opposed to 700 and 10000 for the merchant.
How do you cope with that?
You create 2 different forms:
$user = Yii::$app->user;
if ($user->can('retailer')) {
$model = new RetailerAgreementForm();
}
else {
$model = new MerchantAgreementForm();
}
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
In the form models, you can add the additional validation for your fields:
public function rules()
{
return [
['body', 'string', 'max' => 300],
['price', 'integer', 'max'=> 1000],
];
}
Using different forms for this I believe is the best option.
3) Your form model fields do not correspond to Database model columns 1 to 1.
Consider this example:
Your want to save the address of the agreement, street, state and city. You have a list of cities, states and streets.
The only thing you want to do with the address is save it and load it, eg. nobody is going to search per city.
So you just define a column address(type text) in your table and save the data as a JSON.
Your AgreementForm defines the address as separate fields and validates them accordingly and your Agreement model just validates address to be a string.
Do not duplicate your validation between AgreementForm and Agreement models. They should define different validation rules.
Note: Even if you do not render the form in a view and just post some data, it is good to use a form model to get exactly the fields you want to change and if needed, validate them with additional rules. The only case that I could think to just use the DB model directly is when you want to provide just some basic crud operations for your model.

Multi-select Filter Search in Laravel 4

I need help/guidance in developing a multi-select filter search for my Laravel 4 app.
I have a table in my database called 'accounts'. This table is linked to other tables in the database via the following relationships:
'users' via belongs to (User model has a has many relationship to accounts)
'account_types' via belongs to (AccountType model has a has one relationship to accounts)
In my view I would like 3 multi-select boxes, company names (taken from the accounts table 'company_name' field), account managers (taken from the account_managers table, 'first_name' and 'last_name' fields) and account type (taken from the account_types table, 'type' field).
When a user selects values from these multi-select boxes and submits the form, I need to search the relevant tables and bring back the results. I don't want to use joins for this, as it is very slow. Especially, when bringing back values for the multi-select boxes.
If possible I would like to use Eloquent relationships in a way that brings back the results quickly.
I have this working with joins and query strings but it is very slow, up to 10 to 15 seconds.
I hope someone can help me out with this. Cheers.
OK, I have this working like a charm now by implementing select2, rather than simply loading all contacts in one go. Works much nicer too.
Here's my index method in AdminContactsController.php:
public function index()
{
$contact_names_value = explode(',', Input::get('contact_names_value'));
$accounts_value = explode(',', Input::get('accounts_value'));
$account_managers_value = explode(',', Input::get('account_managers_value'));
// In the view, there is a dropdown box, that allows the user to select the amount of records to show per page. Retrive that value or set a default.
$perPage = Input::get('perPage', 10);
// This code retrieves the order from that has been selected by the user by clicking on table ciolumn titles. The value is placed in the session and is used later in the Eloquent query and joins.
$order = Session::get('contact.order', 'cname.asc');
$order = explode('.', $order);
$message = Session::get('message');
$default = ($perPage === null ? 10 : $perPage);
$contacts_trash = Contact::contactsTrash($order)->get();
$this->layout->content = View::make('admin.contacts.index', array(
'contacts' => Contact::contacts($order, $contact_names_value, $accounts_value, $account_managers_value, $perPage)->paginate($perPage)->appends(array('accounts_value' => Input::get('accounts_value'), 'account_managers_value' => Input::get('account_managers_value'))),
'contacts_trash' => $contacts_trash,
'perPage' => $perPage,
'message' => $message,
'default' => $default
));
}
My scopeContacts method in my Contact.php model:
public function scopeContacts($query, $order, $contact_names_value, $accounts_value, $account_managers_value, $perPage)
{
$query->leftJoin('accounts', 'accounts.id', '=', 'contacts.account_id')
->leftJoin('users', 'users.id', '=', 'accounts.user_id')
->orderBy($order[0], $order[1])
->select(array('contacts.*', DB::raw('contacts.id as cid'), DB::raw('CONCAT(contacts.first_name," ",contacts.last_name) as cname'), DB::raw('CONCAT(users.first_name," ",users.last_name) as amname')));
if (empty($contact_names_value[0])) {
//
} else {
$query = $query->whereIn('contacts.id', $contact_names_value);
}
if (empty($accounts_value[0])) {
//
} else {
$query = $query->whereIn('accounts.id', $accounts_value);
}
if (empty($account_managers_value[0])) {
//
} else {
$query->whereIn('users.id', $account_managers_value);
}
}
Here's my JS code:
$('#contact_names_value').select2({
placeholder: 'Search contacts',
minimumInputLength: 3,
ajax: {
url: '/admin/get-contact',
dataType: 'json',
data: function (term, page) {
return {
contact_names_value: term
};
},
results: function (data, page) {
return {results: data};
}
},
tags: true
});
Here's my method getContactByName implemented in my AdminContactsController.php (similar methods implemented for users and accounts) code:
public function getContactByName()
{
$name = Input::get('contact_names_value');
return Contact::select(array('id', DB::raw('concat(first_name," ",last_name) as text')))->where(DB::raw('concat(first_name," ",last_name)'), 'like', "%$name%")->get();
}
Notice during my select statement, I do a DB::raw and set the 'first_name' and 'last_name' fields to be selected as 'text'. I think this was one of the major issues, as the plugin requires 'id' and 'text' to function.
My route was simply:
Route::get('admin/get-contact', 'AdminContactsController#getContactByName');