PHP ServiceStack 客户端

1.0.6 2024-02-02 14:12 UTC

This package is auto-updated.

Last update: 2024-10-01 00:07:45 UTC


README

ServiceStack and PHP Banner

ServiceStack 的 添加 ServiceStack 参考信息 功能允许客户端通过使用 ServiceStack IntelliJ 插件 直接在 PhpStorm 中生成原生类型 - 提供了一种简单的方法,使客户端能够以类型化的方式访问您的 ServiceStack 服务。

YouTube: youtu.be/WjbhfH45i5k

一流的开发体验

PHP 是世界上最受欢迎的编程语言之一,这得益于其易用性、平台无关性、大型标准库、灵活性和快速的开发体验,这使得它成为流行的网络开发语言,以及像 WordPress、Drupal 和 Joomla 这样的流行 CMS 产品的开发语言,这要归功于其灵活性、可嵌入性和易于定制。

为了在这些环境中最大限度地提高调用 ServiceStack API 的体验,ServiceStack 现在支持 PHP 作为首选的 Add ServiceStack Reference 支持语言,这为 PHP 开发者提供了一个端到端的类型化 API,用于消费 ServiceStack API,包括在 PhpStorm 中的 IDE 集成以及 x dotnet 工具的内置支持,以从单个命令行生成针对远程 ServiceStack 实例的类型化和注解的 PHP DTO。

理想的类型化消息基础 API

为了最大限度地提高 PHP DTO 的效用并启用更丰富的工具支持以及更好的开发体验,PHP DTO 被生成为类型化的 JsonSerializable 类,具有 提升的构造函数 并带有 PHPDoc 类型 注解 - 这在扩展大型 PHP 代码库时非常有价值,并极大地提高了远程 API 的可发现性。DTO 还通过接口标记和注解得到丰富,从而实现了其最优的端到端类型化 API。

PHP DTO 和 JsonServiceClient 库遵循 PHP 命名规范,因此它们可以自然地融入现有的 PHP 代码库。以下是 techstacks.io 生成的 PHP DTO 示例,包含字符串和 int 枚举、一个示例 AutoQuery 以及一个标准的请求和响应 DTO,展示了使用的丰富类型注解和命名规范

enum TechnologyTier : string
{
    case ProgrammingLanguage = 'ProgrammingLanguage';
    case Client = 'Client';
    case Http = 'Http';
    case Server = 'Server';
    case Data = 'Data';
    case SoftwareInfrastructure = 'SoftwareInfrastructure';
    case OperatingSystem = 'OperatingSystem';
    case HardwareInfrastructure = 'HardwareInfrastructure';
    case ThirdPartyServices = 'ThirdPartyServices';
}

enum Frequency : int
{
    case Daily = 1;
    case Weekly = 7;
    case Monthly = 30;
    case Quarterly = 90;
}

// @Route("/technology/search")
#[Returns('QueryResponse')]
/**
 * @template QueryDb of Technology
 * @template QueryDb1 of TechnologyView
 */
class FindTechnologies extends QueryDb implements IReturn, IGet, JsonSerializable
{
    public function __construct(
        /** @var array<int>|null */
        public ?array $ids=null,
        /** @var string|null */
        public ?string $name=null,
        /** @var string|null */
        public ?string $vendorName=null,
        /** @var string|null */
        public ?string $nameContains=null,
        /** @var string|null */
        public ?string $vendorNameContains=null,
        /** @var string|null */
        public ?string $descriptionContains=null
    ) {
    }

    /** @throws Exception */
    public function fromMap($o): void {
        parent::fromMap($o);
        if (isset($o['ids'])) $this->ids = JsonConverters::fromArray('int', $o['ids']);
        if (isset($o['name'])) $this->name = $o['name'];
        if (isset($o['vendorName'])) $this->vendorName = $o['vendorName'];
        if (isset($o['nameContains'])) $this->nameContains = $o['nameContains'];
        if (isset($o['vendorNameContains'])) $this->vendorNameContains = $o['vendorNameContains'];
        if (isset($o['descriptionContains'])) $this->descriptionContains = $o['descriptionContains'];
    }
    
