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

aurelianspodarec's avatar

When you eager load, does that change the dynamics of the model?

Hi there!

So I wonder, when you eager load data, does that change how the method works?

So when you have a Model Product and you eager load one Product Variation and then try to call a method on the Product Variation that gets something from a pivot table, is that different compared to if you didn't eager load the Product Variation but load all instead or?

Because where I don't eager load Product Variation, "one" but display all variations, and I access the method on the product variation, the method works, but as soon as I eager load it, it doesn't. The JSON is exactly the same, but the method doesn't show the data from the pivot table where it was eager loaded "one" product variation.

0 likes
48 replies
kokoshneta's avatar

Do you have the pivot columns included in a ->withPivot() clause in your relation definition in your Product class? And how is a method defined in the Product Variation class supposed to access pivot columns which are stored in the Product object?

If I’m understanding you correctly, you have some sort of logical error in how your methods are set up. When you say you “display all variations, and I access the method on the product variation”, do you mean like this:

$variations = ProductVariation::all();

foreach ($variations as $variation) {
	$x = $variation->myMethod();
}

If that’s what you mean, and that works, then the data that this method is relying on is not pivot table data as far as the Variation object is concerned. If it comes from a different table, presumably you have a relation in the Product Variation class that loads the data when you call the method.

So you need to either:

  • Move the method into the Product class where the pivot data is available, and then refer to the pivot data in the method definition; or
  • Make sure to load the data when you load the variations by defining the relation in your Produc class that loads the Variations as $this->has[One/Many]('ProductVariation', […])->with('pivotDataRelation')
1 like
aurelianspodarec's avatar

@kokoshneta I think I got something else wrong reading your answer. I said Pivot Table because I feel like that might be the issue, because when I call a method that says "hi" it works, but a method that needs to get different data from database, it doesn't. Let me expkain.

I'm not doing that no, I think, if we zoom out a bit, what I have is:

Product , that eager loads ProductVariation like so:

Product Model
----
 $theme = $filters['theme'] ?? 'light';
        $device = $filters['device'] ?? 'desktop';

        $query->with('imageVariations', function($query) use ($theme, $device) {
            $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                        ->where('site_variations.theme', $theme)
                        ->where('site_variations.device', $device);
        })->get();

Now, what I do to display all variation is this:

@foreach ($props->productVariations as $image)
 <img src="{{ asset($image->getFirstMediaUrl('image')) }}"

The productVariation is a method inside the product that's looks like this:

  public function productVariations()
    {
        return $this->hasMany(ProductVariation:class, 'product_id');
    }

And this returns the ProductVariation. When i try to access the image, it works if I don't eager load them with the $query/filter code above.

Now, the reason I said pivot table, is to gain some understanding on how this works.

Because when i try to access the image, like so

<img src="{{ asset($props->productVariations[0]->hi()) }}" />

It does say "hi", , and this is in the ProductVariation model:

  public function hi()
    {
        return "hi";
    }

BUT, when I use a method from Spatie, like below, when eager loading:

<img src="{{ asset($props->productVariations[0]->getFirstMediaUrl('image')) }}" />

That makes me think there's something wrong or something that when eager loading, something works different.

Because when I don't eager load data, meaning no filters, the code above works fine. But when I do eager load the data, and try to access a different table, then the code doesn't get the data/image, but if its simple method, then it will work, which means method works on the ProductVariation but for whatever reason when calling the media table with spatie methods, which has all the information, the id, model id, image itself etc it doesn't work.

Do you know what I mean?

kokoshneta's avatar

@aurelianspodarec Your naming is making it hard to tell what’s what. What is $props, a collection/array of Product objects, or a collection/array of some sort of property?

Where is the eager-loading code located, and where is it getting the $query variable from? What is imageVariations and how does it differ from productVariations? Are they different relations in the Product class? And how does the site_variations table relate to things?

1 like
aurelianspodarec's avatar

@kokoshneta

Okay. I used different naming for context.

So the way I have my site, which is similar to having products is:

I have a Site that contains SitePage. SitePage that has different variations of itself SitePageImage, 2-4 max. SitePageImage (which a better name would be SitePageVariation), that contains the SitePage ID and the SiteVariationID.

SiteVariation contains things like: Id | device | theme 1 | mobile | light 2 | mobile | dark 3 | desktop | light 4 | desktop | dark

So I want to display all Pages, but only one variation. I want to fidlter the SitePageImage by device and theme.

So I go and look into the SiteVariation table, see what matches, get the ID, and then us that ID to get the SitePageImage by site_variation_id.

And it works, I'm doing this. However, as soon as I try to load media, it doesn't work.

So to answer your question, this is how the code looks like:

Site Model

<?php

namespace App\Models\Site;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

use App\Models\Site\SitePage;

class Site extends Model
{
    use HasFactory;
    
    protected $fillable = [
        'user_id',
        'logo',
        'name',
        'slug',
    ];

    public function color()
    {
        return $this->belongsto(SiteBrandColor::class, 'site_brand_color_id');
    }

    public function pages() {
        return $this->hasMany(SitePage::class);
    }

    public function category() {
        return $this->belongsTo(SiteCategoryType::class, 'site_category_type_id');
    }

    public function industry() {
        return $this->belongsTo(SiteCategoryIndustry::class, 'site_category_industry_id');
    }
}

SitePage Model

<?php

namespace App\Models\Site;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

use App\Models\Site\Site;

