I am trying to build a resource collection with a second resource based on a hasManyThrough relationship. When i call the actual relationship on the main resource it does provide me the correct data. Whenever i pass the data to another resource and use the whenLoaded() method, it doesn't chain the data to the main collection.
The model:
public function fields()
{
return $this->hasManyThrough(Field::class, FieldValue::class, 'component_id', 'id', 'id', 'field_id');
}
The controller:
/**
* #return JsonResponse
*/
public function index()
{
$components = Component::with(['fields'])->get();
return ComponentValueResource::collection($components)->response();
}
ComponentValueResource class:
public function toArray($request)
{
return [
'fields' => FieldResource::collection($this->whenLoaded('fields')),
];
}
FieldResource class:
public function toArray($request)
{
$fields = $this->whenLoaded('fields');
return [
'fields' => new ComponentValueResource($fields),
];
}
Output of dd(FieldResource::collection($this->whenLoaded('fields')));
#original: array:9 [
"id" => 1
"base_id" => 1
"component_id" => null
"identifier" => "text input"
"type" => "text"
"position" => 1
"created_at" => "2021-08-22 19:44:23"
"updated_at" => "2021-08-22 19:44:23"
"laravel_through_key" => 6
]
Output from the resource:
{
"data": [
{
"id": 6,
"fields": [
[]
]
},
Related
I've got a Product and a Category models:
class Product extends BaseModel
{
use Uuid;
protected $fillable = [
'barcode',
'name',
'sku',
'description',
'type',
'category_id',
'wholesale_price',
'retail_price',
'base_picture',
'current_stock_level',
'active',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
class Category extends BaseModel
{
protected $fillable = [
'name',
'parent',
'description',
'image',
];
public function product(): HasMany
{
return $this->hasMany(Product::class, 'category_id');
}
In my controller, I'm retrieveing all products and wanted to return the category object the product belongs to in the response, so I'm doing:
class ProductsController extends Controller
{
public function index(): AnonymousResourceCollection
{
$products = Product::all();
return ProductsResource::collection($products->loadMissing('category'));
}
and my resource looks like:
class ProductsResource extends JsonResource
{
public function toArray($request) : array
{
return [
'id' => $this->id,
'type' => 'products',
'attributes' => [
'barcode' => $this->barcode,
'name' => $this->name,
'slug' => $this->slug,
'sku' => $this->sku,
'description' => $this->description,
'type' => $this->type,
// todo return category object?
'category' => new CategoriesResource($this->whenLoaded('category_id')),
'wholesale_price' => $this->wholesale_price,
'retail_price' => $this->retail_price,
'base_picture' => $this->base_picture,
'current_stock_level' => $this->current_stock_level,
'active' => $this->active,
]
];
}
}
but the response I'm getting is:
{
"data": [
{
"id": "a2102c4c-c14a-4d16-af28-e218bcc4fe39",
"type": "products",
"attributes": {
"barcode": "1010101010101",
"name": "phione",
"slug": "phione",
"sku": "w2e2r2",
"description": null,
"type": "services",
"wholesale_price": 54,
"retail_price": 34,
"base_picture": null,
"current_stock_level": 0,
"active": 1
}
}
]
}
I tried loading the relationship differently:
public function index(): AnonymousResourceCollection
{
$products = Product::with('category')->get();
return ProductsResource::collection($products);
}
but the result is the same.
It seems that the relationship is well established because if I run:
$product = Product::first();
dd($product->category);
I can see the category the product belongs to:
#attributes: array:8 [▼
"id" => 2
"name" => "Paper"
"slug" => "paper"
"parent" => 1
"description" => null
"image" => null
"created_at" => "2022-09-20 02:03:05"
"updated_at" => "2022-09-20 02:03:05"
]
what am I missing?
In controller load category relation with eager loading
$products = Product::with('category');
return ProductsResource::collection($products);
and in the ProductsResource file load, the relation category, not category_id
'category' => CategoriesResource::collection($this->whenLoaded('category')),
You don't need to use a collection here. Simply do
$products = Product::with('category')->get();
return response()->json($products);
I'm using PHP8.1 and Laravel 9 for a project in which I've got the following enum:
enum OrderStatuses : string
{
case New = 'new';
case Pending = 'pending';
case Canceled = 'canceled';
case Paid = 'paid';
case PaymentFailed = 'payment-failed';
public function createOrderStatus(Order $order) : OrderStatus
{
return match($this) {
OrderStatuses::Pending => new PendingOrderStatus($order),
OrderStatuses::Canceled => new CanceledOrderStatus($order),
OrderStatuses::Paid => new PaidOrderStatus($order),
OrderStatuses::PaymentFailed => new PaymentFailedOrderStatus($order),
default => new NewOrderStatus($order)
};
}
one of the classes listed in the enum looks like this:
abstract class OrderStatus
{
public function __construct(protected Order $order)
{
}
/**
* Determines whether an order can transition from one status into another
*
* #return bool
*/
abstract public function canBeChanged() : bool;
}
class PaidOrderStatus extends OrderStatus
{
public function canBeChanged(): bool
{
return false;
}
}
all others are basically the same, they just differ on the implementation of the canBeChanged method.
Now, I've got the following resource:
class OrdersResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (string)$this->id,
'type' => 'orders',
'attributes' => [
'status' => $this->status,
'payment_type' => $this->payment_type,
'payment_transaction_no' => $this->payment_transaction_no,
'subtotal' => $this->subtotal,
'taxes' => $this->taxes,
'total' => $this->total,
'items' => OrderItemsResource::collection($this->whenLoaded('orderItems')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
]
];
}
}
which is called from my controller like this:
return (new OrdersResource($order))
->response()->setStatusCode(ResponseAlias::HTTP_OK);
Before implementing the enum my resource was working correctly, it returned the expected data. But after the enum, it's returning [] for the status field.
A sample return is currently looking like this:
"id" => "86b4e2da-76d4-4e66-8016-88a251513050"
"type" => "orders"
"attributes" => array:8 [
"status" => []
"payment_type" => "card"
"payment_transaction_no" => "3kaL92f5UwOG"
"subtotal" => 3005.76
"taxes" => 0
"total" => 3005.76
"created_at" => "2022-08-31T12:47:55.000000Z"
"updated_at" => "2022-08-31T12:47:55.000000Z"
]
]
notice again the value for status.
I've got a casting and a attribute in my Order's model:
protected $casts = [
'status' => OrderStatuses::class,
];
protected function status(): Attribute
{
return new Attribute(
get: fn(string $value) =>
OrderStatuses::from($value)->createOrderStatus($this),
);
}
Furthermore, if I dd the type of $this->status in the toArray method from OrdersResource it says that it is of type Domain\Order\Enums\PaidOrderStatus which is correct.
I tried adding __toString() to PaidOrderStatus class but had no luck. What am I missing?
Update
I've added a test() method to PaidOrderStatus:
class PaidOrderStatus extends OrderStatus
{
public function canBeChanged(): bool
{
return false;
}
public function test() : string
{
return OrderStatuses::Paid->value;
}
}
and then did:
public function toArray($request): array
{
return [
'id' => (string)$this->id,
'type' => 'orders',
'attributes' => [
'status' => ($this->status)->test(),
'payment_type' => $this->payment_type,
'payment_transaction_no' => $this->payment_transaction_no,
'subtotal' => $this->subtotal,
'taxes' => $this->taxes,
'total' => $this->total,
'items' => OrderItemsResource::collection($this->whenLoaded('orderItems')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
]
];
}
and that gave me:
[
"id" => "8a2d6024-a63f-44ba-a145-cede2ecf3aaa"
"type" => "orders"
"attributes" => array:8 [
"status" => "paid"
"payment_type" => "card"
"payment_transaction_no" => "kC9upaoGb2Nd"
"subtotal" => 657.26
"taxes" => 0
"total" => 657.26
"created_at" => "2022-08-31T13:17:25.000000Z"
"updated_at" => "2022-08-31T13:17:25.000000Z"
]
and that worked. But it's a very hacky solution and I'd like to do better.
I'm sure you've already solved this as it's been months ago, but first you don't need the Attribute mutator as you have already defined the cast which will correctly map the string value to the right enum case.
Then in the resource, you just get the value from the enum like so.
'status' => $this->status->value,
I'm trying to create an API for my data tables using Laravel's resource. I have three models with relationships. Every time I hit my api routes to check the result I'm getting a null value in my sub_specializations. Here's the result already JSON formatted.
{
"data":[
{
"first_name":"Rusty",
"last_name":"Ferry",
"specializations":{
"specialization_id":11,
"specialization_name":"Endocrinology"
},
"sub_specializations":null
},
{
"first_name":"Nadia",
"last_name":"Ondricka",
"specializations":{
"specialization_id":22,
"specialization_name":"ENT"
},
"sub_specializations":null
},
{
"first_name":"Erich",
"last_name":"Torphy",
"specializations":{
"specialization_id":2,
"specialization_name":"Cardiologist"
},
"sub_specializations":null
}
]
}
Here are all my resources. This the DoctorsResource
public function toArray($request)
{
return [
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'specializations' => new SpecializationsResource($this->specializations),
'sub_specializations' => new SubSpecializationsResource($this->sub_specializations),
];
}
Specializations Resource
public function toArray($request)
{
return [
'specialization_id' => $this->specialization_id,
'specialization_name' => $this->specialization_name,
];
}
SubSpecializations
public function toArray($request)
{
return [
'sub_specialization_id' => $this->sub_specialization_id,
'sub_specialization_name' => $this->sub_specialization_name,
'doctors' => new DoctorsResource($this->doctors),
];
}
Lastly, this is the controller
protected $user;
public function __construct(Doctors $doctors){
$this->doctors = $doctors;
}
public function index()
{
$doctors = $this->doctors->with('specializations', 'subSpecializations')->get();
return DoctorsResource::collection($doctors);
}
The result that I'm expecting is similar to this
{
"data":[
{
"first_name":"Rusty",
"last_name":"Ferry",
"specializations":{
"specialization_id":11,
"specialization_name":"Endocrinology"
},
"sub_specializations": {
"sub_specialization_name":"value"
}
}
]
}
You have to make sure there is data of Sub Specializations for particular doctor.
If there is data then add that data to Doctor Resource otherwise it will be blank.
Just need to change line in doctor Resource like:
'sub_specializations' => $this->sub_specializations !== null ? new SubSpecializationsResource($this->sub_specializations) : '',
You can do same thing with specializations also.
My Post Model has the following format:
{
"id": 1,
"title": "Post Title",
"type: "sample"
}
Here is my controller method:
public function show($id) {
$post = App\Post::find($id);
$transformedPost = new PostResource($post);
return $transformedPost;
}
Here is how my PostResource looks:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->title,
'type' => $this->convertType($this->type),
];
}
public function convertType($type)
{
return ucfirst($type);
}
So in show/1 response, I should get:
{
"id": 1,
"name": "Post Title",
"type: "Sample"
}
Instead, I am getting:
{
"id": 1,
"title": "Post Title",
"type: "sample"
}
So my PostResource is clearly not working as expected. Key "title" is not being substituted by key "name".
What am I missing here? I know there could be possible duplication of this post but the solutions in other questions seem not working for me.
I am using Laravel 6.x.
//I'm trusting you want to use an Accessor.
//In your Post Model, try something like this
public function getTypeAttribute($value)
{
return ucfirst($value);
}
Your PostResource should now be
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->title,
'type' => $this->type
];
}
Short way;
PostResource;
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->title,
'type' => ucfirst($this->type)
];
}
To initialize my app I have the following route:
/initialize
This returns Taxonomies, Enumerables and a couple of other taxonomy like collections. This saves multiple HTTP requests.
Although with Dingo / Fractal, I cannot see how I can respond with multiple collections?
e.g.
return [
'taxonomies' => $this->response->collection($taxonomies, new TaxonomyTransformer);
'enumerables' => $this->response->collection($enumerables, new EnumerableTransformer);
'otherStuff' => $this->response->collection($otherStuff, new OtherStuffTransformer);
];
return response()->json([
'data' => [
'taxonomies' => $this->fractal->collection($taxonomies, new TaxonomyTransformer);
'enumerables' => $this->fractal->collection($enumerables, new EnumerableTransformer);
'otherStuff' => $this->fractal->collection($otherStuff, new OtherStuffTransformer);
]
], 200);
This should return the JSON in the format you are looking for.
I have the same issue ,and I found the solution from How to use Transformer in one to many relationship. #1054.
Here is the collection I want to return with the transfomer of dingo in my controller.
$user = User::where('email','=',$input['email'])->with('departments')->with('roles')->get();
DepartmentTransformer
class DepartmentTransformer extends TransformerAbstract
{
public function transform($department)
{
return [
'id' => $department['id'],
'name' => $department['name'],
'level' => $department['level'],
'parent_id' => $department['parent_id']
];
}
}
RolesTransformer
class RolesTransformer extends TransformerAbstract
{
public function transform($role)
{
return [
'name' => $role['name'],
'slug' => $role['slug'],
'description' => $role['description'],
'level' => $role['level']
];
}
}
UserTransformer
class UserTransformer extends TransformerAbstract
{
protected $defaultIncludes = ['departments','roles'];
public function transform($user)
{
return [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
'phone' => $user['phone'],
];
}
public function includeDepartments(User $user)
{
$dept = $user->departments;
return $this->collection($dept, new DepartmentTransformer());
}
public function includeRoles(User $user)
{
$rl = $user->roles;
return $this->collection($rl, new RolesTransformer());
}
}
In my controller
$user = User::where('email','=',$input['email'])->with('departments')->with('roles')->get();
return $this->response->collection($user, new UserTransformer());
And I got the result
"data": {
{
"id": 43,
"name": "test7",
"email": "test7#foxmail.com",
"phone": "186********",
"departments": {
"data": {
{
"id": 1,
"name": "业务一部",
"level": 1,
"parent_id": 0
}
}
},
"roles": {
"data": {
{
"name": "agent",
"slug": "agent",
"description": "业务员",
"level": 1
}
}
}
}
}
Please take note of the usage of $defaultIncludes and includeXXX() methonds in the UserTransformer.You can get more detail info from Fractal Doc.