How do I override the REST serializer in Yii2? - php

I'm building a REST API with Yii2. Normally the request response looks something like this:
{
"items": [
{
"id": 1,
...
},
{
"id": 2,
...
},
...
],
"_links": {
"self": {
"href": "http://localhost/users?page=1"
},
"next": {
"href": "http://localhost/users?page=2"
},
"last": {
"href": "http://localhost/users?page=50"
}
},
"_meta": {
"totalCount": 1000,
"pageCount": 50,
"currentPage": 1,
"perPage": 20
}
}
I want to override the serializer so that the fields contained in the "_meta" array are instead included in the root of the array, i.e. the same level as "items" and "_links". How and where do I do that?
Thank you.

According to the documentation you create a new Serializer class. So, basically, you extend yii\rest\Serializer and rewrite the serialize() method. Then you set your custom serializer for your controller.
class MySerializer extends Serializer
{
public function serialize($data)
{
$d = parent::serialize($data);
$m = $d['_meta'];
unset($d['_meta']);
return array_merge($d, $m);
}
}
class MyController extends ActiveController
{
public $serializer = [
'class' => 'yii\rest\MySerializer',
'collectionEnvelope' => 'items',
];
}

Related

Laravel API Resource: How to return infinite nested array of categories with their children?

I use Laravel 6.x and below is my response JSON.
{
"data": [
{
"id": 1,
"name": "quam",
"parent_id": 0
},
{
"id": 2,
"name": "quia",
"parent_id": 1
},
{
"id": 3,
"name": "beatae",
"parent_id": 1
},
{
"id": 4,
"name": "aut",
"parent_id": 2
},
{
"id": 5,
"name": "provident",
"parent_id": 0
},
{
"id": 6,
"name": "voluptate",
"parent_id": 0
},
{
"id": 7,
"name": "vel",
"parent_id": 2
},
{
"id": 8,
"name": "sed",
"parent_id": 3
},
{
"id": 9,
"name": "voluptates",
"parent_id": 0
},
{
"id": 10,
"name": "adipisci",
"parent_id": 6
},
...
]
}
But it want to be like this:
{
"data": [
{
"id": 1,
"name": "quam",
"children": [
{
"id": 2,
"name": "quam"
"children":[
{
"id": 4,
"name": "aut"
},
{
"id": 7,
"name": "vel",
"children": [
...
]
}
]
},
{
"id": 3,
"name": "quam",
"children":[
{
"id": 8,
"name": "sed"
}
]
},
]
},
{
"id": 5,
"name": "provident"
},
{
"id": 6,
"name": "voluptate",
"children": [
{
"id": 10,
"name": "adipisci"
}
]
},
{
"id": 9,
"name": "voluptates"
},
...
}
In fact, I want to remove the parent_id attribute and add children array to each object that consists of other objects have this parent_id.
CategoryResource.php
class CategoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'parent_id' => $this->parent_id,
];
}
}
CategoryController.php
class CategoryController extends Controller
{
public function index()
{
return CategoryResource::collection(Category::all());
}
}
How can I implement this structure?
From what I see your problem is just the relations. To create a "tree resource" you have to load a lot of relations.
IMHO it's not a good choice, expecially if you have to load a lot of elements but generally, structures like these may be a dangerous bottleneck.
Anyway... The easy way it's the eager loading, so you have to add your base model with this attribute (have a look at the official documentation)
class Parent extends Model {
// [...]
/**
* The relationships that should always be loaded.
*
* #var array
*/
protected $with = ['children'];
// [...]
public function children() {
return $this->hasMany('whatever');
}
}
next you have to update your JSON Resource as follows (also for this, have a look at the official documentation about resource relationships).
class CategoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'parent_it' => $this->parent_id,
'childrend' => ChildrenResource::collection($this->whenLoaded('children')),
];
}
}
In this way, since everytime you request a Parent it will eager load its children, the resource will recursively map into a Child resource each relation down to the leaf.
Assuming Category is an eloquent model, Model's can reference themselves in relationships and those relationships can be recursive.
class Category extends Model
{
protected $hidden = ['parent_id'];
public function children()
{
return $this->hasMany('App\Category', 'parent_id')->with('children');
}
}
So now getting the structure you want is as simple as
Category::with('children:id,name,parent_id')->get('id', 'name', 'parent_id');
You have to include the parent_id in the select statement in order for the relationships to work but the $hidden variable I added to the model keeps parent_id from showing up in serialized results. The only caveat here is that all categories will have a children property, which will be empty for Categories that don't have children. So in your toArray method you will have to check for empty children[] and exclude them
First you need to define a relation for retrieving children of the main category which has no parents with this method
/**
* get sub product categories of this category
*
* #return mixed
*/
public function childCategories()
{
return $this->hasMany(Category::class,'parent_id');
}
Then you need to load the children of children categories with this method :
/**
* get recursive all sub categories of this category.
*
* #return mixed
*/
public function childrenCategories()
{
return $this->hasMany(Category::class,'parent_id')->with('childCategories');
}
Now you can retrieve them with this static function
/**
* get all Main categories with children.
*
* #return mixed
*/
public static function allMainCategoriesWithChildren()
{
return self::whereNull('parent_id')->with('childrenCategories')->get();
}
now you can use it in your resource or return it in controller directly
use App\Category;
return Category::allMainCategoriesWithChildren();

