如何更好的封装PHP SDK

2,783 阅读4分钟

什么是sdk

软件开发工具包Software Development Kit, SDK)一般是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等创建应用软件的开发工具的集合。

通常意义的sdk

在web开发领域,我们接触的sdk通常是为了调用开放接口而封装好的公司外的第三方库。比如,各大互联网公司在几年前都做了开发者平台

Github: GitHub API v3

Weibo: API - 微博API

Twitter: dev.twitter.com/

Instagram: www.instagram.com/developer/

开放接口特点

可以理解为,为了调用开放接口,sdk就是为了方便调用才封装好了一些方法和工具。我们可以尝试总结下我们接触的开放接口的特点:

  1. 基于 http, 这种方式是最简单的,跨平台和编程语言的数据交换方式;
  2. 使用 GET / POST 请求方式,传递数据,并且一般都有接入方的凭证,如 TOKEN SIGN 等;
  3. 传递参数不定,请求前都需要进行整理格式化,比如,加入请求凭证 或 json encode 亦或 加密;
  4. 返回数据需要重新整理才能供内部使用,比如是否成功判断,字段名转换;

场景特点传达的几个信息

  1. 我们作为调用方,此时相当于客户端的角色,要请求接口;
  2. 对于调用方的凭证或隐私信息保存要灵活配置,因为可能接口有沙箱环境,那么相关配置如调用方id, key 或私钥文件等都不同;
  3. PHP大法好?!,强大的 array 类型,一下子打包就 post 过去,虽然这样最简单省事,但是这样可读性并不好;提倡把接口数据抽象成一个对象,一个接口对应一个类,利用 传输对象模式
  4. 对软件架构中的防腐层设计有一个概念,系统内部和外部交互进行字段转换,起码字段转换相关逻辑一定单独做;

编码的原则

  • 可读性
  • 可扩展性
  • 避免魔法值,就是写死的数字、字符等,该做常量就做常量,该做成配置就单独抽出来做成配置;

实现

创建接口请求对象的抽象类

<?php

/**
 * Class AbstractRequestBody
 */
abstract class AbstractRequestBody
{
    /**
     * @var string $path
     * 因为每个接口的路径不同,所以需要每个接口都显示的指定请求路径
     */
    protected $path;

    /**
     * @var array $data
     * 我们把每个接口的业务参数最后统一收纳在data数组中
     */
    protected $data = [];

    /**
     * AbstractRequestBody constructor.
     */
    public function __construct()
    {
        $this->setPath();
    }

    /**
     * @return mixed
     * 假如有必要,每个接口都可以进行一些自定义的参数校验
     */
    abstract public function validate();

    /**
     * @param $response
     *
     * @return mixed
     */
    abstract public function transfer(&$response);

    /**
     * @return string
     * 最后发起请求的时候获取接口路径
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * @return mixed
     * 抽象化对象,强制要求每个接口类必须设定接口路径
     */
    abstract protected function setPath();

    /**
     * @return array
     * 需要打包的一些公共参数
     */
    public function package(): array
    {
        $data = $this->getData();
        $params['data'] = json_encode($data, 320);
        $params['version'] = Constants::VERSION;
        $params['nonce_str'] = Utils::getNonceStr();
        $params['sign'] = Utils::sign($params);

        return $params;
    }

    protected function getData()
    {
        return $this->data;
    }
}

OneApi 接口请求类

<?php

/**
 * Class OneApi
 */
class OneApi extends AbstractRequestBody
{
    public function setOneParam($value)
    {
        $this->data['one_param'] = $value;
        
        // 为实现可链式调用
        return $this;
    }

    public function setTwoParam($value)
    {
         $this->data['two_param'] = $value;
        
          // 为实现可链式调用
        return $this;
    }

    /**
     * ...更多参数
     */

     /**
     * 自定义的参数验证
     */
    public function validate()
    {
        // TODO: Implement validate() method.
    }

     /**
     * 字段转换
     */
    public function transfer(&$response)
    {
        // TODO: Implement transfer() method.
    }