    /** @throws Exception */
    public function jsonSerialize(): mixed
    {
        $o = parent::jsonSerialize();
        if (isset($this->ids)) $o['ids'] = JsonConverters::toArray('int', $this->ids);
        if (isset($this->name)) $o['name'] = $this->name;
        if (isset($this->vendorName)) $o['vendorName'] = $this->vendorName;
        if (isset($this->nameContains)) $o['nameContains'] = $this->nameContains;
        if (isset($this->vendorNameContains)) $o['vendorNameContains'] = $this->vendorNameContains;
        if (isset($this->descriptionContains)) $o['descriptionContains'] = $this->descriptionContains;
        return empty($o) ? new class(){} : $o;
    }
    public function getTypeName(): string { return 'FindTechnologies'; }
    public function getMethod(): string { return 'GET'; }
    public function createResponse(): mixed { return QueryResponse::create(genericArgs:['TechnologyView']); }
}

// @Route("/orgs/{Id}", "DELETE")
class DeleteOrganization implements IReturnVoid, IDelete, JsonSerializable
{
    public function __construct(
        /** @var int */
        public int $id=0
    ) {
    }

    /** @throws Exception */
    public function fromMap($o): void {
        if (isset($o['id'])) $this->id = $o['id'];
    }
    
    /** @throws Exception */
    public function jsonSerialize(): mixed
    {
        $o = [];
        if (isset($this->id)) $o['id'] = $this->id;
        return empty($o) ? new class(){} : $o;
    }
    public function getTypeName(): string { return 'DeleteOrganization'; }
    public function getMethod(): string { return 'DELETE'; }
    public function createResponse(): void {}
}

servicestack/client Packagist 包中可用的智能 PHP JsonServiceClient 提供了与其他首选支持客户端平台相同的富有成效、类型化的 API 开发体验。

使用提升的构造函数,可以使 DTO 使用单个构造函数表达式并利用命名参数进行填充,这与通用的 JsonServiceClient 一起,使单个 LOC 中的端到端类型化 API 请求成为可能。

use ServiceStack\JsonServiceClient;
use dtos\Hello;

$client = new JsonServiceClient("https://test.servicestack.net");

/** @var HelloResponse $response */
$response = client->get(new Hello(name:"World"));

HelloResponse 可选类型提示不会改变运行时行为,但它可以使静态分析工具和 PyCharm 等 IDE 提供丰富的智能感知和开发时反馈。

安装

确保您已安装 PHPComposer

PHP 应用进行类型化 API 请求的唯一要求是生成的 PHP DTO 和通用的 JsonServiceClient,可以通过 Composer 项目安装

$ composer require servicestack/client

或者通过将包添加到您的 composer.json 然后安装依赖项

{
  "require": {
    "servicestack/client": "^1.0"
  }
}

PhpStorm ServiceStack 插件

使用 PhpStorm 的 PHP 开发者可以通过从 JetBrains 市场安装 ServiceStack 插件 来获得使用 ServiceStack 服务简化的开发体验

您可以在目录上右键单击,并在上下文菜单中点击 ServiceStack 参考

以启动 添加 PHP ServiceStack 参考 对话框,您可以在其中输入要调用的 ServiceStack 端点的远程 URL 以生成所有 API 的类型化 PHP DTO,默认情况下将保存到 dtos.php

然后只需导入 DTOs 和 JsonServiceClient 即可消费任何远程 ServiceStack API

<?php

require_once __DIR__ . '/vendor/autoload.php'; // Autoload files using Composer autoload
require_once 'dtos.php';

use dtos\FindTechnologies;
use ServiceStack\JsonServiceClient;

$client = JsonServiceClient::create("https://techstacks.io");

$response = $client->send(new FindTechnologies(
    ids: [1,2,4,6],
    vendorName: "Google"));

print_r($response);

如果任何远程 API 更改了 DTO,可以通过在 dtos.php 上右键单击并点击 更新 ServiceStack 参考 来更新

简单的 PHP 命令行实用程序

使用其他 PHP IDE 和文本编辑器,如 VS Code 的开发者可以利用跨平台的 x 命令行实用程序 从命令行生成 PHP DTO。

首先安装适用于您的操作系统的最新 .NET SDK,然后使用以下命令安装 x dotnet 工具

$ dotnet tool install --global x 

添加 ServiceStack 参考

要添加 PHP ServiceStack 参考,只需使用远程 ServiceStack 实例的 URL 调用 x php

$ x php https://techstacks.io

结果

Saved to: dtos.php

使用仅 URL 调用 x php 将使用主机名保存 DTOs,您可以通过指定文件名作为第二个参数来覆盖此行为

