Laravel Error when referencing a resource in a resource - php

I'm using a Laravel Json Resource in my controller, as follows
public function index(Request $request)
{
$itemsWithTranslations = MenuItem::where(['menu_id' => $request->id, 'parent_id' => null])
->with(['children', 'translations'])
->orderBy('sort_order', 'asc')
->get();
return MenuItemResource::collection($itemsWithTranslations);
}
Now I would like to generate a collection, inside this collection with the children for the item that's being shown.
The following code works fine. Notice how I commented out the children reference
class MenuItemResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'text' => $this->title,
// 'children' => MenuItemResource::collection($this->whenLoaded('children')),
'data' => [
'id' => [
'value' => $this->id,
'type' => 'hidden'
],
'title' => [
'value' => $this->title,
'type' => 'text',
'label' => 'Title'
],
'resource_link' => [
'value' => $this->resource_link,
'type' => 'text',
'label' => 'Resource Link'
],
'translations' => MenuItemTranslationResource::collection($this->whenLoaded('translations'))->keyBy(function ($translation) {
return $translation['locale'];
})
]
];
}
}
When I uncomment the children, I get the following error
"Call to undefined method Illuminate\Http\Resources\Json\AnonymousResourceCollection::keyBy()"
Is it wrong, to include a Resource inside a resource? Or how should I go about this?
Model
class MenuItem extends Model
{
protected $table = 'menu_items';
protected $fillable = ['menu_id', 'parent_id', 'title', 'order', 'resource_link', 'html_class', 'is_blank'];
public function translations()
{
return $this->hasMany(MenuItemTranslation::class, 'menu_item_id');
}
public function children()
{
return $this->hasMany(MenuItem::class, 'parent_id');
}
}
Extra Information
When I return the following data, it does return empty as a collection for the children.
MenuItemResource::collection($this->children);
This returns
While if I return the children without a collection, it returns them (for 1 item, which is correct)
return $this->children;
returns

you should use ChildrenResource::collection
'children' => ChildrenResource::collection($this->whenLoaded('children'))
hope this works.
create a ChildrenResource class if not exists.

Related

get limited numbers of data with yii2

I want to get limited numbers of data from database using Yİİ2. I fetched all the record by writing this:
$departures = ArrayHelper::map(
TourDeparture::find()->all(),
'id',
'tour_id'
);
I tried to use limit(5), so that I can get only 5 rows. But I could not. Still, I get the all the rows in the table. How can I achieve that?
Updated: Here is my tourdeparture model
class TourDeparture extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'tour_departure';
}
/**
* {#inheritdoc}
*/
public function rules()
{
return [
[['tour_id', 'start_date', 'end_date', 'price_1adult', 'price_2adult', 'price_3adult', 'price_child', 'price_baby', 'min_guests', 'max_guests', 'status', 'required_min_guest'], 'required'],
[['tour_id', 'min_guests', 'max_guests', 'status', 'required_min_guest'], 'integer'],
[['start_date', 'end_date'], 'safe'],
[['price_1adult', 'price_2adult', 'price_3adult', 'price_child', 'price_baby'], 'number'],
[['tour_id'], 'exist', 'skipOnError' => true, 'targetClass' => Tour::className(), 'targetAttribute' => ['tour_id' => 'id']],
];
}
/**
* {#inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'tour_id' => 'Tour ID',
'start_date' => 'Start Date',
'end_date' => 'End Date',
'price_1adult' => 'Price 1adult',
'price_2adult' => 'Price 2adult',
'price_3adult' => 'Price 3adult',
'price_child' => 'Price Child',
'price_baby' => 'Price Baby',
'min_guests' => 'Min Guests',
'max_guests' => 'Max Guests',
'status' => 'Status',
'required_min_guest' => 'Required Min Guest',
];
}
/**
* Gets query for [[Tour]].
*
* #return \yii\db\ActiveQuery
*/
public function getTour()
{
return $this->hasOne(Tour::className(), ['id' => 'tour_id']);
}
/**
* Gets query for [[TourReservations]].
*
* #return \yii\db\ActiveQuery
*/
public function getTourReservations()
{
return $this->hasMany(TourReservation::className(), ['tour_departure_id' => 'id']);
}
}
$departures = ArrayHelper::map( TourDeparture::find()->limit(5), 'id',
'tour_id' );
I wrote it like above. but ı got all the record
I'm surprised your code worked at all! You are passing the Query class into the map function. ArrayHelper::map is expecting an array and needs the query to be executed using the ->all(). ->limit(5) just adds a new term to the SQL query.
$departures = ArrayHelper::map( TourDeparture::find()->limit(5)->all(), 'id', 'tour_id' );
for retrieve the first 5 rows should be
TourDeparture::find()->orderBy("id")->limit(5)->all()
The code you shared didn't do the limit(5). Anyway, I believe it is caused by the array map,
you may try to simulate the changes as below
$q = TourDeparture::find()->select(['id', 'tour_id'])->limit(5)->asArray()->all();
\yii\helpers\VarDumper::dump($q, $depth = 10, $highlight = true);
For the above, records selected in array form, before array map.
After array map, from here you can compare the changes.
$departures = ArrayHelper::map(
$q,
'id',
'tour_id'
);
\yii\helpers\VarDumper::dump($departures, $depth = 10, $highlight = true);

