Eager load only if relationship exists - laravel

I have a morph relationship, where the subject could have multiple relationships. Their existence depends on morphed model. I need to retrieve all the related models (whereHas() doesn't solve the problem) and I want their relationships being loaded if they exists on particular model (with() won't work, because the relationship doesn't always exist).
Is there anything else (built-in) that I can use to fluently solve this scenario, or hacking is the only way around it?
<?php
...
class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
/**
* This relationship is available for Post model only
*/
public function relationA()
{
// return $this->hasMany(...);
}
}
class Video extends Model
{
/**
* Get all of the video's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
/**
* This relationship is available for Video model only
*/
public function relationB()
{
// return $this->hasMany(...);
}
}
class Comment extends Model
{
/**
* Get all of the owning commentable models.
*/
public function commentable()
{
return $this->morphTo();
}
public static function feed()
{
self::with('commentable')
->withIfExists(['commentable.relationA', 'commentable.relationB'])
// ...
->get();
}
public function scopeWithIfExists($query, $relation)
{
// There is no way to implement such a scope
// in order to reduce umber of queries by eager loading relations
// as we don't know of what type the subject is
// without looking it up in database
}
}

Check out at Query Scopes.
With that you can create a scope to load a relation if it exists, for example:
User::withRelationIfExists('cars')->where(...)
For example: (code not tested)
public function scopeWithRelationIfExists($query, $relation)
{
if (! method_exists(get_class(), $relation)) {
return;
}
return $query->with($relation);
}

Related

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'];

What is the best way to update One to One Polymorphic in Laravel?