$ x php https://techstacks.io Tech

结果

Saved to: Tech.dtos.php

更新 ServiceStack 参考

要更新现有的 ServiceStack 参考,请使用文件名调用 x php

$ x php dtos.php

结果

Updated: dtos.php

这将使用来自 techstacks.io 的最新 PHP 服务器 DTO 更新文件。您还可以通过取消注释 PHP DTO 自定义选项 并更新它们来自定义 DTO 的生成方式

更新所有 PHP DTO

不使用任何参数调用 x php 将更新当前目录中的所有 PHP DTO

$ x php

结果

Updated: Tech.dtos.php
Updated: dtos.php

智能通用 JsonServiceClient

通用的 JsonServiceClient 是一等客户端,具有与其他 一等支持语言 中的智能 ServiceClients 相同的丰富功能集,提供简洁、类型化的灵活 API,支持额外的无类型参数、自定义 URL 和 HTTP 方法、动态响应类型,包括以原始文本和二进制数据格式消费 API 响应。客户端可以通过实例和静态请求、响应和异常过滤器来装饰以支持通用功能。

它包括对多种ServiceStack身份验证选项的原生支持,包括HTTP基本认证和无需状态的Bearer Token身份验证提供者,如API密钥JWT认证,以及用于流行的凭证身份验证提供者的有状态会话以及用于启用自定义身份验证方法的onAuthenticationRequired回调。

内置的身份验证选项包括对自动重试的支持,以透明地认证和重试所需的认证请求,以及刷新令牌Cookie支持,它将在幕后自动透明地获取新的JWT Bearer Tokens,从而实现无摩擦的无状态JWT支持。

上述功能的快照如下所示,为高级公共API

class JsonServiceClient
{
    public static ?RequestFilter $globalRequestFilter;
    public ?RequestFilter $requestFilter;
    public static ?ResponseFilter $globalResponseFilter;
    public ?ResponseFilter $responseFilter;
    public static ?ExceptionFilter $globalExceptionFilter;
    public ?ExceptionFilter $exceptionFilter;
    public ?Callback $onAuthenticationRequired;

    public string $baseUrl;
    public string $replyBaseUrl;
    public string $oneWayBaseUrl;
    public ?string $userName;
    public ?string $password;
    public ?string $bearerToken;
    public ?string $refreshToken;
    public ?string $refreshTokenUri;
    public bool $useTokenCookie;
    public array $headers = [];
    public array $cookies = [];

    public function __construct(string $baseUrl);

    public function setCredentials(?string $userName = null, ?string $password = null) : void;
    public function getTokenCookie();
    public function getRefreshTokenCookie();

    public function get(IReturn|IReturnVoid|string $request, ?array $args = null): mixed;
    public function post(IReturn|IReturnVoid|string $request, mixed $body = null, ?array $args = null): mixed;
    public function put(IReturn|IReturnVoid|string $request, mixed $body = null, ?array $args = null): mixed;
    public function patch(IReturn|IReturnVoid|string $request, mixed $body = null, ?array $args = null): mixed;
    public function delete(IReturn|IReturnVoid|string $request, ?array $args = null): mixed;
    public function options(IReturn|IReturnVoid|string $request, ?array $args = null): mixed;
    public function head(IReturn|IReturnVoid|string $request, ?array $args = null): mixed;
    public function send(IReturn|IReturnVoid|null $request, ?string $method = null, mixed $body = null, 
        ?array $args = null): mixed;

    public function getUrl(string $path, mixed $responseAs = null, mixed $args = null): mixed;
    public function postUrl(string $path, mixed $responseAs = null, mixed $body = null, ?array $args = null): mixed;
    public function putUrl(string $path, mixed $responseAs = null, mixed $body = null, ?array $args = null): mixed;
    public function patchUrl(string $path, mixed $responseAs = null, mixed $body = null, ?array $args = null): mixed;
    public function deleteUrl(string $path, mixed $responseAs = null, mixed $args = null): mixed;
    public function optionsUrl(string $path, mixed $responseAs = null, mixed $args = null): mixed;
    public function headUrl(string $path, mixed $responseAs = null, mixed $args = null): mixed;
    public function sendUrl(string $path, ?string $method = null, mixed $responseAs = null, mixed $body = null, 
        mixed $args = null): mixed