How to populate META and LINKS pagination on laravel?

I need help to populate this kind of pagination on laravel eloquent.
{
"meta": {
"count": 10,
"total": 100
},
"links": {
"first": "http://localhost/page[limit]=10&page[offset]=0",
"last": "http://localhost/page[limit]=10&page[offset]=10",
"next": "http://localhost/page[limit]=10&page[offset]=10",
"prev": "null"
},
"data": [
{
"type": "checklists",
"id": "1"
}
]
}
I have tried this code on Laravel Eloquent.
$data = Model::select('type','id')->paginate(10);
return response()->json(
[
'data' => $data
],200
);
But it shows different format, there is no META and LINKS schema on data populated.
{
"data": {
"current_page": 1,
"data": [
{
"type": "Mechanical Equipment Sales Representative",
"id": 1
}
],
"first_page_url": "http://localhost?page=1",
"from": 1,
"last_page": 4,
"last_page_url": "http://localhost?page=4",
"next_page_url": "http://localhost?page=2",
"path": "http://localhost",
"per_page": 10,
"prev_page_url": null,
"to": 10,
"total": 39
}
}
How to do that? Please help?
You can use API resources: https://laravel.com/docs/eloquent-resources#pagination
Create a collection resource:
php artisan make:resource ModelCollection
Use it in your controller:
$data = Model::select('type','id')->paginate(10);
return new ModelCollection($data);
For Lumen, create a app/Http/Resources/ModelCollection.php file:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ModelCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}

Hide attributes relationship from laravel eloquent resources collection

I'm trying to building API Resource and I wanna hide in collection the relationship attribute.
For example, I want to hide attribute 'permissions' only in RoleCollection. I mean I just only wanna hide this attribute in Collection, not Resource. Because Collection be called from Resource but I don't want to hide it in Resource.
Role.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class Role extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at->format('Y-m-d H:i:s'),
'permissions' => Permission::collection($this->permissions),
];
}
}
RoleCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class RoleCollection extends ResourceCollection
{
public function toArray($request)
{
return parent::toArray($request);
}
}
RoleController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;
use App\Http\Resources\Role as RoleResource;
use App\Http\Resources\RoleCollection;
class RoleController extends Controller
{
public function index()
{
$resource = Role::paginate();
return new RoleCollection($resource);
}
public function show($id)
{
$resource = Role::with('permissions')->find($id);
return new RoleResource($resource);
}
}
Response from: api/role/1
{
"data": {
"id": 1,
"name": "Super Administrador",
"created_at": "2019-05-07 16:45:38",
"updated_at": "2019-05-07 16:45:38",
"permissions": [
{
"id": 1,
"name": "user.list"
},
{
"id": 2,
"name": "user.view"
},
{
"id": 3,
"name": "user.save"
},
{
"id": 4,
"name": "user.delete"
}
]
}
}
Response from : /api/roles
{
"data": [
{
"id": 1,
"name": "Super Administrador",
"created_at": "2019-05-07 16:45:38",
"updated_at": "2019-05-07 16:45:38",
"permissions": [
{
"id": 1,
"name": "user.list"
},
{
"id": 2,
"name": "user.view"
},
{
"id": 3,
"name": "user.save"
},
{
"id": 4,
"name": "user.delete"
}
]
},
{
"id": 2,
"name": "Administrador",
"created_at": "2019-05-07 16:45:38",
"updated_at": "2019-05-07 16:45:38",
"permissions": []
}
],
"links": {
"first": "http://127.0.0.1:32773/api/roles?page=1",
"last": "http://127.0.0.1:32773/api/roles?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://127.0.0.1:32773/api/roles",
"per_page": 15,
"to": 2,
"total": 2
}
}
One way to do this is to use the whenLoaded method. The whenLoaded will return a MissingValue instance when a relationship has not been loaded. Laravel in turn will exclude this property from your resonse.
It is not only useful for hiding properties in certain responses, but also helps with performance. Currently your resource will do a query to fetch Permission models for every Role when this relationship was not loaded.
Your resource could look like:
return [
...
'permissions' => Permission::collection($this->whenLoaded('permissions')),
];
Laravel docs
One of the best way to handle this stuff is to use Fractals.
You can define a Transformer class for all your model, handle whether to include relationships or not every time you call them, and you can also define what attributes to show or to hide.
Basically you have one and only one point where your model can be serialized in JSON and you put all your logic there.
Also you can JSON-ize whole collections, through the single model transformers, very handy!

