Retrieve data from junction table in Yii2 - php

I'm trying to get data from a join table in Yii2 without an additional query. I have 2 models (User, Group) associated via the junction table (user_group). In the user_group table, I want to store extra data (admin flag, ...) for this relation.
What's the best way to add data to the junction table? The link method accepts a parameter extraColumns but I can't figure out how this works.
What's the best way to retrieve this data? I wrote an additional query to get the values out of the junction table. There must be a cleaner way to do this?!
FYI, this is how I defined the relation in the models:
Group.php
public function getUsers() {
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id']);
}
User.php
public function getGroups() {
return $this->hasMany(Group::className(), ['id' => 'group_id'])
->viaTable('user_group', ['user_id' => 'id']);
}

In short: Using an ActiveRecord for the junction table like you suggested is IMHO the right way because you can set up via() to use that existing ActiveRecord. This allows you to use Yii's link() method to create items in the junction table while adding data (like your admin flag) at the same time.
The official Yii Guide 2.0 states two ways of using a junction table: using viaTable() and using via() (see here). While the former expects the name of the junction table as parameter the latter expects a relation name as parameter.
If you need access to the data inside the junction table I would use an ActiveRecord for the junction table as you suggested and use via():
class User extends ActiveRecord
{
public function getUserGroups() {
// one-to-many
return $this->hasMany(UserGroup::className(), ['user_id' => 'id']);
}
}
class Group extends ActiveRecord
{
public function getUserGroups() {
// one-to-many
return $this->hasMany(UserGroup::className(), ['group_id' => 'id']);
}
public function getUsers()
{
// many-to-many: uses userGroups relation above which uses an ActiveRecord class
return $this->hasMany(User::className(), ['id' => 'user_id'])
->via('userGroups');
}
}
class UserGroup extends ActiveRecord
{
public function getUser() {
// one-to-one
return $this->hasOne(User::className(), ['id' => 'user_id']);
}
public function getGroup() {
// one-to-one
return $this->hasOne(Group::className(), ['id' => 'userh_id']);
}
}
This way you can get the data of the junction table without additional queries using the userGroups relation (like with any other one-to-many relation):
$group = Group::find()->where(['id' => $id])->with('userGroups.user')->one();
// --> 3 queries: find group, find user_group, find user
// $group->userGroups contains data of the junction table, for example:
$isAdmin = $group->userGroups[0]->adminFlag
// and the user is also fetched:
$userName = $group->userGroups[0]->user->name
This all can be done using the hasMany relation. So you may ask why you should declare the many-to-many relation using via(): Because you can use Yii's link() method to create items in the junction table:
$userGroup = new UserGroup();
// load data from form into $userGroup and validate
if ($userGroup->load(Yii::$app->request->post()) && $userGroup->validate()) {
// all data in $userGroup is valid
// --> create item in junction table incl. additional data
$group->link('users', $user, $userGroup->getDirtyAttributes())
}

I don't know for sure it is best solution. But for my project it will be good for now :)
1) Left join
Add new class attribute in User model public $flag;.
Append two lines to your basic relation but don't remove viaTable this can (and should) stay.
public function getUsers()
{
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id'])
->leftJoin('user_group', '{{user}}.id=user_id')
->select('{{user}}.*, flag') //or all ->select('*');
}
leftJoin makes possible to select data from junction table and with select to customize your return columns.
Remember that viaTable must stay because link() relies on it.
2) sub-select query
Add new class attribute in User model public $flag;
And in Group model modified getUsers() relation:
public function getUsers()
{
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id'])
->select('*, (SELECT flag FROM user_group WHERE group_id='.$this->id.' AND user_id=user.id LIMIT 1) as flag');
}
As you can see i added sub-select for default select list. This select is for users not group model. Yes, i agree this is litle bit ugly but does the job.
3) Condition relations
Different option is to create one more relation for admins only:
// Select all users
public function getUsers() { .. }
// Select only admins (users with specific flag )
public function getAdmins()
{
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id'],
function($q){
return $q->andWhere([ 'flag' => 'ADMIN' ]);
});
}
$Group->admins - get users with specific admin flag. But this solution doesn't add attribute $flag. You need to know when you select only admins and when all users. Downside: you need to create separate relation for every flag value.
Your solution with using separate model UserGroup still is more flexible and universal for all cases. Like you can add validation and basic ActiveRecord stuff. These solutions are more one way direction - to get stuff out.

Since I have received no answer for almost 14 days, I'll post how I solved this problem. This is not exactly what I had in mind but it works, that's enough for now. So... this is what I did:
Added a model UserGroup for the junction table
Added a relation to Group
public function getUserGroups()
{
return $this->hasMany(UserGroup::className(), ['user_id' => 'id']);
}
Joined UserGroup in my search model function
$query = Group::find()->where('id =' . $id)->with('users')->with('userGroups');
This get's me what I wanted, the Group with all Users and, represented by my new model UserGroup, the data from the junction table.
I thought about extending the query building Yii2 function first - this might be a better way to solve this. But since I don't know Yii2 very well yet, I decided not to do for now.
Please let me know if you have a better solution.