    protected function setPath()
    {
        $this->path = '/path/api';
    }
}

工具类

<?php

/**
 * Class Utils
 */
class Utils
{
    /**
     * @param int $length
     * @return string
     */
    public static function getNonceStr()
    {
    }

    /**
     * 签名算法
     * 可能要需要用到key 或 私钥文件
     * @param array $params
     * @return string
     */
    public static function sign(array $params): string
    {
    }

    /**
     * 验证接口返回数据
     * @param array $params
     * @return bool
     */
    public static function verifySign(array $params): bool
    {
    }
}

常量类

如果常量比较多,建议根据功能分类做成多个文件。

<?php

/**
 * Class Constants
 */
class Constants
{
    const DOMAIN_PROD = 'https://open.api.com';
    const VERSION = '1.0.0';
    const SIGN_TYPE = 'MD5';

    /**
     * 接口其他相关常量
     */

}

配置

这里只做简单示例,配置也可以做成 return array() 的形式,然后加载,或者类似 Laravel 利用 env 函数从环境变量取;

有一个原则是:代码配置安全的检测标准之一是代码库是否可立即开源。

<?php

/**
 * Class Config
 */
class Config
{
    const SIGN_MD5_KEY = '1111111111111';
    const SERVICE_ID = '1213444543545';
}

SdkClient类

<?php

/**
 * Class SdkClient
 */
class SdkClient
{
    /**
     * @param AbstractRequestBody $requestBody
     * @param int $timeout
     * @return string
     */
    public function send($requestBody, int $timeout = 15)
    {
        try {
            // 校验参数
            $requestBody->validate();

            // 拼凑url
            $url = Constants::DOMAIN_PROD . $requestBody->getPath();

            // 获取请求参数
            $data = $requestBody->package();

            // 发送请求并经过一层统一转换
            $response = $this->response(
                $this->post(
                    $url,
                    $data,
                    $timeout
                )
            );
            // 若比较特殊的格式转换,可以在各自请求类中进行响应过滤,响应数据是传引用的,直接处理即可
            $requestBody->transfer($response);
            
        } catch (\Exception $exception) {
            // TODO: 异常处理 $response = ...
        }
        return $response;
    }

    /**
     * 对返回数据统一处理,数据过滤
     * 比如转换成数组,还是对象,如果是对象可以new Response,在构造函数处理
     * @param string $content
     * @return string
     */
    private function response(string $content)
    {
        return $content;
    }

    /**
     * TODO: 这里亦可根据需求,替换成项目可用的http包
     * @param string $url
     * @param array $fields
     * @param int $timeout
     * @return bool|string
     */
    private function post(string $url, array $fields, int $timeout = 10)
    {
        $headers = ['Content-Type: application/json;charset=UTF-8',];
        $ch = curl_init();
        array_push($headers, "Expect:");
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
           // ...
        
        return curl_exec($ch);
    }
}

调用示例

<?php

class Demo
{
    private $sdk_client;

    public function __construct()
    {
        $this->sdk_client = new SdkClient();
    }

    public function testOneApi()
    {
   
        $request = new OneApi();
        $request->setOneParam('3333')
            ->setTwoParam('1111');
        
        print_r($this->sdk_client->send($request));
    }
}

工程目录参考

之前封装了调用新支付开放平台的sdk,工程目录作为参考

image.png

反思

这种封装方式适用在那些场景?

总结这个封装套路是在接了几个支付渠道后,不断尝试总结出来的;并且有借鉴Java中面向对象的设计思路;
特别适合业务参数比较多、接口比较多、逻辑复杂、签名严格的场景下封装抽象出这几个类。

假如参数比较少请求比较简单,比如 GET 请求一两个参数,还没有特别复杂的签名算法的情况下,这种显然是不适合的,这只会徒增工作量。

以上代码只是作为一个参考,尝试总结封装的套路,还有细节和不完善的地方。

扩展

前段时间按照这些思路封装了一个钉钉聊天机器人sdk,可以作为参考,欢迎 star 或 下载 composer require baiyutang/dingtalk-chatbot