Duplicated value in my JSON response in Laravel

I created method show() in controller to return name and timestamps + all permissions this role has. Spatie/Laravel-Permission is responsible for associating users with permissions and roles in my API.
public function show($name)
{
$role = Role::findByName($name);
return response()->json([
$role,
$role->permissions
]);
}
Output:
[
{
"name": "root",
"created_at": "2019-04-08 19:41:49",
"updated_at": "2019-04-08 19:41:49",
"permissions": [
{
"name": "users.store"
},
{
"name": "users.destroy"
},
{
"name": "users.show.id"
},
{
"name": "users.update.id"
},
{
"name": "users.show.name"
},
{
"name": "users.update.name"
}
]
},
[
// This is duplicate
{
"name": "users.store"
},
{
"name": "users.destroy"
},
{
"name": "users.show.id"
},
{
"name": "users.update.id"
},
{
"name": "users.show.name"
},
{
"name": "users.update.name"
}
]
]
As you can see there is useless duplicate of permissions.
But if I will remove $role from my response
return response()->json([
// $role,
$role->permissions
]);
Everything is fine
[
[
{
"name": "users.store"
},
{
"name": "users.destroy"
},
{
"name": "users.show.id"
},
{
"name": "users.update.id"
},
{
"name": "users.show.name"
},
{
"name": "users.update.name"
}
]
]
And when I want to return only $role without permissions like this:
return response()->json([
$role,
// $role->permissions
]);
My output is:
[
{
"name": "root",
"created_at": "2019-04-08 19:41:49",
"updated_at": "2019-04-08 19:41:49"
}
]
When you call $role->permissions in your response, permissions are loaded into the $role object, and then serialized to json. So basically you're sending $role object with loaded permissions, and you're also sending permissions themselves.
Try:
public function show($name)
{
$role = Role::findByName($name)->load('permissions');
return response()->json([
$role
]);
}
you can use Eloquent: API Resources here
public function show($name)
{
return new RoleResource(Role::findByName($name));
}
and RoleResource like this:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class RoleResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'permissions' => $this-> permissions,
];
}
}

How to build nested responses in an Apigility driven application with a ZfcBase-DbMapper based model?

