如何使用基于 ZfcBase-DbMapper 的模型在 Apigility 驱动的应用程序中构建嵌套响应


How to build nested responses in an Apigility driven application with a ZfcBase-DbMapper based model?

我正在开发一个RESTful Web应用程序 - Apigility驱动并基于Zend Framework 2。对于模型层,我正在使用ZfcBase DbMapper.该模型基本上由两个实体组成:ProjectImage1:n),目前实现方式如下:

ProjectCollection extends Paginator
ProjectEntity
ProjectMapper extends AbstractDbMapper
ProjectService implements ServiceManagerAwareInterface
ProjectServiceFactory implements FactoryInterface

Image相同的结构.

当请求资源(/projects[/:id])时,响应的项目实体应包含其Image实体的列表。

那么,如何/应该如何实施这种1:n结构呢?

子问题:

  1. [ DbMapper ] 是否提供了一些"魔法"来"自动"检索此类树结构而无需写入 JOIN(或使用 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 符合解决方案")。

我终于找到了解决方案。(再次感谢@poisa在GitHub上的解决方案建议。简而言之,这个想法是在水合步骤中使用嵌套(image)项列表来丰富(projects)列表项。我实际上不太喜欢这种方式,因为对我来说,水合级别的模型逻辑太多了。但它有效。来吧:

/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 conform",如果不是,什么是更好/"正确"的解决方案。

我没有

使用db-mapper的经验,但我认为可以为您回答问题2

如果提取的项目资源(数组)具有保存类型为 Hal'Collection 对象的键images,则它将自动提取此集合并呈现它,如 Hal 示例中所示。

之所以出现这种"魔术",是因为extractEmbeddedCollection在第 563 行的 renderEntity 方法Hal.php中被调用。

编辑

你写你想要:

["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;

那么它应该可以工作(我不知道如何解释它)。