Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

tal3nce's avatar

Livewire not accepting global query scope data into Component's public property

Okay, here's the deal. I'm quite new to Livewire and Laravel in general, so I was doing "homework" on assignment from "Laravel (6) from scratch" series - building Twitter clone. My post might be quite long because of my inexperience - so most of it should be for sanity check reasons, hope you can stay tuned.

Here goes: one of the things I wanted to do is to refactor the like-dislike system using Livewire components to give it a more AJAX-y feel. This worked using basic Laravel Controller and Policy, let me mention that up front (but it reloads the whole page, hence this excercise).

So, the problem I'm facing is that Livewire doesn't seem to assign all the Tweet object's properties on its public $tweet property, i.e. it doesn't assign the entire Tweet model, just the "basic" properties kept on tweets table. Why is it a problem? Well, to avoid the N+1 issue when loading a timeline or any page that contains multiple tweets (that is, almost any page in the app), I would eager load some data, such as its owner (User model) or number of likes/dislikes and if it's liked/disliked by the authenticated user. But, since this data is needed pretty much every time you load a tweet (e.g. to style the Like button if you liked the tweet), I found it was justified for me to eager load the relationship every time and put the "likes info" into global query scope. So, my Tweet model looks like this:

class Tweet extends Model
{
    use Likeable;

    protected $fillable = ['user_id', 'body'];

    protected $with = 'user';

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    protected static function booted()
    {
        static::addGlobalScope('liked', function ($builder) {
            $builder->leftJoinSub('
                select tweet_id, 
                sum(liked) AS number_of_likes, 
                sum(! liked) AS number_of_dislikes
                
                FROM likes
                GROUP BY tweet_id        
            ', 'likes', 'likes.tweet_id', '=', 'tweets.id');

                if (! auth()->guest()) {
                    $builder->leftJoinSub('
                        SELECT tweet_id, 
                        liked = 1 AS liked_by_user,
                        liked = 0 AS disliked_by_user
                        
                        FROM likes	
                        WHERE user_id = ' . auth()->user()->id,
                    'liked', 'tweets.id', '=', 'liked.tweet_id');
                }
        });
    }


}

As you can probably conclude, the likes table contains user_id, tweet_id and boolean for liked (1) or disliked (0). Pretty basic. So, when calling Tweet model, I would have User relationship on the model and 10 properties available. If I dd($tweet) on mount() method of the Component (or Controller loading the page), this is fine, I get something like this:

 #attributes: array:10 [▼
    "id" => 24
    "user_id" => 3
    "body" => "Est in minus ab qui."
    "created_at" => "2020-04-25 23:29:23"
    "updated_at" => "2020-04-25 23:29:23"
    "tweet_id" => 24
    "number_of_likes" => "4"
    "number_of_dislikes" => "3"
    "liked_by_user" => 0
    "disliked_by_user" => 1
  ]

You can see that this tweet has 4 likes, 3 dislikes and it was disliked by the current user. Pay attention to this, because it is needed for authorization. If the user hasn't liked NOR disliked this tweet, both fields would be null (meaning he can only like or dislike a tweet, but not "unlike" it (i.e. remove a like or dislike, essentially delete that row from the likes table).

So far, looks like every Tweet object was successfully passed to the Livewire Component. HOWEVER, Livewire didn't store all of this data to its public $tweet property. When I try to dd($this->tweet) inside any Livewire method to test it (instead of dd($tweet) passed to mount() method as above, check it out:

#attributes: array:5 [▼
    "id" => 24
    "user_id" => 3
    "body" => "Est in minus ab qui."
    "created_at" => "2020-04-25 23:29:23"
    "updated_at" => "2020-04-25 23:29:23"
  ]

Those fields from global query scope are not assigned. So, when I have, for example unlike() method

public function unlike() {
	$this->authorize('unlike', $this->tweet);

	$this->tweet->unlike(); // just destroys the relationship in the background
}

authorization won't work, because $this->tweet won't pass properties such as liked_by_user or disliked_by_user to the TweetPolicy, i.e. the policy will read them as null, which affects my policy logic.

class TweetPolicy
{
    use HandlesAuthorization;

    public function like(User $user, Tweet $tweet)
    {
        return $tweet->liked_by_user === 0 || $tweet->liked_by_user === null ;
    }

    public function dislike(User $user, Tweet $tweet)
    {
        return $tweet->disliked_by_user === 0 || $tweet->disliked_by_user === null ;
    }

    public function unlike(User $user, Tweet $tweet)
    {
        return ($tweet->liked_by_user !== null || $tweet->disliked_by_user !== null);
	// the second check is essentially unnecessary here, but it's for my OCD purposes
    }
}

In this case, authorization for like() and dislike() will work, but for unlike() it fails because the Policy reads them as null.

