1

我正在开发一个 RESTful Web 应用程序——Apigility驱动并基于Zend Framework 2。对于模型层,我使用的是. 该模型本质上由两个实体组成:和( ),目前实现如下:ZfcBase DbMapperProjectImage1: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 符合解决方案”)。

4

2 回答 2

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

于 2015-03-31T13:50:07.023 回答
0

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

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

之所以会发生这种“魔术”,是因为在第 563 行extractEmbeddedCollection的方法中调用了该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;

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

于 2015-01-12T11:14:10.227 回答