Я разрабатываю веб-приложение 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
ли [ 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;
то он должен работать (я не знаю, как еще объяснить это).