Я разрабатываю веб-приложение RESTful — 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
s (или использовать 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.) Короче, идея состоит в том, чтобы обогатить (projects
) элементы списка с вложенными (image
) списки предметов на этапе гидратации. Мне на самом деле не очень нравится этот путь, так как для меня слишком много модельной логики на уровне гидратации. Но это работает. Вот так:
/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 конформ» и, если нет, то какое решение лучше / «правильнее».
У меня нет опыта работы с db-mapper, но я думаю, что могу ответить на вопрос 2 для тебя.
Если ваш извлеченный ресурс проекта (массив) имеет ключ images
который содержит объект типа Hal\Collection
он автоматически извлечет эту коллекцию и отобразит ее, как показано в примере с Hal.
Это «волшебство» происходит потому, что extractEmbeddedCollection
называется в renderEntity
метод в Hal.php
по линии 563.
Вы пишете, что вы хотите:
["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;
тогда это должно работать (я не знаю, как еще это объяснить).