Как построить вложенные ответы в приложении, основанном на Apigility, с использованием модели на основе ZfcBase-DbMapper?

Я разрабатываю веб-приложение 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 ?

подвопросы:

  1. [ DbMapper ] предоставляет некоторую «магию» для получения таких древовидных структур «автоматически» без написания JOIN s (или использования ORM)?

  2. Apigility ли [ 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 ( 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, если это решение соответствует «Согласие на соответствие» и, если нет, то, что является лучшим / «правильным» решением.

У меня нет опыта работы с db-mapper, но я думаю, что может ответить на вопрос 2 для вас.

Если ваш извлеченный ресурс проекта (массив) имеет ключевые images которые содержат объект типа Hal\Collection он автоматически извлекает эту коллекцию и отображает ее, как показано в примере Hal.

Эта «магия» происходит, потому что extractEmbeddedCollection вызывается в методе Hal.php в 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; 

то он должен работать (я не знаю, как еще объяснить это).