fsc/hateoas-bundle

安装量: 214,447

依赖项: 1

建议者: 1

安全性: 0

星级: 94

关注者: 9

分支: 26

公开问题: 10

类型:symfony-bundle

dev-master / 0.5.x-dev 2014-09-29 16:24 UTC

README

Build Status Latest Stable Version Total Downloads

该组件挂载到JMSSerializerBundle的序列化过程,并提供了HATEOAS功能。目前仅支持添加链接。

尽管有一些测试,请注意这是一个正在进行中的项目。例如,仅支持yaml和注解元数据配置。

安装

composer.json

{
    "require": {
        "fsc/hateoas-bundle": "0.5.x-dev"
    },
    "minimum-stability": "dev"
}

示例应用程序

您可以在以下链接找到使用此组件的symfony 2.1示例应用程序:https://github.com/adrienbrault/symfony-hateoas-sandbox

添加链接

使用以下配置和实体

路由和序列化/ HATEOAS元数据

# routing.yml
api_user_get:
    pattern: /api/users/{id}

api_user_list:
    pattern: /api/users

user_profile:
    pattern: /profile/{user_id}

注意,您还可以使用yaml配置序列化/ HATEOAS元数据,以将序列化排除在模型之外

<?php

// src/Acme/FooBundle/Entity/User.php

use JMS\Serializer\Annotation as Serializer;
use FSC\HateoasBundle\Annotation as Rest;

/**
 * @Rest\Relation("self",               href = @Rest\Route("api_user_get", parameters = { "id" = ".id" }))
 * @Rest\Relation("alternate",          href = @Rest\Route("user_profile", parameters = { "user_id" = ".id" }))
 * @Rest\Relation("users",              href = @Rest\Route("api_user_list"))
 * @Rest\Relation("rss",                href = "http://domain.com/users.rss")
 * @Rest\Relation("from_property_path", href = ".dynamicHref")
 *
 * @Serializer\XmlRoot("user")
 */
class User
{
    /** @Serializer\XmlAttribute */
    public $id;
    public $username;

    public function getDynamicHref() {
        return "dynamic/Href/here";
    }
}

注意,href可以是@Route注解、字符串或属性路径,这些路径将在序列化时解析。

用法

<?php

$user = new User();
$user->id = 24;
$user->username = 'adrienbrault';

$serializedUser = $container->get('serializer')->serialize($user, $format);

结果

<user id="24">
  <username><![CDATA[adrienbrault]]></username>
  <link rel="self" href="http://localhost/api/users/24"/>
  <link rel="alternate" href="http://localhost/profile/24"/>
  <link rel="users" href="http://localhost/api/users"/>
  <link rel="rss" href="http://domain.com/users.rss"/>
</user>

{
    "id": 24,
    "links": [
        "self": {
            "href": "http:\/\/localhost\/api\/users\/24"
        },
        "alternate": {
            "href": "http:\/\/localhost\/profile\/24"
        },
        "users": {
            "href": "http:\/\/localhost\/api\/users"
        },
        "rss": {
            "href": "http:\/\/domain.com\/users.rss"
        }
    ]
}

在运行时对对象添加关系

在某些情况下,您可能希望在运行时对对象添加关系。例如,如果您想要一个根控制器,其中包含对您的不同集合的链接,则可以创建一个带有HATEOAS元数据的Root对象。但如果您只想在用户连接时创建一个指向当前连接用户的“me”关系呢?

我们将使用此示例。

<?php

// src/Acme/FooBundle/Model/Model.php

use JMS\Serializer\Annotation as Serializer;
use FSC\HateoasBundle\Annotation as Rest;

/**
 * @Rest\Relation("users", href = @Rest\Route("api_user_list"))
 * @Rest\Relation("posts", href = @Rest\Route("api_post_list"))
 *
 * @Serializer\XmlRoot("root")
 */
class Root
{

}
<?php

class RootController extends Controller
{
    public function indexAction()
    {
        $root = new Root();

        if (null !== ($user = $this->getUser())) {
            $relationsBuilder = $this->get('fsc_hateoas.metadata.relation_builder.factory')->create();
            $relationsBuilder->add('me', array(
                'route' => 'api_user_get',
                'parameters' => array('id' => $user->getId())
            ));
            $relationsBuilder->add('me2', 'http://api.com/users/32'); // if you want to use the router here

            $this->get('fsc_hateoas.metadata.factory')->addObjectRelations($root, $relationsBuilder->build());
        }

        return new Response($this->get('serializer')->serialize($root, $request->get('_format')));
    }
}