class SitePage extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'slug'
    ];

    protected $with = ['color', 'site', 'categoryPage'];

    public function scopeFilter($query, array $filters) {

        $query->when($filters['color'] ?? false, fn($query, $color) =>
            $query->whereHas('site.color', function ($query) use ($color) {
                $query->where('name', $color);
            })->get()
        );

        // TODO: Come up with better filter name?
        $query->when($filters['pageName'] ?? false, fn($query, $page) =>
            $query->whereHas('categoryPage', function ($query) use ($page) {
                $query->where('name', $page);
            })->get()
        );

        // Breaks the image on filters page
        // $theme = $filters['theme'] ?? 'light';
        // $device = $filters['device'] ?? 'desktop';

        // $query->with('imageVariations', function($query) use ($theme, $device) {
        //     $query->join('site_variations', 'site_variation_id', 'site_variations.id')
        //                 ->where('site_variations.theme', $theme)
        //                 ->where('site_variations.device', $device);
        // })->get();


        $theme = $filters['theme'] ?? 'light';
        $device = $filters['device'] ?? 'desktop';

        $query->with('imageVariations', function($query) use ($theme, $device) {
            $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                        ->where('site_variations.theme', $theme)
                        ->where('site_variations.device', $device);
        })->get();
  
    }


    public function imageVariations()
    {
        return $this->hasMany(SitePageImage::class, 'site_page_id');
    }

    public function allImages()
    {
        return $this->hasMany(SitePageImage::class, 'site_page_id');
    }

    public function color()
    {
        return $this->belongsTo(SiteBrandColor::class,'site_brand_color_id');
    }

    public function site()
    {
        return $this->belongsTo(Site::class, 'site_id');
    }

    public function categoryPage()
    {
        return $this->belongsTo(SiteCategoryPage::class, 'site_category_page_id');
    }
}

and its controller:

<?php

namespace App\Http\Controllers\Inspiration;

use Illuminate\Http\Request;

use App\Http\Controllers\Controller;
use App\Models\Site\SitePage;


class PageController extends Controller
{
    public function index()
    {
        return view('web.inspiration.pages.index', [
            'pages' => SitePage::latest()->filter(
                request(['color', 'page', 'device', 'theme'])
            )->paginate(10)->withQueryString()
        ]);
    }
}

And the SitePageImage

<?php

namespace App\Models\Site;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class SitePageImage extends Model implements HasMedia
{

    // TODO: Renamte this to SitePageVariation
    
    use HasFactory;
    use InteractsWithMedia;
    
    public function variation()
    {
        return $this->belongsTo(SiteVariation::class, 'site_variation_id');
    }

    public function hi()
    {
        return "hi";
    }

    public function registerMediaConversions(Media $media = null): void
    {
        $this
            ->addMediaConversion('preview')
            ->width(100)
            ->height(100);
    }

}

So when I add this code:

$query->with('imageVariations', function($query) use ($theme, $device) {
    $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device);
})->get();

The image won't load.

The Pages are displayed like so:

    
<x-container>
<div class="grid grid-cols-1 md:grid-cols-5 gap-6">
@foreach ($pages as $page)

    <x-card.page :props="$page" />

@endforeach
</div>
</x-container>

And the card page is what you've seen partailly before:

