Prevent recursion in a Laravel Eloquent's self relationship - php

Consider a table of nested locations which has columns for the location_id, its name and a parent_id. This parent field lets you know that you're in a nested location.
Example setup:
Locations
-----------------------------------
location_id | name | parent_id
-----------------------------------
1 | Isle A | NULL
2 | Column 23 | 1
3 | Shelf 2 | 2
4 | Box C | 3
Let's say we have an item sitting in location_id=4.
We now want to easily show the item's full location in a comma separated format. We expect to see:
Isle A, Column 23, Shelf 2, Box C
In order to achieve that, I have the following Laravel Model with a full_name() function which takes care of formatting and recursive calls:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Location extends Model
{
protected $table = 'locations';
protected $primaryKey = 'location_id';
protected $with = [
'location'
];
public function location()
{
return $this->hasMany('App\Location', 'parent_id', 'location_id');
}
public function full_name()
{
return ($this->location ? $this->location->full_name() . ', ' : '') . $this->name;
}
}
Nothing fancy soo far, works correctly and outputs what I need it to.
Now imagine down the line, some other piece of code allows for a mistake and the following happens:
Locations
-----------------------------------
location_id | name | parent_id
-----------------------------------
1 | Isle A | NULL
2 | Column 23 | 3
3 | Shelf 2 | 2
4 | Box C | 3
Notice that location #2's parent_id changed to 3. This creates an infinite loop of joins between 2 and 3. Laravel will not detect this recurrence (after all we're telling it to keep joining) and the execution will eventually die. This is my problem.
A possible solution I thought about would be to keep an ongoing list of "visited" location_id's, and if we stumble upon a repeat, we terminate the execution. If this was a "normal" function, I recon something like this would work:
public function location($recurrence_guard = [])
{
if(in_array($this->location_id, $recurrence_guard)) {
return collect([]); // or null?
}
array_push($recurrence_guard, $this->location_id);
return $this->hasOne('\App\Location(<somehow pass $recurrence_guard_in_here>)', 'location_id', 'parent_id');
}
Clearly this is nonsense and that's not how Laravel's relationships work. I'm looking for ideas on how to pass extra parameters into the relationships or for any better ideas on how to design this.
Thanks!

Related

Laravel get all from with belongTo value

In my Laravel 8 project I have two tables A and B:
| A |
---------
| ID |
| B_ID |
| VALUE |
| B |
-----------
| ID |
| VALUE |
| Private |
In my models I have:
A{
public function b()
{
return $this->belongsTo(B::class);
}
}
B{
public function as()
{
return $this->hasMany(A::class);
}
}
In my controller I have:
public function index()
{
$a = A::join('bs', 'as.b_id', '=', 'b.id')->get(['as.value', 'bs.value']);
return $a;
}
Is'nt there a more elegant Laravel way to do this?
I would like to get all records from table A with the value from table B but without the private value from B. (There could be a lot of entries in Table A, Table B only has some values (<20) ).
When building a query, you may specify which relationships should be eager loaded using the with method. In your case you could write:
A::with('b:value')->('value')
In the with you can precise a specific relationship by its name or a list of column you want : https://laravel.com/docs/8.x/eloquent-relationships
The code I have provided as an example as not been tested and may need some changes but you have the general idea

Eloquent: Get 'max' values from a `hasMany` -> `hasOne` relationship

