php - library - ¿Cómo construir respuestas anidadas en una aplicación impulsada por Apigility con un modelo basado en ZfcBase-DbMapper?
zend framework 3 español (2)
Finalmente encontré una solución. (Gracias una vez más @ poisa por su sugerencia de solución en GitHub.) En resumen, la idea es enriquecer los elementos de la lista ( projects
) con listas de elementos anidados ( image
) en el paso de hidratación. En realidad, no me gusta de esta manera, ya que para mí es mucha lógica modelo en el nivel de hidratación. Pero funciona. Aquí vamos:
/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;
}
...
}
Como ya dije en mi post en GitHub, sería genial recibir retroalimentación de alguien del equipo central de Apigility, si esta solución es "Apigility conform" y, de no ser así, cuál es una solución mejor / "correcta".
Estoy desarrollando una aplicación web RESTful: basada en Apigility y basada en Zend Framework 2 . Para la capa de modelo, estoy usando ZfcBase
DbMapper
. El modelo esencialmente consta de dos entidades: Project
e Image
( 1:n
) y actualmente se implementa así:
ProjectCollection extends Paginator
ProjectEntity
ProjectMapper extends AbstractDbMapper
ProjectService implements ServiceManagerAwareInterface
ProjectServiceFactory implements FactoryInterface
La misma estructura para Image
.
Cuando se solicita el recurso ( /projects[/:id]
), la entidad / entidades del proyecto respondido debe contener una lista de sus / sus entidades de Image
.
Entonces, ¿cómo puede / debe implementarse esta estructura 1:n
?
Sub-preguntas:
¿Ofrece [
DbMapper
] alguna "magia" para recuperar tales estructuras de árbol "automáticamente" sin escribirJOIN
s (o usar un ORM)?¿[
Apigility
] proporciona algo de "magia" para construir respuestas anidadas?
{
"_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
}
EDITAR
La salida que estoy obteniendo actualmente es:
/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"
}
}
}
Entonces funciona para un solo objeto. Pero no para colecciones, donde los artículos individuales incluyen colecciones futuras:
/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
}
EDITAR
Edité mi código y di un paso hacia la meta.
No podría funcionar, ya que mi ProjectService#getProjects()
solo estaba devolviendo los datos de los proyectos de la base de datos, no enriquecidos con las imágenes:
public function getProjects() {
return $this->getMapper()->findAll();
}
editado para:
public function getProjects() {
$projects = $this->getMapper()->findAll();
foreach ($projects as $key => $project) {
$images = $this->getImageService()->getImagesForProject($project[''id'']);
$projects[$key][''images''] = $images;
}
return $projects;
}
y ProjectMapper#findAll()
public function findAll() {
$select = $this->getSelect();
$adapter = $this->getDbAdapter();
$paginatorAdapter = new DbSelect($select, $adapter);
$collection = new ProjectCollection($paginatorAdapter);
return $collection;
}
editado para:
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;
}
Ahora obtengo el resultado deseado:
{
"_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
}
Pero es una solución un poco cutre, ¿no? Lo que estoy haciendo en realidad es: simplemente estoy reemplazando una parte de la funcionalidad de recuperación de datos de Apigility ... De todos modos, no me gusta esta solución y quiero encontrar una mejor (una "solución de conformidad de Apigility").
No tengo experiencia con db-mapper, pero creo que puedo responder la pregunta 2 por ti.
Si el recurso del proyecto extraído (una matriz) tiene una imagen clave que contiene un objeto de tipo Hal/Collection
, extraerá automáticamente esta colección y la representará como se muestra en el ejemplo de Hal.
Esta "magia" sucede porque se llama a renderEntity
en el método Hal.php
en Hal.php
en la línea 563 .
EDITAR
Usted escribe que quiere:
["images": {...}, {...}, {...}]
Pero a lo que realmente deberías aspirar es a esto:
{
"id": "2",
"title": "...",
"_links": {
"self": {
"href": "http://myproject-api.misc.loc/projects/2"
}
},
"_embedded": {
"images": [
{...},
{...},
{...}
]
}
}
¿Cómo extraes tus objetos? ¿Registraste un hidratador en tu mapa de metadatos?
Deberías intentar devolver algo como esto:
use ZF/Hal/Collection
...
$images = new Collection($arrayOfImages);
$project[''images''] = $images;
entonces debería funcionar (no sé cómo explicarlo).