我正在开发一个RESTful Web应用程序 - Apigility驱动并基于Zend Framework 2。对于模型层,我正在使用ZfcBase
DbMapper
.该模型基本上由两个实体组成:Project
和Image
(1:n
),目前实现方式如下:
ProjectCollection extends Paginator
ProjectEntity
ProjectMapper extends AbstractDbMapper
ProjectService implements ServiceManagerAwareInterface
ProjectServiceFactory implements FactoryInterface
Image
相同的结构.
当请求资源(/projects[/:id]
)时,响应的项目实体应包含其Image
实体的列表。
那么,如何/应该如何实施这种1:n
结构呢?
子问题:
[
DbMapper
] 是否提供了一些"魔法"来"自动"检索此类树结构而无需写入JOIN
(或使用 ORM)?[
Apigility
] 是否为构建嵌套响应提供了一些"魔力"?
{
"_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 符合解决方案")。
我终于找到了解决方案。(再次感谢@poisa在GitHub上的解决方案建议。简而言之,这个想法是在水合步骤中使用嵌套(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上的帖子中已经说过的那样,从Apigility核心团队的某个人那里获得反馈会很棒,如果这个解决方案是"Apigility conform",如果不是,什么是更好/"正确"的解决方案。
使用db-mapper的经验,但我认为可以为您回答问题2。
如果提取的项目资源(数组)具有保存类型为 Hal'Collection
对象的键images
,则它将自动提取此集合并呈现它,如 Hal 示例中所示。
之所以出现这种"魔术",是因为extractEmbeddedCollection
在第 563 行的 renderEntity
方法Hal.php
中被调用。
编辑
你写你想要:
["images": {...}, {...}, {...}]
但你真正应该瞄准的是:
{
"id": "2",
"title": "...",
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/2"
}
},
"_embedded": {
"images": [
{...},
{...},
{...}
]
}
}
如何提取对象?您是否在元数据地图中注册了水化器?
您应该尝试返回如下内容:
use ZF'Hal'Collection
...
$images = new Collection($arrayOfImages);
$project['images'] = $images;
那么它应该可以工作(我不知道如何解释它)。