The $this->tweet->unlike() part works, which is expected, because $this->tweet actually is a Tweet object, only with "partial" data: sufficient for the relationship to be destroyed, but not to pass the authorization or instantly have the new number of likes/dislikes updated on the page.

All I want to do is to wire a like/dislike/unlike button to a click event, trigger the respective method and have something like $numberOfLikes and $numberOfDislikes in <span>s instantly updated:

public $tweet;
public $numberOfLikes;
public $numberOfDislikes;

public function mount(Tweet $tweet) {
	$this->tweet = $tweet;
}



public function like() {
	$this->authorize('like', $this->tweet);

	$this->tweet->like();

	$this->numberOfLikes = $this->tweet->number_of_likes;
	$this->numberOfDislikes = $this->tweet->number_of_dislikes;
}

// (...)

public function render() {
	return view('livewire.like-dislike', [
		'tweet' => $this->tweet,
	]);
}

but, since $this->tweet doesn't have number of likes and dislikes, I can't do it. I can hack it, though:

public function like() {
	$this->tweet->like();

	$this->numberOfLikes = Tweet::find($this->tweet->id)->number_of_likes;
	$this->numberOfDislikes = Tweet::find($this->tweet->id)->number_of_dislikes;
}

But this is gross. And fires two more queries. And the whole method can't be authorized.

Let me also mention that I tried to add the likes and dislikes "manually"

public function mount(Tweet $tweet) {
	$this->tweet = $tweet;
	$this->numberOfLikes = $tweet->number_of_likes;
	$this->numberOfDislikes = $tweet->number_of_dislikes;
}

but it yielded the same result. And, again, this only solves the instant update part, not the whole problem.

So, does anyone have any idea why is this not working as expected?

I can send you more info, such as Blade/Livewire component, but I've already written so much that I didn't want to make this post any longer.

Bonus question: is there a possibility NOT to use $numberOfLikes on the Livewire partial, but instead $tweet->number_of_likes and have it update instantly after executing the method (clicking the button)? It would reduce the "visual debt" of the Component.

Thank you!

0 likes
1 reply
tal3nce's avatar

Okay, so I played around again to prove to myself that it's possible to have a much cleaner Component and it's definitely due to failure to pass complete data into public $tweet when mounting it. It's very intuitive to only assign a public $tweet and use that object to render any data instantly on the page, accessing its properties like you would do with an Eloquent model in a Blade view.

So, if I modify my method to

public function like() {
	$this->tweet->update([
		'body' => 'foo'
	]);
}

and I have <span>{{ $tweet->body }}</span> in the view, it instantly swaps out the tweet body to foo when I click the Like button. So, everything works except for assigning the data.

I managed to hack the entire thing to work (still, this doesn't allow for authorization) by doing this:

Class

<?php

namespace App\Http\Livewire;

use App\Tweet;
use Livewire\Component;

class LikeDislike extends Component {
	public $tweet;
	public $numberOfLikes;
	public $numberOfDislikes;
	public $liked;
	public $disliked;

	public function mount(Tweet $tweet) {
		$this->tweet = $tweet;
		$this->numberOfLikes = $tweet->number_of_likes;
		$this->numberOfDislikes = $tweet->number_of_dislikes;
		$this->liked = $tweet->liked_by_user;
		$this->disliked = $tweet->disliked_by_user;
	}

	public function like() {
		$this->tweet->like();
		$this->updateStatus();
	}

	public function dislike() {
		$this->tweet->dislike();
		$this->updateStatus();
	}

	public function unlike() {
		$this->tweet->unlike();
		$this->updateStatus();
	}

	protected function updateStatus() {
		$tweet = Tweet::find($this->tweet->id);
		$this->numberOfLikes = $tweet->number_of_likes;
		$this->numberOfDislikes = $tweet->number_of_dislikes;
		$this->liked = $tweet->liked_by_user;
		$this->disliked = $tweet->disliked_by_user;
	}



	public function render() {
		return view('livewire.like-dislike', [
			'tweet' => $this->tweet
		]);
	}
}

View

<div class="mt-2 flex items-center">
    <div class="flex items-center mr-2
                    {{ $liked ? 'text-blue-500' : 'text-gray-500' }}">

        <button wire:click="{{ $liked ? 'unlike' : 'like' }}">
            // thumbs up svg
        </button>
        <span>{{ $numberOfLikes ?: '0' }}</span>
    </div>

    <div class="flex items-center
                        {{ $disliked ? 'text-blue-500' : 'text-gray-500' }}">
        <button wire:click={{ $disliked ? 'unlike' : 'dislike' }}>
            // thumbs down svg
        </button>
        <span>{{ $numberOfDislikes ?: '0' }}</span>
    </div>
</div>

I think I don't need to mention the flaws and grossness of this solution.

Please or to participate in this conversation.