I have One to One Polymorphic, I want to figure the best way to update existed relationships.
class Image extends Model
{
/**
* Get the owning imageable model.
*/
public function imageable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get the post's image.
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
class User extends Model
{
/**
* Get the user's image.
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
On Update Method in PostController
public function update(Request $request, $id)
{
$post = Post::findOrFail($id);
$post->image()->delete();
$post->image()->save(new Image([
'url'=> $request->input('image_url),
]));
}
How to update Image Relationship without deleting it first?
Thanks
You could try using updateOrCreate on the relationship:
$post->image()->updateOrCreate(
[],
['url' => $request->input('image_url')]
);
If you are always expecting there to be an Image related to the Post you can update the Image instance directly:
$post->image->update([...]);

How to use morphtomany

I'm trying to create a tagging system and now I got two table as the picture below
tags
taggables
I would like to tag the Journal and id based on the `tag_id. However, the journal always create a new record in the taggables table.
Here is my model relationship
Tag
class Tag extends Model
{
public function purchases()
{
return $this
->morphedByMany('Purchase', 'taggable');
}
public function taggables()
{
return $this->hasMany('Taggable');
}
}
Purchase
class Purchase extends Model
{
public function tags()
{
return $this
->morphToMany('Tag', 'taggable');
}
}
Journal
class Journal extends Model
{
public function tags()
{
return $this
->morphToMany('Tag', 'taggable');
}
}
If I understood correctly you are trying to use tags for journals and purchases at the same time.
Then your table structure should be something like:
journals(id, ...)
purchases(id, ...)
tags(id, name)
taggables(id, tag_id, taggable_id, taggable_type)
Then the models that can be tagged should both have a method to retrive their tags:
app/Journal.php
// namespace and use statements
class Journal extends Model
{
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
app/Purchase.php
// namespace and use statements
class Purchase extends Model
{
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
The your tag model that can be applied to both Purchase and Journal should have N methods, where N is the number of different models you are connecting through your polymorphic relation, in this example N=2 (one method to retrive purchases from a tag, and one method to retrive journals from a tag).
app/Tag.php
// namespace and use statements
class Tag extends Model
{
public function journals()
{
return $this->morphedByMany('App\Journals', 'taggable');
}
public function purchases()
{
return $this->morphedByMany('App\Purchase', 'taggable');
}
}
After the relations are set you can retrive the tags assigned to a journal or a purchase:
$journal = \App\Journal::first();
foreach ($journal->tags as $tag) {
// ...
}
or retrive all the journals or purchases linked to a specific tag:
$tag = \App\Tag::first();
// $tag->journals will contain the journals model instances linked to the current tag
// $tag->purchases will contain the purchases model instances linked to the current tag
Note: to define relationships use the FQDN of the related class, for instance: 'App\Tag' instead of 'Tag', otherwise the class would not be found by the autoloader.

how to many to many relationship query in laravel

Brief:
I use laravel 5.6
I have a problem in many to many relation queries.
I have 2 model: Order and Cart
Code:
Cart Model:
class Cart extends Model
{
public function order()
{
return $this->belongsToMany(Order::class);
}
}
Order model :
class Order extends Model
{
public function carts(){
return $this->belongsToMany(Cart::class);
}
}
Cart Migration :
public function up()
{
Schema::create('carts', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->nullable();
$table->integer('price')->nullable();
$table->integer('pass')->default(0);
});
}
Question:
How to get orders that their pass field are in Cart = 1 in?
Thanks!
First, since your Cart has many orders, the relationship should be named "orders" with an s.
You showed only you Cart migration so I cannot guess, but Laravel also expects you to create a "cart-order" pivot table.
If I understood well, you can do what you want like this:
Order::whereHas('carts', function ($query) {
$query->where('pass', 1);
})->get();
You can read more about Eloquent's Many to Many relationships in the Laravel documentation, here.
Try Like this,
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Get all of the owning commentable models.
*/
public function commentable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
class Video extends Model
{
/**
* Get all of the video's comments.
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
//Access like this in your controller
$post = App\Post::find(1);
foreach ($post->comments as $comment) {
//
}
This Link Help you,
https://laravel.com/docs/5.7/eloquent-relationships#polymorphic-relations in that case. Hope this will help you.

Laravel - Delete if no relationship exists

Below is the one of the model. I would like to delete a Telco entry only if no other model is referencing it? What is the best method?
namespace App;
use Illuminate\Database\Eloquent\Model;
class Telco extends Model
{
public function operators()
{
return $this->hasMany('App\Operator');
}
public function packages()
{
return $this->hasMany('App\Package');
}
public function topups()
{
return $this->hasMany('App\Topup');
}
public function users()
{
return $this->morphMany('App\User', 'owner');
}
public function subscribers()
{
return $this->hasManyThrough('App\Subscriber', 'App\Operator');
}
}
You can use deleting model event and check if there any related records before deletion and prevent deletion if any exists.
In your Telco model
protected static function boot()
{
parent::boot();
static::deleting(function($telco) {
$relationMethods = ['operators', 'packages', 'topups', 'users'];
foreach ($relationMethods as $relationMethod) {
if ($telco->$relationMethod()->count() > 0) {
return false;
}
}
});
}
$relationships = array('operators', 'packages', 'topups', 'users', 'subscribers');
$telco = Telco::find($id);
$should_delete = true;
foreach($relationships as $r) {
if ($telco->$r->isNotEmpty()) {
$should_delete = false;
break;
}
}
if ($should_delete == true) {
$telco->delete();
}
Well, I know this is ugly, but I think it should work. If you prefer to un-ugly this, just call every relationship attributes and check whether it returns an empty collection (meaning there is no relationship)
If all relationships are empty, then delete!
After seeing the answers here, I don't feel copy pasting the static function boot to every models that need it. So I make a trait called SecureDelete. I put #chanafdo's foreach, inside a public function in SecureDelete.
This way, I can reuse it to models that need it.
SecureDelete.php
trait SecureDelete
{
/**
* Delete only when there is no reference to other models.
*
* #param array $relations
* #return response
*/
public function secureDelete(String ...$relations)
{
$hasRelation = false;
foreach ($relations as $relation) {
if ($this->$relation()->withTrashed()->count()) {
$hasRelation = true;
break;
}
}
if ($hasRelation) {
$this->delete();
} else {
$this->forceDelete();
}
}
}
Add use SecureDelete to the model that needs it.
use Illuminate\Database\Eloquent\Model;
use App\Traits\SecureDelete;
class Telco extends Model
{
use SecureDelete;
public function operators()
{
return $this->hasMany('App\Operator');
}
// other eloquent relationships (packages, topups, etc)
}
TelcoController.php
public function destroy(Telco $telco)
{
return $telco->secureDelete('operators', 'packages', 'topups');
}
In addition, instead of Trait, you can also make a custom model e.g BaseModel.php that extends Illuminate\Database\Eloquent\Model, put the function secureDelete there, and change your models to extends BaseModel.

Resources