How to change the structure of eager loaded data efficiently in Laravel - laravel

I'm loading a product eagerly with its relationship data in Laravel.
$product = Product::with('attributeValues.attribute')->find($id)->get();
Currently I get the response structure as follows.
[
{
"product_id": 1,
"product_name": "Shirt A",
"attribute_values": [
{
"attribute_value_id": 1,
"attribute_value": "small",
"attribute": {
"attribute_id": 1,
"attribute": "size"
}
},
{
"attribute_value_id": 1,
"attribute_value": "medium",
"attribute": {
"attribute_id": 1,
"attribute": "size"
}
},
...
]
},
...
]
The structure I expected to get is as follows.
[
{
"product_id": 1,
"product_name": "Shirt A",
"attribute": [
{
"attribute_id": 1,
"attribute": "size",
"attribute_values": [
{
"attribute_value_id": 1,
"attribute_value": "small"
},
{
"attribute_value_id": 1,
"attribute_value": "medium"
}
]
},
...
]
},
...
]
The current relationships of the models are as follows
class Product extends Model {
public function attributeValues(){
return $this->belongsToMany(AttributeValue::class, 'product_attributes');
}
}
class AttributeValue extends Model {
public function attribute(){
return $this->belongsTo(Attribute::class);
}
}
class Attribute extends Model { }
Currently I'm succeeded getting this structure correctly by using product and getting it attributes separately using raw queries. I'm trying to achieve a Eloquent way of doing this task.
The tables I have are as follows
products
+----+------+
| id | name |
+----+------+
product_attributes
+----+------------+--------------------+
| id | product_id | attribute_value_id |
+----+------------+--------------------+
attribute_values
+----+--------------+-------+
| id | attribute_id | value |
+----+--------------+-------+
attributes
+----+-----------+
| id | attribute |
+----+-----------+

you can use with inside with using custom eager loading:
$product = Product::with(['attributeValues'=>function($query){
$query->select(['attribute_value_id','attribute_value'])
->with('attribute:attribute_id,attribute');
}])->find($id)->get();

What about using collections methods ? I'm not an expert but i would have done something like this :
$products->map(function($product){
$attribute = $product->attribute_values->groupBy('attribute.attribute')->map(function($groupAttribute,$key){
$attribute_values = $groupAttribute->map(function($attribute) {
return ['attribute_value_id'=>$attribute->attribute_value_id,'attribute_value'=>$attribute->attribute_value];
});
return ['attribute_id' => 1 ,'attribute'=> $key , 'attribute_values' => $attribute_values ];
});
return [
'product_id'=>$product->product_id,
'product_name'=>$product->product_name,
'attribute'=>$attribute
];
});

Just do the following you will get the required result.
$product = Product::with('attribute.attributeValues')->find($id)->get();

The best way is to use JsonResource.
Here you can find a short and easy example
Just define ProductResource, attribyteValueResource, and AttributeResource
Here is the example:
PropertyResource:
use Illuminate\Http\Resources\Json\JsonResource;
class PropertyResource extends JsonResource
{
public function toArray($request): array
{
return [
'product_id' => $this->product_id,
'product_name' => $this->product_name,
'attribute_values' => AttributeResource::collect( $this->whenLoaded('attributeValues') ),
];
}
}
AttributeValueResource:
use Illuminate\Http\Resources\Json\JsonResource;
class AttributeValueResource extends JsonResource
{
public function toArray($request): array
{
return [
'attribute_value_id' => $this->attribute_value_id,
'attribute_value' => $this->attribute_value,
'attribute' => AttributeValueResource::make( $this->whenLoaded('attribute') ),
];
}
}
AttributeResource:
use Illuminate\Http\Resources\Json\JsonResource;
class AttributeResource extends JsonResource
{
public function toArray($request): array
{
return [
'attribute_id' => $this->attribute_id,
'attribute' => $this->attribute,
];
}
}
Then just use them:
$product = ProductResource::make(
Product::load('attributeValues.attribute')->find($id)
);

Related

spatie/laravel-medialibrary, filters on getMedia() function not working

