how to use collection on same model laravel resources - php

We are currently developing a feature in codotto.com where a user can comment on an IT meetup. Each comment can have an answer to it. We are only allowing for one-level deep answers, so something like:
- Comment 1
- Answer to comment 1
- Answer to comment 1
- Comment 2
- Answer to comment 2
- Answer to comment 2
I have the following database structure:
// meetup_messages
- id
- user_id
- meetup_id
- meetup_message_id (nullable) -> comments that do not answer will have this set to nullable
In my model I define the answers as a HasMany relationship:
class MeetupMessage extends Model
{
// ...
public function answers(): HasMany
{
return $this->hasMany(self::class, 'meetup_message_id');
}
}
Then on my controller, I get all comments that do not have answers:
public function index(
IndexMeetupMessageRequest $request,
Meetup $meetup,
MeetupMessageService $meetupMessageService
): MeetupMessageCollection
{
$meetupMessages = MeetupMessage::with([
'user',
// 'answers' => function ($query) {
// $query->limit(3);
// }
'answers'
])
->whereNull('meetup_message_id')
->whereMeetupId($meetup->id)
->paginate();
return new MeetupMessageCollection($meetupMessages);
}
Then on my MeetupMessageCollection:
class MeetupMessageCollection extends ResourceCollection
{
public function toArray($request)
{
return parent::toArray($request);
}
}
Then on my MeetupMessageResource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
class MeetupMessageResource extends JsonResource
{
public function toArray($request)
{
return collect([
// 'answers' => new MeetupMessageCollection($this->whenLoaded('answers')),
])
->when(
is_null($this->meetup_message_id) && $this->relationLoaded('answers'),
function (Collection $collection) {
$collection->put('answers', MeetupMessageCollection::collection($this->answers));
}
);
}
}
But I get the following error: Call to undefined method App\\Models\\Meetup\\MeetupMessage::mapInto(). How can I still use MeetupMessageCollection by passing the answers to it?

As #matialauriti pointed out, you cant use resource collections inside collections in Laravel
class MeetupMessageResource extends JsonResource
{
public function toArray()
{
return [
'answers' => new MeetupMessageCollction($this->answers) // ❌ You can't do this
]
}
}
My solution was to pull my resource formation to a private method and re-use it if answers is present:
class MeetupMessageResource extends JsonResource
{
public function toArray($request)
{
return collect($this->messageToArray($this->resource))
->when($this->relationLoaded('user'), function (Collection $collection) {
$collection->put('user', $this->userToArray($this->user));
})
// ✅ Now I don't need to use Resources inside my API Resource class
->when(
is_null($this->meetup_message_id) && $this->relationLoaded('answers'),
function (Collection $collection) {
$answers = $this
->answers
->map(function (MeetupMessage $answer) {
return array_merge(
$this->messageToArray($answer),
['user' => $this->userToArray($answer->user)]
);
});
$collection->put('answers', $answers);
}
);
}
private function messageToArray(MeetupMessage $meetupMessage): array
{
return [
'id' => $meetupMessage->id,
'message' => Purify::config(MeetupMessageService::CONFIG_PURIFY)->clean($meetupMessage->message),
'answersCount' => $this->whenCounted('answers'),
'createdAt' => $meetupMessage->created_at,
];
}
}

Related

Getting parent parents data in a pivot table in Laravel

