bear/resource

面向对象服务的超媒体框架

1.22.3 2024-05-23 11:50 UTC

README

面向对象服务的超媒体框架

Scrutinizer Code Quality codecov Type Coverage Continuous Integration

BEAR.Resource 是一个超媒体框架,允许资源表现得像对象。它允许对象具有RESTful网络服务的好处,例如客户端-服务器、统一接口、无状态、具有互连性和分层组件的资源表达式。

为了为现有的领域模型或应用程序数据引入灵活性和持久性,您可以通过在开发中将应用程序方法设置为REST中心,通过引入API作为您开发的驱动力来这样做。

资源对象

资源对象是具有资源行为的对象。

  • 1个URI资源映射到1个类,它通过资源客户端检索。
  • 向具有命名参数的方法发出请求,该方法响应统一资源请求。
  • 通过请求,该方法更改资源状态并返回自身 $this
<?php
namespace MyVendor\Sandbox\Blog;

class Author extends ResourceObject
{
    public $code = 200;

    public $headers = [
        'Content-Type' => 'application/json'
    ];

    public $body = [
        'id' =>1,
        'name' => 'koriym'
    ];

    /**
     * @Link(rel="blog", href="app://self/blog/post?author_id={id}")
     */
    public function onGet(int $id): static
    {
        return $this;
    }

    public function onPost(string $name): static
    {
        $this->code = 201; // created
        // ...
        return $this;
    }

    public function onPut(int $id, string $name): static
    {
        $this->code = 204; // no content
        //...
        return $this;
    }

    public function onDelete($id): static
    {
        $this->code = 204; // no content
        //...
        return $this;
    }
}

实例检索

您可以通过使用解决依赖关系的注入器来检索客户端实例。

use BEAR\Resource\ResourceInterface;

$resource = (new Injector(new ResourceModule('FakeVendor/Sandbox')))->getInstance(ResourceInterface::class);