I am trying to get media for certain modal as follows:
public function show(Gallery $gallery)
{
$images = $gallery->getMedia('default'); //returns images
// $images = $gallery->getMedia('default', ['name', 'original_url']); //returns empty
return response()->json(['data' => $images]);
}
when not adding filters I get the correct data as follows:
{
"data": {
"686cc1b1-cfdc-425d-9b93-c99290f0d35e": {
"name": "Screenshot from 2022-06-27 23-29-29",
"file_name": "Screenshot-from-2022-06-27-23-29-29.png",
"uuid": "686cc1b1-cfdc-425d-9b93-c99290f0d35e",
"preview_url": "",
"original_url": "http://app.test/storage/media/25/Screenshot-from-2022-06-27-23-29-29.png",
"order": 1,
"custom_properties": [],
"extension": "png",
"size": 10546
}
}
}
but when I use filters I get empty array.
You have to retrieve single media, then you can chain property name
$gallery->getMedia('default')[0]->name;
or
$gallery->getFirstMedia('default')->name;
If you want to return json with name and original_url field for every media, that can be achieved by using API resource, something like this:
$images = $gallery->getMedia('default');
return MediaResource::collection($images);
and in MediaResource class you have:
public function toArray($request)
{
return [
'name' => $this->name,
'original_url' => $this->original_url
];
}

How to get a collection of multiple ralationships using Eloquent?

I have a table that contains foreign keys of product properties and their values.
product_id
feature_id
value_id
1
1
2
1
1
3
1
2
4
How to get a collection of that values using Eloquent?
Something like this
product => [
id => 1,
...
features => Collection: [
Feature: [
id => 1,
name => Size,
values => [
Value: [
id => 2,
name => M
],
Value: [
id: 3,
name: L
]
]
],
Feature: [
id => 2,
name => Color,
values => [
Value: [
id => 4,
name => Red
]
]
]
]
]
This is my solution.
class Product extends Model
{
protected $appends = ['features'];
protected array $features;
protected bool $featuresAppend = false;
public function getFeaturesAttribute(): Collection
{
if ($this->featuresAppend === false) {
$this->appendFeatures();
}
return collect($this->features);
}
public function productFeatures(): BelongsToMany
{
return $this->belongsToMany(Feature::class, 'product_features');
}
public function productFeatureValues(): BelongsToMany
{
return $this->belongsToMany(
FeatureValue::class,
'product_features',
'product_id',
'value_id'
);
}
protected function appendFeatures(): void
{
// Get futures and values by relationships
$features = $this->productFeatures()->get()->keyBy('id');
$values = $this->productFeatureValues()->get()->keyBy('id');
// Create features-values structure
$features->each(
function ($f) {
$this->features[$f->id] = new \StdClass();
$this->features[$f->id]->feature = $f;
}
);
// Appends values to future
$values->each(
function ($v) {
if (array_key_exists($v->feature_id, $this->features)) {
$this->features[$v->feature_id]->values[$v->id] = $v;
}
}
);
$this->featuresAppend = true;
}
}
I'm looking for a better solution to this problem or a solution using native Laravel features.

Join pivot has one array

I guys, i have 2 relations tables, and when listing all my messages, is shown the pivot columns relation, but i need to show the data has one array, is there a method in eloquent cant make this happen?
I searched and i no that is possible manipulate with collection methods but i wonder if there is another way.
My Model query is:
public function messages()
{
return $this->belongsToMany(Message::class,'message_users')->withPivot('is_read','sent_at');
}
This is how it is:
{
"data": [
{
"id": 4,
"title": "test",
"body": "<p>test</p>",
"pivot": {
"user_id": 1,
"message_id": 4,
"is_read": 0,
"sent_at": "2019-06-05 12:59:11"
}
}
]
}
This is how i want:
{
"data": [
{
"id": 4,
"title": "test",
"body": "<p>test</p>",
"user_id": 1,
"message_id": 4,
"is_read": 0,
"sent_at": "2019-06-05 12:59:11"
}
]
}
You can do next: in User model write toArray() method as
/**
* Convert the model instance to an array.
*
* #return array
*/
public function toArray(): array
{
$attributes = $this->attributesToArray();
$attributes = array_merge($attributes, $this->relationsToArray());
// Detect if there is a pivot value and return that as the default value
if (isset($attributes['pivot'] && isset($attributes['pivot']['user_id']))) {
$attributes['user_id'] = $attributes['pivot']['user_id'];
$attributes['message_id'] = $attributes['pivot']['message_id'];
$attributes['is_read'] = $attributes['pivot']['is_read'];
$attributes['sent_at'] = $attributes['pivot']['sent_at'];
unset($attributes['pivot']);
}
return $attributes;
}

