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

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

我正在开发 RESTful 网络应用程序 -- Apigility driven and based on the Zend Framework 2. For the model layer I'm using the ZfcBase DbMapper。该模型基本上由两个实体组成:ProjectImage (1:n),目前是这样实现的:

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

Image 的相同结构。

请求资源 (/projects[/:id]) 时,响应的项目​​ entity/entities 应包含 its/their Image 个实体的列表。

那么,can/should这个1:n结构是如何实现的呢?

子问题:

  1. 是否 [DbMapper] 提供一些 "magic" 来检索这样的树结构 "automatically" 而无需编写 JOINs(或使用 ORM) ?

  2. [Apigility] 是否提供一些 "magic" 用于构建嵌套响应?


{
    "_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 conform solution")。

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

如果您提取的项目资源(一个数组)有一个键 images 保存一个 Hal\Collection 类型的对象,它会自动提取这个集合并按照您在 Hal 示例中显示的那样呈现它。

发生这个"magic"是因为在Hal.phpon line 563.

renderEntity方法中调用了extractEmbeddedCollection

编辑

你写你想要的:

["images": {...}, {...}, {...}]

但你真正应该瞄准的是:

{
    "id": "2",
    "title": "...",
    "_links": {
        "self": {
            "href": "http://myproject-api.misc.loc/projects/2"
        }
    },
    "_embedded": {
        "images": [ 
            {...}, 
            {...}, 
            {...}
        ]
    }
}

你如何提取你的对象?您是否在元数据地图中注册了水龙头?

你应该尝试 return 这样的事情:

use ZF\Hal\Collection

...

$images = new Collection($arrayOfImages);

$project['images'] = $images;

那么它应该可以工作(我不知道还能怎么解释)。

我终于找到了解决办法。 (再次感谢GitHub上的@poisa for his solution suggestion。)简而言之,想法是用嵌套的(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 上的 post 中所说的那样,如果这个解决方案是 "Apigility conform",那么从 Apigility 核心团队的某个人那里得到反馈会很棒,而且,如果不是,什么是更好的/"correct" 解决方案。