fcn从头开始_从头开始构建REST API-实施

fcn从头开始

We ended the first part of this tutorial with all the basic layers of our API in place. We have our server setup, authentication system, JSON input/output, error management and a couple of dummy routes. But, most importantly, we wrote the README file that defines resources and actions. Now it’s time to deal with these resources.

在本教程的第一部分中 ,我们已经完成了API的所有基本层。 我们拥有服务器设置,身份验证系统,JSON输入/输出,错误管理和一些虚拟路由。 但是,最重要的是,我们编写了定义资源和操作的README文件。 现在该处理这些资源了。

创建和更新联系人 (Creating and updating contacts)

We have no data right now, so we can start with contact creation. Current REST best practices suggest that create and update operations should return a resource representation. Since the core of this article is the API, the code that deals with the database is very basic and could be done better. In a real world application you probably would use a more robust ORM/Model and validation library.

我们目前没有数据,因此我们可以开始创建联系人。 当前的REST最佳实践建议创建和更新操作应返回资源表示形式 。 由于本文的核心是API,因此处理数据库的代码非常基础,可以做得更好。 在实际的应用程序中,您可能会使用更强大的ORM /模型和验证库。

$app->post(
'/contacts',
function () use ($app, $log) {

$body = $app->request()->getBody();

$errors = $app->validateContact($body);

if (empty($errors)) {
$contact = \ORM::for_table('contacts')->create();

if (isset($body['notes'])) {
$notes = $body['notes'];
unset($body['notes']);
}

$contact->set($body);

if (true === $contact->save()) {

// Insert notes
if (!empty($notes)) {
$contactNotes = array();
foreach ($notes as $item) {
$item['contact_id'] = $contact->id;
$note = \ORM::for_table('notes')->create();
$note->set($item);
if (true === $note->save()) {
$contactNotes[] = $note->asArray();
}
}
}

$output = $contact->asArray();
if (!empty($contactNotes)) {
$output['notes'] = $contactNotes;
}
echo json_encode($output, JSON_PRETTY_PRINT);
} else {
throw new Exception("Unable to save contact");
}

} else {
throw new ValidationException("Invalid data", 0, $errors);
}
}
);

We are in the /api/v1 group route here, dealing with the /contacts resource with the POST method. First we need the body of the request. Our middleware ensures that it is a valid JSON or we would not be at this point in the code. The method $app->validateContact() ensures that the provided data is sanitized and performs basic validation; it makes sure that we have at least a first name and a unique valid email address. We can reasonably think that the JSON payload could contain both contact and notes data, so I’m processing both. I’m creating a new contact, with my ORM specific code, and in case of success I insert the linked notes, if present. The ORM provides me with objects for both contact and notes containing the ID from the database, so finally I produce a single array to encode in JSON. The JSON_PRETTY_PRINT option is available from version 5.4 of PHP, for older version you can ask Google for a replacement.

我们在/api/v1组路由中,使用POST方法处理/contacts资源。 首先,我们需要请求的主体。 我们的中间件确保它是有效的JSON,否则我们现在不在代码中。 方法$app->validateContact()确保提供的数据被清理并执行基本验证; 这样可以确保我们至少有一个名字和一个唯一的有效电子邮件地址。 我们可以合理地认为JSON有效负载可以同时包含联系数据和便笺数据,因此我正在对两者进行处理。 我正在使用我的ORM特定代码创建一个新联系人,如果成功,请插入链接的注释(如果有)。 ORM为我提供了联系人和包含数据库ID的便笺的对象,因此最终我生成了一个数组以JSON编码。 JSON_PRETTY_PRINT选项可从PHP 5.4版本开始使用,对于较旧的版本,您可以要求Google进行替换。

The code for updating a contact is pretty similar, the only differences are that we are testing the existence of the contact and notes before processing data, and the validation differs slightly.

更新联系人的代码非常相似,唯一的区别是我们在处理数据之前正在测试联系人和注释的存在,并且验证略有不同。

$contact = \ORM::forTable('contacts')->findOne($id);
if ($contact) {
$body = $app->request()->getBody();
$errors = $app->validateContact($body, 'update');

// other stuff here...
}

