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

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()
;
}

Related

Laravel / Eloquent : hasManyThrough question

I have 3 tables:
Contract, with id
Contract_User, with contract_id and user_id
User, with id
I'm trying to use hasManyThrought to get the User information when I have a contract, but I'm having trouble.
Is it possible?
Thanks
I agree with Tim Lewis. I think this is just a "simple" BelongsToMany. Depending on what your models look like, here is an example of what they could/should look like.
If you are using something other than the default foreign keys, you'll need to provide those column(s) as extra parameters. You can find more on that in the link above as well. Hope this helps!
User.php
/**
* The contracts that belong to the user.
*/
public function contracts()
{
return $this->belongsToMany('App\Contract');
}
Contract.php
/**
* The users that belong to the contract.
*/
public function users()
{
return $this->belongsToMany('App\User');
}
Then somewhere in your controller, you'd get to the user's contracts by:
$user = App\User::find(1);
foreach ($user->contracts as $contract) {
// $contract->name;
}

How to use custom SELECT with JOINs and GROUP BY in Laravel model?

I want to use sophisticated SELECT query with JOINs and GROUP BY in Laravel model.
Сoncretely I want to make a messager in my application. Here is table "messages" with all messages. Now I want to create model called "Dialog". Keep in mind here is no table "dialogs", a dialog is a result of joining and grouping.
Example of query:
SELECT
cl.name AS client_name,
COUNT(m.id) AS messages_count,
MAX(m.created_at) AS last_message,
COUNT(m.id) > SUM(m.viewed_by_client) AS has_new_for_client,
COUNT(m.id) > SUM(m.viewed_by_user) AS has_new_for_user
FROM messages AS m
INNER JOIN clients AS c ON m.client_id = c.id
GROUP BY c.id
Of cource I can use raw SQL queries. But I want to use Eloquent relations later with all its benefits. For example:
$dialog->client->full_name
$dialog->client->order->ordered_items
I had an idea to create a VIEW in database from my query and to use this view as a fake table in the model. But it seems to me not ideal solution.
So, how can I use JOINs and GROUP BY in Eloquent when I do not have a real table for model entities? Or may be some different solutions for my task?
You can have a database table without an Eloquent model but not the other way around. That said, there's no rule against making more than 1 model per table. Not really standard practice though.
I experimented with making a model that would inherit from another model but the boot method didn't work as expected so I dropped it.
I think you could get all the information you take from that query with accessors in your Client model. Since your query has no where clause, a scope is not really necessary but it could also be done with that.
OPTION 1: Accessors
# App\Client
class Client extends Model
{
// Standard Eloquent relationship
public function messages()
{
return $this->hasMany(App\Message::class);
}
// Accessor $client->client_name
public function getClientNameAttribute()
{
return $this->name;
}
// Accessor $client->last_message
public function getLastMessageAttribute()
{
// Load relationship only if it hasn't been loaded yet
if(!$this->relationshipLoaded('messages'))
$this->load('messages');
// use max() method from collection to get the results
return $this->messages->max('created_at');
}
// Accessor $client->has_new_for_client
public function getHasNewForClientAttribute()
{
// Load relationship only if it hasn't been loaded yet
if(!$this->relationshipLoaded('messages'))
$this->load('messages');
return $this->messages->count() > $this->messages->sum('viewed_by_client');
}
// Accessor $client->has_new_for_user
public function getHasNewForUserAttribute()
{
// Load relationship only if it hasn't been loaded yet
if(!$this->relationshipLoaded('messages'))
$this->load('messages');
return $this->messages->count() > $this->messages->sum('viewed_by_user');
}
}
And then you can access all the properties dynamically
$dialog = Client::withCount('messages')->find($id);
$dialog->client_name;
$dialog->messages_count;
$dialog->has_new_for_client;
$dialog->has_new_for_user;
$dialog->last_message;
However if you're converting $dialog to an array or json format, accessors will be lost unless you append them. In the same way, you can hide the attributes you don't want to show.
This can be done globally for the model
protected $appends = ['client_name', 'has_new_for_client', 'has_new_for_user', 'last_message'];
protected $hidden = ['name'];
or locally for the query
$dialog->setHidden(['name']);
$dialog->setAppends(['client_name', 'has_new_for_client', 'has_new_for_user', 'last_message'];
OPTION 2: Query scopes
# App\Client
class Client extends Model
{
public function scopeDialog($query)
{
$query->select('name as client_name')
->withCount('messages') // the default name will be messages_count
->selectRaw('max(m.created_at) as last_message')
->selectRaw('count(m.id) > sum(m.viewed_by_client) as has_new_for_client')
->selectRaw('count(m.id) > sum(m.viewed_by_user) as has_new_for_user')
->join('messages as m', 'm.client_id', 'clients.id')
->groupBy('clients.id');
}
}
And then just call it like you would any scope Client::dialog()->...
OPTION 3: Just use whatever methods are already available instead of writing more logic
$dialog = Client::with('messages')->find($id);
// client_name
$dialog->name
// messages_count
$dialog->messages->count()
// last_message
$dialog->messages->max('created_at')
// has_new_for_client
($dialog->messages->count('id') > $dialog->messages->count('viewed_by_client'))
// has_new_for_user
($dialog->messages->count('id') > $dialog->messages->count('viewed_by_user'))
Create dialogs table and put 'dialog_id' column into the messages table. Each message has a dialog and a client. Create relationships in each model. So you can access attributes over models as you want. By doing this, this code works;
$dialog->client->full_name
$dialog->client->order->ordered_items
I am trying to detail example about how to get User Model's Accessor in another model with using relationship
Suppose, we have User table & Comment Table...
Now, Suppose I appends User's Profile Full URL in User model using "getProfilePhotoUrlAttribute" Method. when I call User model eloquent then it's appends User Profile Image automatically.
but Now I wants to get that user's profile Full URL in with Comments then we can't access Accessor using Join because with join we can join only out DataBase's Table Columns. If we have profile_photo_path column & doesn't have profile_photo_url named column as we define accessor function name then we can't access using jjoin. in this case we wants to use Relationship method
For example:-
Case :- 1 You wants to Get the user's comments with User details
In this case, User have one or more than one comments So we need to use One TO Many Relation
App/Models/User.php file
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = [
'profile_photo_url',
];
/**
* Get the URL to the user's profile photo.
*
* #return string
*/
public function getProfilePhotoUrlAttribute()
{
... here return full profile URL (concat profile_path with storage/public location path)...
}
/**
* Get the user's comments with User details.
*
* One To Many relation
*/
public function comments()
{
return $this->hasMany(Comment::class);
}
Now then, use Model eloquent Query like below
$user = User::with('comments')->where('id', '=', '2')->get();
echo '<pre>';
print_r($user->toarray());
Case :- 2 You wants to Get the user details of the all comments.
In this case, we need to use Many TO One Relation
App/Models/Comment.php file
/**
* Get the user details of the comments.
*
* One To Many (Inverse) / Belongs To
*/
public function user()
{
return $this->belongsTo(User::class);
}
then use Model eloquent Query like below
$comments = Comment::where('deal_id', '=', '45')->get();
print_r($comments->toarray());
foreach ($comments as $comment) {
print_r($comment->user->toarray());
echo $comment->user->profile_photo_url;
echo "<br/>";
}
NOTE:- I used Latest version - it is Laravel 8, So Syntax may vary as per your Laravel Version
For More Detail with Output Data check here my answer on another question
& you can check it in Laravel Official Documentation

Using eloquent relation

I am trying to understand how to effectively use Eloquent relationships to have some high level functions in the model.
I have a subscription app with 2 tables, 'users' and 'subscriptions'.
This is a legacy system so I cannot just change things in any way I want.
Table users (model App\User)
id
email
active (0/1)
join_date
address etc
phone
Table subscriptions (model App\Subscription)
id
user_id
box_id (what the person is subscribed to get)
amount
Users are marked active or not active.
I would like to have a static method on the Subscription model that will give me all the active subscriptions. This data is then fed into other parts of the application.
This is derived by joining subscriptions to users and filtering based on the active column.
The query is like this:
SELECT users.*, subscriptions.*
FROM subscriptions
JOIN users ON users.id = subscriptions.user_id
WHERE users.active = 1
Subscription model
class Subscription extends Model
{
public static function allActive()
{
// This works except it doesn't use the eloquent relationship
return static::where('users.active', 1)
->join('users', 'users.id', '=', 'subscriptions.user_id')
->select('users.*','subscriptions.*')
->get();
}
public function user()
{
return $this->belongsTo(User::class);
}
}
User model
class User extends Authenticatable
{
use Notifiable;
public function subscriptions()
{
return $this->hasMany(Subscription::class);
}
}
I would use it like this:
$subscriptions = \App\Subscription::allActive()->toArray();
print_r($subscriptions);
I have 2 questions.
How do I rewrite the allActive function to use the relationship I already defined? Any solution should generate SQL with a JOIN.
In the returned data, how do I separate the columns from the two separate tables so that it is clear which table the data came from?
Given the relationships you have wired up, to get only active subscriptions from the model class you will have to do it this way:
class Subscription extends Model
{
public static function allActive()
{
$activeSubcriptions = Subscription::whereHas('user', function($query){
$query->where('active', 1) //or you could use true in place of 1
})->get();
return $activeSubcriptions;
}
public function user()
{
return $this->belongsTo(User::class);
}
}
Thats working with closures in Laravel, quite an efficient way of writing advanced eloquent queries.
In the callback function you will do pretty much anything with the $query object, its basically working on the User model since you mentioned it as the first parameter of the ->whereHas
Note that that variable has to have EXACTLY the same name used in declaring the relationship
The above i suppose answers your first question, however its highly recommended that you do most of this logic in a controller file
To answer question 2, when you execute that get() it will return Subscription objects array so to access the info based on columns you will have to go like:
$subscriptions = \App\Subscription::allActive();
foreach($subscriptions as $subscription){
$amount = $subscription->amount; //this you access directly since we working with the subscription object
$box_id = $subscription->box_id;
//when accessing user columns
$email = $subscription->user->email; //you will have to access it via the relationship you created
$address = $subscription->user->address;
}

Laravel -- Flatten data appended via `with`

I've got two models, User and Seminar. In English, the basic idea is that a bunch of users attend any number of seminars. Additionally, exactly one user may volunteer to speak at each of the seminars.
My implementation consists of a users table, a seminars table, and a seminar_user pivot table.
The seminar_user table has a structure like this:
seminar_id | user_id | speaking
-------------|-----------|---------
int | int | bool
The relationships are defined as follows:
/** On the Seminar model */
public function members()
{
return $this->belongsToMany(User::class);
}
/** On the User model */
public function seminars()
{
return $this->belongsToMany(Seminar::class);
}
I am struggling to figure out how to set up a "relationship" which will help me get a Seminar's speaker. I have currently defined a method like this:
public function speaker()
{
return $this->members()->where('speaking', true);
}
The reason I'd like this is because ultimately, I'd like my API call to look something like this:
public function index()
{
return Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
}
The problem is that since the members relationship is actually a belongsToMany, even though I know there is only to ever be a single User where speaking is true, an array of User's will always be returned.
One workaround would be to post-format the response before sending it off, by first setting a temp $seminars variable, then going through a foreach and setting each $seminar['speaker'] = $seminar['speaker'][0] but that really stinks and I feel like there should be a way to achieve this through Eloquent itself.
How can I flatten the data that is added via the with call? (Or rewrite my relationship methods)
Try changing your speaker function to this
public function speaker()
{
return $this->members()->where('speaking', true)->first();
}
This will always give you an Item as opposed to a Collection that you currently receive.
You can define a new relation on Seminar model as:
public function speaker()
{
return $this->belongsToMany(User::class)->wherePivot('speaking', true);
}
And your query will be as:
Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
Docs scroll down to Filtering Relationships Via Intermediate Table Columns

Retrieve data from junction table in Yii2

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.

Categories