    public function sendAll(array $requestDtos): mixed;       # Auto Batch Reply Requests
    public function sendAllOneWay(array $requestDtos): void;  # Auto Batch Oneway Requests

更改默认服务器配置

服务Stack服务器也可以通过修改NativeTypesFeature插件上的默认配置来覆盖上述默认值,例如:

var nativeTypes = this.GetPlugin<NativeTypesFeature>();
nativeTypes.MetadataTypesConfig.AddResponseStatus = true;
...

可以通过PhpGenerator添加PHP特定功能

PhpGenerator.ServiceStackImports.Add("MyNamespace\\Type");

自定义DTO类型生成

额外的PHP特定自定义可以静态配置,例如PreTypeFilterInnerTypeFilterPostTypeFilter(所有语言中均可用),可用于在生成的DTO输出中注入自定义代码。

使用PreTypeFilter在类型定义前后生成源代码,例如,这将在非枚举和接口类型上附加自定义的MyAnnotation注解。

PhpGenerator.PreTypeFilter = (sb, type) => {
    if (!type.IsEnum.GetValueOrDefault() && !type.IsInterface.GetValueOrDefault())
    {
        sb.AppendLine("#[MyAnnotation]");
    }
};

InnerTypeFilter在类型定义之后立即被调用,可以用于生成所有类型和接口的共同成员,例如:

PhpGenerator.InnerTypeFilter = (sb, type) => {
    sb.AppendLine("public ?int $id;");
    sb.AppendLine("public function getId(): int { return $this->id ?? ($this->id=rand()); }");
};

还有PrePropertyFilterPostPropertyFilter用于在属性前后生成源代码,例如:

PhpGenerator.PrePropertyFilter = (sb , prop, type) => {
    if (prop.Name == "Id")
    {
        sb.AppendLine("#[IsInt]");
    }
};

输出自定义代码

为了在生成复杂的类型化DTO时提供更大的灵活性,您可以使用[Emit{Language}]属性在每个类型或属性之前生成代码。

这些属性可用于在多种语言中生成不同的属性或注解,以启用不同验证库的客户验证,例如:

[EmitCode(Lang.Php, "// App User")]
[EmitPhp("#[Validate]")]
public class User : IReturn<User>
{
    [EmitPhp("#[IsNotEmpty]", "#[IsEmail]")]
    [EmitCode(Lang.Swift | Lang.Dart, new[]{ "@isNotEmpty()", "@isEmail()" })]
    public string Email { get; set; }
}

这将生成PHP DTO中的[EmitPhp]代码

// App User
#[Validate]
class User implements JsonSerializable 
{
    public function __construct(
        #[IsNotEmpty]
        #[IsEmail]
        /** @var string|null */
        public ?string email=null
    ) {
    }
    //...
}

而通用的[EmitCode]属性允许您使用相同的语法在多种语言中输出相同的代码。

PHP参考示例

让我们通过一个简单的例子来看看我们如何使用ServiceStack的PHP DTO注解在我们的PHP JsonServiceClient中。

首先,我们需要通过在项目文件夹上右键单击并单击ServiceStack Reference...(如上图所示)将PHP参考添加到远程ServiceStack服务。

这将导入远程服务的DTO到您的本地项目中,看起来类似于:

<?php namespace dtos;
/* Options:
Date: 2023-10-14 08:05:09
Version: 6.111
Tip: To override a DTO option, remove "//" prefix before updating
BaseUrl: https://techstacks.io

//GlobalNamespace: dtos
//MakePropertiesOptional: False
//AddServiceStackTypes: True
//AddResponseStatus: False
//AddImplicitVersion: 
//AddDescriptionAsComments: True
//IncludeTypes: 
//ExcludeTypes: 
//DefaultImports: 
*/

// @Route("/technology/{Slug}")
#[Returns('GetTechnologyResponse')]
class GetTechnology implements IReturn, IRegisterStats, IGet, JsonSerializable
{
    public function __construct(
        /** @var string|null */
        public ?string $slug=null
    ) {
    }

    /** @throws Exception */
    public function fromMap($o): void {
        if (isset($o['slug'])) $this->slug = $o['slug'];
    }
    