I have an interesting eloquent challenge and my knowledge is not quite there to work it through, hoping you smart folks can give me some guidance. I have read this: Getting just the latest value on a joined table with Eloquent and it is very close to what I want, but I have an extra level of tables in there.
I have two tables like this:
Users have many Assessments. Each Assessment hasOne body_fat_testing (and 4 other hasOnes that I need to use as well).
Each body_fat_testing table has several columns I'm interested (see below) in.
I'd like to get the "best" result from each "grandchild", per user.
So out of all the assessments, the best lean_mass, body_fat_percentage, etc
I have this so far:
$users = User::with(
[
'trainer',
'firstAssessment.body_fat_testing',
'firstAssessment.circumference_measurement',
'firstAssessment.exercise_testing',
'firstAssessment.overhead_squat_movement_screen',
'firstAssessment.push_ups_movement_screen',
'lastAssessment.body_fat_testing',
'lastAssessment.circumference_measurement',
'lastAssessment.exercise_testing',
'lastAssessment.overhead_squat_movement_screen',
'lastAssessment.push_ups_movement_screen',
]
)->find($request->input('user_ids'));
...
(in User.php)
public function lastAssessment()
{
return $this->hasOne('App\Assessment')->latest('assessment_date');
}
public function firstAssessment()
{
return $this->hasOne('App\Assessment')->orderBy('assessment_date');
}
There are about 30 total values I want the "best" of. Any ideas on how to accomplish this without looping through each and every value individually?
assessment table:
+----+---------+-----+--------+---------------+
| id | user_id | age | weight | assessed_date |
+----+---------+-----+--------+---------------+
| 1 | 7 | 24 | 204 | 2019-10-29 |
+----+---------+-----+--------+---------------+
body_fat_testing table:
+----+---------------+-----------+---------------------+
| id | assessment_id | lean_mass | body_fat_percentage |
+----+---------------+-----------+---------------------+
| 1 | 1 | 130.97 | 21.10 |
+----+---------------+-----------+---------------------+
Try this approach (it assumes you have models for your three tables and the relationships between them exist):
First, you'll relate your User model to your BodyFatTesting model, with this:
public function bodyFatTestings() {
return $this->hasManyThrough("App\BodyFatTesting", "App\Assessment");
}
Then, you'll create a secondary method bestBodyFatPercentage like this:
public function bestBodyFatTesting() {
return $this->bodyFatTestings()->max('body_fat_testing.body_fat_percentage');
}
To use it, just get a model in whichever way you prefer and call bestBodyFatTesting().
From there I think you can adapt it to your other "has ones".
You can also do it this way:
public function bestLeanMass()
{
return $this->hasOneThrough('App\BodyFatTesting', 'App\Assessment')
->selectRaw('user_id, max(lean_mass) as aggregate')
->groupBy('user_id');
}
Not sure which way I like better. The former is a bit easier to understand but creates more methods. This one is harder to understand but fewer methods.

Can we make a "Class Level Relationship" in Laravel, as opposed to an "Object Level Relationship"?

Consider the following scenario:
There are couple of entities in my Laravel application like the following:
Post
Page
Image
Video
All the above entities can have CustomFieldValues, which is another entity. The structure of the custom_field_values table is as follows:
ID
entity_id
custom_field_definition_id
value
[Timestamp fields]
All the CustomFieldValues belong to a single CustomFieldDefinition entity. Its table custom_field_definitions looks like following:
ID
parent_entity_name
definition_name
[Timestamp fields]
Following are some sample data from the custom_field_definitions table:
| ID | parent_entity_name | definition_name |
|----|--------------------|-------------------|
| 1 | Post | AuthorTwitterUrl |
| 2 | Page | SeoTitle |
| 3 | Image | OriginalSourceUrl |
| 4 | Video | MpaaRating |
As you can see, CustomFieldDefinitions are definitions of extra data, that we can store about each type of entity.
Following are some sampel data from the custom_field_values table:
| ID | entity_id | custom_field_definition_id | value |
|----|-----------|----------------------------|-----------------------------------|
| 1 | 1 | 1 | https://twitter.com/StackOverflow |
| 2 | 1 | 2 | My Page's SEO Title |
| 3 | 1 | 3 | http://example.com/image.jpg |
| 4 | 1 | 4 | G – General Audiences |
A little description about the data contained in the custom_field_values table:
CustomFieldValue:1: The value for CustomFieldDefinition:1 and its entity 1 (Post:1, in this case, because CustomFieldDefinition:1 is related to Post.) is "https://twitter.com/StackOverflow".
CustomFieldValue:2: The value for CustomFieldDefinition:2 and its entity 1 (Page:1, in this case, because CustomFieldDefinition:2 is related to Page.) is "My Page's SEO Title".
CustomFieldValue:3: The value for CustomFieldDefinition:3 and its entity 1 (Image:1, in this case, because CustomFieldDefinition:3 is related to Image.) is "http://example.com/image.jpg".
CustomFieldValue:4: The value for CustomFieldDefinition:4 and its entity 1 (Video:1, in this case, because CustomFieldDefinition:4 is related to Video.) is "G – General Audiences".
custom_field_values table's entity_id can refer to any entity class, therefore it is not a foreign key in the DB level. Only in combination with custom_field_definition_id we can find to which entity it actually refers to.
Now, all is well and good, until I need to add a relationship called customFieldDefinitions to any of the entities (Say Post.).
class Post extends Model {
public function customFieldDefinitions(){
$this -> hasMany ('CustomFieldDefinition');
}
}
The above does not work, because the datapoint that indicates the CustomFieldDefinition's relationship is not a foreign key field in the custom_field_definitions table, named post_id. We have to somehow build the relationship based on the fact that some records in the custom_field_definitions table has "Post" as the value of the field parent_entity_name.
CustomFieldDefinition::where('parent_entity_name', '=', 'Post');
The above snippet fetches the CustomFieldDefinitions that are related to the Post, however, it is not possible to do something like the following with the relationship:
class Post extends Model {
public function customFieldDefinitions(){
$this
-> hasMany ('CustomFieldDefinition')
-> where ('parent_entity_name', '=', 'Post')
;
}
}
The where constraint works. But Laravel also injects the ID of the current Post object into the set of constraints.
So, what I want to do is, not consider the current object's ID at all, and build a "Class Leavel Relationship", and not an "Object Level Relationship".
Is this possible under Laravel?
There might be a workaround but I'm not pretty sure about it.
What you could try doing is to define a mutated attribute and set it as the local key of the relationship:
class Post extends Model
{
public function getEntityNameAttribute()
{
return 'Post';
}
public function customFieldDefinitions()
{
return $this->hasMany(
'CustomFieldDefinition',
'parent_entity_name',
'entity_name'
);
}
}
You could also go further and define a trait which could be used by all your models which have customFieldDefinitions. It could look like:
trait HasCustomFieldDefinitionsTrait
{
public function getEntityNameAttribute()
{
return (new ReflectionClass($this))->getShortName();
}
public function customFieldDefinitions()
{
return $this->hasMany(
'CustomFieldDefinition',
'parent_entity_name',
'entity_name'
);
}
}
Then you can use it wherever needed:
class Post extends Model
{
use HasCustomFieldDefinitionsTrait;
}
class Video extends Model
{
use HasCustomFieldDefinitionsTrait;
}
class Page extends Model
{
use HasCustomFieldDefinitionsTrait;
}
class Image extends Model
{
use HasCustomFieldDefinitionsTrait;
}
Instead of hasMany(), you can create One To Many (Polymorphic) relationship between Post, Page, Image, Video and CustomFieldDefinition.
More about polymorphic relationships here.

