Lazy load Eloquent relation with parameter - laravel

I want to lazyload the products with a Laravel Eloquent Entity
/**
* This is in my Orders model
*/
public function product($lang = '') {
switch ($lang){
case '': return $this->belongsTo(ViewProducts::class, 'product_id', 'id');
case 'en_us': return $this->belongsTo(ViewProductsEnUs::class, 'product_id', 'id');
}
}
/**
* This is what works. But its not lazy
*/
$orders = Oders::where('email','me#me.com')
->get();
foreach ($orders as $order){
$product = $order->products('en_us')->first();
}
/**
* This is what I want. Lazyload the product in a different language 'en_us'
*/
$orders = Oders::where('email','me#me.com')
->with('products', // I want to pass the param 'en_us' in any way //)
->get();
Anybody out there who knows how to pass a param (in any way) to the Eloquent Entity so I can lazyload my product data?
I appreciate your help.

Thanks you #TimLewis!
Instead of one relation 'product_' defining multiple relations like
/**
* In my Orders model
*/
public function product_() {
return $this->belongsTo(ViewProducts::class, 'product_id', 'id');
}
public function product_en_us(){
return $this->belongsTo(ViewProductsEnUs::class, 'product_id', 'id');
}
Works!
It's not totally perfect because you have to use it like so:
$productRelation = 'product_' . $lang;
$orders = Oders::where('email','me#me.com')
->with($productRelation)
->get();
// And then get the data like this
$orders->{$productRelation};
But for me it is totally fine!

Related

Laravel Polymorphic Relationships with order by

Laravel version 7.2.5.
I am using Polymorphic Relationships to store the access logs (login, logout) for multi-role application.
The data storing part is working completely fine. Now, I need to show the list of records in desc format with pagination. But, It's loading the data in the asc format
SomeModel.php
class SomeModel extends Model
{
/**
* Polymorphic Relationships
*/
public function loginLog()
{
return $this->morphMany(LoginLog::class, 'employable');
}
public function show($token)
{
return self::where([
'token' => $token,
])->with([
'loginLog:employable_id,ip,created_at,updated_at'
])->first() ?? null;
}
}
I did find the solution. But, somehow it doesn't feel the appropriate way to solve this issue.
Here is the link Laravel order results by column on polymorphic table
Try this
class SomeModel extends Model
{
/**
* Polymorphic Relationships
*/
public function loginLog()
{
return $this
->morphMany(LoginLog::class, 'employable')
->latest();
}
}
I found another way to solve this issue...
class SomeModel extends Model
{
/**
* Polymorphic Relationships
*/
public function loginLog()
{
return $this->morphMany(LoginLog::class, 'employable');
}
public function show($token, $pagination = 0)
{
return self::where([
'token' => $token,
])->with([
'loginLog' => function ($query) use ($pagination) {
return $query->select([
'employable_id',
'ip',
'created_at',
'updated_at',
])
->skip($pagination)
->take(\Common::paginationLimit())
->orderBy('id', 'DESC');
}
])
->orderBy('id', 'DESC')
->first('id') ?? null;
}
}
Since I do not need the base table's parameters, therefore, I am fetching only id from it.
Also, with this way I am able to use the pagination too (I am using loadmore pagination).

Laravel - one-to-one relation through pivot table with eager load

I have this relationship
A Movement can have multiples steps
A Step can belongs to multiples Movements
So a had to create a pivot table and a belongsToMany relationship, but my pivot table have some extras columns, like finished and order
I want to have two relationships, one to get all steps from a movement and another one to get the current step from the movement (the last finished step)
I know how to get all steps
public function steps()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->orderBy('pivot_order');
}
But how about the current step? I need this kind of relationship, but returning only one record and be able to eager load it cause I'm passing it to vue.js
public function current_step()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->where('finished', true)
->orderBy('pivot_order', 'desc');
}
Notice, I'd like to do that without extras packages
alternative solution, but with extra package: Laravel hasOne through a pivot table (not the answer marked as correct, the answer from #cbaconnier)
A different approach from the answer provided by #mrhn is to create a custom relationship. Brent from Spatie did an excellent article about it
Although my answer will do the exact same queries than the one provided by staudenmeir's package it makes me realized that either you use the package, this answer or #mrhn answer, you may avoid the n+1 queries but you may still ends up will a large amount of hydrated models.
In this scenario, I don't think it's possible to avoid one or the other approach. The cache could be an answer though.
Since I'm not entirely sure about your schema, I will provide my solution using the users-photos example from my previous answer.
User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function photos()
{
return $this->belongsToMany(Photo::class);
}
public function latestPhoto()
{
return new \App\Relations\LatestPhotoRelation($this);
}
}
LastestPhotoRelation.php
<?php
namespace App\Relations;
use App\Models\User;
use App\Models\Photo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class LatestPhotoRelation extends Relation
{
/** #var Photo|Builder */
protected $query;
/** #var User */
protected $user;
public function __construct(User $user)
{
parent::__construct(Photo::query(), $user);
}
/**
* #inheritDoc
*/
public function addConstraints()
{
$this->query
->join(
'user_photo',
'user_photo.photo_id',
'=',
'photos.id'
)->latest();
// if you have an ambiguous column name error you can use
// `->latest('movement_movement_steps.created_at');`
}
/**
* #inheritDoc
*/
public function addEagerConstraints(array $users)
{
$this->query
->whereIn(
'user_photo.user_id',
collect($users)->pluck('id')
);
}
/**
* #inheritDoc
*/
public function initRelation(array $users, $relation)
{
foreach ($users as $user) {
$user->setRelation(
$relation,
null
);
}
return $users;
}
/**
* #inheritDoc
*/
public function match(array $users, Collection $photos, $relation)
{
if ($photos->isEmpty()) {
return $users;
}
foreach ($users as $user) {
$user->setRelation(
$relation,
$photos->filter(function (Photo $photo) use ($user) {
return $photo->user_id === $user->id; // `user_id` came with the `join` on `user_photo`
})->first() // Photos are already DESC ordered from the query
);
}
return $users;
}
/**
* #inheritDoc
*/
public function getResults()
{
return $this->query->get();
}
}
Usage
$users = \App\Models\User::with('latestPhoto')->limit(5)->get();
The main difference from Brent's article, is that instead of using a Collection we are returning the latest Photo Model.
Laravel has a way to create getters and setters that act similar to columns in the database. These can perfectly solve your problem and you can append them to your serialization.
So instead your current_step is gonna be an accessor (getter). The syntax is getCurrentStepAttribute() for the function which will make it accessible on the current_step property. To avoid N + 1, eager load the steps when you retrieve the model(s) with the with('steps') method. Which is better than running it as a query, as it will execute N times always.
public function getCurrentStepAttribute() {
return $this->steps
->where('finished', true)
->sortByDesc('pivot_order')
->first();
}
Now you can use the append property on the Movement.php class, to include your Eloquent accessor.
protected $appends = ['current_step'];