    /** @throws Exception */
    public function jsonSerialize(): mixed
    {
        $o = [];
        if (isset($this->slug)) $o['slug'] = $this->slug;
        return empty($o) ? new class(){} : $o;
    }
    public function getTypeName(): string { return 'GetTechnology'; }
    public function getMethod(): string { return 'GET'; }
    public function createResponse(): mixed { return new GetTechnologyResponse(); }
}

class GetTechnologyResponse implements JsonSerializable
{
    public function __construct(
        /** @var DateTime */
        public DateTime $created=new DateTime(),
        /** @var Technology|null */
        public ?Technology $technology=null,
        /** @var array<TechnologyStack>|null */
        public ?array $technologyStacks=null,
        /** @var ResponseStatus|null */
        public ?ResponseStatus $responseStatus=null
    ) {
    }

    /** @throws Exception */
    public function fromMap($o): void {
        if (isset($o['created'])) $this->created = JsonConverters::from('DateTime', $o['created']);
        if (isset($o['technology'])) $this->technology = JsonConverters::from('Technology', $o['technology']);
        if (isset($o['technologyStacks'])) $this->technologyStacks = JsonConverters::fromArray('TechnologyStack', $o['technologyStacks']);
        if (isset($o['responseStatus'])) $this->responseStatus = JsonConverters::from('ResponseStatus', $o['responseStatus']);
    }
    