What Eloquent model would you use to:
Get all Pages that its Company brand color is red/slug.
Where brand color is a pivot table and stored as an ID on the Company table.
So you have brand_colors id, name, slug, hex
Have company where it has brand_color_id
And Pages that has just the company ID.
I want to grab all pages whose company brand color is X.
How would you go about it?
This table has the company_id
class WebsitePage extends Model
{
use HasFactory;
public function scopeFilter($query, array $filters) {
// Get all pages with a color that the company has
$query->when($filters['color'] ?? false, fn($query, $color) =>
$query->whereHas('color', fn ($query) =>
$query->where('name', $color)
)
);
}
// Where URL slug 'red' color, associated with company brand_color_id matches brandColor slug
// or just simpley call $company->color() ?
public function color()
{
// return $this->with(Company::class, 'brand_color_id');
// return $this->belongsTo(Company::class, 'company_id');
// dd($this->with())
// return Company::with('color.red');
}
This table has brand_color_id, etc.
class Company extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
'slug',
'description'
];
public function pages() {
return $this->hasMany(WebsitePage::class);
}
public function color() {
return $this->belongsTo(BrandColor::class, 'brand_color_id');
}
}
This table has: id | name | slug | hex
class BrandColor extends Model
{
use HasFactory;
}
So knowing this, how would you get all pages whose company color is the one filtered?
In terms of the page controller, this is what I'm doing:
class PageController extends Controller
{
public function index(WebsitePage $websitePage)
{
// return view('web.inspiration.pages.index', [
// 'pages' => WebsitePage::latest()->paginate(30)
// ]);
return view('web.inspiration.pages.index', [
'pages' => WebsitePage::latest()->filter(
request(['color'])
)->paginate(30)->withQueryString()
]);
}
}
Your model layout isn't described that well, but I think you just want to define a HasOneThrough relationship to join WebsitePage to BrandColor:
class WebsitePage extends Model
{
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function color(): HasOneThrough
{
return $this->hasOneThrough(BrandColor::class, Company::class);
}
}
class Company extends Model
{
public function color(): BelongsTo
{
return $this->belongsTo(BrandColor::class);
}
}
class BrandColor extends Model
{
public function company(): HasOne
{
return $this->hasOne(Company::class);
}
}
Then in your controller, just use a condition on your whereHas() and with() calls:
class PageController extends Controller
{
public function index(): View
{
$color = request("color");
$pages = WebsitePage
::whereHas("color", fn ($q) => $q->where("color", $color))
->with(["color" => fn ($q) => $q->where("color", $color)])
->paginate(30)
->withQueryString();
return view("web.inspiration.pages.index", compact($pages));

Create nested API

I'm trying to make an api that have lists and inside each list there is anther list inside of it called cards and the cards list is the cards of this list.
I tried to show it in index function and didn't work it was like this:
public function index()
{
// $list = List -> cards();
$list = List::cards();
return response( $list );
}
Card Model:
public function list()
{
return $this->belongsTo( List::class() );
}
Card Model:
public function cards()
{
return $this->hasMany( Card::class() );
}
What i want to output is json data like this:
"lists":[
'name':listname
'cards':[
'card one': card name,
]
]
If you use Laravel framework use Resource for response, in Resource of laravel you can load cards. For example in ListController :
public function index()
{
return ListResource::collection(List::all()->paginate());
}
And in ListResource :
public function toArray($request)
{
'cards' => CardResource::collection('cards');
}
belongsTo or hasMany accepts model name as a first argument. In your case you need to pass your model class name in your relations methods.
public function list()
{
return $this->belongsTo(List::class);
}
and
public function cards()
{
return $this->hasMany(Card::class);
}
So if you want to receive models including relations you can use with method.
return response(List::query()->with('cards'));
You can use resources.
Http\Resources\List:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class List extends JsonResource
{
public function toArray($request)
{
$cards = [];
foreach ($this->cards as $card) {
$cards[] = $card->name;
}
return [
'name' => $this->name,
'cards' => $cards,
];
}
}
Http\Controllers\ListController:
namespacce App\Http\Controllers;
use App\Http\Resources\List as ListResource;
use App\Components\List;
class ListController extends Controller
{
$lists = List::query()->get();
return ListResource::collection($lists)->response();
}

laravel 5.7 data not passed to the view

I'm trying to pass my article data to the single page article named article.blade.php although all the data are recorded into the database but when I tried to return them in my view, nothing showed and the [ ] was empty. Nothing returned.
this is my articleController.php
<?php
namespace App\Http\Controllers;
use App\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function single(Article $article)
{
return $article;
}
}
this is my model:
<?php
namespace App;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use Sluggable;
protected $guarded = [];
protected $casts = [
'images' => 'array'
];
public function sluggable()
{
return [
'slug' => [
'source' => 'title'
]
];
}
public function path()
{
return "/articles/$this->slug";
}
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
and this is my Route
Route::get('/articles/{articleSlug}' , 'ArticleController#single');
Change your code to
class ArticleController extends Controller
{
public function single(Article $article)
{
return view('article', compact('article'));
}
}
change route to
Route::get('/articles/{article}' , 'ArticleController#single');
And model
public function getRouteKeyName()
{
return 'slug';
}
See docs https://laravel.com/docs/5.7/routing#route-model-binding
You might not be getting any data because you have not specified that you're using title_slug as the route key for model binding in your model.
Add this to your model class and it should give you the data
public function getRouteKeyName()
{
return 'slug';
}
Then you can return the data in json, view or other format.
Depending on what you try to archive, you need to either ...
return $article->toJson(); // or ->toArray();
.. for json response or ..
return view(..., ['article' => $article])
for passing a the article to a certain view

Conditional responses in Laravel Eloquent API resources

Laravel 5.7. I have a model Audio, with the fields id and title. An Audio can have many AudioVersions, where AudioVersion has id, audio_id (referring to Audio) and url.
Now, I have two parent models, Foo and Bar, which can have many Audio models.
Audio:
class Audio extends Model
{
public function versions()
{
return $this->hasMany('App\AudioVersion', 'audio_id');
}
}
AudioVersion:
class AudioVersion extends Model
{
public function audio()
{
return $this->belongsTo('App\AudioContent');
}
}
Foo:
class Foo extends Model
{
public function audioContents()
{
return $this->morphToMany('App\Audio', 'audio_contentable', 'audio_contentable');
}
}
I have an Eloquent API resource, FooResource, which returns its Audio objects:
FooResource:
class FooResource extends JsonResource
{
public function toArray($request)
{
return [
'audio' => AudioResource::collection($this->audioContents),
];
}
}
AudioResource:
class AudioResource extends JsonResource
{
public function toArray($request)
{
return [
'urls' => $this->versions,
];
}
}
My problem is that in the audio key of my FooResource I only want to return Audios which have AudioVersions related to them. I.e. if I have an Audio with no AudioVersions, I do not want that Audio included in the Foo's audio key. I can't find a way to do this deep conditional logic in Eloquent / Resources.
In the FooResource class, you could filter() the collection before passing it to the collection method.
class FooResource extends JsonResource
{
public function toArray($request)
{
$audioContents = $this->audioContents()->filter(function($audio, $key) {
return $audio->versions->count();
}
return [
'audio' => AudioResource::collection($audioContents),
];
}
}
You need to do something like this. This is the example, you only need to add join inside with so that only those audios will be fetched which have audio versions.
$audioContents = AudioContents::with([
'audio' => function ($query) use ($SpecificID) {
return $query->join("audio_versions")
->on("audio_versions.audio_id", "=", "audios.id");
}
])->get();
Try this and let me know if you are facing any issue.
In the end I added a scope to Audio:
public function scopeHasVersions($query)
{
return $query->whereHas('versions');
}
Then in FooResource:
return [
'audio' => AudioResource::collection($this->audioContents()->hasVersions()->get()),
];

Laravel query with multiple where not returning expected result

I'm trying to build a query from a Repository in a Model with 2 where clauses.
This is the data I have in a MySql table:
id name environment_hash
1 online_debit abc
2 credit_cart abc
I want to query by name and environment_hash. To do this, I created the method findByHashAndMethod() (see below).
But when I use it in my controller, like this:
$online_debit = $this->ecommercePaymentMethodRepository->findByHashAndMethod($hash, 'online_debit')->first();
or this:
$credit_card = $this->ecommercePaymentMethodRepository->findByHashAndMethod($hash, 'credit_cart')->first();
I keep getting both rows and not only the ones filtered. What's wrong with the code?
This is my PaymentMethodRepository.php
class EcommercePaymentMethodRepository extends BaseRepository
{
public function findByHashAndMethod($hash = null, $payment_method)
{
$model = $this->model;
if($hash)
{
$filters = ['environment_hash' => $hash, 'name' => $payment_method];
$this->model->where($filters);
}
else
{
$this->model->where('environment_hash', Auth::user()->environment_hash)
->where('name', $payment_method);
}
return $model;
}
public function model()
{
return EcommercePaymentMethod::class;
}
}
And this is my model EcommercePaymentMethod.php
<?php
namespace App\Models;
use Eloquent as Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class EcommercePaymentMethod extends Model
{
use SoftDeletes;
public $table = "ecommerce_payment_methods";
protected $dates = ['deleted_at'];
public $fillable = [
"name",
"payment_processor_id",
"active",
"environment_hash"
];
protected $casts = [
"name" => "string"
];
public function payment_processor()
{
return $this->hasOne('App\Models\EcommercePaymentProcessor');
}
}
While I am not entirely sure why ->first() would ever return more than one result, your Repository method had some few glaring issues that's prone to errors.
class EcommercePaymentMethodRepository extends BaseRepository
{
// 1. Do not put optional parameter BEFORE non-optional
public function findByHashAndMethod($payment_method, $hash = null)
{
// 2. Call ->model() method
$model = new $this->model();
// 3. Logic cleanup
if (is_null($hash)) {
$hash = Auth::user()->environment_hash;
}
return $model->where('environment_hash', $hash)
->where('name', $payment_method);
}
public function model()
{
return EcommercePaymentMethod::class;
}
}

Categories