solvire/php-hypermedia-api

PHP Hypermedia 启用 API 包装器,用于 HTTP REST

v0.3 2015-10-02 16:17 UTC

This package is not auto-updated.

Last update: 2024-09-28 18:16:11 UTC


README

仓库: php-hypermedia-api

Build Status Latest Stable Version Scrutinizer Code Quality Build Status License

没有 REST 为你服务!

让我们直说吧。你的 API 很可能不是 RESTful 的。我甚至不再喜欢使用这个术语。

遗憾的是,我不再维护这个项目了。PHP 社区似乎不需要这种结构。

安装

composer require solvire/php-hypermedia-api

兼容性

运行在

  • php 5.5+
  • php 7
  • HHVM

框架

  • Laravel

历史

这项工作最初是为了将 LeadFerret 前端用户 API 带入合规性。这是我几年前开始的项目,但从未有机会完成。深夜和咖啡推动它前进。

哲学

为什么?

"知道该问什么有时比知道答案更重要。" - 聪明的家伙

当你随机访问一个网页时,作者足够关心,会提供一种方式让你导航服务器。大多数 API 只会返回你请求的内容。你必须阅读文档才能知道下一步该去哪里以及该记录实际上代表什么。

超媒体 API

应用程序的根目录 / 应该提供足够的信息,使机器可以自己穿越服务器。

这个库旨在包装一个非特定框架,并提供了易于生成超媒体上下文的实用工具。这个上下文是返回数据的含义。

为了使 REST 有效,它需要以超媒体的形式提供上下文。再次参考 Roy T. Fielding 对这个主题的讨论。

HATEOAS - 超媒体作为应用程序状态的引擎

架构

JSON-Schema

借鉴谷歌开发者可发现性服务,我们选择使用 JSON Schema 定义结构。具体来说,是 JSON Schema 草案 04

拥有一个模式显然有一些好处。验证、客户端生成等。

为了使 JSON Schema 正确运行,响应类型需要被严格定义。没有猜测。

一些功能在某种程度上模仿了 Django Rest Framework (DRF)。主要是它们处理视图的方式,我们称之为 renderers

模式

在创建一个 Web 服务时,默认的模式会从资源根目录返回。这是 JSON-Schema 控制器的一部分,显示了应用级别的一些项目。

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#",
  "basePath": "/public/api",
  "baseUrl": "https://api.yourdomain.com",
  "description": "API Cool End-User API",
  "documentationUrl": "http://docs.domain.apiary.io/#",
  "id": "appname:v1",
  "name": "appname",
  "version": "v1.0"
}

类结构

PHP Hypermedia API

表示控制器

这个对象将是对资源所有数据和上下文的权威回应,与之相连的是渲染器,它们可以展示和处理与数据交互。

渲染器

这是进出数据库的门户。它将数据放入并经过序列化器移动到数据库中。反之亦然。一些也可以仅解析数组以提供静态内容。它们应该是输出无关的。序列化器被附加。

序列化器

在这里定义资源结构。每个字段都有一个对象,具有非常具体的类型。

数据字段

为每个你最喜欢的数据类型都有一个对象。下面是列表。

  • 布尔字段
  • 字符字段
  • 数据字段
  • 数据字段集合
  • 日期字段
  • 日期时间字段
  • 双精度字段
  • 电子邮件字段
  • 文件字段
  • 浮点字段
  • IP地址字段
  • 图片字段
  • 整数字段
  • 列表字段
  • 点字段
  • 只读字段
  • 正则表达式字段
  • 分割点字段
  • 时间字段
  • URL字段
  • UUID字段
  • 电话字段
  • 性别字段(归一化)
  • 变换字段(分形)
  • 序列化字段(递归)

数据字段条件属性

对于每个数据字段,都有一组属性会影响序列化器的功能。大多数属性指的是可能发生的转换,而一些是验证器或过滤器。

只读

readOnly

被标记为'readOnly'=>true的字段可能无法写入。序列化器应在确定数据写入时忽略此字段。

这类字段的例子包括日期字段或账户状态字段。

默认:false

只写

writeOnly

被标记为'writeOnly'=>true的字段不会显示给最终用户。它们将被写入,并且可以应用过滤器或验证器。

这类字段的例子包括密码或积分增加。

默认:false

必填字段

required

如果字段被标记为'required'=>true,则在更新期间字段不存在时系统会抛出错误。将此设置为false是不必要的,因为这是默认设置。

默认:false

允许空值

allowNull

如果字段被标记为'allowNull'=>true,序列化器将允许您传递包含字段的null值。记录代理也应尊重此请求,并将数据字段设置为null。

默认:false

允许空字符串

allowEmpty