结果

无用户连接

GET /api 会导致

<root>
  <link rel="users" href="http://localhost/api/users"/>
  <link rel="posts" href="http://localhost/api/posts"/>
</root>

用户32已连接

GET /api 会导致

<root>
  <link rel="users" href="http://localhost/api/users"/>
  <link rel="posts" href="http://localhost/api/posts"/>
  <link rel="me" href="http://localhost/api/users/32"/>
  <link rel="me2" href="http://localhost/api/users/32"/>
</root>

禁用链接

在特定情况下,可能希望禁用关系链接的输出。可以像这样关闭输出

$container->get('fsc_hateoas.serializer.metadata_helper')->disableLinks();

JSON格式

该组件支持在序列化为JSON时自定义链接和嵌入式关系的键。它们由以下配置控制

# app/config/config.yml

fsc_hateoas:
    json:
        links_key: _links         # default: links
        relations_key: _embedded  # default: relations

上述配置将导致有效的hal+json序列化。

Pagerfanta处理器

默认配置

fsc_hateoas:
    pagerfanta:
        xml_elements_names_use_serializer_metadata: true

使用此配置,pagerfanta处理器将使用序列化器的xml根名称元数据来确定每个结果应使用哪个xml元素名称。(即:/** @Serializer\XmlRootName("user") */ class User {}

示例

<?php

use Pagerfanta\Pagerfanta;
use Pagerfanta\Adapter\DoctrineORMAdapter;

public function getListAction($page = 1, $limit = 10)
{
    $query = $this->get('doctrine')->getRepository('User')->createQueryXXX();
    $pager = new Pagerfanta(new DoctrineORMAdapter($query)); // or any Pagerfanta adapter
    $pager->setCurrentPage($page);
    $pager->setMaxPerPage($limit);

    $this->get('serializer')->getSerializationVisitor('xml')->setDefaultRootName('users');

    return new Response($this->get('serializer')->serialize($pager, 'xml')));
}

GET /list?page=3 会导致

<users page="3" limit="10" total="234">
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
  ...
</users>

添加Pagerfanta导航链接

Pagerfanta本身不创建指向自我/下一个/上一个/最后一页/第一页的链接(仅在嵌入式关系时)。

示例

<?php

use Symfony\Component\HttpFoundation\Request;
use Pagerfanta\Pagerfanta;
use Pagerfanta\Adapter\DoctrineORMAdapter;

public function getListAction(Request $request, $page = 1, $limit = 10)
{
    $query = $this->get('doctrine')->getRepository('User')->createQueryXXX();
    $pager = new Pagerfanta(new DoctrineORMPager($query)); // or any Pagerfanta adapter
    $pager->setCurrentPage($page);
    $pager->setMaxPerPage($limit);

    $this->get('fsc_hateoas.metadata.relations_manager')->addBasicRelations($pager); // Automatically add self/first/last/prev/next links

    $this->get('serializer')->getSerializationVisitor('xml')->setDefaultRootName('users');

    return new Response($this->get('serializer')->serialize($pager, 'xml')));
}

GET /list?page=3 会导致

<users page="3" limit="10" total="234">
  <link rel="self" href="http://localhost/api/users?limit=10&amp;page=3"/>
  <link rel="first" href="http://localhost/api/users?limit=10&amp;page=1"/>
  <link rel="last" href="http://localhost/api/users?limit=10&amp;page=24"/>
  <link rel="previous" href="http://localhost/api/users?limit=10&amp;page=2"/>
  <link rel="next" href="http://localhost/api/users?limit=10&amp;page=4"/>
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
  ...
</users>

嵌入关系

有时,您的表示具有需要获取服务或需要分页的嵌入式关系。要使用此组件嵌入关系,您创建一个简单的Relation元数据(例如使用注解),并添加额外的"content"参数。

示例

路由和控制台

# routing.yml
api_user_get:
    pattern: /api/users/{id}

api_user_friends_list:
    pattern: /api/users/{id}/friends
<?php