For that purpose I've created a simple extension, that allows to attach columns in junction table to child model in relation as properties.
So after setting up this extension you will be able to access junction table attributes like
foreach ($parentModel->relatedModels as $childModel)
{
$childModel->junction_table_column1;
$childModel->junction_table_column2;
....
}
For more info please have look at
Yii2 junction table attributes extension
Thanks.

Related

Doctrine & Symfony - Get data from database with parameter on array column

Good afternoon,
I try to find, from my database specific users with a role which is passed in parameter to the query. However, I have "null" result currently.
The users class is like this example from Symfony Documentation.
This is my CustomerRepository:
class CustomerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Customer::class);
}
public function findByRole(string $role)
{
return $this->getEntityManager()->createQuery(
'SELECT c
FROM App\Entity\Customer c
WHERE c.roles IN (:role)'
)
->setParameter(':role', $role)
->getResult()
;
}
}
I expect to return an array which contains one or several users who have the role.
c.roles is a list that can contains multiple items (roles), so please can you reverse the where like this :
WHERE :role IN c.roles
PS: insure the case of string $role is upper (e.g "ROLE_ADMIN").
First of all in the class from the example you have given does not have declared variable called roles, therefore you cannot write this
... WHERE c.roles IN (:role)'
So in your User class you should have something like this
private $roles;
Also as it said here you can have "multiple tables and JOIN them
in your queries."
So you would have 1 table for all your roles, one for the users and one called user_roles where you will store the roles for each user.
The next step I would approach is to search this new table for the user I want and then retrieve all rows containing the different roles in an array.
Also there are other ways - stated here. The method I recommend when storing arrays is to have a VARCHAR field (or TEXT) and then use (in your case) the Symfony serializer component to serialize() the array when saving in the DB and to deserialize()it when needed.
Good evening,
Thx for your help.
To resolve this problem, I considered that a User have only one role in application, in this context, the solution is like this:
public function findByRole(string $role, int $isActive = 1): ?Customer
{
return $this->getEntityManager()->createQuery(
'SELECT c
FROM App\Entity\Cutomer c
WHERE c.roles LIKE :role
AND c.isActive = :isActive'
)
->setParameters([
':role' => '%'.$role.'%',
':isActive' => $isActive,
])
->getOneOrNullResult()
;
}

Laravel relationship with pivot table

I have 3 tables in my database :
users (id);
interests (id);
users_interests (user_id, interests_id);
I want to be able to fetch all the user's interests in this way :
$interests = $user->interests
This is what I wrote in the User.php model, following laravel's doc:
public function interests() {
return $this->hasManyThrough(
'App\Interest', 'App\UserInterest',
'user_id', 'id', 'interest_id'
);
}
but it returns empty even though the user has a game. So there has to be something I'm doing wrong
Anyone to help me ?
I think a belongs to many would do the job:
public function interests() {
return $this->belongsToMany(
'App\Interest',
'users_interests',
'user_id',
'interests_id'
);
}
Quite similar to the example in the docs
If you were to rename users_interests table to interest_user and the column
interests_id to the singular form you would just need the first parameter:
public function interests() {
return $this->belongsToMany(App\Interest::class);
}
From my understanding the hasManyThrough is used to jump forward within a relation (also described in the docs):
The "has-many-through" relationship provides a convenient shortcut for
accessing distant relations via an intermediate relation.

Yii2. Models related

I have 3 models: Items, Serials and SerialsCategories. When I show Item form (to create or update) I need to show the serials which belongs to a categoryId selected in a previous step. A serial can belong to more than one category.
Right now I have on my Item model:
public function getSerialsTypeByCategory() {
return (new SerialType)->getByCategory($this->itemCategoryId);
}
On my SerialType model:
public function getByCategory($itemCategoryId) {
return SerialTypeItemCategory::find()->select(['serialTypeId'])->where(['itemCategoryId' => $itemCategoryId])->all();
}
This is working, it does what I need but ... Is this the proper way? is there a better way?
it's not wrong what you are doing. but there is something more -
check this link:
Working with Relational Data
if you use ->hasOne and ->hasMany to define relations, your model gains some extra benefits, like joining with lazy or eager loading:
Item::findOne($id)->with(['categories'])->all();
with a relation, you can also use ->link and ->unlink, to add/delete related data without having to think about linked fields.
Further, it is easy to define relations via junction table:
class Order extends ActiveRecord
{
public function getItems()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->viaTable('order_item', ['order_id' => 'id']);
}
}

Gii CRUD in Yii2 Generating Junction Table Relations