通过任何一种方法,都可以提供解决URI(例如 app://self/user)到映射的 Sandbox\Resource\App\User 的资源客户端。

资源请求

使用URI和查询请求资源。

$user = $resource->get('app://self/user', ['id' => 1]);
  • 此请求将1传递到符合 PSR0Sandbox\Resource\App\User 类中的 onGet($id) 方法。
  • 检索到的资源有3个属性 codeheadersbody
var_dump($user->body);
Array
(
 [name] => Athos
 [age] => 15
 [blog_id] => 0
)

有两种请求方式。一种是 eager request,另一种是 lazy reuqust

急切请求

急切请求是一种立即执行请求的方法。它与正常的PHP方法执行相同。有其他不同的编写方式,有一些原因。

$user = $resource
  ->get
  ->uri('app://self/user')
  ->withQuery(['id' => 1])
  ->eager
  ->request();
$user = $resource->get->uri('app://self/user')(['id' => 1]);
$user = $resource->uri('app://self/user')(['id' => 1]); // 'get' request method can be omitted

延迟请求

正如延迟加载一样,直到需要时才请求资源。一个常见的用例是将 资源请求对象 分配给模板,而不是实例。

// get `ResourceRequest` objcet 
$user = $resource->get->uri('app://self/user')->withQuery(['id' => 1]);
// assign to the template
echo "User resource body is {$user}"; // same in the template enigne template

它是一个可调用对象,您可以使用其他参数调用它;

// invoke
$user1 = $user(); // $id = 1
$user2 = $user(['id' => 2);

超媒体

资源可以包含指向其他相关资源的超链接。超链接通过带有 #[Link] 属性的方法显示。(PHP8.x)

use BEAR\Resource\Annotation\Link;
#[Link(rel: 'blog', href: 'app://self/blog?author_id={id}')]

或使用 @Link 注解。(PHP 7.x)

/**
 * @Link(rel="blog", href="app://self/blog?author_id={id}")
 */

关系名称由 rel 设置,链接URI由 href(超参考)设置。URI可以使用 URI模板rfc6570)分配当前资源值。

在链接中,有几种类型 selfnewcrawl 可以用来有效地创建资源图。

linkSelf

linkSelf 获取链接资源。

$blog = $resource
    ->get
    ->uri('app://self/user')
    ->withQuery(['id' => 0])
    ->linkSelf('blog')
    ->eager
    ->request();

app://self/user 资源请求的结果跳过了 blog 链接,检索到 app://self/blog 资源。就像点击网页上的链接一样,它被下一个资源替换。

linkNew

linkNew 将链接资源添加到响应中。

$user = $resource
    ->get
    ->uri('app://self/user')
    ->withQuery(['id' => 0])
    ->linkNew('blog')
    ->eager
    ->request();

$blog = $user['blog'];

在网页中,这就像“在新窗口中打开页面”,传递当前资源的同时也检索下一个资源。

爬取

爬取会遍历资源列表(数组),按顺序检索它们的链接,通过这种方式可以构建一个更复杂的资源图。就像爬虫爬取网页一样,资源客户端爬取超链接并创建资源图。

让我们考虑作者、帖子、元数据、标签、标签/名称,它们都通过资源图相互连接。每个资源都有一个超链接。在资源图中添加名称帖子树,在每个资源上添加超链接href在@link注释中。

在作者资源中有一个指向帖子资源的超链接。这是一个1:n的关系。

#[Link(rel: 'post-tree', href: 'app://self/post?author_id={id}')]
public function onGet($id = null)

在帖子资源中有一个指向元数据和标签资源的超链接。这也是一个1:n的关系。

#[Link(crawl='post-tree', rel: 'meta', href: 'app://self/meta?post_id={id}')]
#[Link(crawl='post-tree', rel: 'tag', href: 'app://self/tag?post_id={id}')]
public function onGet($author_id)
{

在标签资源中有一个超链接,只有一个与该ID对应的标签/名称资源的ID。这是一个1:1的关系。

#[Link(crawl='post-tree', rel: 'tag_name', href: 'app://self/tag/name?tag_id={tag_id}')]
public function onGet($post_id)

设置爬取名称并发送请求。

$graph = $resource
  ->get
  ->uri('app://self/marshal/author')
  ->linkCrawl('post-tree')
  ->eager
  ->request();

资源客户端通过使用rel名称连接到带有@link注释的爬取名称,查找资源并创建资源图。

var_export($graph->body);

array (
    0 =>
    array (
        'name' => 'Athos',
        'post' =>
        array (
            0 =>
            array (
                'author_id' => '1',
                'body' => 'Anna post #1',
                'meta' =>
                array (
                    0 =>
                    array (
                        'data' => 'meta 1',
                    ),
                ),
                'tag' =>
                array (
                    0 =>
                    array (
                        'tag_name' =>
                        array (
                            0 =>
                            array (
                                'name' => 'zim',
                            ),
                        ),
                    ),
 ...

HATEOAS(Hypermedia as the Engine of Application State)

资源客户端随后将下一个行为作为超链接,从该链接开始改变应用程序状态。例如,在订单资源中,通过使用POST创建订单,然后从订单状态到付款资源使用PUT方法进行付款。

订单资源

/**
 * @Link(rel="payment", href="app://self/payment{?order_id, credit_card_number, expires, name, amount}", method="put")
 */
public function onPost($drink)

客户端代码

    $order = $resource
        ->post
        ->uri('app://self/order')
        ->withQuery(['drink' => 'latte'])
        ->eager
        ->request();

    $payment = [
        'credit_card_number' => '123456789',
        'expires' => '07/07',
        'name' => 'Koriym',
        'amount' => '4.00'
    ];

    // Now use a hyperlink to pay
    $response = $resource->href('payment', $payment);

    echo $response->code; // 201

付款方法由订单资源通过超链接提供。尽管订单和付款之间的关系已改变,客户端代码没有任何变化,你可以在如何GET一杯咖啡了解更多关于HATEOAS的信息。

绑定参数

绑定参数

您可以将方法参数绑定到一个“外部值”。外部值可以是Web上下文或任何其他资源状态。

Web上下文参数

例如,而不是“拉取”$_GET或任何全局Web上下文值,您可以绑定PHP超级全局值到方法参数。

use Ray\WebContextParam\Annotation\QueryParam;

class News extends ResourceObject
{
    public function foo(#[QueryParam] string $id) : ResourceObject
    {
      // $id = $_GET['id'];

上面的例子是一个键名和参数名相同的情况。当它们不匹配时,您可以指定keyparam值。

use Ray\WebContextParam\Annotation\CookieParam;

class News extends ResourceObject
{
    #[CookieParam(key: 'id'] 
    public function foo(string $tokenId) : ResourceObject
    {
      // $tokenId = $_COOKIE['id'];

完整列表

use Ray\WebContextParam\Annotation\QueryParam;
use Ray\WebContextParam\Annotation\CookieParam;
use Ray\WebContextParam\Annotation\EnvParam;
use Ray\WebContextParam\Annotation\FormParam;
use Ray\WebContextParam\Annotation\ServerParam;

class News extends ResourceObject
{
    public function onGet(
        #[QueryParam('use_id')] string $userId,        // $_GET['use_id'];
        #[CookieParam('id')] string $tokenId,          // $_COOKIE['id'] or "0000" when unset;
        #[EnvParam('app_mode')] string $app_mode,      // $_ENV['app_mode'];
        #[FormParam('token')] string $token,           // $_POST['token'];
        #[ServerParam('SERVER_NAME') #server           // $_SERVER['SERVER_NAME'];
    ) : ResourceObject {

绑定参数对于测试也非常有用。

资源参数

我们可以使用@ResourceParam注释将另一个资源的状态绑定到一个参数上。

use BEAR\Resource\Annotation\ResourceParam;

class News extends ResourceObject
{
    /**
     * @ResourceParam(param=“name”, uri="app://self//login#nickname")
     */
    public function onGet(string $name) : ResourceObject
    {

在这个例子中,app://self//loginnickname属性绑定到$name

资源表示

每个资源都有一个用于表示的渲染器。这个渲染器是资源的依赖项,因此它通过注入器进行注入。除了JsonModule之外,您还可以使用HalModule,它使用HAL(Hyper Application Laungage)渲染器。

$modules = [new ResourceModule('MyVendor\Sandbox'), new JsonModule]:
$resource = Injector::create(modules)
  ->getInstance('BEAR\Resource\ResourceInterface');

当资源以字符串形式输出时,注入的资源渲染器将被使用,然后显示为资源表示。

echo $user;

// {
//     "name": "Aramis",
//     "age": 16,
//     "blog_id": 1
// }

在这种情况下,$user是渲染器内部的ResourceObject。这不是一个字符串,所以它被当作数组或对象处理。

echo $user['name'];

// Aramis

echo $user->onGet(2);

// {
//     "name": "Yumi",
//     "age": 15,
//     "blog_id": 2
// }

延迟加载

$user = $resource
  ->get
  ->uri('app://self/user')
  ->withQuery(['id' => 1])
  ->request();

$smarty->assign('user', $user);

在非eager request()中,不是资源请求结果,而是一个请求对象被检索。当它在资源请求输出时的模板中分配给模板引擎时,即模板中的{$user},执行资源请求和资源渲染,并显示为字符串。

嵌入资源

@Embed 注解使得将外部资源嵌入变得更加容易。例如,在 HTML 中使用 <img src="image_url"><iframe src="content_url">,嵌入的资源通过 src 字段指定。

class News extends ResourceObject
{
    #[Embed(rel: 'weather', src: 'app://self/weather/today')]
    public function onGet(): static
    {
        $this->body = [
            'headline' => "...",
            'sports'] = "..."
        ];
        
        return $this;
    }
}

weather 资源就像在这个 News 资源中的 headlinesports 一样被嵌入。

HAL (Hypertext Application Language)

HAL Module 将资源表示改为 HAL

当嵌入的资源存在时进行评估。

    // create resource client with HalModule
    $resource = (new Injector(new HalModule(new ResourceModule('FakeVendor\Sandbox'))))->getInstance(ResourceInterface::class);
    // request
    $news = $resource
        ->get
        ->uri('app://self/news')
        ->withQuery(['date' => 'today'])
        ->request();
    // output
    echo $news . PHP_EOL;

结果

    "headline": "40th anniversary of Rubik's Cube invention.",
    "sports": "Pieter Weening wins Giro d'Italia.",
    "_links": {
        "self": {
            "href": "/api/news?date=today"
        }
    },
    "_embedded": {
        "weather": [
            {
                "today": "the weather of today is sunny",
                "_links": {
                    "self": {
                        "href": "/api/weather?date=today"
                    },
                    "tomorrow": {
                        "href": "/api/weather/tomorrow"
                    }
                }
            }
        ]
    }
}

一个演示应用程序代码可在 此处 获取。

表示

ResourceObject 转换为字符串以获取资源视图。

$userView = (string) $resource->get('app://self/user?id=1');
echo $userView; // get JSON

您可以通过注入渲染器来更改视图格式(媒体类型)。以下示例说明了如何将简单的 JSON 表示渲染器“注入”到构造函数中。通常是通过依赖注入库进行注入。

class User extends ResourceObject
{
    public function __construct()
    {
        $this->setRenderer(new class implements RenderInterface{
            public function render(ResourceObject $ro)
            {
                $ro->headers['content-type'] = 'application/json';
                $ro->view = json_encode($ro->body);

                return $ro->view;
            }
        });
    }
}

传输

REST 表示“表示状态传输”。ResourceObject 中的 transfer() 方法将资源视图输出到客户端。

$user = $resource->get('app://self/user?id=1');
$user->transfer(new class implements TransferInterface {
	public function __invoke(ResourceObject $ro, array $server)
	{
	    foreach ($ro->headers as $label => $value) {
	        header("{$label}: {$value}", false);
	    }
	    http_response_code($ro->code);
	    echo $ro->view;
	}
);

以上是一个简单的 HTTP 响应传输示例。在不同的环境中(如“控制台”、“流”或“套接字服务器”)可能会进行更改。

性能提升

资源客户端是可序列化的,具有巨大的性能提升。在生产环境中推荐使用。

use BEAR\Resource\ResourceInterface;

// save
$resource = (new Injector(new ResourceModule('FakeVendor/Sandbox')))->getInstance(ResourceInterface::class);
$cachedResource = serialize($resource);

// load
$resource = unserialize($cachedResource);
$news = $resource->get('app://self/news');

注解/属性

BEAR.Resource 可以与 PHP 7/8 的 doctrine/annotation 或 PHP8 的 Attributes 一起使用。请参阅旧版 README(v1.4) 中的注解代码示例。

安装

composer require bear/resource

面向资源的框架

BEAR.Sunday 是一个面向资源的框架。在 BEAR.Sunday 中,除了 BEAR.Resource 之上的网络行为之外,还增加了 Google guice 风格的 DI/AOP 系统 Ray,并且是一个网络应用程序框架。

请访问 BEAR.Sunday 网站

另请参阅

测试 BEAR.Resource

以下是安装 BEAR.Resource 的源代码并运行单元测试和演示的步骤。

composer create-project bear/resource BEAR.Resource
cd BEAR.Resource
./vendor/bin/phpunit
php demo/run.php