Laravel Eloquent Inner Join on Self Referencing Table

I'm trying to inner join a users table to itself using an eloquent model. I've looked everywhere but can't seem to find a solution to this without creating two queries which is what I am currently doing.
A users table has a many to many relationship itself through the pivot table friends
I tried and failed inner joining Users::class to itself. The best I can get at an inner join is by running two queries and seeing if there is an overlap. Thus one person has reached out to the other and vice versa.
friends | users
----------|------
send_id | id
receive_id| name
is_blocked|
sample data & expected result
users.id | name
---------|------
1 | foo
2 | bar
3 | baz
friends
send_id | receive_id | is_blocked
--------|------------|-----------
1 | 2 | 0
2 | 1 | 0
1 | 3 | 0
3 | 1 | 1
2 | 3 | 0
The user should have an eloquent relationship called friends. It should be what you expect comes out of requestedFriends or receivedFriends just joined.
foo->friends
returns `baz`
bar->friends
returns `foo`
baz->friends
returns empty collection
currently using
// User.php
public function requestedFriends()
{
$left = $this->belongsToMany(User::class, 'friends','send_id','receive_id')
->withPivot('is_blocked')
->wherePivot('is_blocked','=', 0)
->withTimestamps();
return $left;
}
public function receivedFriends()
{
$right = $this->belongsToMany(User::class, 'friends','receive_id','send_id')
->withPivot('is_blocked')
->wherePivot('is_blocked','=', 0)
->withTimestamps();
return $right;
}
public function friends()
{
$reqFriends = $this->requestedFriends()->get();
$recFriends = $this->receivedFriends()->get();
$req = explode(",",$recFriends->implode('id', ', '));
$intersect = $reqFriends->whereIn('id', $req);
return $intersect;
}
Research so far
Laravel Many to many self referencing table only works one way -> old question, but still relevant
https://github.com/laravel/framework/issues/441#issuecomment-14213883 -> yep, it works… but one way.
https://laravel.com/docs/5.8/collections#method-wherein
currently the only way I have found to do this in eloquent.
https://laravel.com/docs/5.7/queries#joins -> Ideally I would find a solution using an innerjoin onto itself, but no matter which way I put the id's I couldn't get a solution to work.
A solution would
A solution would inner join a self referencing table using eloquent in laravel 5.7 or 5.8, where a relationship only exists if send_id & receive_id are present on multiple rows in the friends table.
OR
Somehow let the community know that this can't be done.
Thanks in advance!
I have not checked this solution in every detail yet, but I have written a "ManyToMany" Class extending the "BelongsToMany" Class shipped with laravel, which appears to work.
The class basically just overrides the "get" method, duplicating the original query, "inverting" it and just performing a "union" on the original query.
<?php
namespace App\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class ManyToMany extends BelongsToMany
{
/**
* Execute the query as a "select" statement.
*
* #param array $columns
* #return \Illuminate\Database\Eloquent\Collection
*/
public function get($columns = ['*'])
{
// duplicated from "BelongsToMany"
$builder = $this->query->applyScopes();
$columns = $builder->getQuery()->columns ? [] : $columns;
// Adjustments for "Many to Many on self": do not get the resulting models here directly, but rather
// just set the columns to select and do some adjustments to also select the "inverse" records
$builder->addSelect(
$this->shouldSelect($columns)
);
// backup order directives
$orders = $builder->getQuery()->orders;
$builder->getQuery()->orders = [];
// clone the original query
$query2 = clone($this->query);
// determine the columns to select - same as in original query, but with inverted pivot key names
$query2->select(
$this->shouldSelectInverse( $columns )
);
// remove the inner join and build a new one, this time using the "foreign" pivot key
$query2->getQuery()->joins = array();
$baseTable = $this->related->getTable();
$key = $baseTable.'.'.$this->relatedKey;
$query2->join($this->table, $key, '=', $this->getQualifiedForeignPivotKeyName());
// go through all where conditions and "invert" the one relevant for the inner join
foreach( $query2->getQuery()->wheres as &$where ) {
if(
$where['type'] == 'Basic'
&& $where['column'] == $this->getQualifiedForeignPivotKeyName()
&& $where['operator'] == '='
&& $where['value'] == $this->parent->{$this->parentKey}
) {
$where['column'] = $this->getQualifiedRelatedPivotKeyName();
break;
}
}
// add the duplicated and modified and adjusted query to the original query with union
$builder->getQuery()->union($query2);
// reapply orderings so that they are used for the "union" rather than just the individual queries
foreach($orders as $ord)
$builder->getQuery()->orderBy($ord['column'], $ord['direction']);
// back to "normal" - get the models
$models = $builder->getModels();
$this->hydratePivotRelation($models);
// If we actually found models we will also eager load any relationships that
// have been specified as needing to be eager loaded. This will solve the
// n + 1 query problem for the developer and also increase performance.
if (count($models) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $this->related->newCollection($models);
}
/**
* Get the select columns for the relation query.
*
* #param array $columns
* #return array
*/
protected function shouldSelectInverse(array $columns = ['*'])
{
if ($columns == ['*']) {
$columns = [$this->related->getTable().'.*'];
}
return array_merge($columns, $this->aliasedPivotColumnsInverse());
}
/**
* Get the pivot columns for the relation.
*
* "pivot_" is prefixed ot each column for easy removal later.
*
* #return array
*/
protected function aliasedPivotColumnsInverse()
{
$collection = collect( $this->pivotColumns )->map(function ($column) {
return $this->table.'.'.$column.' as pivot_'.$column;
});
$collection->prepend(
$this->table.'.'.$this->relatedPivotKey.' as pivot_'.$this->foreignPivotKey
);
$collection->prepend(
$this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->relatedPivotKey
);
return $collection->unique()->all();
}
}
I came across the same problem quite some time ago and have thus been following this problem closely and have made a lot of research. I have come across some of the solutions you have also found, and some more, and also have thought of other solutions that I summed here, mostly how to get both user_ids in the same column. I am afraid they will all not work well. I am also afraid that using any custom classes will stop you from using all of Laravel's handy relation features (especially eager loading). So I still thought what one could do, and, until one comes up with a hasMany-function on many columns, I think I have come up with a possible solution yesterday. I will show it first and then apply it to your project.
My project
Initial solution
In my project, one user partners with another one (= partnership) and then later will be assigned a commission. So I had the following tables:
USERS
id | name
---------|------
1 | foo
2 | bar
17 | baz
20 | Joe
48 | Jane
51 | Jim
PARTNERSHIPS
id | partner1 | partner2 | confirmed | other_columns
----|-----------|-----------|-----------|---------------
1 | 1 | 2 | 1 |
9 | 17 | 20 | 1 |
23 | 48 | 51 | 1 |
As each user should always have only one active partnership, the non-active being soft-deleted, I could have helped myself by just using the hasMany function twice:
//user.php
public function partnerships()
{
$r = $this->hasMany(Partnership::class, 'partner1');
if(! $r->count() ){
$r = $this->hasMany(Partnership::class, 'partner2');
}
return $r;
}
But if I had wanted to lookup all partnerships of a user, current and past, this of course, wouldn't have worked.
New solution
Yesterday, I came up with the solution, that is close to yours, of using a pivot table but with a little difference of using another table:
USERS
(same as above)
PARTNERSHIP_USER
user_id | partnership_id
--------|----------------
1 | 1
2 | 1
17 | 9
20 | 9
48 | 23
51 | 23
PARTNERSHIPS
id | confirmed | other_columns
----|-----------|---------------
1 | 1 |
9 | 1 |
23 | 1 |
// user.php
public function partnerships(){
return $this->belongsToMany(Partnership::class);
}
public function getPartners(){
return $this->partnerships()->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}])->get();
}
public function getCurrentPartner(){
return $this->partnerships()->latest()->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}])->get();
}
// partnership.php
public function users(){
return $this->belongsToMany(User::class);
}
Of course, this comes with the drawback that you always have to create and maintain two entrances in the pivot table but I think this occasional extra load for the database -- how often will this be altered anyway? -- is preferable to having two select queries on two columns every time (and from your example it seemed that you duplicated the entries in your friends table anyway).
Applied to your project
In your example the tables could be structured like this:
USERS
id | name
---------|------
1 | foo
2 | bar
3 | baz
FRIENDSHIP_USER
user_id | friendship_id
---------|------
1 | 1
2 | 1
3 | 2
1 | 2
FRIENDSHIPS
id |send_id* | receive_id* | is_blocked | [all the other nice stuff
--------|---------|-------------|------------|- you want to save]
1 | 1 | 2 | 0 |
2 | 3 | 1 | 0 |
[*send_id and receive_id are optional except
you really want to save who did what]
Edit: My $user->partners() looks like this:
// user.php
// PARTNERSHIPS
public function partnerships(){
// 'failed' is a custom fields in the pivot table, like the 'is_blocked' in your example
return $this->belongsToMany(Partnership::class)
->withPivot('failed');
}
// PARTNERS
public function partners(){
// this query goes forth to partnerships and then back to users.
// The subquery excludes the id of the querying user when going back
// (when I ask for "partners", I want only the second person to be returned)
return $this->partnerships()
->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}]);
}