Laravel resource return empty array

I have resource where i get product data trough third table but having hard time make relationships work on models so it return empty array.
Logic
Product has many barcodes
Barcodes can have (belongsTo) damage
In damage we get product trough barcode table (we store barcode_id)
I also included fillable part of each column so you can see columns in database.
Code
Product model
class Product extends Model
{
protected $fillable = [
'name', 'slug', 'stock', 'cover', 'description', 'sku', 'price', 'discount',
];
public function barcodes()
{
return $this->hasMany(Barcode::class);
}
}
Barcode model
class Barcode extends Model
{
protected $fillable = [
'product_id', 'sku', 'serial_number', 'price', 'discount',
];
public function product()
{
return $this->belongsTo(Product::class);
}
public function damages()
{
return $this->hasMany(DamageProduct::class);
}
}
DamageProduct model
class DamageProduct extends Model
{
protected $fillable = [
'outlet_id', 'user_id', 'barcode_id', 'description',
];
public function barcode()
{
return $this->belongsTo(Barcode::class);
}
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
DamageProductsResource resource
class DamageProductsResource extends JsonResource
{
public function toArray($request)
{
$arrayData = [
'id' => $this->id,
'outlet' => new OutletsResource($this->whenLoaded('outlet')),
'user' => new usersResource($this->whenLoaded('user')),
'barcode' => new BarcodeResource($this->whenLoaded('barcode')),
'description' => $this->description,
];
return $arrayData;
}
}
Result
Any idea?
Update
In case you need to see how BarcodeResource resource looks like here it is:
public function toArray($request)
{
$arrayNull = [
'id' => $this->id,
'product' => new ProductsResource($this->whenLoaded('product')),
'sku' => $this->sku,
'serial_number' => $this->serial_number ? (Int) $this->serial_number : null,
'price' => (Int) $this->price,
'discount' => $this->discount ? (Int) $this->discount : null,
];
}
I would say you simply forgot the return statement in your BarcodeResource
public function toArray($request)
{
$arrayNull = [
'id' => $this->id,
'product' => new ProductsResource($this->whenLoaded('product')),
'sku' => $this->sku,
'serial_number' => $this->serial_number ? (Int) $this->serial_number : null,
'price' => (Int) $this->price,
'discount' => $this->discount ? (Int) $this->discount : null,
];
return $arrayNull; // this is missing
}

Eager Load Pivot in nested BelongsToMany with Api Resource

I need your help!
I'm having problems returning pivot table information when using ApiResources.
If I have a model like this:
Post.php
public function likes()
{
return $this->belongsToMany(Like::class)
->withPivot(['points']) // I want this in my PostResource::collection !
}
When defining its Resources:
LikeResource.php
public function toArray($request)
{
return [
'like_field' => $this->like_field
];
}
PostResource.php
public function toArray($request)
{
return [
'title' => $this->title,
'likes' => LikeResource::collection($this->whenLoaded('likes'))
];
}
Then in PostController.php
return PostResource::collection(Post::with('likes')->get())
It will return something like this:
Controller Response
[
{
'title' => 'Post 1'
'likes' => [
{
'like_field' => 'Test'
},
{
'like_field' => 'Test 2'
}
]
},
{
'title' => 'Post 2',
...
}
]
The problem is, using that LikeResource::collection() it does not appends pivot information. How could I add 'points' of the pivot table when defining that PostResource??
Thats all,
Thx!
Solution
Well, simply reading a bit in Laravel Docs, to return pivot information you just has to use the method $this->whenPivotLoaded()
So, the PostResource becomes:
public function toArray($request)
{
return [
'title' => $this->title,
'likes' => LikeResource::collection($this->whenLoaded('likes')),
'like_post' => $this->whenPivotLoaded('like_post', function() {
return $this->pivot->like_field;
})
];
}

Laravel 5.5 API resources for collections (standalone data)

I was wondering if it is possible to define different data for item resource and collection resource.
For collection I only want to send ['id', 'title', 'slug'] but the item resource will contain extra details ['id', 'title', 'slug', 'user', etc.]
I want to achieve something like:
class PageResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
'email' => $this->user->email,
],
];
}
}
class PageResourceCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
];
}
}
PageResourceCollection will not work as expected because it uses PageResource so it needs
return [
'data' => $this->collection,
];
I could duplicate the resource into PageFullResource / PageListResource and PageFullResourceCollection / PageListResourceCollection but I am trying to find a better way to achieve the same result.
The Resource class has a collection method on it. You can return that as the parameter input to your ResourceCollection, and then specify your transformations on the collection.
Controller:
class PageController extends Controller
{
public function index()
{
return new PageResourceCollection(PageResource::collection(Page::all()));
}
public function show(Page $page)
{
return new PageResource($page);
}
}
Resources:
class PageResource extends Resource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
'email' => $this->user->email,
],
];
}
}
class PageResourceCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection->transform(function($page){
return [
'id' => $page->id,
'title' => $page->title,
'slug' => $page->slug,
];
}),
];
}
}
If you want the response fields to have the same value in the Resource and Collection, you can reuse the Resource inside the Collection
PersonResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
class PersonResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
// return parent::toArray($request);
return [
'id' => $this->id,
'person_type' => $this->person_type,
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'created_at' => (string) $this->created_at,
'updated_at' => (string) $this->updated_at,
];
}
}
PersonCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PersonCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function toArray($request)
{
// return parent::toArray($request);
return PersonResource::collection($this->collection);
}
}
The accepted answer works, if you are not interested in using links and meta data. If you want, simply return:
return new PageResourceCollection(Page::paginate(10));
in your controller. You should also look to eager load other dependent relationships before passing over to the resource collection.

