灵活资源包 / activeresource
使用基于RESTful资源API的数据库
Requires
- php: >=7.3|^8.0
- guzzlehttp/guzzle: ^6.2
- optimus/onion: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9
- vimeo/psalm: ^4.11
README
PHP ActiveResource实现,不依赖于任何框架。
使用基于RESTful资源API的数据库,类似于ActiveRecord模式。
作者说明
这个项目开始是因为我似乎找不到一个好的、维护良好且易于使用的基于PHP的ActiveResource包。尽管它仍然处于初级阶段,但我已经在两个不同的项目中使用了它,与两个完全不同的API交互:一个由我构建和维护,另一个是第三方。
我希望你会觉得它和我一样有用。
如果您有任何建议或潜在的功能请求,请随时通过brent@brentscheffler.com联系我
安装
composer require nimbly/activeresource
快速入门
此快速入门指南假设API
- 接受并响应JSON(application/json)
- 使用HTTP响应代码来指示响应状态(200 OK,404 Not Found,400 Bad Request等)
- 基于资源 - 您与名词(资源)交互,而不是动词
如果您正在使用的API没有这些假设,没关系,只需确保阅读完整配置选项、自定义Response
和Error
类以及中间件的文档。
创建连接
$options = [ Connection::OPTION_BASE_URI => 'https://someapi.com/v1/', Connection::OPTION_DEFAULT_HEADERS => [ 'Authorization' => 'Bearer MYAPITOKEN', ] ]; $connection = new Connection($options);
将连接添加到ConnectionManager
ConnectionManager::add('default', $connection);
创建您的模型
use ActiveResource\Model; /** * Because the class name is "Users", ActiveResource assumes the API endpoint for this resource is "users". */ class Users extends Model { /** * The single user object can be found at $.data.user * * Sample response payload: * * { * "data": { * "user": { * "id": "1", * "name": "Foo Bar", * "email": "foo@bar.com" * } * } * } * * */ protected function parseFind($payload) { return $payload->data->user; } protected function parseAll($payload) { return $payload->data->users; } } /** * Because the class name is "Posts", ActiveResource assumes the API endpoint for this resource is "posts". */ class Posts extends Model { // Manually set the endpoint for this resource. Now ActiveResource will hit "blogs" when making calls // from this model. protected $resourceName = 'blogs'; /** * A blog post has an author object embedded in the response that * maps to a User object. */ protected function author($data) { return $this->includesOne(Users::class, $data); } protected function parseFind($payload) { return $payload->data->post; } protected function parseAll($payload) { return $payload->data->posts; } } class Comments extends Model { /** * A comment has an author object embedded in the response that * maps to a User object. */ protected function author($data) { return $this->includesOne(Users::class, $data); } protected function parseFind($payload) { return $payload->data->comment; } protected function parseAll($payload) { return $payload->data->comments; } }
使用您的模型
$user = new User; $user->name = 'Brent Scheffler'; $user->email = 'brent@brentscheffler.com'; $user->save(); $post = new Posts; $post->title = 'Blog post'; $post->body = 'World\'s shortest blog post'; $post->author_id = $user->id; $post->save(); // Update the author (user) $post->author->email = 'brent@nimbly.io'; // Oops, save failed... Wonder what happened. if( $post->author->save() == false ) { // Looks like that email address is already being used $code = $post->getResponse()->getStatusCode(); // 409 $error = $post->getResponse()->getStatusPhrase(); // Conflict } // Get user ID=1 $user = User::find(1); // Update the user $user->status = 'inactive'; $user->save(); // Get all the users $users = Users::all(); // Pass in some query params to find only active users $users = Users::all(['status' => 'active']); // Delete user ID=1 $user->destroy(); // Get the response code $statusCode = $user->getResponse()->getStatusCode(); // 204 No Content // Pass in a specific header for this call $post = Posts::all([], ['X-Header-Foo' => 'Bar']); // Get blog post ID=1 $post = Posts::find(1); // Get all comments through Posts resource. The effective query would be GET#/blogs/1/comments $comments = Comments::allThrough($post); // Or... $comments = Comments::allThrough("blogs/1");
这就完了
ActiveResource的使用就是这样。希望您的API主要是RMM Level 2,这使得配置变得容易。
配置
ActiveResource允许您在代码中连接到任意数量的RESTful API。
- 创建
Connection
实例 - 使用
ConnectionManager::add
分配名称并将Connection
添加到其连接池。
连接
创建一个新的Connection
实例,表示与API的连接。构造函数接受两个参数
选项
选项数组可能包含
defaultUri
字符串 每个请求前缀的默认URI。例如:http://some.api.com/v2/
defaultHeaders
数组 键 => 值对的头部信息,用于每个请求。
defaultQueryParams
数组 键 => 值对,用于每个请求的URL查询部分。
defaultContentType
字符串 默认Content-Type请求头部,用于包含消息体的请求(PUT,POST,PATCH)。默认为application/json
。一些常见的MIME类型字符串在Connection
类上作为类常量可用。
responseClass
字符串 用于解析包括头部和主体的响应的响应类名称。默认为ActiveResource\Response
类。有关更多信息,请参阅“响应”部分。
collectionClass
字符串 用于处理响应中返回的模型数组的集合类名称。集合类必须在其构造函数中接受数据数组。默认为ActiveResource\Collection
类。将其设置为null
以返回一个简单的PHP模型实例数组。
此选项对于您正在使用具有像Laravel的Illuminate\Support\Collection
这样的强大集合实用程序的框架、库或自定义代码非常有用。
updateMethod
字符串 用于更新的HTTP方法。默认为put
。
updateDiff
布尔值 ActiveResource是否可以在更新时仅发送资源的修改字段。
middleware
数组 要执行的中介类数组。有关更多信息,请参阅中介部分。
log
布尔值 告诉ActiveResource记录所有请求和响应。默认为false
。在生产环境中不要使用此选项。您可以通过ConnectionManager的getLog()方法访问日志。
所有上述字符串选项均可在Connection
类上的类常量中找到。
HttpClient
GuzzleHttp\Client
的可选实例。如果您不提供实例,将自动创建一个没有设置选项的实例。
示例
$options = [ Connection::OPTION_BASE_URI => 'http://api.someurl.com/v1/', Connection::OPTION_UPDATE_METHOD => 'patch', Connection::OPTION_UPDATE_DIFF => true, Connection::OPTION_RESPONSE_CLASS => \My\Custom\Response::class, Connection::OPTION_MIDDLEWARE => [ \My\Custom\Middleware\Authorize::class, \My\Custom\Middleware\Headers::class, ] ]; $connection = new \ActiveResource\Connection($options);
ConnectionManager
使用ConnectionManager::add
添加一个或多个Connection实例。这允许您在代码中使用任意数量的API。如果您主要与单个API交互,可以将名称设置为default
,而无需在每个模型上指定连接名称。
如果您确实需要与多个API交互,请确保给它们不同的连接名称。您可能需要创建一个具有connectionName属性的抽象BaseModel,并从BaseModel扩展您的实际模型。
示例
ConnectionManager::add('yourConnectionName', $connection);
响应
尽管ActiveResource附带了一个基本的Response类(该类只是对响应体进行JSON解码),但每个API都以其独特的负载和编码进行响应,因此建议您提供自己的自定义响应类,该类扩展\ActiveResource\ResponseAbstract
。请参阅连接选项responseClass
。
必需方法实现
decode
接受响应的原始负载内容。应返回表示数据的数组或\StdClass对象。有关详细信息,请参阅预期数据格式。
isSuccessful
应返回一个布尔值,指示请求是否成功。一些API不遵循严格的REST模式,可能会对所有请求返回HTTP状态码200。在这种情况下,负载中通常有一个属性指示请求是否成功。
Response对象也是一个很好的方式来包含任何其他方法来访问与负载无关的数据或头信息。这完全取决于您正在工作的API的响应体中的数据。
示例
class Response extends \ActiveResource\ResponseAbstract { public function decode($payload) { return json_decode($payload); } public function isSuccessful() { return $this->getStatusCode() < 400; } public function getMeta() { return $this->getPayload()->meta; } public function getEnvelope() { return $this->getPayload()->envelope; } }
预期数据格式
为了使ActiveResource正确地填充您的Model实例,解码后的响应负载必须格式化为以下模式
{ "property1": "value", "property2": "value", "property3": "value", "related_single_resource": { "property1": "value", "property2": "value" }, "related_multiple_resources": [ { "property1": "value", "property2": "value" } ] }
示例
{ "id": "1234", "title": "Blog post", "body": "This is a blog post", "author": { "id": "32135", "name": "John Doe", "email": "jdoe@example.com" }, "comments": [ { "id": "18319", "body": "This is a comment", "author": { "id": "49913", "name": "Jane Doe", "email": "jane.doe@example.com" } }, { "id": "18320", "body": "This is another comment", "author": { "id": "823194", "name": "Thomas Quigley", "email": "tquigley@example.com" } } ] }
如果您正在使用的API的数据格式不是这样的,您需要将其转换成这样的格式。这可以在(并且应该)您的Response
类的decode
方法中完成。
模型
创建您的模型类,并从\ActiveResource\Model
扩展它们。
属性
connectionName
要使用的连接名称。默认为default
。
resourceName
API资源URI的名称。默认为类的名称的小写。
resourceIdentifier
要用作ID的属性名称。默认为id
。
readOnlyProperties
只读属性名称数组。当设置为null或空数组时,所有属性都是可写的。
fillableProperties
当设置为属性名称数组时,只有这些属性在调用fill()方法时允许进行批量分配。如果为null,则所有属性都可以进行批量分配。
excludedProperties
在保存/更新模型到API时排除的属性名称数组。如果为null或空数组,则在保存模型时发送所有属性。
静态方法
find
根据其ID查找资源的单个实例。假设负载将返回单个对象。
all
获取资源的所有实例。假设负载将返回一个对象数组。
delete
根据其ID销毁(删除)资源。
findThrough
通过另一个资源查找资源。例如,如果您需要通过其帖子 /posts/1234/comments/5678
获取评论。
allThrough
通过另一个资源获取资源的所有实例。例如,如果您需要通过其帖子 /posts/1234/comments
获取评论。
connection
获取模型的 Connection
实例。
request
获取最后一个请求对象。
response
获取最后一个响应对象。
实例方法
fill
使用键/值对数组批量分配对象属性。
save
保存或更新实例。
destroy
销毁(删除)实例。
getConnection
获取模型的 Connection
实例。
getRequest
获取最后一个请求的 Request
对象。
getResponse
获取最后一个请求的 Response
对象。
includesOne
通知模型类响应包含另一个模型类的单个实例。然后 ActiveResource 将创建该模型的实例并用数据填充。
includesMany
通知模型类响应包含另一个模型类的实例数组。然后 ActiveResource 将创建一个填充了模型实例的集合。
parseFind
通知模型类在响应有效载荷中查找单个资源的数据位置。该方法在调用 find
和 findThrough
静态方法以及 save
实例方法时调用。该 parseFind
方法接受一个包含解码有效载荷的单个参数,并应返回一个包含实例数据的对象或关联数组。如果您没有在模型上指定此方法,ActiveResource 将传递完整有效载荷以填充模型。除非您正在使用的 API 在响应根中返回所有相关数据,否则您 必须 实现此方法。有关更多信息,请参阅期望数据格式。
parseAll
通知模型类在响应有效载荷中查找资源数组的数据库。该方法在调用 all
和 allThrough
静态方法时调用。该方法接受一个包含解码响应有效载荷的单个参数,并应返回一个包含实例数据的对象或关联数组。如果您没有在模型上指定此方法,ActiveResource 将传递完整有效载荷以填充模型。除非您正在使用的 API 在响应根中返回所有相关数据,否则您 必须 实现此方法。有关更多信息,请参阅期望数据格式。
encode
在发送请求之前调用,用于格式化和编码模型实例为请求正文。默认为 json_encode()。如果您需要为正在使用的 API 不同的格式或编码,则应覆盖此方法。
reset
将模型的状态重置为其原始状态 - 即所有已修改的属性都会被撤销。
original
返回属性的原始值。
您还可以定义与模型将发送数据的实例属性同名的方法。然后,您可以修改数据或更常见的是,创建一个表示数据的新的模型实例。
例如,假设您正在与一个具有博客帖子、用户和评论的博客 API 进行交互。您创建了三个模型类来表示 API 资源。
用户
class Users extends \ActiveResource\Model { }
评论
class Comments extends \ActiveResource\Model { public function author($data) { return $this->includesOne(Users::class, $data); } }
帖子
class Posts extends \ActiveResource\Model { public function author($data) { return $this->includesOne(Users::class, $data); } public function comments($data) { return $this->includesMany(Comments::class, $data); } /** * You can find the blog post data in $.data.post in the payload */ protected function parseFind($payload) { return $payload->data->post; } /** * You can find the collection of post data in $.data.posts in the payload */ protected function parseAll($payload) { return $payload->data->posts; } }
现在获取博客帖子编号 7。
$posts = Posts::find(7);
API 的响应如下所示
{ "data": { "post": { "id": 7, "title": "Blog post", "body": "I am a short blog post", "author": { "id": 123, "name": "John Doe", "email": "jdoe@example.com" }, "created_at": "2016-12-03 15:36:12", "comments": [ { "id": 8, "body": "Great article!", "author": { "id": 567, "name": "Thomas Quigley", "email": "tquigley@example.com" }, "created_at": "2016-12-04 09:18:45" }, { "id": 9, "body": "Love the way your write", "author": { "id": 4178, "name": "Jane Johnson", "email": "jjohnson@example.com" }, "created_at": "2016-12-04 11:29:18" } ] } } }
ActiveResource 将自动为 Posts 实例上的评论和作者(用户)填充模型实例。然后,可以修改并更新这些实例或甚至删除它们。
中间件
ActiveResource 中的中间件由出色的 Onion 包管理 - "一个没有依赖项的独立中间件库"。
您的中间件类必须实现 Onion 的 LayerInterface 类并实现 peel
方法。
输入对象是一个 ActiveResource\Request
实例。输出是一个 ActiveResource\ResponseAbstract
实例。
示例
class Authorize implements LayerInterface { /** * * @param \ActiveResource\Request $object */ public function peel($object, \Closure $next) { // Add a query param to the URL (&foo=bar) $object->setQuery('foo', 'bar'); // Do some HMAC authorization logic here // ... // ... // Now add the HMAC headers $object->setHeader('X-Hmac-Timestamp', $timestamp); $object->setHeader('Authorization', "HMAC {$hmac}"); // Send the request off to the next layer $response = $next($object); // Now let's slip in a spoofed header into the response object $response->setHeader('X-Spoofed-Response-Header', 'Foo'); // How about we completely change the response status code? $response->setStatusCode(500); // Return the response return $response; } }
日志记录
您可以通过在 Connection
上启用 log
选项来激活每个 ActiveResource 调用的请求和响应日志记录。要访问日志数据,请调用连接上的 getLog
方法。由于内存占用和安全原因,不要 在生产环境中使用日志记录。
示例
$connection = new Connection([ Connection::OPTION_BASE_URI => 'https://someurl.com/v1/', Connection::OPTION_LOG => true, ]); ConnectionManager::add('yourConnectionName', $connection); $post = Post::find(12); $connection = ConnectionManager::get('yourConnectionName'); $log = $connection->getLog(); // Or... $post->getConnection()->getLog(); // Or... Post::connection()->getLog();
快速入门示例
查找单个资源
$user = User::find(123);
获取所有资源
$users = User::all();
创建新资源
$user = new User; $user->name = 'Test User'; $user->email = 'test@example.com'; $user->save();
更新资源
$user = User::find(123); $user->status = 'INACTIVE'; $user->save();
快速分配属性
$user = User::find($id); $user->fill([ 'name' => 'Buckley', 'email' => 'buckley@example.com', ]); $user->save();
销毁(删除)资源
$user = User::find($id); $user->destory(); // Or... User::delete($id);
常见问题解答
我该如何在每次请求中都发送一个授权头?
如果授权方案是 Basic 或 Bearer,在创建连接对象时,在 defaultHeaders
选项数组中添加头是最简单的方法。
示例
$options = [ Connection::OPTION_BASE_URI => 'http://myapi.com/v2/', Connection::OPTION_DEFAULT_HEADERS => [ 'Authorization' => 'Bearer MYAPITOKEN', ], ]; $connection = new Connection($options);
对于稍微复杂一些的授权方案(例如 HMAC),请使用中间件方法。有关更多信息,请参阅中间件部分。
我正在处理的 API 响应负载中的所有数据都返回在同一个根路径下。我是否真的需要在每个模型上都有 parseFind 和 parseAll 方法?
不需要。创建一个具有 parseFind
和 parseAll
方法的抽象基类 BaseModel。然后从该 BaseModel 扩展所有您的模型。
我该如何处理 JSON-API 响应?
在您的 Response
对象的 decode
方法中,您需要做很多工作,但这可以完成。ActiveResource 正在寻找解码后的有效载荷数据具有特定的格式。有关更多信息,请参阅预期数据格式。对于需要以 JSON-API 格式的请求,您需要在模型的 encode
方法中做大量工作。
我该如何访问响应对象以提取头信息、状态码或解析和错误负载?
您可以通过模型实例方法 getResponse
访问最后 API 请求的 Response
对象。响应对象具有检索响应头、状态和主体的方法。或者,您也可以通过模型的 response
静态方法静态地访问响应对象。
我该如何在特定的 HTTP 响应代码上抛出异常?
Response
对象有一个名为 throwable
的受保护数组属性。默认情况下,HTTP 状态 500 会抛出 ActiveResourceResponseException
。您可以在您的 Response
类中用任何您想要的 HTTP 状态代码集覆盖此数组。或者将其设置为空数组以 永不 抛出异常。
包括超时在内的连接问题 总是 抛出 GuzzleHttp\Exception\ConnectException
。
我正在工作的 API 有一个端点,它根本不符合 ActiveResource 模式,我该如何调用该端点?
您可以通过获取 Connection
对象实例并使用 buildRequest
和 send
方法来发送自定义请求。
$connection = ConnectionManager::get('yourConnectionName'); $request = $connection->buildRequest('post', '/some/oddball/endpoint', ['param1' => 'value1'], ['foo' => 'bar', 'fox' => 'sox'], ['X-Custom-Header', 'Foo']); $response = $connection->send($request);
您将获得一个 ResponseAbstract
对象的实例。