    /** @throws Exception */
    public function jsonSerialize(): mixed
    {
        $o = [];
        if (isset($this->created)) $o['created'] = JsonConverters::to('DateTime', $this->created);
        if (isset($this->technology)) $o['technology'] = JsonConverters::to('Technology', $this->technology);
        if (isset($this->technologyStacks)) $o['technologyStacks'] = JsonConverters::toArray('TechnologyStack', $this->technologyStacks);
        if (isset($this->responseStatus)) $o['responseStatus'] = JsonConverters::to('ResponseStatus', $this->responseStatus);
        return empty($o) ? new class(){} : $o;
    }
}

默认情况下,生成的PHP DTO使用默认的dtos命名空间,您可以将其作为单个类引用

use dtos\GetTechnology;
use dtos\GetTechnologyResponse;

或者使用以下方式在单行中组合:

use dtos\{GetTechnology,GetTechnologyResponse};

$request = new GetTechnology();

制作类型化API请求

在PHP中制作API请求与所有其他ServiceStack服务客户端相同,通过使用JsonServiceClient发送填充的请求DTO,它返回类型化响应DTO。

因此,我们只需要从servicestack/client包中的JsonServiceClient和任何使用生成的PHP ServiceStack引用的DTO来制作任何API请求,例如:

<?php

require_once __DIR__ . '/vendor/autoload.php'; // Autoload files using Composer autoload
require_once 'dtos.php';

use dtos\GetTechnology;
use dtos\GetTechnologyResponse;
use ServiceStack\JsonServiceClient;

$client = JsonServiceClient::create("https://techstacks.io");

/** @var GetTechnologyResponse $response */
$response = $client->get(new GetTechnology(slug:"ServiceStack"));

$tech = $response->technology; // typed to Technology

echo "$tech->name by $tech->vendorName from $tech->productUrl\n";
echo "$tech->name TechStacks:\n";
print_r($response->technologyStacks);

PHPDoc类型注解

虽然PHP是一种动态语言,其本身对静态类型和泛型的支持有限,但通过使用PHPDoc类型提示来注释API类型响应,你可以在诸如PhpStorm这样的智能IDE中获取到许多静态类型和智能感知的好处,从而实现类型化语言的功能。

/** @var GetTechnologyResponse $response */
$response = $client->get(new GetTechnology(slug:"ServiceStack"));
echo $response->technology->name . PHP_EOL; //intelli-sense

/** @var QueryResponse<TechnologyView> $response */
$response = $client->get(new FindTechnologies(ids:[2,4,8]));

/** @var TechnologyView[] $results */
$results = $response->results;
echo $results[0]->name . PHP_EOL; //intelli-sense

构造函数初始化

所有PHP引用DTO都实现了提升的构造函数,这使得它们使用我们在C#中熟悉的命名参数语法通过构造函数表达式来填充变得更加容易,因此,而不是

$request = new Authenticate();
$request->provider = "credentials";
$request->userName = $userName;
$request->password = $password;
$request->rememberMe = true;
$response = $client->post($request);

你可以使用单个构造函数表达式来填充DTO,而不会丢失PHP的静态类型优势。

$response = $client->post(new Authenticate(
    provider: "credentials",
    userName: "test",
    password: "test",
    rememberMe: true));

在类型化API请求中发送额外参数

许多AutoQuery服务利用隐式约定来查询在AutoQuery请求DTO上未显式定义的字段,这些字段可以通过指定额外的参数与类型化请求DTO一起查询,例如

/** @var QueryResponse<TechnologyView> $response */
$response = $client->get(new FindTechnologies(), args:["vendorName" => "ServiceStack"]);

使用URL制作API请求

除了制作类型化API请求外,还可以使用相对或绝对URL调用服务,例如

$client->getUrl("/technology/ServiceStack", responseAs:new GetTechnologyResponse());

$client->getUrl("https://techstacks.io/technology/ServiceStack", 
    responseAs:new GetTechnologyResponse());

// https://techstacks.io/technology?Slug=ServiceStack
$args = ["slug" => "ServiceStack"]
client.getUrl("/technology", args:$args, responseAs:new GetTechnologyResponse()); 

以及向自定义URL发送POST请求DTO

$client->postUrl("/custom-path", $request, args:["slug" => "ServiceStack"]);

$client->postUrl("http://example.org/custom-path", $request);

原始数据响应

JsonServiceClient也支持原始数据响应,如stringbyte[],这些响应一旦在请求DTO上使用IReturn<T>标记声明,就会得到类型化API。

public class ReturnString : IReturn<string> {}
public class ReturnBytes : IReturn<byte[]> {}

然后可以像通常一样访问它们,它们的响应类型被转换为PHP的stringByteArray,用于原始byte[]响应。

/** @var string $str */ 
$str = client.get(new ReturnString());

/** @var ByteArray $data */ 
$data = client->get(new ReturnBytes());

使用基本认证进行身份验证

JsonServiceClient实现了基本认证支持,并且遵循在C#服务客户端中提供的相同API,其中可以单独设置userName/password属性,例如

$client = new JsonServiceClient($baseUrl);
$client->username = user;
$client->password = pass;

$response = client->get(new SecureRequest());

或使用$client->setCredentials()同时设置它们。

使用凭据进行身份验证

或者,您可以通过添加PHP引用到您的远程ServiceStack实例,并发送填充的Authenticate请求DTO来进行身份验证,例如

$request = new Authenticate();
$request->provider = "credentials";
$request->userName = $userName;
$request->password = $password;
$request->remember_me = true;

$response = client->post(request);

这将填充JsonServiceClient,其中包含会话Cookie,这些Cookie将在随后的请求中透明地发送以进行认证请求。

使用JWT进行身份验证

使用bearerToken属性使用JWT令牌通过ServiceStack JWT提供者进行身份验证。

$client->bearerToken = $jwt;

或者,您可以使用刷新令牌而不是JWT。

$client->refreshYoken = $refreshToken;

客户端将自动使用刷新令牌获取新的JWT Bearer Token,用于认证请求。

使用API密钥进行身份验证

使用bearerToken属性使用API密钥进行身份验证。

$client->bearerToken = $apiKey;

透明处理401未授权响应

如果服务器返回401未授权响应,无论是由于客户端未认证,还是配置的Bearer Token或API Key已过期或被取消,您可以使用onAuthenticationRequired回调在自动重试原始请求之前重新配置客户端,例如

$client = new JsonServiceClient(BASE_URL);
$authClient = new JsonServiceClient(AUTH_URL);

$client->onAuthenticationRequired = new class($client, $authClient) implements Callback {
    public function __construct(public JsonServiceClient $client, public JsonServiceClient $authClient) {}
    public function call(): void {
        $this->authClient->setCredentials("test", "test");
        $this->client->bearerToken = $this->authClient->get(new Authenticate())->getRefreshTokenCookie();
    }
};

// Automatically retries requests returning 401 Responses with new bearerToken
$response = client->get(new Secured());

自动刷新访问令牌

在JWT中,使用刷新令牌支持,您可以使用refresh_token属性指示服务客户端在自动重试由于无效或过期的JWT而失败的请求之前,在后台自动获取新的JWT令牌,例如

// Authenticate to get new Refresh Token
$authClient = new JsonServiceClient(AUTH_URL);
$authClient.userName = $userName;
$authClient.password = $password;
$authResponse = $authClient->get(new Authenticate());

// Configure client with RefreshToken
$client->refreshToken = $authResponse->refreshToken;

// Call authenticated Services and clients will automatically retrieve new JWT Tokens as needed
$response = client->get(new Secured());

当需要将刷新令牌发送到不同的ServiceStack服务器时,请使用refreshTokenUri属性,例如

$client->refreshToken = $refreshToken;
$client->refreshTokenUri = AUTH_URL . "/access-token";

DTO 自定义选项

在大多数情况下,您只需直接使用生成的 PHP DTO 即可,但您也可以通过覆盖默认选项来进一步自定义 DTO 的生成方式。

生成的 DTO 的头部显示了 PHP 原生类型支持的选项及其默认值。默认值以 // 为注释前缀显示。要覆盖值,请移除 // 并指定 右侧的值。任何未注释的值都将发送到服务器以覆盖任何服务器默认值。

DTO 注释允许自定义 DTO 的生成方式。生成 DTO 所使用的默认选项会在生成的 DTO 的头部注释中重复出现,以 PHP 注释 // 开头的选项是来自服务器的默认值,任何未注释的值都将发送到服务器以覆盖任何服务器默认值。

<?php namespace dtos;
/* Options:
Date: 2023-10-14 08:05:09
Version: 6.111
Tip: To override a DTO option, remove "//" prefix before updating
BaseUrl: https://techstacks.io

//GlobalNamespace: dtos
//MakePropertiesOptional: False
//AddServiceStackTypes: True
//AddResponseStatus: False
//AddImplicitVersion: 
//AddDescriptionAsComments: True
//IncludeTypes: 
//ExcludeTypes: 
//DefaultImports: 
*/

我们将逐一介绍上述选项,以了解它们如何影响生成的 DTO。

更改默认服务器配置

服务Stack服务器也可以通过修改NativeTypesFeature插件上的默认配置来覆盖上述默认值,例如:

//Server example in C#
var nativeTypes = this.GetPlugin<NativeTypesFeature>();
nativeTypes.MetadataTypesConfig.AddResponseStatus = true;
...

我们将逐一介绍上述选项,以了解它们如何影响生成的 DTO。

全局命名空间

这允许您指定 DTO 要生成的命名空间。

<?php namespace dtos;

添加响应状态

自动在所有 Response DTO 上添加 $responseStatus 属性,无论它是否已经被定义。

class GetTechnologyResponse implements JsonSerializable
{
    ...
    /** @var ResponseStatus|null */
    public ?ResponseStatus $responseStatus=null
}

添加隐式版本

允许您指定客户端发送的所有 Request DTO 中自动填充的版本号。

class GetTechnology implements IReturn, IRegisterStats, IGet, JsonSerializable
{
    public int $version = 1;
    ...
}

这可以让您知道现有客户端正在使用哪个版本的 Service Contract,从而便于实现 ServiceStack 的 推荐的版本策略

包含类型

用作白名单,以指定您希望代码生成的类型。

/* Options:
IncludeTypes: GetTechnology,GetTechnologyResponse

仅生成 GetTechnologyGetTechnologyResponse DTO。

class GetTechnology implements IReturn, IRegisterStats, IGet, JsonSerializable

// ...
class GetTechnologyResponse implements JsonSerializable
// ...

包含泛型类型

使用 .NET 的类型名称包含泛型类型,即类型名称后跟反引号和泛型参数的数量,例如

IncludeTypes: IReturn`1,MyPair`2

包含请求 DTO 和其依赖类型

您可以使用请求 DTO 的 .* 后缀包含一个请求 DTO 及其所有依赖类型,例如

/* Options:
IncludeTypes: GetTechnology.*

这将包括 GetTechnology 请求 DTO、GetTechnologyResponse 响应 DTO 以及它们引用的所有类型。

包含 C# 命名空间中的所有类型

如果您的 DTO 被分组到不同的命名空间中,可以使用 /* 后缀包括所有 DTO,例如

/* Options:
IncludeTypes: MyApp.ServiceModel.Admin/*

这将包括 MyApp.ServiceModel.Admin C# 命名空间中的所有 DTO。

包含标签组中的所有服务

按标签 分组的服务 可以用于 IncludeTypes,其中可以使用花括号指定标签,格式为 {tag}{tag1,tag2,tag3},例如

/* Options:
IncludeTypes: {web,mobile}

或单独指定

/* Options:
IncludeTypes: {web},{mobile}

排除类型

用作黑名单,以指定您希望从生成中排除的类型。

/* Options:
ExcludeTypes: GetTechnology,GetTechnologyResponse

将排除生成 GetTechnologyGetTechnologyResponse DTO。

默认导入

使用 DefaultImports 指定生成的 PHP DTO 中的附加导入。

/* Options:
...
DefaultImports: MyType
*/

这将包括在生成的 DTO 中的类型

use MyType;

自定义序列化

servicestack/client 客户端库允许灵活的序列化自定义,您可以更改不同的 .NET 类型如何序列化和反序列化为原生 PHP 类型。

为了说明这一点,我们将通过说明如何将包含二进制数据的属性的序列化到 Base64 的方式来实现。

首先,我们指定 PHP DTO 生成器为流行的 .NET 二进制数据类型发出 ByteArray 类型提示

PhpGenerator.TypeAliases[typeof(byte[]).Name] = "ByteArray";
PhpGenerator.TypeAliases[typeof(Stream).Name] = "ByteArray";

在 PHP 应用程序中,我们可以指定用于反序列化具有 ByteArray 数据类型的属性的序列化和反序列化程序,该程序将二进制数据转换为/从 Base64

use JsonSerializable;

class ByteArray implements JsonSerializable
{
    public ?string $data;

