如何使用基于 ZfcBase-DbMapper 的模型在 Apigility 驱动的应用程序中构建嵌套响应?
How to build nested responses in an Apigility driven application with a ZfcBase-DbMapper based model?
我正在开发 RESTful 网络应用程序 -- Apigility driven and based on the Zend Framework 2. For the model layer I'm using the ZfcBase
DbMapper
。该模型基本上由两个实体组成:Project
和 Image
(1:n
),目前是这样实现的:
ProjectCollection extends Paginator
ProjectEntity
ProjectMapper extends AbstractDbMapper
ProjectService implements ServiceManagerAwareInterface
ProjectServiceFactory implements FactoryInterface
Image
的相同结构。
请求资源 (/projects[/:id]
) 时,响应的项目 entity/entities 应包含 its/their Image
个实体的列表。
那么,can/should这个1:n
结构是如何实现的呢?
子问题:
是否 [DbMapper
] 提供一些 "magic" 来检索这样的树结构 "automatically" 而无需编写 JOIN
s(或使用 ORM) ?
[Apigility
] 是否提供一些 "magic" 用于构建嵌套响应?
{
"_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
{
"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"
}
}
}
所以它适用于一个单一的对象。但不适用于集合,其中单个项目包含更多集合:
/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
}
编辑
我编辑了我的代码并向目标迈出了一步。
它无法工作,因为我的 ProjectService#getProjects()
只是从数据库返回项目数据,没有用图像丰富:
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()
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;
}
现在我得到了想要的输出:
{
"_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 conform solution")。
我没有使用 db-mapper 的经验,但我认为可以为您回答问题 2。
如果您提取的项目资源(一个数组)有一个键 images
保存一个 Hal\Collection
类型的对象,它会自动提取这个集合并按照您在 Hal 示例中显示的那样呈现它。
发生这个"magic"是因为在Hal.php
on line 563.
的renderEntity
方法中调用了extractEmbeddedCollection
编辑
你写你想要的:
["images": {...}, {...}, {...}]
但你真正应该瞄准的是:
{
"id": "2",
"title": "...",
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/2"
}
},
"_embedded": {
"images": [
{...},
{...},
{...}
]
}
}
你如何提取你的对象?您是否在元数据地图中注册了水龙头?
你应该尝试 return 这样的事情:
use ZF\Hal\Collection
...
$images = new Collection($arrayOfImages);
$project['images'] = $images;
那么它应该可以工作(我不知道还能怎么解释)。
我终于找到了解决办法。 (再次感谢GitHub上的@poisa for his solution suggestion。)简而言之,想法是用嵌套的(image
)项目列表丰富(projects
)列表项目水化步。我实际上不太喜欢这种方式,因为它对我来说在水化水平上的模型逻辑太多了。但它有效。我们开始吧:
/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;
}
...
}
正如我在 GitHub 上的 post 中所说的那样,如果这个解决方案是 "Apigility conform",那么从 Apigility 核心团队的某个人那里得到反馈会很棒,而且,如果不是,什么是更好的/"correct" 解决方案。
我正在开发 RESTful 网络应用程序 -- Apigility driven and based on the Zend Framework 2. For the model layer I'm using the ZfcBase
DbMapper
。该模型基本上由两个实体组成:Project
和 Image
(1:n
),目前是这样实现的:
ProjectCollection extends Paginator
ProjectEntity
ProjectMapper extends AbstractDbMapper
ProjectService implements ServiceManagerAwareInterface
ProjectServiceFactory implements FactoryInterface
Image
的相同结构。
请求资源 (/projects[/:id]
) 时,响应的项目 entity/entities 应包含 its/their Image
个实体的列表。
那么,can/should这个1:n
结构是如何实现的呢?
子问题:
是否 [
DbMapper
] 提供一些 "magic" 来检索这样的树结构 "automatically" 而无需编写JOIN
s(或使用 ORM) ?[
Apigility
] 是否提供一些 "magic" 用于构建嵌套响应?
{
"_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
{
"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"
}
}
}
所以它适用于一个单一的对象。但不适用于集合,其中单个项目包含更多集合:
/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
}
编辑
我编辑了我的代码并向目标迈出了一步。
它无法工作,因为我的 ProjectService#getProjects()
只是从数据库返回项目数据,没有用图像丰富:
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()
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;
}
现在我得到了想要的输出:
{
"_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 conform solution")。
我没有使用 db-mapper 的经验,但我认为可以为您回答问题 2。
如果您提取的项目资源(一个数组)有一个键 images
保存一个 Hal\Collection
类型的对象,它会自动提取这个集合并按照您在 Hal 示例中显示的那样呈现它。
发生这个"magic"是因为在Hal.php
on line 563.
renderEntity
方法中调用了extractEmbeddedCollection
编辑
你写你想要的:
["images": {...}, {...}, {...}]
但你真正应该瞄准的是:
{
"id": "2",
"title": "...",
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/2"
}
},
"_embedded": {
"images": [
{...},
{...},
{...}
]
}
}
你如何提取你的对象?您是否在元数据地图中注册了水龙头?
你应该尝试 return 这样的事情:
use ZF\Hal\Collection
...
$images = new Collection($arrayOfImages);
$project['images'] = $images;
那么它应该可以工作(我不知道还能怎么解释)。
我终于找到了解决办法。 (再次感谢GitHub上的@poisa for his solution suggestion。)简而言之,想法是用嵌套的(image
)项目列表丰富(projects
)列表项目水化步。我实际上不太喜欢这种方式,因为它对我来说在水化水平上的模型逻辑太多了。但它有效。我们开始吧:
/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;
}
...
}
正如我在 GitHub 上的 post 中所说的那样,如果这个解决方案是 "Apigility conform",那么从 Apigility 核心团队的某个人那里得到反馈会很棒,而且,如果不是,什么是更好的/"correct" 解决方案。