Laravel sum() method on collection returning results for all items in table

Given the following table
gallery
+----+---------------+--------------+---------+
| id | gallery_title | viewcount | user_id |
+----+---------------+--------------+---------+
| 1 | Animals | 10 | 1 |
| 2 | Cars | 5 | 1 |
| 3 | Houses | 2 | 2 |
+----+---------------+--------------+---------+
user
+----+----------+
| id | username |
+----+----------+
| 1 | Bob |
| 2 | James |
+----+----------+
and the following classes
class Gallery extends Model
....
public function user()
{
return $this->belongsTo('App\User');
}
and
class User extends Model
....
public function galleries()
{
return $this->hasMany('App\Gallery');
}
calling $galleryCollections= Auth::user()->galleries; returns an array of collections in which I can iterate through
foreach ($galleryCollections as $galleryCollection)
{
$viewcount += $galleryCollection->viewcount;
}
print $viewcount; #returns 15
and so far everything works as expected, correctly up until this point.
However if I accidentally called $galleryCollection->sum('viewcount'), which is the last value from the iteration, the returned value is 17, as it's simply running the following SQL select sum('viewcount') as aggregate from 'gallery'.
I'm struggling to understand what exactly what is happening here. It's almost as if it's calling the 'sum()' method on the gallery class itself without passing in any 'where' values. I'd at least expect it to call the sum() method on the Collection, but instead it's going back to the database.
Is it because my Gallery class does not implement a 'sum()' method, and therefore it uses the Parent Model class and ignores the Gallery class?
If you want to count through sql, then:
Auth::user()->galleries()->sum('viewCount');
But, Auth::user()->galleries->sum('viewCount'); will sum on the collection Auth::user()->galleries.
Auth::user()->galleries() is a queryBuilder whereas Auth::user()->galleries is a collection of galleries of that user.

Categories