class UserController extends Controller
{
    public function getUserFriendsAction($id, $page = 1, $limit = 20)
    {
        $pager = $this->get('acme.foo.user_manager')->getUserFriendsPager($id, $page, $limit);

        $this->get('fsc_hateoas.metadata.relations_manager')->addBasicRelations($pager); // Automatically add self/first/last/prev/next links

        $this->get('serializer')->getSerializationVisitor('xml')->setDefaultRootName('users');

        return new Response($this->get('serializer')->serialize($pager, 'xml'));
    }

    public function getUserAction($id)
    {
        $user = ...;

        return new Response($this->get('serializer')->serialize($user, 'xml'));
    }
}

模型和序列化/ HATEOAS元数据

注意,您还可以使用yaml配置序列化/ HATEOAS元数据,以将序列化排除在模型之外

<?php

// src/Acme/FooBundle/Entity/User.php

use JMS\Serializer\Annotation as Serializer;
use FSC\HateoasBundle\Annotation as Rest;

// The bundle will automatically add navigation links to the embedded pagerfanta using the correct route

/**
 * @Rest\Relation("self", href = @Rest\Route("api_user_get", parameters = { "id" = ".id" }))
 * @Rest\Relation("friends",
 *     href =  @Rest\Route("api_user_friends_list", parameters = { "id" = ".id" }),
 *     embed = @Rest\Content(
 *         provider = {"acme.foo.user_manager", "getUserFriendsPager"},
 *         providerArguments = { ".id", 1, 5 },
 *         serializerXmlElementName = "users"
 *     )
 * )
 *
 * @Serializer\XmlRoot("user")
 */
class User
{
    ...
}

定义用于获取嵌入数据的提供者服务

<?php

// This is the class behing the "acme.foo.user_manager" service
class UserManager
{
    public function getUserFriendsPager($userId, $page = 1, $limit = 20)
    {
        $doctrineQuery = ...;

        $pager = Pagerfanta(new DoctrineORMAdapter($doctrineQuery));
        $pager->setCurrentPage($page);
        $pager->setMaxPerPage($limit);

        return $pager;
    }
}

结果

GET /api/users/42 会导致

<user>
  <link rel="self" href="http://localhost/api/users/42"/>
  <link rel="friends" href="http://localhost/api/users/42/friends"/>
  <users rel="friends" page="1" limit="5" total="134">
    <link rel="self" href="http://localhost/api/users/42/friends?limit=10&amp;page=1"/>
    <link rel="first" href="http://localhost/api/users/42/friends?limit=10&amp;page=1"/>
    <link rel="last" href="http://localhost/api/users/42/friends?limit=10&amp;page=27"/>
    <link rel="next" href="http://localhost/api/users/42/friends?limit=10&amp;page=2"/>
    <user>
      <link rel="self" href="..."/>
    </user>
    <user>
      <link rel="self" href="..."/>
    </user>
    <user>
      <link rel="self" href="..."/>
    </user>
    <user>
      <link rel="self" href="..."/>
    </user>
    <user>
      <link rel="self" href="..."/>
    </user>
  </users>
</user>

以及GET /api/users/42/friends 会导致

<users rel="friends" page="1" limit="20" total="134">
  <link rel="self" href="http://localhost/api/users/42/friends?limit=20&amp;page=1"/>
  <link rel="first" href="http://localhost/api/users/42/friends?limit=20&amp;page=1"/>
  <link rel="last" href="http://localhost/api/users/42/friends?limit=20&amp;page=7"/>
  <link rel="next" href="http://localhost/api/users/42/friends?limit=20&amp;page=2"/>
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
  <user>
    <link rel="self" href="..."/>
  </user>
</users>

从属性嵌入关系

除了定义嵌入资源的服务之外,您还可以嵌入主资源属性的资源。

<?php
// src/Acme/FooBundle/Entity/User.php

use JMS\Serializer\Annotation as Serializer;
use FSC\HateoasBundle\Annotation as Rest;

/**
 * @Rest\Relation("self", href = @Rest\Route("api_user_get", parameters = { "id" = ".id" }))
 * @Rest\Relation("friends",
 *     href =  @Rest\Route("api_user_friends_list", parameters = { "id" = ".id" }),
 *     embed = @Rest\Content(
 *         property = ".friends"
 *     )
 * )
 *
 * @Serializer\XmlRoot("user")
 */
class User
{
    ...

    /**
     * @var array<User>
     */
    private $friends;
}

这将序列化friends属性并将其嵌入为关系。

表单视图处理器

您可以对表单视图进行序列化。(仅在XML中可用,如果需要在JSON中,请尝试提交PR :) )

