1

Let's consider the following example: a thread has posts, and the posts also have a "thread" relation. The title of each post must include the title of the parent thread.

class Thread extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
class Post extends Model
{
    public function thread()
    {
        return $this->belongsTo(Thread::class);
    }

    public function getTitleAttribute(string $title): string
    {
        return $this->thread->title . ': ' . $title;
    }
}

What I want to achieve:

//when we load the posts using the thread...
$posts = $thread->posts;

//...I want the "thread" relation of each post to be automatically set to $thread, so that:
$posts->first()->thread === $thread //true

By default it's not true. And if we do this:

$array = $thread->posts->toArray();

this will cause loading of the thread for each post one by one from DB which is super non-optimal. Is there some elegant Laravel technique to setup relations of the just loaded models?

Stalinko
  • 3,319
  • 28
  • 31
  • then get the posts with their thread `$posts = $thread->posts()->with('thread')->get();` if you dont want the one extra query, use map. `$thread->posts->map(function($post) use ($thread) {return $post->setRelation('thread', $thread);});` – N69S Jun 10 '22 at 11:16
  • @N69S that's not optimal. That will create a lot of $thread objects and assign to each post. Also that will cause an extra SQL request to load already loaded thread. – Stalinko Jun 10 '22 at 11:19
  • @N69S the "map" is good. But where should I place it so that it works every time the "posts" are loaded through the "$thread"? – Stalinko Jun 10 '22 at 11:20
  • Use the map function then. it will still be the same, a lot of thread objects... – N69S Jun 10 '22 at 11:20
  • You can't automate it since you dont know when a post a an attribute of a thread or not. – N69S Jun 10 '22 at 11:21
  • @N69S well in my certain case I know all the attributes and their relationships. So I'd like to automate it as much as possible. – Stalinko Jun 10 '22 at 11:22
  • 1
    just add another method to the thread model for the map() function and call that everytime you want to achieve that result ? – Ashraf Kamarudin Jun 10 '22 at 11:27
  • @AshrafKamarudin we got the same idea ^^ – N69S Jun 10 '22 at 11:31

1 Answers1

1

You can lazy load them like this

$posts = $thread->posts()->with('thread')->get();

If you dont want the extra query, you can use map()

$thread->posts->map(function($post) use ($thread) {
    return $post->setRelation('thread', $thread);
});

This will lead to the same amount of object but will also lead to loop of references.

//this is defined and doesn't use more object or launch other queries
$thread->posts->first()->thread->posts()->first()->thread; 

if you want to Automate it, I suggest you create a function on Thread model to get the posts threaded.

public function loadThreadedPosts()
{
    $this->posts->map(function($post) {
        return $post->setRelation('thread', $this);
    });
    return $this;
}

//then you can
$thread->loadThreadedPosts()->posts;

If you want it to automatically be done when you specifically call for the relation "posts" on the Thread::class model, add this method to your Thread::class to overwrite the function present in the Trait HasAttributes at your own risk

    /**
     * Get a relationship value from a method.
     *
     * @param  string  $method
     * @return mixed
     *
     * @throws \LogicException
     */
    protected function getRelationshipFromMethod($method)
    {
        $relation = $this->$method();

        if (! $relation instanceof Relation) {
            if (is_null($relation)) {
                throw new LogicException(sprintf(
                    '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method
                ));
            }

            throw new LogicException(sprintf(
                '%s::%s must return a relationship instance.', static::class, $method
            ));
        }

        return tap($relation->getResults(), function ($results) use ($method) {
            if ($method == "posts") {
                $results->map(function($post) {
                    return $post->setRelation('thread', $this);
                });
            }
            $this->setRelation($method, $results);
        });
    }

Hope you understand that this overwrites a vendor method and might lead to future issues, also I dont think that this one method works with eager loading (for example: Thread::with('posts')->get()) and I dont know what else might get broken/have unexpected behavior.

As I said, at your own risk (bet/hope ->loadThreadedPosts() looks more interesting now)

N69S
  • 16,110
  • 3
  • 22
  • 36
  • Thanks. Looks nice ) the only thing I don't like that it still makes you write `loadedThreadedPosts` every time. Would be cool to have some hook like "afterRealtionLoaded" so that Laravel would fire this code automatically. But I couldn't find such option in Laravel. – Stalinko Jun 10 '22 at 11:48
  • @Stalinko as i said, when a post is loaded, it doesn't know if it is from a parent relation or from direct query. – N69S Jun 10 '22 at 13:27
  • that's right. But `Post` doesn't have to know that. `Thread` knows that it loads `posts` and it can trigger the setting of the reverse relations. – Stalinko Jun 10 '22 at 13:35
  • @Stalinko that would need you to override the `protected function getRelationshipFromMethod($method)` of the base eloquent trait/Concerns `HasAttribute` to intercept the relation loading if the it is a "post" in the thread model. – N69S Jun 10 '22 at 13:40
  • @Stalinko here is what you are looking for, a partial solution for the case when you do `$thread->posts` (only) – N69S Jun 10 '22 at 13:49