Laravel Dingo API - How to respond with multiple collections / transformers? - php

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.

Related

Whenever I use Laravel resource with paginate method, I get a response with "links" and "meta" keys. I want to change these keys to a new format

I used laravel resources for my api responses and added paginate method. Upon using paginate method I always get a result like this where laravel by default gives three keys namely "data", "links" and "meta". But I want to change the resource to my own need.
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28#example.com",
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort#example.com",
}
],
"links":{
"first": "http://example.com/pagination?page=1",
"last": "http://example.com/pagination?page=1",
"prev": null,
"next": null
},
"meta":{
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/pagination",
"per_page": 15,
"to": 10,
"total": 10
}
}
But I want a result like this
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28#example.com",
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort#example.com",
}
],
"metadata": {
"pagination": {
"offset": 50,
"limit": 25,
"previousOffset": 25,
"nextOffset": 75,
"currentPage": 3,
"pageCount": 40,
"totalCount": 1000
}
}
}
How can I be able to achieve this. I am using Laravel 7.*
My controller code:
public function index(Request $request)
{
return DiscussionResource::collection($this->discussion->getDiscussionList($request));
}
My Model method looks like this:
public function getDiscussionList($request){
return $this->ofSearch($request)
->orderBy('created_at', config('settings.pagination.order_by'))
->paginate(config('settings.pagination.per_page'));
}
My resource looks like this:
class DiscussionResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'question_id' => $this->question_id,
'user_id' => $this->user_id,
'user_image' => $this->user->userProfile->image,
'user_role' => $this->user->type,
'comment' => $this->comment,
'is_pinned' => $this->is_pinned,
'created_at' => $this->created_at->toDateString()
];
}
}
There are so many ways to do that in laravel and here you go 2 ways of it:
First Way: you can create a custom PaginatedResourceResponse and override the default paginationLinks. for example like below:
use Illuminate\Http\Resources\Json\PaginatedResourceResponse;
class CustomPaginatedResourceResponse extends PaginatedResourceResponse
{
protected function paginationLinks($paginated)
{
return [
'prev' => $paginated['prev_page_url'] ?? null,
'next' => $paginated['next_page_url'] ?? null,
];
}
protected function meta($paginated)
{
$metaData = parent::meta($paginated);
return [
'current_page' => $metaData['current_page'] ?? null,
'total_items' => $metaData['total'] ?? null,
'per_page' => $metaData['per_page'] ?? null,
'total_pages' => $metaData['total'] ?? null,
];
}
}
then override toResponse method(Actually, the toResponse method converts resource collection to responses)
public function toResponse($request)
{
return $this->resource instanceof AbstractPaginator
? (new CustomPaginatedResourceResponse($this))->toResponse($request)
: parent::toResponse($request);
}
You may consider overriding other methods if you want to customize your response furthermore.
Second Way: you can just override the toResponse method in the ResourceCollection and make it as you wish!
More Ways HERE!
If you want to customize the metadata you can take the help of the with() method that comes with laravel for collections.
// in DiscussionResource file
public function with($request)
{
return [
'meta' => [
'key' => 'value',
],
];
}
If you want to customize it from controller you may do something like this
return (DiscussionResource::collection($this->discussion->getDiscussionList($request)))
->additional(['meta' => [
'key' => 'value',
]]);
And in case you want it for a single resource you can modify it at the toArray() method
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
For more details you can check this out https://laravel.com/docs/7.x/eloquent-resources#adding-meta-data

Conditionaly Laravel Resource

TicketResource.php
public function toArray($request) {
return [
'id' => $this->id,
'user_id' => $this->user_id,
'title' => $this->title,
'body' => $this->body,
'status' => $this->status,
'created_at' => $this->created_at->toDateTimeString(),
];
}
CommentResource.php
public function toArray($request) {
return [
'id' => $this->id,
'body' => $this->body,
'user_id' => $this->user_id,
'created_at' => $this->created_at->toDateTimeString()
];
}
TicketController.php
public function index() {
return TicketResource::collection(Ticket::all());
}
public function show(Ticket $id) {
$ticket = $id;
return new TicketResource($ticket);
}
Model Ticket.php
public function comments() {
return $this->hasMany('App\Comment');
}
Model Comment.php
public function ticket() {
return $this->belongsTo('App\Ticket');
}
routes/api.php
Route::get('tickets', 'TicketController#index');
Route::get('tickets/{id}', 'TicketController#show');
I want when I request to tickets/{id} URL, I expect to receive this response:
{
"data": {
"id": 1,
"user_id": 2,
"title": "lorem",
"body": "epsum",
"status": "open",
"created_at": "2020-03-04 18:14:56",
"comments": [
{
"id": 1,
"body": "equi",
"user_id": 1,
"created_at": "2020-03-05 18:14:56",
}
]
}
}
On the contrary, when I visit tickets URL, I don't want the comments to be added on each ticket.
How can I implement that?
You need to add relation
This is my model class:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
public function children()
{
return $this->hasMany(Category::class, 'parent_id', 'id')->select('categories.id',
'categories.cid AS key',
'categories.name AS label',
'categories.type',
'categories.lvl');
}
}
in my controller:
$parents = Category::select(
'categories.id',
'categories.id AS key',
'categories.name AS label')->where('lvl', 1)->get();
foreach ($parents as $item) {
$item->children
}
return Response::json($parents, 200, array('Content-Type' => 'application/json;charset=utf8'), JSON_UNESCAPED_UNICODE);
Result:
[
{
"id":2,
"key":2,
"label":"parent label",
"children":[
{
"id":17,
"key":"92697f63-5c50-11ea-80df-5cf9ddf839d3",
"label":"child label",
"type":"Category",
"lvl":2,
}
]
}
]