ZF2 form element collection validation

So I have a "simple" form
class SiteAddForm extends Form
{
public function __construct()
{
parent::__construct('add_site_form');
$site = new SiteFieldSet();
$this->add($site);
}
public function getTemplate()
{
return 'site_add.phtml';
}
}
The form it self does nothing. It adds a field_set and returns a template name.
The SiteFieldSet looks likes:
class SiteFieldSet
extends FieldSet
implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('site');
$name = new Text('name');
$this->add($name);
$domains = new Collection('domains');
$domains->setTargetElement(new DomainFieldSet())
->setShouldCreateTemplate(true);
$this->add($domains);
}
public function getTemplate()
{
return 'site.phtml';
}
/**
* Should return an array specification compatible with
* {#link Zend\InputFilter\Factory::createInputFilter()}.
*
* #return array
*/
public function getInputFilterSpecification()
{
return [
'name' => [
'required' => true,
'validators' => [
new StringLength([
'min' => 200,
])
]
],
'domains' => [
'required' => true,
],
];
}
}
It adds a text and collection element to the fieldset. The field set implements InputFilterProviderInterface to validate the data thrown into it.
The name must be at least 200 chars (for testing) and the collection is required.
But now comes my problem. With the field set that is thrown into the collection, code:
class DomainFieldSet
extends FieldSet
implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('domain');
$host = new Url('host');
$this->add($host);
$language = new Select('language', [
'value_options' => [
'nl_NL' => 'NL',
],
]);
$this->add($language);
$theme = new Select('theme', [
'value_options' => [
'yeti' => 'Yeti',
]
]);
$this->add($theme);
}
public function getTemplate()
{
return 'domain.phtml';
}
/**
* Should return an array specification compatible with
* {#link Zend\InputFilter\Factory::createInputFilter()}.
*
* #return array
*/
public function getInputFilterSpecification()
{
return [
'host' => [
'required' => true,
'validators' => [
new StringLength([
'min' => 200,
])
]
],
'language' => [
'required' => true,
],
'theme' => [
'required' => true,
],
];
}
}
Again nothing special. There are now three elements defined host, theme & language. Again the field set implements InputFilterProviderInterface. So there must be an getInputFilterSpecification in the class.
When I fill in the form
site[name] = "test"
site[domains][0][host] = 'test'
site[domains][0][theme] = 'yeti'
site[domains][0][language] = 'nl_NL'
It gives an error for site[name] saying it must be atleast 200 chars, so validations "works"
But it should also give an error on site[domains][0][host] that it needs to be atleast 200 chars (code was copy pasted, and the use is correct).
So why doesn't the validation kicks in, and or how can I solve the issue so a element/field set inside a collection is properly validated
Try using setValidationGroup in the form __construct method
like:
public function __construct()
{
$this->add(array(
'type' => 'Your\Namespace\SiteFieldSet',
'options' => array(
'use_as_base_fieldset' => true,
),
));
$this->setValidationGroup(array(
'site' => array(
'domain' => array(
'host',
'language',
'theme',
),
),
));
}
or this may also work...
$this->setValidationGroup(FormInterface::VALIDATE_ALL);

Categories