0

I'm using Laravel 8. I want to create a relationship to get a user's active picture or most recent with some conditions to get only one picture.

Here is my model

user
- id
- name

picture
- id
- user_id
- type -> nullable
- created_date
- expiry_date

class User extends Model
{
    public function activePictureOrMostRecent()
    {
        return $this->hasOne(Picture::class, 'user_id')...
    }    
}

In my relationship I want to separate my pictures in 2 categories, typed picture and untyped picture so if type is null then it's an untyped picture.

So now let's talk about the conditions about how to get the active picture:

First I'll sort by created_date desc and then:

  • If there is an active TYPED picture by the date (if created_date is <= today and expiry_date >= today) I take it
  • Else if I don't have active TYPED picture I want to take the active UNTYPED picture (same condition with the date)
  • Else if I don't have active UNTYPED picture I want the most recent TYPED picture
  • And finally if I have no most recent TYPED picture I take the most recent UNTYPED picture

It is possible to create this relationship ?

John
  • 4,711
  • 9
  • 51
  • 101
  • _It is possible to create this relationship ?_ Yes. But how are you going to call this function? Like `User::with('activePictureOrMostRecent')` ? – nice_dev Dec 07 '22 at 12:02
  • @nice_dev Yes like that or like `$user->activePictureOrMostRecent` / `$user->activePictureOrMostRecent()` / `$user->{'activePictureOrMostRecent'}` – John Dec 07 '22 at 12:08
  • or `mostRecentActivePictureIfNotMostRecentInactivePicture` relation. doesnt make sense to create such relation, better off splitting in into two relations. An append seems to be a better idea. – N69S Dec 07 '22 at 12:17
  • @John Ok, it would need a couple of orWhere on a hasMany relation – nice_dev Dec 07 '22 at 12:24
  • @N69S I agree but actually I'm building a generic function foreach model and it's easier to do like this for the moment. Our database is messy I have to make a choice without time. – John Dec 07 '22 at 13:00
  • @nice_dev Can you show me a snippet ? I don't know how can I pass to another condition in my relationship – John Dec 07 '22 at 13:00
  • @John You can take hints from [this](https://stackoverflow.com/questions/16995102/laravel-eloquent-query-using-where-with-or-and-or) – nice_dev Dec 07 '22 at 13:18
  • @nice_dev Ok thank you, so with this if the first where return something it will not executed the second ? – John Dec 07 '22 at 13:28
  • @John Yes. For the outside where's there, it will be where->(...)->orWhere(...)->orWhere(...) etc – nice_dev Dec 07 '22 at 13:36
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250231/discussion-between-john-and-nice-dev). – John Dec 07 '22 at 13:40

1 Answers1

0

AFAIK it's not possible to model this using (built-in) Laravel 8 relationships, its generally pretty limited what can be modelled using relationships alone.

However, you can model this as an attribute for example (https://laravel.com/docs/7.x/eloquent-mutators#defining-an-accessor) which can be used similar to a relationship in most places:

class User extends Model
{
    public function getActivePictureOrMostRecentAttribute(){
        //logic for retrieving the picture

        if ($candidate = $this->pictures->whereNotNull("type")->where(...)->get()) return $candidate
        ...
        return $this->pictures->whereNull("type")->latest();
    }

which can then be used like this

$user = User::find(...);
$image = $user->activePictureOrMostRecent

There are some caveats to using attributes instead of relationships, mainly everyhitng related to eager loading will not work

//cant eager load activePictureOrMostRecent
User::with("activePictureOrMostRecent");
//or
$user->load("activePictureOrMostRecent");

This can be a problem when serializing this data to JSON, since the data is not sent, in that case you could use appends to have the attribute be sent as well (https://laravel.com/docs/9.x/eloquent-serialization#appending-values-to-json)

dsalex1
  • 422
  • 2
  • 8
  • it is possible, it's just a complex query containing a reunion of the same table result with a simple order by and a limit to 1. but the question is, is it worth the hassle? can you even call it a relation at that point ? would be better if it was split in two relations (active and inactive) and let the consumption of the result decide which to use. – N69S Dec 07 '22 at 12:14
  • @N69S is it possible to write the relationship such that the method is in-fact a `Illuminate\Database\Eloquent\Relations\Relation` instance? I don't know a way without implementing all the Relation contracts yourself, essentially writing a new relationship type – dsalex1 Dec 07 '22 at 12:19
  • You can create custom relation with `return $this->newHasOne($query, new Model())` and respect the restriction of a one to one relation. it's just too much work for no performance gain. – N69S Dec 07 '22 at 12:28
  • @N69S thats smart, didn't think of the new[type] methods, thank you. And while that has no performance gain it makes especially nested eager loading so much easier, e.g. `Country::with("posts.user.activePictureOrMostRecent:id,filename")` – dsalex1 Dec 07 '22 at 16:03