peej/tonic

RESTful Web应用PHP微型框架

维护者

详细信息

github.com/peej/tonic

主页

源代码

问题

安装次数: 141 304

依赖项: 4

建议者: 0

安全: 0

星级: 624

关注者: 50

分支: 127

开放问题: 36

v3.3 2015-02-18 19:42 UTC

This package is not auto-updated.

Last update: 2024-09-14 13:20:29 UTC


README

PHP库/框架,用于在尊重RESTful设计5个原则的基础上构建Web应用。

  • 为每个“事物”分配一个ID(即URI)
  • 将事物链接在一起(HATEOAS)
  • 使用标准方法(即标准接口)
  • 具有多种表示的资源(即标准文档格式)
  • 无状态通信

有关更多信息,请参阅Tonic网站.

工作原理

一切皆资源,资源被定义为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示例了解如何操作。

贡献

  1. 在GitHub上分叉代码。

  2. 使用Composer的--dev选项安装开发依赖项(或者您可以在系统上自行安装PHPSpec和Behat)。

    php composer.phar --dev install

  3. 编写一个spec,然后修改代码使其通过。

  4. 创建一个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项目的孤立分支。