    public function __construct(?string $data=null)
    {
        $this->data = isset($data) ? base64_decode($data) : null;
    }

    public function jsonSerialize(): mixed
    {
        return base64_encode($this->data);
    }
}

检查工具

为了帮助客户检查API响应,servicestack/client库还包含了一系列有用的工具,可以快速可视化API输出。

对于基本的缩进对象图,您可以使用Inspect::dump来捕获,使用Inspect::printDump来打印任何API响应的输出,例如:

$orgName = "php";

$opts = [
    "http" => [
        "header" => "User-Agent: gist.cafe\r\n"
    ]
];
$context = stream_context_create($opts);
$json = file_get_contents("https://api.github.com/orgs/{$orgName}/repos", false, $context);
$orgRepos = array_map(function($x) {
    $x = get_object_vars($x);
    return [
        "name"        => $x["name"],
        "description" => $x["description"],
        "url"         => $x["url"],
        "lang"        => $x["language"],
        "watchers"    => $x["watchers"],
        "forks"       => $x["forks"],
    ];
}, json_decode($json));
usort($orgRepos, function($a,$b) { return $b["watchers"] - $a["watchers"]; });

echo  "Top 3 {$orgName} GitHub Repos:\n";
Inspect::printDump(array_slice($orgRepos, 0, 3));

echo  "\nTop 10 {$orgName} GitHub Repos:\n";
Inspect::printDumpTable(array_map(function($x) {
    return [
        "name"        => $x["name"],
        "lang"        => $x["lang"],
        "watchers"    => $x["watchers"],
        "forks"       => $x["forks"],
    ];
}, array_slice($orgRepos, 0, 10)));

输出

Top 3 php GitHub Repos:
[
    {
        name: php-src,
        description: The PHP Interpreter,
        url: https://api.github.com/repos/php/php-src,
        lang: C,
        watchers: 36122,
        forks: 7653
    },
    {
        name: web-php,
        description: The www.php.net site,
        url: https://api.github.com/repos/php/web-php,
        lang: PHP,
        watchers: 785,
        forks: 532
    },
    {
        name: php-gtk-src,
        description: The PHP GTK Bindings,
        url: https://api.github.com/repos/php/php-gtk-src,
        lang: C++,
        watchers: 201,
        forks: 59
    }
]

对于表格形式的查询结果集,您可以使用Inspect::table来捕获,使用Inspect::printTable来以人类友好的Markdown表格格式打印API结果集,例如:

echo "\nTop 10 $orgName Repos:\n"
Inspect::printTable(array_slice($orgRepos, 0, 10));

输出

Top 10 php GitHub Repos:
+------------------------------------------------+
|      name      |    lang    | watchers | forks |
|------------------------------------------------|
| php-src        | C          |    36122 |  7653 |
| web-php        | PHP        |      785 |   532 |
| php-gtk-src    | C++        |      201 |    59 |
| web-qa         | PHP        |       68 |    39 |
| phd            | PHP        |       68 |    44 |
| web-bugs       | PHP        |       58 |    68 |
| presentations  | HTML       |       45 |    27 |
| web-doc-editor | JavaScript |       43 |    37 |
| systems        | C          |       41 |    27 |
| web-wiki       | PHP        |       35 |    29 |
+------------------------------------------------+