如果字段被标记为'allowEmpty'=>true,序列化器将允许您传递空字符串''。记录代理也应尊重此请求,并将数据字段设置为空。请注意,null和空字符串不是同一个概念。它们应该被区别对待。

默认:false

默认值

defaultValue

很多时候,您可能想要设置一个默认值,以便如果数据不在数据库中或未传递,它已经设置好了。'defaultValue'=>'something'

默认:null

列名 - 别名

API响应值的一个大问题是设置键并来回转换。设置列名将有助于可读性和可用性。'columnName'=>'mapped_column'。例如,您的用户不需要知道主键是CompanyNameID。'id'就足够了。

示例:主键、敏感名称、难看的名称

默认:null - 使用您的数据库列名

验证器

这尚未实现,但您可以假设我们将允许传递验证器的链。我们可能会标准化,但这还没有发生。欢迎提出建议。

输出特定属性

样式

这尚未实现,但您可能可以传递与输出相关的样式。

帮助文本

对于表单字段和UI组件,提供有关其的信息可能对人类界面有所帮助。尚未实现。

序列化器示例

这是一个公司记录序列化器的示例。它扩展了LaravelModelSerializer,可以显式与Laravel模型接口。在某个时候,我们将与symfony集成,但不是现在。

use Solvire\API\Serializers\DataFields\CharField;
use Solvire\API\Serializers\DataFields\BooleanField;
use Solvire\API\Serializers\DataFields\IPAddressField;
use Solvire\API\Serializers\DataFields\DateTimeField;
use Solvire\API\Serializers\DataFields\EmailField;
use Solvire\API\Serializers\DataFields\IntegerField;
use Solvire\API\Serializers\LaravelModelSerializer;
use Solvire\API\Serializers\DataFields\SplitPointField;


/**
 *
 * @author solvire <info@scotttactical.com>
 * @package Serializers
 * @namespace App\Http\Controllers\API\Serializers
 */
class CompanySerializer extends LaravelModelSerializer
{

    public function initDataFields()
    {

        $this->addField('id', new IntegerField(['readOnly'=>true, 'columnName'=>'ComapnyID']))
             ->addField('name', new CharField(['columnName'=>'Company_name']))
             ->addField('address', new CharField(['columnName'=>'Address']))
             ->addField('city', new CharField(['columnName'=>'City']))
             ->addField('state', new CharField(['columnName'=>'State']))
             ->addField('zip', new CharField(['columnName'=>'Zip']))
             ->addField('county', new CharField())
             ->addField('area_code', new IntegerField(['columnName'=>'AreaCode']))
             ->addField('phone', new CharField(['columnName'=>'Comp_phone']))
             ->addField('domain', new CharField())
             ->addField('sic', new CharField(['columnName'=>'SIC']))
             ->addField('revenue', new IntegerField(['columnName'=>'Rev']))
             ->addField('employees', new IntegerField(['columnName'=>'Emps']))
             ->addField('exp_id', new IntegerField(['writeOnly'=>true,'columnName'=>'ExpID']))
             ->addField('naics', new IntegerField(['columnName'=>'NAICS']))
             ->addField('year_founded', new IntegerField())
             ->addField('f1000', new IntegerField())
             ->addField('alexa', new IntegerField())
             ->addField('fbpage', new CharField())
             ->addField('opt_out', new BooleanField(['writeOnly'=>true]))
             ->addField('location', new SplitPointField(['latitudeColumn' => 'latitude', 'longitudeColumn' => 'longitude', 'allowNull' => true]));
    }


}

这里有一个健康检查序列化器的示例。由于这只是心跳响应,我们实际上不需要与数据库接口。它不需要数据绑定。在这种情况下,我们只是扩展了ArraySerializer。基本上只输出,尽管我们可能会找到一些输入用例。

use Solvire\API\Serializers\ArraySerializer;
use Solvire\API\Serializers\DataFields\CharField;
use Solvire\API\Serializers\DataFields\BooleanField;
use Solvire\API\Serializers\DataFields\IPAddressField;
use Solvire\API\Serializers\DataFields\DateTimeField;

/**
 *
 * @author solvire <info@scotttactical.com>
 * @package Serializers
 * @namespace LeadFerret\Http\Controllers\API\Serializers
 */
class HealthSerializer extends ArraySerializer
{

    public function initDataFields()
    {
        $this->addField('status', new CharField());
        $this->addField('server_data', new CharField());
        $this->addField('ip', new IPAddressField());
        $this->addField('you', new CharField());
        $this->addField('stage', new CharField());
        $this->addField('alive', new BooleanField());
        $this->addField('timestamp', new DateTimeField());
    }

}

服务器响应将类似于以下内容。它应该将字段格式化为它们正确的数据类型。未来将为各种字段添加更多灵活性。例如,设置时区和添加翻译等功能。

