peej / tonic
RESTful Web应用PHP微型框架
Requires
- php: >=5.3.0
Requires (Dev)
- behat/behat: 2.*
- henrikbjorn/phpspec-code-coverage: 1.*@dev
- peej/phpdoctor: dev-master
- phpspec/phpspec: 2.0.*@dev
Suggests
- pimple/pimple: A simple dependancy injection container to handle your services
- smarty/smarty: PHP templating engine for building response representations
This package is not auto-updated.
Last update: 2024-09-14 13:20:29 UTC
README
PHP库/框架,用于在尊重RESTful设计5个原则的基础上构建Web应用。
- 为每个“事物”分配一个ID(即URI)
- 将事物链接在一起(HATEOAS)
- 使用标准方法(即标准接口)
- 具有多种表示的资源(即标准文档格式)
- 无状态通信
工作原理
一切皆资源,资源被定义为PHP类。注解将URI与资源关联,将HTTP方法与类方法关联。
/**
* This class defines an example resource that is wired into the URI /example
* @uri /example
*/
class ExampleResource extends Tonic\Resource {
/**
* @method GET
*/
function exampleMethod() {
return new Response(Response::OK, 'Example response');
}
}
类方法可以执行所需的任何逻辑,然后返回一个Response对象、一个包含状态码和响应体的数组,或者只是一个响应体字符串。
如何开始
最佳入门方法是在您的系统上运行hello world示例,为此您需要一个运行PHP5.3+的Web服务器。
安装
安装Tonic最简单的方法是通过Composer,如果您不使用或不熟悉Composer,我建议您阅读有关它的资料。
将Tonic添加到您的composer.json文件中,并运行composer install/update
#composer.json
{
"require": {
"peej/tonic": "3.*"
}
}
$ curl -sS https://getcomposer.org/installer | php
$ php composer.phar install
或者,您可以从GitHub下载Tonic并将其手动放置到您的项目中。
引导
要引导Tonic,请使用提供的web/dispatch.php脚本,并配置您的Web服务器通过提供的.htaccess文件将所有请求推送到它。
为了开发目的,您可以使用PHP的内置Web服务器,运行以下命令
$ php -S 127.0.0.1:8080 vendor/bin/dispatch.php
或者
$ php -S 127.0.0.1:8080 web/dispatch.php
一旦您需要更多,您可以编写自己的具有自定义行为的分发器。
基本原理是创建Tonic\Application的实例,并将Tonic\Request实例传递给它的getResource()方法。然后,传入的请求将与您的资源类之一匹配,执行它,并输出响应。
一个非常基本的最低分发器看起来像这样
require_once '../vendor/autoload.php';
$app = new Tonic\Application(array(
'load' => 'example.php'
));
$request = new Tonic\Request();
$resource = $app->getResource($request);
$response = $resource->exec();
$response->output();
特性
URI注解
资源通过其@uri注解附加到其URL上
/**
* @uri /example
*/
class ExampleResource extends Tonic\Resource { }
除了简单的URI字符串之外,您还可以使用正则表达式,以便资源与一系列URI相关联
/**
* @uri /example/([a-z]+)
*/
class ExampleResource extends Tonic\Resource {
/**
* @method GET
*/
function exampleMethod($parameter) {
...
}
}
URL模板和Rails路由风格的@uri注解也受到支持
/**
* @uri /users/{username}
*/
class ExampleResource extends Tonic\Resource {
/**
* @method GET
*/
function exampleMethod($username) {
...
}
}
/**
* @uri /users/:username
*/
class ExampleResource extends Tonic\Resource {
/**
* @method GET
*/
function exampleMethod($username) {
...
}
}
多个资源可以匹配同一URI,或者同一资源可以有多个URI
/**
* @uri /example
* @uri /example/([a-z]+)
*/
class ExampleResource extends Tonic\Resource { }
/**
* @uri /example/apple
* @priority 2
*/
class AnotherExampleResource extends Tonic\Resource { }
通过使用带数字的@priority注解,在所有匹配的资源中,将使用后缀数字最高的资源。
请求对象
资源方法可以通过请求对象访问传入的HTTP请求。
请求对象公开请求的所有元素,包括HTTP方法、请求数据和内容类型。
请求头可以通过以驼峰命名法命名的公共属性访问。
/**
* @uri /example
*/
class ExampleResource extends Tonic\Resource {
/**
* @method GET
*/
function exampleMethod() {
echo $this->request->userAgent;
}
}
安装点
为了使资源更具可移植性,可以通过提供命名空间名称到URL空间映射来“安装”它们。该命名空间内的每个资源实际上都将具有URL空间前缀附加到其@uri注解。
$app = new Tonic\Application(array(
'mount' => array('myBlog' => '/blog')
));
资源注解缓存
解析资源注解会有性能损失。为了消除这种损失并消除需要预先加载所有资源类的需求(以及允许代码缓存),可以使用缓存来存储资源注解数据。
在构造时将缓存对象传递给Application对象会导致该缓存用于读取和存储资源注释元数据,而不是从源代码令牌中读取。Tonic包含两个缓存类,一个将缓存存储在磁盘上,另一个使用APC数据存储。
然后,而不是显式包含你的资源类文件,Application对象会根据缓存中存储的路径为你加载它们,并忽略“load”配置选项。
$app = new Tonic\Application(array(
'load' => '../resources/*.php', // look for resource classes in here
'cache' => new Tonic\MetadataCacheFile('/tmp/tonic.cache') // use the metadata cache
));
方法条件
可以通过自定义注释将条件添加到方法中,这些注释映射到另一个类的方法。只有当所有条件返回而不抛出Tonic异常时,资源方法才会匹配。
/**
* @method GET
* @hascookie foo
*/
function exampleMethod() {
...
}
function hasCookie($cookieName) {
if (!isset($_COOKIE[$cookieName])) throw new Tonic\ConditionException;
}
基本资源类提供了一些内置的条件。
@priority number Higher priority method takes precident over other matches
@accepts mimetype Given mimetype must match request content type
@provides mimetype Given mimetype must be in request accept array
@lang language Given language must be in request accept lang array
@cache seconds Send cache header for the given number of seconds
您还可以向条件添加代码,以便在资源方法之前和之后执行。例如,您可能希望以可重用的方式对资源方法的请求输入进行JSON解码和对响应输出进行JSON编码。
/**
* @method GET
* @json
*/
function exampleMethod() {
...
}
function json() {
$this->before(function ($request) {
if ($request->contentType == "application/json") {
$request->data = json_decode($request->data);
}
});
$this->after(function ($response) {
$response->contentType = "application/json";
$response->body = json_encode($response->body);
});
}
响应异常
当请求对象和资源对象遇到不想处理的问题时,它们可以抛出Tonic\Exception,并将控制权交还给分发器。
如果您不想在Resource类中处理问题,您可以抛出自己的Tonic\Exception并在分发器中处理它。请参阅auth示例了解如何操作。
贡献
-
在GitHub上分叉代码。
-
使用Composer的--dev选项安装开发依赖项(或者您可以在系统上自行安装PHPSpec和Behat)。
php composer.phar --dev install
-
编写一个spec,然后修改代码使其通过。
-
创建一个pull request。
不喜欢修改代码?那么 在GitHub问题跟踪器中报告您的问题。
有关更多信息,请阅读代码。从“web/dispatch.php”中的分发器和“src/Tyrell”目录中的Hello world开始。
食谱
依赖注入容器
您可能需要一种处理项目依赖项的方法。作为一个轻量级的HTTP框架,Tonic不会为您处理此操作,但它使您能够轻松地连接自己的依赖注入容器(例如,Pimple http://pimple.sensiolabs.org/)。
例如,要构造一个Pimple容器并将其提供给加载的资源,调整您的dispatcher.php如下所示
require_once '../src/Tonic/Autoloader.php';
require_once '/path/to/Pimple.php';
$app = new Tonic\Application();
// set up the container
$app->container = new Pimple();
$app->container['dsn'] = 'mysql://user:pass@localhost/my_db';
$app->container['database'] = function ($c) {
return new DB($c['dsn']);
};
$app->container['dataStore'] = function ($c) {
return new DataStore($c['database']);
};
$request = new Tonic\Request();
$resource = $app->getResource($request);
$response = $resource->exec();
$response->output();
输入处理
尽管Tonic提供了HTTP请求的原始输入数据,但它不会尝试解释这些数据。例如,如果您想将所有传入的JSON数据处理成数组,您可以这样做
require_once '../src/Tonic/Autoloader.php';
$app = new Tonic\Application();
$request = new Tonic\Request();
// decode JSON data received from HTTP request
if ($request->contentType == 'application/json') {
$request->data = json_decode($request->data);
}
$resource = $app->getResource($request);
$response = $resource->exec();
$response->output();
我们还可以以相同的方式自动编码响应
$response = $resource->exec();
// encode output
if ($response->contentType == 'application/json') {
$response->body = json_encode($response->body);
}
$response->output();
RESTful建模
REST系统由单个资源和包含单个资源的集合资源组成。以下是一个实现“object”集合资源和存储在其内的“object”资源的示例
/**
* @uri /objects
*/
class ObjectCollection extends Tonic\Resource {
/**
* @method GET
* @provides application/json
*/
function list() {
$ds = $this->app->container['dataStore'];
return json_encode($ds->fetchAll());
}
/**
* @method POST
* @accepts application/json
*/
function add() {
$ds = $this->app->container['dataStore'];
$data = json_decode($this->request->data);
$ds->add($data);
return new Tonic\Response(Tonic\Response::CREATED);
}
}
/**
* @uri /objects/:id
*/
class Object extends Tonic\Resource {
/**
* @method GET
* @provides application/json
*/
function display() {
$ds = $this->app->container['dataStore'];
return json_encode($ds->fetch($this->id));
}
/**
* @method PUT
* @accepts application/json
* @provides application/json
*/
function update() {
$ds = $this->app->container['dataStore'];
$data = json_decode($this->request->data);
$ds->update($this->id, $data);
return $this->display();
}
/**
* @method DELETE
*/
function remove() {
$ds = $this->app->container['dataStore'];
$ds->delete($this->id);
return new Tonic\Response(Tonic\Response::NOCONTENT);
}
}
处理错误
当发生错误时,Tonic会抛出一个扩展Tonic\Exception类的异常。您可以修改前端控制器以捕获这些异常并处理它们。
$app = new Tonic\Application();
$request = new Tonic\Request();
try {
$resource = $app->getResource($request);
} catch(Tonic\NotFoundException $e) {
$resource = new NotFoundResource($app, $request);
}
try {
$response = $resource->exec();
} catch(Tonic\Exception $e) {
$resource = new FatalErrorResource($app, $request);
$response = $resource->exec();
}
$response->output();
用户认证
需要保护资源?以下是一个不错的模式。
/**
* @uri /secret
*/
class SecureResource extends Tonic\Resource {
/**
* @method GET
* @secure aUser aPassword
*/
function secret() {
return 'My secret';
}
function secure($username, $password) {
if (
isset($_SERVER['PHP_AUTH_USER']) && $_SERVER['PHP_AUTH_USER'] == $username &&
isset($_SERVER['PHP_AUTH_PW']) && $_SERVER['PHP_AUTH_PW'] == $password
) {
return;
}
throw new Tonic\UnauthorizedException;
}
}
$app = new Tonic\Application();
$request = new Tonic\Request();
$resource = $app->getResource($request);
try {
$response = $resource->exec();
} catch(Tonic\UnauthorizedException $e) {
$response = new Tonic\Response(401);
$response->wwwAuthenticate = 'Basic realm="My Realm"';
}
$response->output();
如果您想保护一组资源而不想对它们进行注释,可以将注释添加到父类中,它将继承到重写的子方法中,或者您可以将安全逻辑添加到资源的构造函数中,以便无论注释如何,其所有请求方法都是安全的。
/**
* @uri /secret
*/
class SecureResource extends Tonic\Resource {
private $username = 'aUser';
private $password = 'aPassword';
function setup() {
if (
!isset($_SERVER['PHP_AUTH_USER']) || $_SERVER['PHP_AUTH_USER'] != $this->username ||
!isset($_SERVER['PHP_AUTH_PW']) || $_SERVER['PHP_AUTH_PW'] != $this->password
) {
throw new Tonic\UnauthorizedException;
}
}
/**
* @method GET
*/
function secret() {
return 'My secret';
}
}
响应模板化
使用模板引擎生成输出是一种流行的方法,可以将视图与应用逻辑分离。您可以轻松创建一个条件方法,添加一个过滤器以通过模板引擎(如Smarty或Twig)传递响应。
/**
* @uri /templated
*/
class Templated extends Tonic\Resource {
/**
* @method GET
* @template myView.html
*/
function pretty() {
return new Tonic\Response(200, array(
'title' => 'All you pretty things',
'foo' => 'bar'
));
}
function template($templateName) {
$this->after(function ($response) use ($templateName) {
$smarty = $this->app->smarty;
if (is_array($response->body)) {
$smarty->assign($response->body);
}
$response->body = $smarty->fetch($templateName);
});
}
}
$app = new Tonic\Application();
$app->smarty = new Smarty\Smarty();
$request = new Tonic\Request();
$resource = $app->getResource($request);
$response = $resource->exec();
$response->output();
完整示例
对于完整的项目示例,请查看“example”分支,它是一个包含暴露MySQL数据库表的Tonic项目的孤立分支。