I'm developing a RESTful web application -- Apigility driven and based on the Zend Framework 2. For the model layer I'm using the ZfcBase DbMapper. The model essentially consists of two entities: Project and Image (1:n) and is currently implemented like this:
ProjectCollection extends Paginator
ProjectEntity
ProjectMapper extends AbstractDbMapper
ProjectService implements ServiceManagerAwareInterface
ProjectServiceFactory implements FactoryInterface
The same structure for Image.
When the resource (/projects[/:id]) is requested, the responsed project entity/entities should contain a list of its/their Image entities.
So, how can/should this 1:n structure be implemented?
Subquestions:
Does [DbMapper] provide some "magic" for retrieving such tree structures "automatically" without to write JOINs (or use an ORM)?
Does [Apigility] provide some "magic" for building nested responses?
{
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects?page=1"
},
"first": {
"href": "http://myproject-api.misc.loc/projects"
},
"last": {
"href": "http://myproject-api.misc.loc/projects?page=1"
}
},
"_embedded": {
"projects": [
{
"id": "1",
"title": "project_1",
"images": [
{
"id": "1",
"title": "image_1"
},
{
"id": "2",
"title": "image_2"
}
],
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/1"
}
}
},
{
"id": "2",
"title": "project_2",
"images": [
{
"id": "3",
"title": "image_3"
},
{
"id": "4",
"title": "image_4"
}
],
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/1"
}
}
}
]
},
"page_count": 1,
"page_size": 25,
"total_items": 1
}
EDIT
The output I'm currentliy getting is:
/projects/:id
{
"id": "1",
"title": "...",
...
"_embedded": {
"images": [
{
"id": "1",
"project_id": "1",
"title": "...",
...
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/images/1"
}
}
},
{
"id": "2",
"project_id": "1",
"title": "...",
...
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/images/2"
}
}
},
{
"id": "3",
"project_id": "1",
"title": "...",
...
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/images/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/1"
}
}
}
So it works for one single object. But not for collections, where single items include futher collections:
/projects
{
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects?page=1"
},
"first": {
"href": "http://myproject-api.misc.loc/projects"
},
"last": {
"href": "http://myproject-api.misc.loc/projects?page=24"
},
"next": {
"href": "http://myproject-api.misc.loc/projects?page=2"
}
},
"_embedded": {
"projects": [
{
"id": "1",
"title": "...",
... <-- HERE I WANT TO GET ["images": {...}, {...}, {...}]
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/1"
}
}
},
{
"id": "2",
"title": "...",
... <-- HERE I WANT TO GET ["images": {...}, {...}, {...}]
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/2"
}
}
},
{
"id": "3",
"title": "...",
... <-- HERE I WANT TO GET ["images": {...}, {...}, {...}]
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/3"
}
}
}
]
},
"page_count": 24,
"page_size": 3,
"total_items": 72
}
EDIT
I edited my code and made a step to the goal.
It could not work, since my ProjectService#getProjects() was just returning the projects' data from the database, not enriched with the images:
public function getProjects() {
return $this->getMapper()->findAll();
}
edited to:
public function getProjects() {
$projects = $this->getMapper()->findAll();
foreach ($projects as $key => $project) {
$images = $this->getImageService()->getImagesForProject($project['id']);
$projects[$key]['images'] = $images;
}
return $projects;
}
and the ProjectMapper#findAll()
public function findAll() {
$select = $this->getSelect();
$adapter = $this->getDbAdapter();
$paginatorAdapter = new DbSelect($select, $adapter);
$collection = new ProjectCollection($paginatorAdapter);
return $collection;
}
edited to:
public function findAll() {
$select = $this->getSelect();
$adapter = $this->getDbAdapter();
$paginatorAdapter = new DbSelect($select, $adapter);
// #todo Replace the constants with data from the config and request.
$projects = $paginatorAdapter->getItems(0, 2);
$projects = $projects->toArray();
return $projects;
}
Now I get the wished output:
{
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects"
}
},
"_embedded": {
"projects": [
{
"id": "1",
"title": "...",
...
"_embedded": {
"images": [
{
"id": "1",
"project_id": "1",
"title": "...",
...
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/images/1"
}
}
},
{
...
},
{
...
}
]
},
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/1"
}
}
},
{
"id": "2",
"title": "...",
...
"_embedded": {
"images": [
...
]
},
...
}
]
},
"total_items": 2
}
But it's a little bit crappy solution, isn't it? What I'm actually doing, is: I'm just replacing a part of the Apigility data retrieving functionality... Anyway, I don't like this solution and want to find a better one (an "Apigility conform solution").
I have finally found a solution. (Thanks once again # poisa for his solution suggestion on GitHub.) In short, the idea is to enrich the (projects) list items with nested (image) items lists on the hydration step. I actually don't really like this way, since it's too much model logic on the hydration level for me. But it works. Here we go:
/module/Portfolio/config/module.config.php
return array(
...
'zf-hal' => array(
'metadata_map' => array(
...
'Portfolio\\V2\\Rest\\Project\\ProjectEntity' => array(
'entity_identifier_name' => 'id',
'route_name' => 'portfolio.rest.project',
'route_identifier_name' => 'id',
'hydrator' => 'Portfolio\\V2\\Rest\\Project\\ProjectHydrator',
),
'Portfolio\\V2\\Rest\\Project\\ProjectCollection' => array(
'entity_identifier_name' => 'id',
'route_name' => 'portfolio.rest.project',
'route_identifier_name' => 'id',
'is_collection' => true,
),
...
),
),
);
Portfolio\Module
class Module implements ApigilityProviderInterface {
...
public function getHydratorConfig() {
return array(
'factories' => array(
// V2
'Portfolio\\V2\\Rest\\Project\\ProjectHydrator' => function(ServiceManager $serviceManager) {
$projectHydrator = new ProjectHydrator();
$projectHydrator->setImageService($serviceManager->getServiceLocator()->get('Portfolio\V2\Rest\ImageService'));
return $projectHydrator;
}
),
);
}
...
}
Portfolio\V2\Rest\Project\ProjectHydrator
namespace Portfolio\V2\Rest\Project;
use Zend\Stdlib\Hydrator\ClassMethods;
use Portfolio\V2\Rest\Image\ImageService;
class ProjectHydrator extends ClassMethods {
/**
* #var ImageService
*/
protected $imageService;
/**
* #return ImageService the $imageService
*/
public function getImageService() {
return $this->imageService;
}
/**
* #param ImageService $imageService
*/
public function setImageService(ImageService $imageService) {
$this->imageService = $imageService;
return $this;
}
/*
* Doesn't need to be implemented:
* the ClassMethods#hydrate(...) handle the $data already as wished.
*/
/*
public function hydrate(array $data, $object) {
$object = parent::hydrate($data, $object);
if ($object->getId() !== null) {
$images = $this->imageService->getImagesForProject($object->getId());
$object->setImages($images);
}
return $object;
}
*/
/**
* #see \Zend\Stdlib\Hydrator\ClassMethods::extract()
*/
public function extract($object) {
$array = parent::extract($object);
if ($array['id'] !== null) {
$images = $this->imageService->getImagesForProject($array['id']);
$array['images'] = $images;
}
return $array;
}
}
Portfolio\V2\Rest\Project\ProjectMapperFactory
namespace Portfolio\V2\Rest\Project;
use Zend\ServiceManager\ServiceLocatorInterface;
class ProjectMapperFactory {
public function __invoke(ServiceLocatorInterface $serviceManager) {
$mapper = new ProjectMapper();
$mapper->setDbAdapter($serviceManager->get('PortfolioDbAdapter_V2'));
$mapper->setEntityPrototype($serviceManager->get('Portfolio\V2\Rest\Project\ProjectEntity'));
$projectHydrator = $serviceManager->get('HydratorManager')->get('Portfolio\\V2\\Rest\\Project\\ProjectHydrator');
$mapper->setHydrator($projectHydrator);
return $mapper;
}
}
Portfolio\V2\Rest\Project\ProjectMapper
namespace Portfolio\V2\Rest\Project;
use ZfcBase\Mapper\AbstractDbMapper;
use Zend\Paginator\Adapter\DbSelect;
use Zend\Db\ResultSet\HydratingResultSet;
class ProjectMapper extends AbstractDbMapper {
...
/**
* Provides a collection of all the available projects.
*
* #return \Portfolio\V2\Rest\Project\ProjectCollection
*/
public function findAll() {
$resultSetPrototype = new HydratingResultSet(
$this->getHydrator(),
$this->getEntityPrototype()
);
$select = $this->getSelect();
$adapter = $this->getDbAdapter();
$paginatorAdapter = new DbSelect($select, $adapter, $resultSetPrototype);
$collection = new ProjectCollection($paginatorAdapter);
return $collection;
}
/**
* Provides a project by ID.
*
* #param int $id
* #return \Portfolio\V2\Rest\Project\ProjectEntity
*/
public function findById($id) {
$select = $this->getSelect();
$select->where(array(
'id' => $id,
));
$entity = $this->select($select)->current();
return $entity;
}
...
}
As I already said in my post on GitHub, it would be great to get a feedback from someone from the Apigility core team, wheter this solution is "Apigility conform" and, if not, what is a better/"correct" solution.
I have no experience with db-mapper, but I think can answer question 2 for you.
If your extracted project resource (an array) has a key images that holds an object of type Hal\Collection it will automatically extract this collection and render it as you show in your Hal example.
This "magic" happens because extractEmbeddedCollection is called in the renderEntity method in Hal.php on line 563.
EDIT
You write that you want:
["images": {...}, {...}, {...}]
But what you should actually aim for is this:
{
"id": "2",
"title": "...",
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/2"
}
},
"_embedded": {
"images": [
{...},
{...},
{...}
]
}
}
How do you extract your objects? Did you register a hydrator in your metadata map?
You should try to return something like this:
use ZF\Hal\Collection
...
$images = new Collection($arrayOfImages);
$project['images'] = $images;
then it should work (I don't know how else to explain it).

Categories