Laravel 5.3 return result only if another row in the same table doesn't exist

Basically I have a followers table in which user_id and followable_id is stored. If for example user A follows user B a record is created. And if B follows A then a new record is created. I need to return result only if A is following B, but if B is also following A then do not return anything.
I tried various solutions non of them worked. Maybe someone could point me to the right direction thanks.
EDIT:
This works however it's ugly hacky and I want to avoid this:
$iFollows = Followers::where('user_id', '=', $currentUser->id)->get();
$myFollowers = Followers::where('followable_id', '=', $currentUser->id)->get();
foreach($myFollowers as $key => $value){
foreach($iFollows as $iFollow){
if($value->user_id == $iFollow->followable_id){
$myFollowers->forget($key);
}
}
}
Database Structure:
Is there a way to achieve same thing but with laravel query builder?
User implements trait called Followable
trait Followable
{
/**
* Collection of followers attached to this object
*
* #return Query|Collection
*/
public function followers()
{
return $this->morphToMany(
User::class, // related
'followable', // name
'followers', // table
'followable_id', // foreignKey
'user_id' // otherKey
)->where('status', 1)->withPivot('created_at', 'updated_at');
}
/**
* #return mixed
*/
public function followings()
{
return $this->morphToMany(
User::class, // related
'followable', // name
'followers', // table
'user_id', // foreignKey
'followable_id' // otherKey
)->where('status', 1)->withPivot('created_at', 'updated_at');
}
For me I think your code looks ok, just add comment to the code for the coder to easily understand what is happening.
I have a one query here to get what you're looking for, get all followers who doesn't follow you back:
$myFollowers = Followers::selectRaw('followers.*')
->leftJoin('followers AS followers2', 'followers.user_id', '=', 'followers2.followable_id')
->where('followers.followable_id', '=', $currentUser->id)
->whereNull('followers2.id')
->get();
Ok I have finally solved my issue with this method:
/**
* Class FollowableTrait
* #package App\Traits
*/
trait Followable
{
/**
* #param array $ids
* #return mixed
*/
public function newFollowers($ids)
{
return $this->morphToMany(
User::class, // related
'followable', // name
'followers', // table
'followable_id', // foreignKey
'user_id' // otherKey
)->where('status', 1)->whereNotIn('user_id', $ids)->withPivot('created_at', 'updated_at');;
}
Then in my controller:
$ids = $currentUser->followings()->pluck('followable_id')->toArray();
$newFollowers = $currentUser->newFollowers($ids)->get();

Laravel Eloquent get relationship with keyBy

I have a Product model with a hasMany relationship
public function pricing()
{
return $this->hasMany('App\ProductPrice', 'prod_id', 'id');
}
I then get the relationship
Product::with('pricing')->all();
How can I retrieve the pricing relationship with the id as the key. I know I can do it on a Collection with keyBy('id) but it doesn't work on a query.
I want to acheive the same results as below but I want to get it from the Product relationship.
ProductPrice::keyBy('id')
You have to create your own relationship:
<?php
namespace App\Helpers\Classes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class HasManyKeyBy extends HasMany
{
private $keyBy;
public function __construct($keyBy, Builder $query, Model $parent, string $foreignKey, string $localKey)
{
$this->keyBy = $keyBy;
parent::__construct($query, $parent, $foreignKey, $localKey);
}
public function getResults()
{
return parent::getResults()->keyBy($this->keyBy);
}
protected function getRelationValue(array $dictionary, $key, $type)
{
return parent::getRelationValue($dictionary, $key, $type)->keyBy($this->keyBy);
}
}
For the sake of simplicity I also recommend you to create a trait:
<?php
namespace App\Helpers\Traits;
use Illuminate\Database\Eloquent\Relations\HasMany;
trait HasManyKeyBy
{
/**
* #param $keyBy
* #param $related
* #param null $foreignKey
* #param null $localKey
* #return HasMany
*/
protected function hasManyKeyBy($keyBy, $related, $foreignKey = null, $localKey = null)
{
// copied from \Illuminate\Database\Eloquent\Concerns\HasRelationships::hasMany
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return new \App\Helpers\Classes\HasManyKeyBy($keyBy, $instance->newQuery(),
$this, $instance->getTable().'.'.$foreignKey, $localKey);
}
}
Now, you can include this trait into your model, and use $this->hasManyKeyBy protected method:
[...]
class Product extends Model
{
use HasManyKeyBy;
public function pricing()
{
return $this->hasManyKeyBy('id', ProductPrice::class, 'prod_id', 'id');
}
[...]
}
A quick workaround is to replace the current relation in you array using setRelation method. In your case:
$product = Product::with('pricing')->all();
$product->setRelation('pricing', $product->pricing->keyBy('id'));
What you could also do is define an accessor:
/**
* #return Collection
*/
public function getPricingByIdAttribute() : Collection
{
return $this->pricing->keyBy('id');
}
Then on each product returned in the Collection you can get the pricing by the id using:
$pricing = $product->pricing_by_id;
Make sure to still eager load the pricing if necessary:
$products = Product::query()->with('pricing')->get();
Also, when returning the Products in e.g. an API using json you could use appending to json:
$products->each->append('pricing_by_id');
The problem is that you can't 'keyBy' an existing relationship.
However, you can create a 'fake' attribute that you return which can be keyed. So instead:
$products = Product::with('pricing') -> all();
$products -> keyedPricing = $products -> pricing -> keyBy('id');
$products -> addVisible('keyedPricing');

How to access model hasMany Relation with where condition?

I created a model Game using a condition / constraint for a relation as follows:
class Game extends Eloquent {
// many more stuff here
// relation without any constraints ...works fine
public function videos() {
return $this->hasMany('Video');
}
// results in a "problem", se examples below
public function available_videos() {
return $this->hasMany('Video')->where('available','=', 1);
}
}
When using it somehow like this:
$game = Game::with('available_videos')->find(1);
$game->available_videos->count();
everything works fine, as roles is the resulting collection.
MY PROBLEM:
when I try to access it without eager loading
$game = Game::find(1);
$game->available_videos->count();
an Exception is thrown as it says "Call to a member function count() on a non-object".
Using
$game = Game::find(1);
$game->load('available_videos');
$game->available_videos->count();
works fine, but it seems quite complicated to me, as I do not need to load related models, if I do not use conditions within my relation.
Have I missed something? How can I ensure, that available_videos are accessible without using eager loading?
For anyone interested, I have also posted this issue on http://forums.laravel.io/viewtopic.php?id=10470
I think that this is the correct way:
class Game extends Eloquent {
// many more stuff here
// relation without any constraints ...works fine
public function videos() {
return $this->hasMany('Video');
}
// results in a "problem", se examples below
public function available_videos() {
return $this->videos()->where('available','=', 1);
}
}
And then you'll have to
$game = Game::find(1);
var_dump( $game->available_videos()->get() );
I think this is what you're looking for (Laravel 4, see http://laravel.com/docs/eloquent#querying-relations)
$games = Game::whereHas('video', function($q)
{
$q->where('available','=', 1);
})->get();
//use getQuery() to add condition
public function videos() {
$instance =$this->hasMany('Video');
$instance->getQuery()->where('available','=', 1);
return $instance
}
// simply
public function videos() {
return $this->hasMany('Video')->where('available','=', 1);
}
Just in case anyone else encounters the same problems.
Note, that relations are required to be camelcase. So in my case available_videos() should have been availableVideos().
You can easily find out investigating the Laravel source:
// Illuminate\Database\Eloquent\Model.php
...
/**
* Get an attribute from the model.
*
* #param string $key
* #return mixed
*/
public function getAttribute($key)
{
$inAttributes = array_key_exists($key, $this->attributes);
// If the key references an attribute, we can just go ahead and return the
// plain attribute value from the model. This allows every attribute to
// be dynamically accessed through the _get method without accessors.
if ($inAttributes || $this->hasGetMutator($key))
{
return $this->getAttributeValue($key);
}
// If the key already exists in the relationships array, it just means the
// relationship has already been loaded, so we'll just return it out of
// here because there is no need to query within the relations twice.
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
// If the "attribute" exists as a method on the model, we will just assume
// it is a relationship and will load and return results from the query
// and hydrate the relationship's value on the "relationships" array.
$camelKey = camel_case($key);
if (method_exists($this, $camelKey))
{
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
This also explains why my code worked, whenever I loaded the data using the load() method before.
Anyway, my example works perfectly okay now, and $model->availableVideos always returns a Collection.
If you want to apply condition on the relational table you may use other solutions as well.. This solution is working from my end.
public static function getAllAvailableVideos() {
$result = self::with(['videos' => function($q) {
$q->select('id', 'name');
$q->where('available', '=', 1);
}])
->get();
return $result;
}
public function outletAmenities()
{
return $this->hasMany(OutletAmenities::class,'outlet_id','id')
->join('amenity_master','amenity_icon_url','=','image_url')
->where('amenity_master.status',1)
->where('outlet_amenities.status',1);
}
I have fixed the similar issue by passing associative array as the first argument inside Builder::with method.
Imagine you want to include child relations by some dynamic parameters but don't want to filter parent results.
Model.php
public function child ()
{
return $this->hasMany(ChildModel::class);
}
Then, in other place, when your logic is placed you can do something like filtering relation by HasMany class. For example (very similar to my case):
$search = 'Some search string';
$result = Model::query()->with(
[
'child' => function (HasMany $query) use ($search) {
$query->where('name', 'like', "%{$search}%");
}
]
);
Then you will filter all the child results but parent models will not filter.
Thank you for attention.
Model (App\Post.php):
/**
* Get all comments for this post.
*/
public function comments($published = false)
{
$comments = $this->hasMany('App\Comment');
if($published) $comments->where('published', 1);
return $comments;
}
Controller (App\Http\Controllers\PostController.php):
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function post($id)
{
$post = Post::with('comments')
->find($id);
return view('posts')->with('post', $post);
}
Blade template (posts.blade.php):
{{-- Get all comments--}}
#foreach ($post->comments as $comment)
code...
#endforeach
{{-- Get only published comments--}}
#foreach ($post->comments(true)->get() as $comment)
code...
#endforeach

Resources