We can optimize further by mapping the same code to more than one method, for example I’m mapping the PUT and PATCH methods to the same code:

我们可以通过将同一代码映射到多个方法来进一步优化,例如,我将PUTPATCH方法映射到同一代码:

$app->map(
'/contacts/:id',
function ($id) use ($app, $log) {

// Update code here...

)->via('PUT', 'PATCH');

列出联系人 (Listing contacts)

Now that we have some contacts in our database it’s time to list and filter. Let’s start simple:

现在我们的数据库中已有一些联系人,是时候列出和过滤了。 让我们开始简单:

// Get contacts
$app->get(
'/contacts',
function () use ($app, $log) {

$contacts = array();
$results = \ORM::forTable('contacts');

$contacts = $results->findArray();
echo json_encode($contacts, JSON_PRETTY_PRINT);
}
);

The statement that retrieves the data depends on your ORM. Idiorm makes it simple and returns an associative array or an empty one, that is encoded in JSON and displayed. In case of an error or exception, the JSON middleware that we wrote earlier catches the exception and converts it into JSON. But let’s complicate it a bit…

检索数据的语句取决于您的ORM。 Idiorm使其变得简单,并返回一个关联数组或一个空数组,该数组以JSON编码并显示出来。 如果发生错误或异常,我们之前编写的JSON中间件将捕获该异常并将其转换为JSON。 但是让我们复杂一点……

字段,过滤器,排序和搜索 (Fields, filters, sorting and searching)

A good API should allow to us to limit the fields retrieved, sort the results, and use basic filters or search queries. For example, the URL:

一个好的API应该允许我们限制检索到的字段,对结果进行排序,并使用基本的过滤器或搜索查询。 例如,URL:

/api/v1/contacts?fields=firstname,email&sort=-email&firstname=Viola&q=vitae

Should return all the contacts named “Viola” where the first name OR email address contains the string vitae, they should be ordered by alphabetically descending email address (-email) and I want only the firstname and email fields. How do we do this?

应该返回所有名称为“ Viola”的联系人,其中名字OR电子邮件地址包含字符串vitae ,他们应该按字母降序排列的电子邮件地址( -email )进行排序,而我只需要firstnameemail字段。 我们如何做到这一点?

$app->get(
'/contacts',
function () use ($app, $log) {

$contacts = array();
$filters = array();
$total = 0;

// Default resultset
$results = \ORM::forTable('contacts');

// Get and sanitize filters from the URL
if ($rawfilters = $app->request->get()) {
unset(
$rawfilters['sort'],
$rawfilters['fields'],
$rawfilters['page'],
$rawfilters['per_page']
);
foreach ($rawfilters as $key => $value) {
$filters[$key] = filter_var($value, FILTER_SANITIZE_STRING);
}

}

// Add filters to the query
if (!empty($filters)) {
foreach ($filters as $key => $value) {
if ('q' == $key) {
$results->whereRaw(
'(`firstname` LIKE ? OR `email` LIKE ?)',
array('%'.$value.'%', '%'.$value.'%')
);
} else {
$results->where($key,$value);
}
}

}

// Get and sanitize field list from the URL
if ($fields = $app->request->get('fields')) {
$fields = explode(',', $fields);
$fields = array_map(
function($field) {
$field = filter_var($field, FILTER_SANITIZE_STRING);
return trim($field);
},
$fields
);
}

// Add field list to the query
if (is_array($fields) && !empty($fields)) {
$results->selectMany($fields);
}

// Manage sort options
if ($sort = $app->request->get('sort')) {
$sort = explode(',', $sort);
$sort = array_map(
function($s) {
$s = filter_var($s, FILTER_SANITIZE_STRING);
return trim($s);
},
$sort
);
foreach ($sort as $expr) {
if ('-' == substr($expr, 0, 1)) {
$results->orderByDesc(substr($expr, 1));
} else {
$results->orderByAsc($expr);
}
}
}

// Pagination logic
$page = filter_var(
$app->request->get('page'),
FILTER_SANITIZE_NUMBER_INT
);
if (!empty($page)) {

$perPage = filter_var(
$app->request->get('per_page'),
FILTER_SANITIZE_NUMBER_INT
);
if (empty($perPage)) {
$perPage = 10;
}

// Total after filters and
// before pagination limit
$total = $results->count();

// Pagination "Link" headers go here...

$results->limit($perPage)->offset($page * $perPage - $perPage);
}

$contacts = $results->findArray();

// ORM fix needed
if (empty($total)) {
$total = count($contacts);
}
$app->response->headers->set('X-Total-Count', $total);

echo json_encode($contacts, JSON_PRETTY_PRINT);
}
);

First I define a default result set (all contacts), then I extract the full query string parameters into the $rawfilters array, unsetting the keys fields, sort, page and per_page, I’ll deal with them later. I sanitize keys and values to obtain the final $filters array. The filters are then applied to the query using the ORM specific syntax. I do the same for the field list and sort options, adding the pieces to our result set query. Only then I can run the query with findArray() and return the results.

首先,我定义一个默认结果集(所有联系人),然后将完整的查询字符串参数提取到$rawfilters数组中, $rawfilters设置键fieldssortpageper_page ,稍后再处理。 我清理键和值以获得最终的$filters数组。 然后使用特定于ORM的语法将过滤器应用于查询。 我对字段列表和排序选项执行相同的操作,将片段添加到结果集查询中。 只有这样,我才能使用findArray()运行查询并返回结果。

分页逻辑和标题 (Pagination logic and headers)

It’s a good idea to provide a way to limit the returned data. In the code above I provide the page and per_page parameters. After validation they can be passed to the ORM to filter the results:

提供一种限制返回数据的方法是一个好主意。 在上面的代码中,我提供了pageper_page参数。 验证之后,可以将它们传递到ORM以过滤结果:

$results->limit($perPage)->offset(($page * $perPage) - $perPage);

Before that I obtain a count of the total results, so I can set the X-Total-Count HTTP header. Now I can compute the Link header to publish the pagination URLs like this:

在此之前,我获得了总结果的计数,因此可以设置X-Total-Count HTTP标头。 现在,我可以计算Link标头来发布分页网址,如下所示:

Link: <https://mycontacts.dev/api/v1/contacts?page=2&per_page=5>; rel="next",<https://mycontacts.dev/api/v1/contacts?page=20&per_page=5>; rel="last"

The pagination URLs are calculated using the actual sanitized parameters:

使用实际的已清理参数计算分页URL:

$linkBaseURL = $app->request->getUrl()
. $app->request->getRootUri()
. $app->request->getResourceUri();

// Adding fields
if (!empty($fields)) {
$queryString[] = 'fields='
. join(
',',
array_map(
function($f){
return urlencode($f);
},
$fields
)
);
}

// Adding filters
if (!empty($filters)) {
$queryString[] = http_build_query($filters);
}

// Adding sort options
if (!empty($sort)) {
$queryString[] = 'sort='
. join(
',',
array_map(
function($s){
return urlencode($s);
},
$sort
)
);
}

if ($page < $pages) {
$next = $linkBaseURL . '?' . join(
'&',
array_merge(
$queryString,
array(
'page=' . (string) ($page + 1),
'per_page=' . $perPage
)
)
);
$links[] = sprintf('<%s>; rel="next"', $next);
}

First I calculate the current base URL for the resource, then I add the fields, filters and sort options to the query string. In the end I build the full URLs by joining the pagination parameters.

首先,我计算资源的当前基本URL,然后将字段,过滤器和排序选项添加到查询字符串中。 最后,我通过加入分页参数来构建完整的URL。

联系方式和自动加载 (Contact details and autoloading)

At this point fetching the details of a single contact is really easy:

此时,获取单个联系人的详细信息确实很容易:

$app->get(
'/contacts/:id',
function ($id) use ($app, $log) {

// Validate input code here...

$contact = \ORM::forTable('contacts')->findOne($id);
if ($contact) {
echo json_encode($contact->asArray(), JSON_PRETTY_PRINT);
return;
}
$app->notFound();
}
);

We try a simple ORM query and encode the result, if any, or a 404 error. But we could go further. For contact creation, it’s reasonable enough that we may want the contact and the notes, so instead of making multiple calls we can trigger this option using query string parameters, for example:

我们尝试一个简单的ORM查询并对结果进行编码(如果有)或404错误。 但是我们可以走得更远。 对于创建联系人,我们可能需要联系人和便笺是足够合理的,因此,除了拨打多个电话外,我们还可以使用查询字符串参数来触发此选项,例如:

https://mycontacts.dev/api/v1/contacts/1?embed=notes

We can edit the code to:

我们可以将代码编辑为:

// ...
if ($contact) {

$output = $contact->asArray();

if ('notes' === $app->request->get('embed')) {
$notes = \ORM::forTable('notes')
->where('contact_id', $id)
->orderByDesc('id')
->findArray();

if (!empty($notes)) {
$output['notes'] = $notes;
}
}

echo json_encode($output, JSON_PRETTY_PRINT);
return;
}
// ...

If we have a valid contact and an embed parameter that requests the notes, we run another query, searching for linked notes, in reverse order by ID (or date or whatever we want). With a full featured ORM/Model structure we could, and should, make a single query to our database, in order to improve performance.

如果我们有一个有效的联系人和一个请求注释的embed参数,我们将运行另一个查询,以相反的顺序按ID(或日期或所需的日期)搜索链接的注释。 使用功能齐全的ORM /模型结构,我们可以并且应该对我们的数据库进行单个查询,以提高性能。

快取 (Caching)

Caching is important for our application’s performance. A good API should at least allow client side caching using the HTTP protocol’s caching framework. In this example I’ll use ETag and in addition to this we will add a simple internal cache layer using APC. All these features are powered by a middleware. A year ago Tim wrote about Slim Middleware here on Sitepoint, coding a Cache Middleware as example. I’ve expanded his code for our API\Middleware\Cache object. The middleware is added the standard way during our applications’s bootstrap phase:

缓存对于我们应用程序的性能很重要。 一个好的API至少应允许使用HTTP协议的缓存框架进行客户端缓存。 在此示例中,我将使用ETag ,此外,我们还将使用APC添加一个简单的内部缓存层。 所有这些功能都由中间件提供支持。 一年前, Tim在Sitepoint上写了有关Slim中间件的文章,以缓存中间件为例。 我已经为我们的API\Middleware\Cache对象扩展了他的代码。 在我们应用程序的引导阶段,以标准方式添加了中间件:

$app->add(new API\Middleware\Cache('/api/v1'));

The Cache constructor accepts a root URI as a parameter, so we can activate the cache from /api/v1 and its subpaths in the main method.

Cache构造函数接受根URI作为参数,因此我们可以在main方法中从/api/v1及其子路径激活缓存。

public function __construct($root = '')
{
$this->root = $root;
$this->ttl = 300; // 5 minutes

}

We also set a default TTL of 5 minutes, that can be overridden later with the $app->config() utility method.

我们还将默认TTL设置为5分钟,以后可以使用$app->config()实用程序方法将其覆盖。

// Cache middleware
public function call()
{
$key = $this->app->request->getResourceUri();
$response = $this->app->response;

if ($ttl = $this->app->config('cache.ttl')) {
$this->ttl = $ttl;
}

if (preg_match('|^' . $this->root . '.*|', $key)) {

// Process cache here...

}

// Pass the game...
$this->next->call();
}

The initial cache key is the resource URI. If it does not match with our root we pass the action to the next middleware. The next crossroad is the HTTP method: we want to clean the cache on update methods (PUT, POST and PATCH) and read from it on GET requests:

初始缓存键是资源URI。 如果它与我们的根目录不匹配,则将操作传递给下一个中间件。 下一个十字路口是HTTP方法:我们要使用更新方法(PUT,POST和PATCH)清除缓存,并在GET请求中从中读取:

$method = strtolower($this->app->request->getMethod());
if ('get' === $method) {
// Process cache here...
} else {
if ($response->status() == 200) {
$response->headers->set(
'X-Cache',
'NONE'
);
$this->clean($key);
}
}

If a successful write action has been performed we clean the cache for the matching key. Actually the clean() method will clean all objects whose key starts with $key. If the request is a GET the cache engine starts working.

如果执行了成功的写操作,我们将清除高速缓存中的匹配密钥。 实际上, clean()方法将清除所有键以$key开头的对象。 如果请求是GET,则缓存引擎开始工作。

if ('get' === $method) {
$queryString = http_build_query($this->app->request->get());
if (!empty($queryString)) {
$key .= '?' . $queryString;
}

$data = $this->fetch($key);
if ($data) {

// Cache hit... return the cached content
$response->headers->set(
'Content-Type',
'application/json'
);
$response->headers->set(
'X-Cache',
'HIT'
);
try {

$this->app->etag($data['checksum']);
$this->app->expires($data['expires']);
$response->body($data['content']);
} catch (\Slim\Exception\Stop $e) {
}
return;
}

// Cache miss... continue on to generate the page
$this->next->call();

if ($response->status() == 200) {

// Cache result for future look up
$checksum = md5($response->body());
$expires = time() + $this->ttl;

$this->save(
$key,
array(
'checksum' => $checksum,
'expires' => $expires,
'content' => $response->body(),
)
);

$response->headers->set(
'X-Cache',
'MISS'
);
try {
$this->app->etag($checksum);
$this->app->expires($expires);
} catch (\Slim\Exception\Stop $e) {
}
return;
}

} else {
// other methods...
}

First I’m computing the full key, it is the resource URI including the query string, then I search for it in the cache. If there are cached data (cache hit) they are in the form of an associative array made by expiration date, md5 checksum and actual content. The first two values are used for the Etag and Expires headers, the content fills the response body and the method returns. In Slim the $app->etag() method takes care of the headers of type If-None-Match from the client returning a 304 Not Modified status code.

首先,我要计算完整键,它是包含查询字符串的资源URI,然后在缓存中搜索它。 如果存在缓存的数据(缓存命中),它们将是由到期日期,md5校验和和实际内容组成的关联数组的形式。 前两个值用于EtagExpires标头,内容填充响应主体,方法返回。 在Slim中, $app->etag()方法处理来自客户端的If-None-Match类型的标头,并返回304 Not Modified状态码。

If there are no cached data (cache miss) the action is passed to the other middleware and the response is processed normally. Our cache middleware is called again before rendering (like an onion, remember?), this time with the processed response. If the final response is valid (status 200) it gets saved in the cache for reuse and then sent to the client.

如果没有缓存的数据(缓存未命中),则将操作传递给其他中间件,并正常处理响应。 我们的缓存中间件在渲染之前被再次调用(就像洋葱一样,还记得吗?),这次是处理后的响应。 如果最终响应有效(状态200),它将保存在缓存中以备重用,然后发送给客户端。

REST速率限制 (REST Rate limit)

Before it’s too late we should have a way to limit clients’ calls to our API. Another middleware comes to our help here.

在为时已晚之前,我们应该有一种方法可以限制客户对我们API的调用。 另一个中间件在这里为我们提供帮助。

$app->add(new API\Middleware\RateLimit('/api/v1'));
public function call()
{
$response = $this->app->response;
$request = $this->app->request;

if ($max = $this->app->config('rate.limit')) {
$this->max = $max;
}

// Activate on given root URL only
if (preg_match('|^' . $this->root . '.*|', $this->app->request->getResourceUri())) {

// Use API key from the current user as ID
if ($key = $this->app->user['apikey']) {

$data = $this->fetch($key);
if (false === $data) {

// First time or previous perion expired,
// initialize and save a new entry

$remaining = ($this->max -1);
$reset = 3600;

$this->save(
$key,
array(
'remaining' => $remaining,
'created' => time()
),
$reset
);
} else {

// Take the current entry and update it

$remaining = (--$data['remaining'] >= 0)
? $data['remaining'] : -1;

$reset = (($data['created'] + 3600) - time());

$this->save(
$key,
array(
'remaining' => $remaining,
'created' => $data['created']
),
$reset
);
}

// Set rating headers

$response->headers->set(
'X-Rate-Limit-Limit',
$this->max
);

$response->headers->set(
'X-Rate-Limit-Reset',
$reset
);

$response->headers->set(
'X-Rate-Limit-Remaining',
$remaining
);

// Check if the current key is allowed to pass
if (0 > $remaining) {

// Rewrite remaining headers
$response->headers->set(
'X-Rate-Limit-Remaining',
0
);

// Exits with status "429 Too Many Requests" (see doc below)
$this->fail();
}

} else {
// Exits with status "429 Too Many Requests" (see doc below)
$this->fail();
}

}

$this->next->call();

}

We allow for a root path to be passed and, like the cache middleware, we can set other parameters like rate.limit from our application config. This middleware uses the $app->user context created by the authentication layer; the user API key is used as key for APC cache. If we don’t find data for the given key we generate them: I’m storing the remaining calls, the timestamp of creation of the value, and I give it a TTL of an hour. If there are data in the APC I recalculate the remaining calls and save the updated values.

我们允许传递根路径,并且像缓存中间件一样,我们可以从应用程序配置中设置其他参数,例如rate.limit 。 该中间件使用认证层创建的$app->user上下文; 用户API密钥用作APC缓存的密钥。 如果找不到给定键的数据,则会生成它们:我将存储其余的调用,值创建的时间戳,并将其设置为一个小时的TTL。 如果APC中有数据,我将重新计算剩余的调用并保存更新的值。

Then I’m setting the X-Rate-Limit–* headers (it’s a convention, not a standard) and if the user has no remaining calls left I’m resetting X-Rate-Limit-Remaining to zero and fail with a 429 Too Many Requests status code. There’s a little workaround here: I cannot use Slim’s $app->halt() method to output the error because Apache versions prior to 2.4 don’t support the status code 429 and convert it silently into a 500 error. So the middleware uses its own fail() method:

然后,我设置X-Rate-Limit–*标头(这是一个约定,不是标准的),如果用户没有剩余的通话,我会将X-Rate-Limit-Remaining重置为零,并以429 Too Many Requests失败429 Too Many Requests状态代码。 这里有一些解决方法:我无法使用Slim的$app->halt()方法来输出错误,因为2.4之前的Apache版本不支持状态代码429并将其静默地转换为500错误。 因此,中间件使用其自己的fail()方法:

protected function fail()
{
header('HTTP/1.1 429 Too Many Requests', false, 429);

// Write the remaining headers
foreach ($this->app->response->headers as $key => $value) {
header($key . ': ' . $value);
}
exit;
}

The method outputs a raw header to the client and, since the standard response flow is interrupted, it outputs all the headers that were previously generated by the response.

该方法将原始标头输出到客户端,并且由于标准响应流被中断,因此该方法将输出先前由响应生成的所有标头。

我们从这里去哪里? (Where do we go from here?)

We’ve covered a lot of stuff here, and we have our basic API that respects common best practices, but there are still many improvements we can add. For example:

我们在这里介绍了很多内容,并且我们拥有尊重通用最佳实践的基本API,但是仍然可以添加许多改进。 例如:

  • use a more robust ORM/Model for data access

    使用更强大的ORM /模型进行数据访问
  • use a separate validation library that injects into the model

    使用注入模型的单独的验证库
  • use dependency injection to take advantage of other key/value storage engines instead of APC

    使用依赖注入来利用其他键/值存储引擎而不是APC
  • build a discovery service and playground with Swagger and similar tools

    使用Swagger和类似工具构建发现服务和游乐场

  • build a test suite layer with Codeception

    使用Codeception构建测试套件层

The full source code can be found here. As always I encourage you to experiment with the sample code to find and, hopefully, share your solutions. Happy coding!

完整的源代码可以在这里找到。 与往常一样,我鼓励您尝试使用示例代码来查找并希望共享您的解决方案。 祝您编码愉快!

翻译自: https://www.sitepoint.com/build-rest-api-scratch-implementation/

fcn从头开始

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值