{
    "alive": true,
    "ip": "192.168.56.1",
    "server_data": "LF API",
    "stage": "development",
    "status": "OK",
    "timestamp": {
        "date": "2015-09-25 22:23:51.000000",
        "timezone": "UTC",
        "timezone_type": 3
    },
    "you": "HTTPie/0.9.2"
}

我在这里包含了一个类,其中包含两种数据字段类型的示例:TransformerField 和 SerializerField。如您预期,它们执行复杂的嵌套序列化。以下类下方是一个示例输出。

use Solvire\API\Serializers\DataFields\CharField;
use Solvire\API\Serializers\DataFields\BooleanField;
use Solvire\API\Serializers\DataFields\DateTimeField;
use Solvire\API\Serializers\DataFields\IntegerField;
use Solvire\API\Serializers\DataFields\PhoneField;
use Solvire\API\Serializers\LaravelModelSerializer;
use Solvire\API\Serializers\DataFields\SerializerField;
use Solvire\API\Serializers\DataFields\TransformerField;
use Solvire\API\Serializers\DataFields\SplitPointField;
use LeadFerret\Models\Transformers\API\DepartmentToCategoryTransformer;
use Solvire\API\Serializers\DataFields\GenderField;


/**
 * Serialize a contact object
 *
 * @author solvire <info@scotttactical.com>
 * @package Serializers
 * @namespace LeadFerret\Http\Controllers\API\Serializers
 */
class ContactSerializer extends LaravelModelSerializer
{

    /**
     * (non-PHPdoc)
     * @see \Solvire\API\Serializers\BaseSerializer::initDataFields()
     */
    public function initDataFields()
    {

        // the serializer field takes a closure
        $company = function ($model) {
            return $model->company;
        };

        $getModel = function ($model){
            return $model;
        };

        $this->addField('id', new IntegerField(['readOnly'=>true, 'columnName'=>'ContactID']))
             ->addField('first_name', new CharField(['columnName'=>'first_name']))
             ->addField('last_name', new CharField(['columnName'=>'last_name']))
             ->addField('title', new CharField(['columnName'=>'user_title']))
             ->addField('phone', new PhoneField(['columnName'=>'contact_phone']))
             ->addField('created', new DateTimeField(['columnName'=>'created_on','format'=>'Y-m-d H:i:s']))
             ->addField('score', new IntegerField())
             ->addField('crowd_score', new IntegerField())
             ->addField('title_level', new CharField(['columnName'=>'title_levels']))
             ->addField('gender', new GenderField(['maleValue'=>'Male','femaleValue'=>'Female'])) // normalized gender values
             ->addField('ads_status', new IntegerField(['writeOnly'=>true]))
             ->addField('useragent', new CharField())
             ->addField('device', new CharField(['writeOnly'=>true,'columnName'=>'uadevice']))
             ->addField('opt_out', new BooleanField(['writeOnly'=>true]))
             ->addField('company', new SerializerField( ['serializer'=>new CompanySerializer(), 'callback' => $company] ))
             ->addField('professional_categories', new TransformerField(['transformer'=> (new DepartmentToCategoryTransformer()), 'callback' => $getModel ]));
    }
}

序列化字段基本上是一个经过测试的数据字段,用于引入已定义的序列化器。

转换器可以将您所需格式的数据进行转换。实际上,我有一个独特的案例,我必须将许多字段组合在一起来创建数据,因此我在闭包中传递了模型。

输出结果如下

{
    "currentItemCount": 1,
    "items": [
        {
            "company": {
                "address": "4209 Technology Drive",
                "alexa": 0,
                "area_code": 510,
                "city": "Fremont",
                "county": "Alameda",
                "domain": "123.com",
                "employees": 15,
                "f1000": 0,
                "fbpage": "",
                "id": 38252,
                "location": {
                    "latitude": 37.520000457764,
                    "longitude": -121.95999908447
                },
                "naics": 0,
                "name": "3PARdata, Inc.",
                "phone": "510-413-5999",
                "revenue": 35,
                "sic": "3572",
                "state": "CA",
                "year_founded": 1999,
                "zip": "94538"
            },
            "created": {
                "date": "2015-09-30 06:06:55.000000",
                "timezone": "UTC",
                "timezone_type": 3
            },
            "crowd_score": 0,
            "first_name": "Jeff",
            "gender": "male",
            "id": 12756,
            "last_name": "Song",
            "phone": "510-413-1234",
            "professional_categories": [
                "engineering",
                "photographer"
            ],
            "score": 38,
            "title": "Vice President of Engineering and Founder",
            "title_level": "VP",
            "useragent": "Chrome"
        }
    ],
    "itemsPerPage": "50",
    "pageIndex": 1,
    "totalItems": 1,
    "totalPages": 1
}