Override response of Rest authentication(HttpBearerAuth) in yii2

I have token based authorization, for which i have did below changes.
In User model, override findIdentityByAccessToken() method as below.
public static function findIdentityByAccessToken($token, $type = null)
{
$userlogin = Userdevices::find()->where(['access_token' => $token])->one();
if ($userlogin == array()) {
return null;
} else {
$User = Users::findOne(['id' => $userlogin->user_id]);
if (!count($User))
{
return null;
}
else {
$dbUser = [
'id' => $User->id,
];
return new static($dbUser);
}
}
}
In Controller, I add behaviors() as below.
public function behaviors()
{
$behaviors[] = [
'class' => \yii\filters\ContentNegotiator::className(),
'formats' => [
'application/json' => \yii\web\Response::FORMAT_JSON,
],
];
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::className(),
];
return $behaviors;
}
When API does not get token or token is not valid it gives below response
{
"name": "Unauthorized",
"message": "You are requesting with an invalid credential.",
"code": 0,
"status": 401,
"type": "yii\\web\\UnauthorizedHttpException"
}
I want to change response as per my requirement as below.
{
"code": 401,
"name": "Unauthorized",
"is_logout": "Y",
"status": "error",
"message": "logout"
}
You can change format of response using beforeSend event of yii\web\Response.
For example add following methods in your api controller:
public function init()
{
parent::init();
\Yii::$app->response->on(
\yii\web\Response::EVENT_BEFORE_SEND,
[$this, 'beforeResponseSend']
);
}
public function beforeResponseSend(\yii\base\Event $event)
{
/**
* #var \yii\web\Response $response
*/
$response = $event->sender;
if ($response->data['status'] == 401) {
$response->data = [
'code' => 401,
'name' => 'Unauthorized',
'is_logout' => 'Y',
'status' => 'error',
'message' => 'logout',
];
}
}
The init method of controller registers the beforeSend event. The beforeResponseSend method handles the event and changes the response format.
If you want to format response in multiple controller it might be better to put the event handler into own class for example
namespace app\components;
class ErrorResponseHelper
{
public static function beforeResponseSend(Event $event)
{
// ... formating code ...
}
}
And register the event in config/web.php
return [
// ...
'components' => [
'response' => [
'class' => 'yii\web\Response',
'on beforeSend' => [
\app\components\ErrorResponseHelper::class,
'beforeResponseSend',
],
],
],
];
But be careful with this solution because this way the \app\components\ErrorResponseHelper::beforeResponseSend will be called during each request.

Laravel API Resource doesn't work in Controller Method

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)
];
}

Laravel transformer collection error

I'm using fractal in Laravel 5.2. I'm using a transformer on a collection like this:
public function allFromCompany()
{
$users = UserModel::all();
return $this->response->collection($users, new UserTransformer);
}
UserTransformer
class UserTransformer extends Fractal\TransformerAbstract
{
public function transform(UserModel $user)
{
return [
'user' => [
'id' => $user->id,
'role' =>
[
'role_id' => $user->role_id,
'name' => $user->role->name
],
'company' =>
[
'company_id' => $user->company_id,
'company' => $user->company->name,
],
'active' => $user->active,
'name' => $user->name,
'lastname' => $user->lastname,
'address' => $user->address,
'zip' => $user->zip,
'email' => $user->email
]
];
}
}
But when I do it like that I receive an error:
{
"status_code": 500,
"debug": {
"line": 10,
"file": "/home/vagrant/Code/forum/app/Src/v1/User/UserTransformer.php",
"class": "Symfony\\Component\\Debug\\Exception\\FatalThrowableError",
"trace": [
"#0 /home/vagrant/Code/forum/vendor/league/fractal/src/Scope.php(338): Src\\v1\\User\\UserTransformer->transform(Object(Src\\v1\\User\\User))",
When I try this with one item:
return $this->response->item($user, new UserTransformer);
It works.
It's pretty old question, and I ran into it :) but if anyone has similar problem, maybe you've forgot to "use Helpers" something like
class UserController extends Controller
{
use Helpers;
public function index()
{
$users = User::all();
return $this->collection($users, new UsersTransformer);
}

Categories