I have three models: Person, Feature and PersonFeature. PersonFeature is a junction table with two foreign keys, person_id referencing id in the person table and feature_id referencing id in the feature table.
My question is does Gii in Yii2 generate all the relevant relations. These are the relation in each of the three models
Person:
public function getPersonfeatures()
{
return $this->hasMany(Personfeature::className(), ['personid' => 'id']);
}
Feature:
public function getPersonfeatures()
{
return $this->hasMany(Personfeature::className(), ['featureid' => 'id']);
}
PersonFeature:
public function getPerson()
{
return $this->hasOne(Person::className(), ['id' => 'personid']);
}
public function getFeature()
{
return $this->hasOne(Feature::className(), ['id' => 'featureid']);
}
But when I browse other posts I see that there exists a 'viaTable' operation for example:
public function getPerson() {
return $this->hasMany(Person::className(), ['id' => 'personid'])
->viaTable('personfeature', ['featureid' => 'id']);}
So basically my question is, is Yii supposed to generate this for me? Or can I manually add it in?
Cheers
The last function (with viaTable) is a many to many relationship, that function can be used just as any other relational function (for instance in a ->with() query).
You don't need a model for your join table, unless you want to use it for something else.
Hope this helps.

getting the value of an extra pivot table column laravel

I have a phone_models, phone_problems, and a phone_model_phone_problem pivot table. The pivot table has an extra column 'price'.
PhoneModel:
class PhoneModel extends \Eloquent
{
public function problems()
{
return $this->belongsToMany('RL\Phones\Entities\PhoneProblem')->withPivot('price');
}
}
PhoneProblem:
class PhoneProblem extends \Eloquent
{
public function models()
{
return $this->belongsToMany('PhoneModel')->withPivot('price');
}
}
What I'm trying to do is get the price of a specific phone with a specific problem.
This is how I have it now but I feel like Laravel has a built in Eloquent feature I can't find to do this in a much simpler way:
$model = $this->phoneService->getModelFromSlug($model_slug);
$problem = $this->phoneService->getProblemFromSlug($problem_slug);
all this does is select the specific model and problem from their slug.
then what I do is with those credentials I get the price like so:
$row = DB::table('phone_model_phone_problem')
->where('phone_model_id', '=', $model->id)
->where('phone_problem', '=', $problem->id)
->first();
so now I can get the price like so $row->price but I feel like there needs to be a much easier and more 'Laravel' way to do this.
When using Many to Many relationships with Eloquent, the resulting model automatically gets a pivot attribute assigned. Through that attribute you're able to access pivot table columns.
Although by default there are only the keys in the pivot object. To get your columns in there too, you need to specify them when defining the relationship:
return $this->belongsToMany('Role')->withPivot('foo', 'bar');
Official Docs
If you need more help the task of configuring the relationships with Eloquent, let me know.
Edit
To query the price do this
$model->problems()->where('phone_problem', $problem->id)->first()->pivot->price
To get data from pivot table:
$price = $model->problems()->findOrFail($problem->id, ['phone_problem'])->pivot->price;
Or if you have many records with different price:
$price = $model->problems()->where('phone_problem', $problem->id)->firstOrFail()->pivot->price;
In addition.
To update data in the pivot you can go NEW WAY:
$model->problems()->sync([$problemId => [ 'price' => $newPrice] ], false);
Where the 2nd param is set to false meaning that you don't detach all the other related models.
Or, go old way
$model->problems()->updateExistingPivot($problemId, ['price' => $newPrice]);
And remind you:
To delete:
$model->problems()->detach($problemId);
To create new:
$model->problems()->attach($problemId, ['price' => 22]);
It has been tested and proved working in Laravel 5.1 Read more.
Laravel 5.8~
If you want to make a custom pivot model, you can do this:
Account.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Account extends Model
{
public function users()
{
return $this->belongsToMany(User::class)
->using(AccountUserPivot::class)
->withPivot(
'status',
'status_updated_at',
'status_updated_by',
'role'
);
}
}
AccountUserPivot.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class AccountUserPivot extends Pivot
{
protected $appends = [
'status_updated_by_nice',
];
public function getStatusUpdatedByNiceAttribute()
{
$user = User::find($this->status_updated_by);
if (!$user) return 'n/a';
return $user->name;
}
}
In the above example, Account is your normal model, and you have $account->users which has the account_user join table with standard columns account_id and user_id.
If you make a custom pivot model, you can add attributes and mutators onto the relationship's columns. In the above example, once you make the AccountUserPivot model, you instruct your Account model to use it via ->using(AccountUserPivot::class).
Then you can access everything shown in the other answers here, but you can also access the example attribute via $account->user[0]->pivot->status_updated_by_nice (assuming that status_updated_by is a foreign key to an ID in the users table).
For more docs, see https://laravel.com/docs/5.8/eloquent-relationships (and I recommend press CTRL+F and search for "pivot")

Categories