Как построить вложенные ответы в приложении, управляемом 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] предоставить некоторую «магию» для извлечения таких древовидных структур «автоматически» без записи JOINs (или использовать ORM)?

  2. Делает [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»).

1

Решение

Я наконец нашел решение. (Еще раз спасибо @ 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 конформ» и, если нет, то какое решение лучше / «правильнее».

1

Другие решения

У меня нет опыта работы с 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;

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

0

По вопросам рекламы [email protected]