laravel - change value in array for API Resources

{
"data": {
"username": "candy",
"certificates": [
{
"id": 11,
"category_id": 1,
"certname": "cert name test",
"created_at": "2018-08-18 00:58:12",
"updated_at": "2018-08-18 00:58:12"
}
]
}
}
Above is a response by using Eloquent: API Resources.
I would like to put category name instead of category_id.
Below is resource class
public function toArray($request)
{
return [
'nickname' => $this->nickname,
'certificates' => $this->certificates,
];
}
certificates are in array (hasMany relationship)
certificates belongsTo category
In this case, in the resource class you can fetch category name for each certificate and attach it.
public function toArray($request)
{
$certificateList = $this->certificates;
foreach($certificateList as $ceritificate) {
// fetch only the category name from the database.
// attach to the certificate.
$categoryName = Category::where('id', $certificate->category_id)->pluck('name')->first();
$certificate->setAttribute('category_name, $categoryName);
}
// then as usual
return [
'nick_name' => $this->nick_name,
'certificates' => $certificateList
];
}
This Might Help,
Run a foreach loop
$category = "Your SELECT Query";
foreach ($category as $value) {
$categoryId = json_decode($value->category_id);
$x = Category::select('certname')->whereIn('id', $categoryId)->get();
foreach ($x as $as) {
$y[] = $as->certname;
}
$value->certname = $y;
you can just do it like this
in controler
return CertificateResource::collection(Certificate::with('category')->get());
in CertificateResource
return [
'nickname' => $this->nickname,
'certificates' => $this->certificates,
'category_name'=>$this->whenLoaded('category', function () {
return $this->category->category_name;
}),
];

laravel resource collection has blank output

I am trying to get a json output of a collection but all I am getting is a data [].
I have 2 tables.
TABLE:Player ||
id |
name
and
TABLE:FlightTime || id | vehicle | totaltime | player_id (foreign key)
I am trying to get all the flighttime entry for a particular player as a collection.
My FlightTime resource:
class FlightTime extends JsonResource
{
public function toArray($request)
{
return [
'vehicle' => $this->vehicle,
'total-time' => $this->totaltime,
];
}
}
And my FlightTimeCollection:
class FlightTimeCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection,
'test' => 'did this work',
];
}
}
My api route is set as
Route::get('/v1/player/{name}/flights', 'PlayerController#showFlightbyName');
And I have this in my controller:
public function showFlightbyName($name)
{
$pid = Player::select('id')->where('name', $name)->first();
return new FlightTimeCollection(FlightTime::where('player_id', $pid)->get());
}
Now when I visit localhost/api/v1/player/Angel/flights, No error is thrown. But all I am getting is this.
{
"data": [],
"test": "did this work"
}
I just can't figure out what's going on here and what I did wrong.
You should treat the toArray() method in your FliightTimeCollection the same way you treat it in FlightTimeResource. The collection will be automatically wrapped in data => [stuff]'.
So, like this:
class FlightTimeCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'vehicle' => $this->vehicle,
'another_field' => $this->field2,
// ...
];
}
}
Result of that would be
{
"data": [
{"vehicle": "some value", "another_field": "some value"},
// ...
],
}
I got it working. $pid is an obj and I needed to get the value out of $pid->id
public function showFlightbyName($name)
{
$pid = Player::select('id')->where('name', $name)->first();
return new FlightTimeCollection(FlightTime::where('player_id', $pid->id)->get());
}

Resources