建议您的客户开发者根据表单构建请求,这具有许多优势,并可以从客户端移除一些逻辑。测试您的API也非常简单,只需跟随表单的链接,然后使用symfony DomCrawler填充并提交表单。

<?php

class UserController extends Controller
{
    public function getEditFormAction(User $user)
    {
        $formFactory = $this->getKernel()->getContainer()->get('form.factory');
        $form = $formFactory->createBuilder('user')
            ->add('name', 'text')
            ->add('email', 'email')
            ->add('gender', 'choice', array(
                'choices' => array('m' => 'male', 'f' => 'female')
            ))
            ->getForm();
        $formView = $this->get('fsc_hateoas.factory.form_view')->create($form, 'PUT', 'api_user_edit'); // Create form view and add method/action data to the FormView

        $this->get('fsc_hateoas.metadata.relations_manager')->addBasicRelations($formView); // Automatically add self links to the form

        $this->get('serializer')->getSerializationVisitor('xml')->setDefaultRootName('form');

        return new Response($this->get('serializer')->serialize($formView, $request->get('_format')));
    }
}

结果

<form method="PUT" action="http://localhost/api/users/25">
    <input type="text" name="form[name]" required="required" value="Adrien"/>
    <input type="email" name="form[email]" required="required" value="monsti@gmail.com"/>
    <select name="form[gender]" required="required">
        <option value="m" selected="selected">male</option>
        <option value="f">female</option>
    </select>
</form>

RelationUrlGenerator

您可以利用hateoas组件知道如何创建对象关系的URL。如果您想生成对象的self URL,这将非常有用。

$user = ...
$userUrl = $container->get('fsc_hateoas.routing.relation_url_generator')->generateUrl($user, 'self')

您甚至可以使用控制器特性。

<?php

use FSC\HateoasBundle\Controller\HateoasTrait;

class UserController extends Controller
{
    public function createUserAction(Request $request)
    {
        $user = new User();

        ... // you own stuff

        return Response('', 201, array(
            'Location' => $this->generateSelfUrl($user),
        ));
    }
}

关系属性

有一个属性数组可以设置在关系上,这些属性将被序列化为链接的属性。这可以用于标记链接为模板化等。示例

/**
 * @Rest\Relation("search", href = "http://domain.com/search?{&q}", attributes = { "templated" = "true" })
 */
class User
{
}

结果

<user id="24">
  <link rel="search" href="http://domain.com/search?{&q}" templated="true" />
</user>

{
    "id": 24,
    "links": [
        "search": {
            "href": "http:\/\/domain.com/search?{&q}",
            "templated": "true"
        }
    ]
}

路由选项

使用不同的路由器

此组件支持与其注册不同的路由器。例如,可以使用不同的路由器用于模板化URL。要注册路由器,您需要将服务标记为fsc_hateoas.url_generator,并且您可以提供一个alias,这样您就不必写出完整的服务名称。例如,如果您想通过使用Hautelook Templated URI Bundle来提供URI模板(RFC-6570),这非常有用。

示例

services:
    test.url_generator.prepend:
        class: FSC\HateoasBundle\Tests\Functional\TestBundle\Routing\PrependUrlGenerator
        arguments:
            - @router
        tags:
            - { name: fsc_hateoas.url_generator, alias: prepend }

然后您可以通过在注解中使用options来使用此路由器。示例

/**
 * @Rest\Relation("self", href = @Rest\Route("api_user_get", parameters = { "id" = ".id" }, options = { "router" = "prepend" }))
 */
class User
{

}

创建绝对/相对URL

您可以通过指定为路由的option来强制链接为绝对或相对。示例

/**
 * @Rest\Relation("self", href = @Rest\Route("api_user_get", parameters = { "id" = ".id" }, options = { "absolute" = true }))
 */
class User
{

}

条件性地排除链接

您可以在关系上添加条件,以确定是否应该排除链接。例如

/**
 * @Rest\Relation(
 *      "parent",
 *      href = @Rest\Route("api_post_get", parameters = { "id" = ".parent.id"}),
 *      excludeIf = { ".parent" = null }
 * )
 */
class Post
{
}

如果parent的值为null,则不会包含parent链接。这也可以通过YAML配置来完成。

relations:
    - rel: parent
        href:
            route: api_post_get
            parameters: { id: .parent.id }
        exclude_if:
            ".parent": ~