@props([ 'props' ])
<p style="    word-break: break-all;">{{ $props->imageVariations[0] }}</p>
<div>
<article class="js-site-page group relative cursor-pointer border rounded-xl h-[400px] border-gray-200 overflow-hidden">

    {{-- @foreach ($props->imageVariations as $image)
        {{-- <img src="{{ asset($image->getFirstMediaUrl('image')) }}"  data-id="{{ $image->site_variation_id }}" class="js-image bg-cover w-full object-cover group-hover:scale-105 transition-transform"/> --}}
        {{-- {{ dd($image->getFirstMediaUrl('image'))}} --}}
        {{-- {{ $image->getFirstMediaUrl('image') }} --}}
    {{-- @endforeach --}}
    {{  $props->site->name }}
    - {{ $props->name}}
    {{-- <img src="{{ asset($props->imageVariations[0]->getMedia('image')) }}" /> --}}
    <img src="{{ asset($props->imageVariations[0]->hi()) }}" />
  
    <img src="{{ asset($props->imageVariations[0]->getFirstMediaUrl('image')) }}" />
    ```

    Which I tried a few things, you can see but yeah.

    Do you know what I mean now?
kokoshneta's avatar

@aurelianspodarec All right, so it’s in a scope – I hadn’t understood that. That makes more sense.

At a glance, I think this is syntactically wrong:

$query->with('imageVariations', function($query) use ($theme, $device) {
    $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device);
})->get();

The ->with() function takes an array when used like this, so it should be:

$query->with(['imageVariations' => function($query) use ($theme, $device) {
    $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device);
}])->get();

I’ll need a bit more time to check the rest through to get the logic properly sorted in my head, but see if that changes anything.

1 like
aurelianspodarec's avatar

@kokoshneta Wrapping that in the array doesn't change anything at all. Everything seems to be exactly as it was.

I'm using Spate Media Library, so I'm trying to access the table Spatie has created, "media" to get the image as well, with spatie methods, for more context.

Also, the card-page code:

    <img src="{{ asset($props->imageVariations[0]->hi()) }}" />
  
    <img src="{{ asset($props->imageVariations[0]->getFirstMediaUrl('image')) }}" />

Outputs this:

 <img src="https://curated-ui.test/hi">
  
    <img src="https://curated-ui.test/">

which without filtering it outputs the correct full url.

Where the hi() method, as seen in other comment, does get accessed, so the correct model is used, but for whatever reason the method from spatie doesn't work when filtering code is uncommented - makes me think if maybe the method from spatie is being used incorrectly in this case, but feels like it has to do with loading something.

kokoshneta's avatar

@aurelianspodarec Wait, hang on – why are you fetching records in your scope, and not returning anything? Your scope should return the $query object it receives, with scoping constraints added to it, but you don’t return anything.

I don't know what happens exactly when you apply a scope that doesn't return its query builder like it's supposed to, but I imagine it probably just doesn’t load any records at all, which means you’ll end up with models that are completely empty. Since the hi() method doesn’t rely on any properties but can be used from an empty object, that will still work, but anything that requires properties being set will fail.

I think your scope function should look like this:

public function scopeFilter($query, array $filters) {
	$query->when($filters['color'] ?? false, fn($query, $color) =>
		$query->whereHas('site.color', function ($query) use ($color) {
			$query->where('name', $color);
		});
	);

	// TODO: Come up with better filter name?
	$query->when($filters['pageName'] ?? false, fn($query, $page) =>
		$query->whereHas('categoryPage', function ($query) use ($page) {
			$query->where('name', $page);
		});
	);

	$theme = $filters['theme'] ?? 'light';
	$device = $filters['device'] ?? 'desktop';

	$query->with(['imageVariations' => function($query) use ($theme, $device) {
		$query->join('site_variations', 'site_variation_id', 'site_variations.id')
			->where('site_variations.theme', $theme)
			->where('site_variations.device', $device);
		}
	]);

	return $query;
}
1 like
kokoshneta's avatar

@aurelianspodarec Time to try testing with a less complex scenario, then. For example something like this:

$pageId = 1; // Or whatever, just some random page ID that exists
$page1 = SitePage::filter()->find($pageId);
$page2 = SitePage::with('imageVariations')->find($pageId);

dd($page1, $page2);

See how those two objects differ: are the relations correctly loaded in neither, one or both of them – and if they’re loaded in both, is the relation data the same in both as well?

1 like
aurelianspodarec's avatar

@kokoshneta So when I tried to load with and without eager loading, the data was exactly the same. So when I output the data with the filter or without, the json is the smae.

Though in terms of the code you provided, not sure how that's meant to be used or where. Isn't filter expecting array of filters? I put that in my model and tried on my controller but it does need some properties to work with. Not sure how to use the code you've sent or where exactly.

aurelianspodarec's avatar

@kokoshneta For example, when the filter is commented, I got this JSON:

{"id":1551,"site_page_id":514,"site_variation_id":1,"image":"https:\/\/i.imgur.com\/YIy8LcQ.png","created_at":"2022-06-06T12:27:41.000000Z","updated_at":"2022-06-06T12:27:41.000000Z"}

When the filter is uncommented, i get this json:

{"id":3,"site_page_id":514,"site_variation_id":3,"image":"https:\/\/i.imgur.com\/mjZn29x.png","created_at":"2022-06-06T12:27:35.000000Z","updated_at":"2022-06-06T12:27:35.000000Z","device":"desktop","theme":"light"}

So its exactly the same, except the image(which shouldn't be there) and of course the id which is normal.

But on the last one I can't get the use the method to get the image from media table.

Is that any helpful?

Oh wait, the second one that doesn't actually work has additional data of device and theme, maybe that shouldn't be there, maybe tha's the issue? Not sure. But then that's how I filter the variation by the correct filter

kokoshneta's avatar

@aurelianspodarec Are you getting JSON when you dd() the object itself?!? That’s… odd. It should be an object of class SitePage, and dd()-ing it shouldn’t convert it to JSON.

I deliberately left out the filters so that it would only filter by imageVariations, but for it to work, you’d have to give the filter parameter a default value by declaring the filter function as public function scopeFilter($query, array $filters = []).

1 like
aurelianspodarec's avatar

@kokoshneta Ah okay.

So I put that here:

class PageController extends Controller
{
    public function index()
    {
        return view('web.inspiration.pages.index', [
            // 'pages' => SitePage::latest()->filter(
            //     request(['color', 'page', 'device', 'theme'])
            // )->paginate(10)->withQueryString()
            $pageId = 514, // Or whatever, just some random page ID that exists
            $page1 = SitePage::filter()->find($pageId),
            $page2 = SitePage::with('imageVariations')->find($pageId),
            
            // 'pages' => 
            dd($page1, $page2),
        ]);
    }
}

And I got this response: Bare in mind, the image table shoulnd't be there the only valid image would be if it came from curated.test and not from other sites, as that's just test.

aurelianspodarec's avatar

@kokoshneta So this does work correctly.

But as you can see, the second part with gets all imageVariation, which is correct, but then they need to be filtered, as only one variation can be shown at the time. So the query I did would solve that issue. But of course using spatie methods wont get the image loaded from the media table.

kokoshneta's avatar

@aurelianspodarec Okay, so we can see from that that the relationship itself does work, but the scope doesn’t load it. I don’t think I’ve ever seen an attempt to add an eager-loading inside a scope before; it may just be that $query->with() doesn’t work at all.

Try commenting out the entire part of the scope function that attempts to load the relation and instead just add $query->with('imageVariations') and then run this:

$pageId = 514;
$page = SitePage::filter()->find($pageId);

dd($page1);

Then we can see if it’s possible to load a relation like this inside of a scope declaration at all.

1 like
aurelianspodarec's avatar

@kokoshneta So I put this:

 public function scopeFilter($query, array $filters = []) {
        $query->with('imageVariations');
        return $query;
    } 

And controller:

  $pageId = 514,
            $page = SitePage::filter()->find($pageId),

            dd($page)

And the results seems like it works it seems

aurelianspodarec's avatar

@kokoshneta And when I then displaythe data like so:

  $pageId = 514,
           'pages' => SitePage::filter()->find($pageId),
 <div>
        {{ $pages->imageVariations[0]->getFirstMediaUrl('image') }}
    </div>

It does get the image URL, so that works

aurelianspodarec's avatar

@kokoshneta And when I do filtering manually, this works:

public function scopeFilter($query, array $filters = []) {
        $query->with('imageVariations', function($query) {
            // Number 3 is the variation ID
            $query->where('site_variation_id', '3');
        });
        return $query;
    } 

Which makes me feel like join or something in my other filter doesn't work.

I mean, the number 3 needs to be dynamic, and thatneeds to be fetched in the variation table by columns but ufff.

I wonder, if you were to do this a different way al together, how would you create the filter? I suppose something is wrong like you say the way it loads with my filter I guess. Hard to say for me, still learning xd

When I do this as well:

'pages' => SitePage::latest()->filter(request(['device']))->paginate(10)->withQueryString(),

    public function scopeFilter($query, array $filters) {



        $query->with('imageVariations', function($query) {

            $query->where('site_variation_id', '1');
        });

        return $query;
    } 
  @foreach ($pages as $page)
    <div class="mb-4">

            @if($page->imageVariations)
                @foreach ($page->imageVariations as $image)
                <div>{{ $image->getFirstMediaUrl('image') }}</div>
                @endforeach
            @endif
            {{ $page->name }}
    </div>
    @endforeach

THis also shows the image url

aurelianspodarec's avatar

@kokoshneta Not sure if this is any good, but trying other stuff:

I put this back in, but added first() at the end of the query.

 $theme = $filters['theme'] ?? 'light';
        $device = $filters['device'] ?? 'desktop';
    
        $query->with(['imageVariations' => function($query) use ($theme, $device) {
            $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device)->first();
            }
        ]);

        return $query;

I display it: {{ $page->imageVariations }}

and get the data:

[{"id":3,"site_page_id":514,"site_variation_id":3,"image":"https:\/\/i.imgur.com\/mjZn29x.png","created_at":"2022-06-06T12:27:35.000000Z","updated_at":"2022-06-06T12:27:35.000000Z","device":"desktop","theme":"light"}] 

But now I try to access the image: {{ $page->imageVariations->getFirstMediaUrl('image') }} and I get this

Method Illuminate\Database\Eloquent\Collection::getFirstMediaUrl does not exist.

Which I mean I feel like reading this https://stackoverflow.com/questions/32077571/call-to-undefined-method-illuminate-database-eloquent-collection this should be correct?

aurelianspodarec's avatar

@kokoshneta I feel like... doing more test

  {{ $page->imageVariations->hi() }}

This throws error:

Method Illuminate\Database\Eloquent\Collection::hi does not exist.

but that cleary exists

class SitePageImage extends Model implements HasMedia
{

    // TODO: Renamte this to SitePageVariation
    
    use HasFactory;
    use InteractsWithMedia;
    
    public function variation()
    {
        return $this->belongsTo(SiteVariation::class, 'site_variation_id');
    }

    public function hi()
    {
        return "hi";
    }

    public function registerMediaConversions(Media $media = null): void
    {
        $this
            ->addMediaConversion('preview')
            ->width(100)
            ->height(100);
    }

}

so ther's no way the other function would work

However, when I uncommented the ->first() from the query, and do this instead:

  @foreach ($pages as $page)
    <div class="mb-4">
        {{-- {{ $page->imageVariations->hi() }} --}}
            @if($page->imageVariations)
                @foreach ($page->imageVariations as $image)
                <div>{{ $image->hi() }}</div>
                {{-- <div>{{ $image->getFirstMediaUrl('image') }}</div> --}}
                @endforeach
            @endif
            {{ $page->name }}
    </div>
    @endforeach

The hi function works , but getFirstMediaUrl doesn't.

kokoshneta's avatar

@aurelianspodarec Okay, so we’ve established that adding an eager-loading with() in a scope does work. So far so good. And you’ve found the reason for the error (it was trying to run the function on a collection rather than on the individual object). So the issue does seem to be with the table joining.

Looking at your join clause and the Laravel docs, I think perhaps I see what the problem is. The where clauses are being applied to the main query (on the site_page_images table), and not to the join itself (the subquery on the site_variations table), which I think is wrong.

Try this instead:

$query->with(['imageVariations' => function($query) use ($theme, $device) {
	$query->join('site_variations', function($join)  use ($theme, $device) {
		$join->on('site_variation_id', '=', 'site_variations.id') 
				 ->where('theme', $theme)
				 ->where('device', $device);
	}
}

If that doesn’t work either, try adding this just before return $query in the scope function:

dd($query->toSql());

That will show you the actual SQL query being executed, which may help you figure out what’s going wrong, and where.

1 like
aurelianspodarec's avatar

@kokoshneta So I did paste this code:

   $query->with(['imageVariations' => function($query) use ($theme, $device) {
                $query->join('site_variations', function($join)  use ($theme, $device) {
                    $join->on('site_variation_id', '=', 'site_variations.id')
                            ->where('theme', $theme)
                            ->where('device', $device);
                });
            }
        ]);

And that didn't work.

So then I dd the SQL, and got this:

^ "select * from `site_pages` order by `created_at` desc"

Which ummmm, umm.

There seem to be no difference with this code either

$query->with(['imageVariations' => function($query) use ($theme, $device) {
            $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device);
            }
        ]);
        dd($query->toSql());

the output seems to be the same

^ "select * from `site_pages` order by `created_at` desc"

Really not sure what to make out of this or how to go around this - while still using spatie functions and not rewriting them or creating my own implementations or relationship and all that... since that's why I installed the library xd

It feels like the filter needs to be done before the join? Umm.

kokoshneta's avatar
Level 27

@aurelianspodarec Oh, I forgot to add in a table name. It should of course be $join->on('site_page_images.site_variation_id', '=', 'site_variations.id'). I’m not sure if that actually makes a difference in MySQL (which I assume you use), but it would in SQL Server.

If that doesn’t work, let’s try a workaround. What happens if you add a filtering scope to the variation class and then add that to the eager-loading? Like so:

In class SitePageImage:

public function scopeFiltered($query, $theme, $device) {
	$query->join('site_variations', 'site_variation_id', 'site_variations.id')
		->where('site_variations.theme', $theme)
		->where('site_variations.device', $device);

	dd($query);
	return $query;
}

In class SitePage:

public function filteredImageVariations() {
	return $this->imageVariations()->filtered(request('theme'), request('device'));
}

In your controller:

$page = SitePage::with('filteredImageVariations')->latest();

That should output the SQL query used to fetch the variations, not the one used to fetch the site pages.

———

Alternatively, without joins, you can try this in your variations class:

public function scopeFiltered($query, $theme, $device) {
	$query->whereIn('site_variation_id', function($query) use ($theme, $device) {
		$query->select('id')
			->from('site_variations')
			->where('theme', $theme)
			->where('device', $device)
	});

	dd($query);
	return $query;
}
1 like
aurelianspodarec's avatar

@kokoshneta So I've done these changes:

In class SitePageImage:

<?php

namespace App\Models\Site;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class SitePageImage extends Model implements HasMedia
{

    // TODO: Renamte this to SitePageVariation
    
    use HasFactory;
    use InteractsWithMedia;
    
    public function scopeFiltered($query, $theme, $device) {
        $query->join('site_variations', 'site_variation_id', 'site_variations.id')
            ->where('site_variations.theme', $theme)
            ->where('site_variations.device', $device);
    
        // dd($query);
        return $query;
    }

    public function variation()
    {
        return $this->belongsTo(SiteVariation::class, 'site_variation_id');
    }

    public function hi()
    {
        return "hi";
    }

    public function registerMediaConversions(Media $media = null): void
    {
        $this
            ->addMediaConversion('preview')
            ->width(100)
            ->height(100);
    }

}

In class SitePage:

<?php

namespace App\Models\Site;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

use App\Models\Site\Site;

class SitePage extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'slug'
    ];

    protected $with = ['color', 'site', 'categoryPage'];

    public function scopeFilter($query, array $filters) {
         
        $query->when($filters['color'] ?? false, fn($query, $color) =>
            $query->whereHas('site.color', function ($query) use ($color) {
                $query->where('name', $color);
            })
        );
    

        // TODO: Come up with better filter name?
        $query->when($filters['pageName'] ?? false, fn($query, $page) =>
            $query->whereHas('categoryPage', function ($query) use ($page) {
                $query->where('name', $page);
            })
        );
    
        // with('filteredImageVariations')
        $theme = $filters['theme'] ?? 'light';
        $device = $filters['device'] ?? 'desktop';
    
        $query->with(['filteredImageVariations' => function($query) use ($theme, $device) {
            $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device);
            }
        ]);
       
        return $query;
    }

    public function filteredImageVariations() {
        return $this->imageVariations()->filtered(request('theme'), request('device'));
    }
    
    public function imageVariations()
    {
        return $this->hasMany(SitePageImage::class, 'site_page_id');
    }

    public function allImages()
    {
        return $this->hasMany(SitePageImage::class, 'site_page_id');
    }

    public function color()
    {
        return $this->belongsTo(SiteBrandColor::class,'site_brand_color_id');
    }

    public function site()
    {
        return $this->belongsTo(Site::class, 'site_id');
    }

    public function categoryPage()
    {
        return $this->belongsTo(SiteCategoryPage::class, 'site_category_page_id');
    }
}

and its controller:

<?php

namespace App\Http\Controllers\Inspiration;

use Illuminate\Http\Request;

use App\Http\Controllers\Controller;
use App\Models\Site\SitePage;


class PageController extends Controller
{
    public function index()
    {
        return view('web.inspiration.pages.index', [
            // 'pages' => SitePage::latest()->filter(
            //     request(['color', 'page', 'device', 'theme'])
            // )->paginate(10)->withQueryString()

            'pages' => SitePage::with('filteredImageVariations')->latest()->paginate(10)

            // $pageId = 514, // Or whatever, just some random page ID that exists
            // $page1 = SitePage::filter()->find($pageId),
            // $page2 = SitePage::with('imageVariations')->find($pageId),
            
            // // 'pages' => 
            // dd($page1, $page2),

        //     $pageId = 514,
        //    'pages' => SitePage::filter()->find($pageId),

            // dd($page)



            // $pageId = 514,
            // 'pages' => SitePage::latest()->filter(request(['device']))->paginate(10)->withQueryString(),
        ]);
    }
}

I did add pagination and such because otherwise it wouldn't work just blank screen, now the image shows with the above code, but how would I filter it now? I did try to change this as it was before with the controller, and instead put this filteredImageVariations in the filter, but that doesn't seem it works.

The image shows with this code though, just the other parts are broken, which is expected based on the code, but then how would we add this back?

       'pages' => SitePage::latest()->filter(
                request(['color', 'page', 'device', 'theme'])
            )->paginate(10)->withQueryString()

I tried to:

 $query->with(['filteredImageVariations' => function($query) use ($theme, $device) {
            $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device);
            }
        ]);

Which actually, if I live the above code, and keep this in the controller:

'pages' => SitePage::with('filteredImageVariations')->latest()->paginate(10)

It doesn't even matter what I write here lol:

  $query->with(['filtesfsdfsdredasdfsdjkfsdImageVariations' => function($query) use ($theme, $device) {
            $query->join('site_variations', 'site_variation_id', 'site_variations.id')
                ->where('site_variations.theme', $theme)
                ->where('site_variations.device', $device);
            }
        ]);

The image still shows.

I must have been misunderstanding something lol

Oh I see...

The controller reads this code:

   public function filteredImageVariations() {
        return $this->imageVariations()->filtered(request('theme'), request('device'));
    }

Hence the above code with random name doesn't get executed. Makes sense now lol

kokoshneta's avatar

@aurelianspodarec Okay, if that works, we’re getting somewhere.

What happens if you do this:

Leave SitePageImage as it is, with the scope still there.

In SitePage:

public function scopeFilter($query, array $filters) {
	$query->when($filters['color'] ?? false, fn($query, $color) =>
		$query->whereHas('site.color', function ($query) use ($color) {
			$query->where('name', $color);
		})
	);

	// TODO: Come up with better filter name?
	$query->when($filters['pageName'] ?? false, fn($query, $page) =>
		$query->whereHas('categoryPage', function ($query) use ($page) {
			$query->where('name', $page);
		})
	);

	$theme = $filters['theme'] ?? 'light';
	$device = $filters['device'] ?? 'desktop';

	$query->with([
		'imageVariations' => function($query) use ($theme, $device) {
			$query->filtered($theme, $device);
		}
	]);
	
	return $query;
}

And then in your controller go back to this:

SitePage::latest()->filter(
	request(['color', 'page', 'device', 'theme'])
)->paginate(10);

That should, I think, give the desired result.

(If that works, you can delete the filteredImageVariations relation from the SitePage class.)

1 like
aurelianspodarec's avatar

@kokoshneta I tried to do what you said but that Doesn't work :(

If we delete this code:

 $query->with([
            'imageVariations' => function($query) use ($theme, $device) {
                $query->filtered($theme, $device);
            }
        ]);

Then it works, but of course, that breaks the filtering functionality.

SitePage

<?php

namespace App\Models\Site;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

use App\Models\Site\Site;

class SitePage extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'slug'
    ];

    protected $with = ['color', 'site', 'categoryPage'];

    public function scopeFilter($query, array $filters) {
        $query->when($filters['color'] ?? false, fn($query, $color) =>
            $query->whereHas('site.color', function ($query) use ($color) {
                $query->where('name', $color);
            })
        );
    
        // TODO: Come up with better filter name?
        $query->when($filters['pageName'] ?? false, fn($query, $page) =>
            $query->whereHas('categoryPage', function ($query) use ($page) {
                $query->where('name', $page);
            })
        );
    
        $theme = $filters['theme'] ?? 'light';
        $device = $filters['device'] ?? 'desktop';
    
        $query->with([
            'imageVariations' => function($query) use ($theme, $device) {
                $query->filtered($theme, $device);
            }
        ]);
        
        return $query;
    }

    public function filteredImageVariations() {
        return $this->imageVariations()->filtered(request('theme'), request('device'));
    }
    
    public function imageVariations()
    {
        return $this->hasMany(SitePageImage::class, 'site_page_id');
    }

    public function allImages()
    {
        return $this->hasMany(SitePageImage::class, 'site_page_id');
    }

    public function color()
    {
        return $this->belongsTo(SiteBrandColor::class,'site_brand_color_id');
    }

    public function site()
    {
        return $this->belongsTo(Site::class, 'site_id');
    }

    public function categoryPage()
    {
        return $this->belongsTo(SiteCategoryPage::class, 'site_category_page_id');
    }
}

PageController

<?php

namespace App\Http\Controllers\Inspiration;

use Illuminate\Http\Request;

use App\Http\Controllers\Controller;
use App\Models\Site\SitePage;


class PageController extends Controller
{
    public function index()
    {
        return view('web.inspiration.pages.index', [
            // 'pages' => SitePage::latest()->filter(
            //     request(['color', 'page', 'device', 'theme'])
            // )->paginate(10)->withQueryString()

            'pages' => SitePage::latest()->filter(
                request(['color', 'page', 'device', 'theme'])
            )->paginate(10)

            // 'pages' => SitePage::with('filteredImageVariations')->latest()->paginate(10)

            // $pageId = 514, // Or whatever, just some random page ID that exists
            // $page1 = SitePage::filter()->find($pageId),
            // $page2 = SitePage::with('imageVariations')->find($pageId),
            
            // // 'pages' => 
            // dd($page1, $page2),

        //     $pageId = 514,
        //    'pages' => SitePage::filter()->find($pageId),

            // dd($page)



            // $pageId = 514,
            // 'pages' => SitePage::latest()->filter(request(['device']))->paginate(10)->withQueryString(),
        ]);
    }
}

The blade file:

 @foreach ($pages as $page)
    <div class="mb-4">
        <img src="{{ $page->imageVariations[0]->getFirstMediaUrl('image') }}" />

What do you think of the general issue of this?

I'm basically doing:

Packagge
Product
ProductVariation

Media

And I'm displaying product variation that gets the image, based on the filtered query for product, package and product variation. So its almost like get all product variation form Nike package, and get all shoes(product) that have dark shoe color (product variation).

I mean, I suppose most eCommerce don't do it like that right. If it was just get variations that would be easy right, but because of how I try to do it, its not?

Or am I wrong. I mean, what is the difference between this code and what would be in e-commerce?

What do you think or what solutions do you think there are for this in general? I obviously appreciate the help a lot, but I'm just wondering how you see this. As I'm surprised tbh and still learning :D

You said SQL I mean, worst case scenario I'll need to change the database? lol

Or there's still more solutions to this that can be tried?

aurelianspodarec's avatar

@kokoshneta Another thing for example, if I do this:

{{ dd($page->imageVariations[0]->hi()) }}

The hi method does work and displays hi.

But as soon as I do this:

 {{ dd($page->imageVariations[0]->getFirstMediaUrl('image')) }}

It doesn't.

So its still the collection issue right.

kokoshneta's avatar

@aurelianspodarec No, if hi() works, but getFirstMediaUrl() gives an empty result (I assume it doesn’t throw an exception that the method doesn’t exist), that just means that the variation object is empty. If it had been a collection, the hi() call would have thrown an exception.

What do you get if you show the SQL query when filtering the relation loading, like so:

$query->with([
	'imageVariations' => function($query) use ($theme, $device) {
		$query->filtered($theme, $device);
		//Add the following line:
		dd($query->toSql());
	}
]);

What does that dd() show you?

1 like
aurelianspodarec's avatar

@kokoshneta The variation object isn't empty I'm 100% sure.

Because when you do $page->imageVariations - it actually shows you the variation JSON. You can see the variation id, etc...

Running the query you provided, shows this:

^ "select * from `site_page_images` inner join `site_variations` on `site_variation_id` = `site_variations`.`id` where `site_page_images`.`site_page_id` in (514, 515, 516, 517, 518, 519, 520, 521, 522, 523) and `site_variations`.`theme` = ? and `site_variations`.`device` = ? ◀"
aurelianspodarec's avatar

@kokoshneta When I do this:

<img src="{{ $page->imageVariations[0] }}" />

I get this inside the SRC

{"id":3,"site_page_id":514,"site_variation_id":3,"image":"https:\/\/i.imgur.com\/mjZn29x.png","created_at":"2022-06-06T12:27:35.000000Z","updated_at":"2022-06-06T12:27:35.000000Z","device":"desktop","theme":"light"}

So the variation isn't empty. If that's what you mean (note, the 'image' here needs to be deelted' as we are trying to get the image from 'media' table)

kokoshneta's avatar

@aurelianspodarec Okay, that looks correct – both the SQL query and the variation object. So the problem, then, is probably with the Spatie Media package (or probably not the package itself, but something in your implementation of it).

I’ve never used that and am not familiar with it, but what do you get if you do dd($page->imageVariations[0]->getMedia()? That should return a collection of Media objects according to the Spatie docs. Are you sure that images have been associated with the particular variation object?

1 like
aurelianspodarec's avatar

@kokoshneta Yes. So I know that my code works because it does get the correct image. it does not get the image only when there is a filter present.

So when i take the filter away, the spatie function does work:

<img src="https://domain.test/storage/16/giG764KX8ycuJFbZiL7lMpk5PkziTi-metaMTY1NDA5NzI4OTU5My5qcGVn-.jpg">

If I getMedia its the same effect as with the other functions, they are basically the same thing just give different data, I've tested it like the other one and its empty when the filter code is uncommented.

So getMedia and the other function gives empty array of items.

But if we comment out the filtering code, it will get the item and display an image, but the issue is its going to show all images and we want only one at all times based on what the user selects/filters.

MichalOravec's avatar

When you use join always add select as well. That most likely caused the issue.

1 like
kokoshneta's avatar

@MichalOravec Always good practice, but it doesn’t seem to have been the issue here, since a join clause with an explicit select statement worked exactly the same as the one without.

aurelianspodarec's avatar

@MichalOravec You're talking about this right: https://laravel.com/docs/9.x/queries#inner-join-clause well, I guess this https://laravel.com/docs/9.x/queries#select-statements but...

What am I meant to select?

I'm bit confused on this example:

 ->select('users.*', 'contacts.phone', 'orders.price')

Show me all numbers of all users and order prices.

And the reason I say what I'm meant to select, is because I believe the Spatie function would select what is needed? $media = $yourModel->getFirstMedia(); https://spatie.be/docs/laravel-medialibrary/v10/basic-usage/retrieving-media

I don't even see a url column in the media table that Spatie has created. So Id assume the URL is created on the fly or something

MichalOravec's avatar

@kokoshneta @aurelianspodarec I meant this piece of code

$query->with([
	'imageVariations' => function($query) use ($theme, $device) {
		$query->filtered($theme, $device);
		//Add the following line:
		dd($query->toSql());
	}
]);

The reasult of dd is

select * from `site_page_images` inner join `site_variations` on `site_variation_id` = `site_variations`.`id` where `site_page_images`.`site_page_id` in (514, 515, 516, 517, 518, 519, 520, 521, 522, 523) and `site_variations`.`theme` = ? and `site_variations`.`device` = ? 

select * from is a problem.

In class SitePageImage I added ->select('site_page_images.*')

public function scopeFiltered($query, $theme, $device) {
	return $query->select('site_page_images.*')
        ->join('site_variations', 'site_variation_id', 'site_variations.id')
		->where('site_variations.theme', $theme)
		->where('site_variations.device', $device);
}
1 like
MichalOravec's avatar

@aurelianspodarec

With this

<img src="{{ $page->imageVariations[0] }}" />

you get:

{"id":3,"site_page_id":514,"site_variation_id":3,"image":"https:\/\/i.imgur.com\/mjZn29x.png","created_at":"2022-06-06T12:27:35.000000Z","updated_at":"2022-06-06T12:27:35.000000Z","device":"desktop","theme":"light"}

Why image is as a link to the imgur.com? I've never use spatie media library, but I don't think that it upload images to the imgur.com.

By the way instead of $page->imageVariations[0] I prefer to use a collection method $page->imageVariations->first()

1 like
aurelianspodarec's avatar

@MichalOravec :O

It seems to work, thank you :D but I just need some time to go over all of the code and test this and try to understand what's happening.

This is just ripping my brain apart now lol Just very weirdly structured code I got now.

aurelianspodarec's avatar

@MichalOravec Why the image has imgur? Because that's something I did as I didn't knew better.

I have changed my database structure 3times since I started the project when we met lol

Recently was a major change.

I'm still learning how to structure things and name and...

I need to change the name from SitePageImages to SitePageVariations which is a pivot table, it shoudln't contain the image.

The media table should contain the image, which is the change I'm doing now. I didn't want to clean this up while i was getting help to reduce confusion, at the same time I understand this is confusing. I will need to clean this up now and then revise this again.

So I've must built like 3projects since I started, except I've been re-doing the same project over and over again lol

But I think maybe with you or jtwr and someone else I figured out that images should go into one table usually, so I'll keep all images in one table like spatie, makes sense what spatie is doing.

But yeah, I mean there's a lot to this, I'm sure all of this is simple to you but I'm still learning :D

But what you mentioned needs to be deleted and imageVariation name needs to be changed as well as the table name etc...

In other words, I have this:

Brand
Product
ProductImage

media

When it should be:

brand
product
productvariation - pivot table

media

I believe.

And I was doing this:

brand
product
productImage

I dont know is it just me or its hard to follow stuff on this thread, I think Laracst needs to improve threats a bit and maybe also highlight new stuff etc

aurelianspodarec's avatar

@MichalOravec So I've figured out one issue with this so far. Which I also solved my way but...

Each page has minimum of 2variations and maximum of 4.

It seems when you filter by $theme and the variation doesn't exist, the Page still shows:

@if($props->imageVariations->first())
    <img src="{{ asset($props->imageVariations->first()->getFirstMediaUrl('image')) }}" class="js-image bg-cover w-full object-cover group-hover:scale-105 transition-transform"/>
   @endif

I just guard the code like so so it doesn't break.

I feel like that's because one where returns true maybe?

Since each page image/variatoin will have:

Mobile - Light Desktop - Light

Mobile - Dark Desktop - Dark

The light mode is the default mode, but when I switch to say mobile dark, and that variation doesn't exist, the Page still shows, but it shouldn't. pages?device=mobile&theme=dark

So if we get the variation from the SitePageImage with this code:

public function scopeFiltered($query, $theme, $device) {
        $query->select('site_page_images.*')
            ->join('site_variations', 'site_variation_id', 'site_variations.id')
            ->where('site_variations.theme', $theme)
            ->where('site_variations.device', $device);

        return $query;
    }

If it doesn't exist, how would the SitePage would know? Is this like drilling props in react, where you need redux to tell the higher components that something happen type of situation?

Or I suppose we need to check it in the

$query->with(['imageVariations' => function($query) use ($theme, $device) {
                $query->filtered($theme, $device);
            }
        ]);

And perhaps here say, if the value is null, don't show myself aka Page.

I suppose both of the theme and device need to be true, for the Page to show.

Or would you just do simple if else statement in blade and check if array is empty and if yes don't show?

So this:

@if($props->imageVariations->first())
<div>
<article class="js-site-page group relative cursor-pointer border rounded-xl h-[400px] border-gray-200 overflow-hidden">

    

I got this working, I do have my expected result, BUT I wonder if there is a different way so we don't need to use the if statement in blade. If the array is empty, don't show the page... Would we use the query SQL instead for this? Or am I overthinking at this point lol

aurelianspodarec's avatar

@MichalOravec Actually, what I wrote above, the if statement doesn't solve this

@if($props->imageVariations->first())

the reason being is because the database only fetches 10results, and when I hide this, only 6resutls displays on the page, so we need to tell the database to keep looking for 10results and ignore those who don't match the query actually.

So I guess reading the above, what change would I need to make for that? It seems like I need Redux but for Laravel?

MichalOravec's avatar

@aurelianspodarec Sorry I will not play this game with you. 48 replies in one thread is too much.

If you have an another issue just create a new thread.

Please or to participate in this conversation.