如何使用基于ZfcBase-DbMapper的模型在Apigility驱动的应用程序中构建嵌套响应? [英] How to build nested responses in an Apigility driven application with a ZfcBase-DbMapper based model?
问题描述
我正在开发RESTful Web应用程序- Apigility 驱动并基于 ZfcBase
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
与Image
相同的结构.
当请求资源(/projects[/:id]
)时,响应的项目实体应包含其/c3实体的列表.
When the resource (/projects[/:id]
) is requested, the responsed project entity/entities should contain a list of its/their Image
entities.
那么,如何/应该实现这种1:n
结构?
So, how can/should this 1:n
structure be implemented?
子问题:
-
[
DbMapper
]是否提供一些魔术"来自动"检索此类树结构而无需编写JOIN
(或使用ORM)?
Does [
DbMapper
] provide some "magic" for retrieving such tree structures "automatically" without to writeJOIN
s (or use an ORM)?
[Apigility
]是否为构建嵌套响应提供了一些魔术"?
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
}
编辑
我当前得到的输出是:
/projects/:id
/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
/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
}
编辑
我编辑了代码,并朝着目标迈出了一步.
I edited my code and made a step to the goal.
这不起作用,因为我的 ProjectService#getProjects()
只是从数据库中返回项目的数据,而没有充实图像:
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();
}
修改为:
public function getProjects() {
$projects = $this->getMapper()->findAll();
foreach ($projects as $key => $project) {
$images = $this->getImageService()->getImagesForProject($project['id']);
$projects[$key]['images'] = $images;
}
return $projects;
}
和 ProjectMapper#findAll()
and the ProjectMapper#findAll()
public function findAll() {
$select = $this->getSelect();
$adapter = $this->getDbAdapter();
$paginatorAdapter = new DbSelect($select, $adapter);
$collection = new ProjectCollection($paginatorAdapter);
return $collection;
}
修改为:
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
}
但这是一个糟糕的解决方案,不是吗?我实际上正在做的是:我只是替换了Apigility数据检索功能的一部分...无论如何,我不喜欢这种解决方案,而是想找到一个更好的解决方案("Apigility兼容解决方案").
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").
推荐答案
我终于找到了解决方案. (再次感谢@ poisa 为他的
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
/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
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
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
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
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;
}
...
}
正如我在GitHub上的帖子中已经说过的,从Apigility核心团队得到某人的反馈非常好,而该解决方案是"Apigility兼容"的,如果不是,那么什么是更好/正确的"解决方案.
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.
这篇关于如何使用基于ZfcBase-DbMapper的模型在Apigility驱动的应用程序中构建嵌套响应?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!