phplaraveleloquentlaravel-11laravel-resource

Can I display nested relationships in Laravel Resources?


I have a fairly common setup - 2 main models and 1 pivot with additional information regarding their relationship. I'm using Laravel API Resources and they work great, but I can't figure out a way how to display the data without the need of also displaying the pivot data.

Let's ilustrate with simplified situation:

Models:

class Report extends BaseModel
{
    public function reportCountries(): HasMany
    {
        return $this->hasMany(ReportCountry::class, 'report', 'id');
    }
}

class ReportCountry extends BaseModel
{
    public function report(): BelongsTo
    {
        return $this->belongsTo(Report::class, 'report', 'id');
    }

    public function countryEntity(): BelongsTo
    {
        return $this->belongsTo(Country::class, 'country', 'id');
    }
}

class Country extends BaseModel
{
    protected $table = 'countries';
}

The Controller:

class ReportsController
{
    public function show(int $id): ReportResource
    {
        $report = Report::with(['reportCountries.countryEntity'])->findOrFail($id);

        return new ReportResource($report);
    }
}

And finally the Resources:


/**
 * @mixin Report
 */
class ReportResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'countries' => ReportCountryResource::collection($this->whenLoaded('reportCountries')), // I'd love to use CountryResource here instead
            // bunch of other data
        ];
    }
}

/**
 * @mixin ReportCountry
 */
class ReportCountryResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id, // I don't really need to show this, I'm interested only in CountryResource below:
            'country' => new CountryResource($this->whenLoaded('countryEntity')),
        ];
    }
}

/**
 * @mixin Country
 */
class CountryResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
        ];
    }
}

What I would love to do is ignore ReportCountryResource and load CountryResource directly in ReportResource, something like this:

/**
 * @mixin Report
 */
class ReportResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'countries' => CountryResource::collection($this->whenLoaded('reportCountries.countryEntity')),
            // bunch of other data
        ];
    }
}

But when I tried to load this way, the CountryResource was displaying data from the pivot (ReportCountryResource) and not from the main model (CountryResource).

Is there a way how to do this? I'm now creating bunch of Resources for pivot models, which I don't really need :/


Solution

  • So after more failing and tweaking I got it working by using another HasManyThrough relationship.

    So in my model I now have this:

    class Report extends BaseModel
    {
        public function reportCountries(): HasMany
        {
            return $this->hasMany(ReportCountry::class, 'report', 'id');
        }
    
        public function countries(): HasManyThrough
        {
            return $this->hasManyThrough(Country::class, ReportCountry::class, 'report', 'id', 'id', 'country');
        }
    }
    

    which allows me to ditch the pivot resources

    class ReportsController
    {
        public function show(int $id): ReportResource
        {
            $report = Report::with(['countries'])->findOrFail($id);
    
            return new ReportResource($report);
        }
    }
    
    class ReportResource extends JsonResource
    {
        public function toArray(Request $request): array
        {
            return [
                'id' => $this->id,
                'countries' => CountryResource::collection($this->whenLoaded('countries')), 
                // bunch of other data
            ];
        }
    }
    

    So I'm using this approach unless someone points something better