# 扩展包开发

扩展包是对软件功能的扩充,下面我们开发一个多网关上传扩展包,目前支持阿里云 OSS 上传。

本项目地址:https://github.com/houdunwang/uploader (opens new window)

向军大叔每晚八点在 抖音 (opens new window)bilibli (opens new window) 直播

xj-small

# 软件特点

  • 支持多网关处理业务
  • 提供 provider 与 facade 支持,完美集成 Laravel 框架
  • 单元测试全覆盖,保证代码健壮
  • 发布到 Github 与 packagist.org 开源共享
  • 使用简单可快速集成到项目中

# 基础构建

# 创建目录

$ mkdir uploader

# composer 配置

$ cd uploader
$ composer init # 一直回车
This command will guide you through creating your composer.json config.

Package name (<vendor>/<name>) [xj/uploader]: houdunwang/uploader
Description []:
Author [houdunwang <2300071698@qq.com>, n to skip]:
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []:
License []:

Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]?
Search for a package:
Would you like to define your dev dependencies (require-dev) interactively [yes]?
Search for a package:

{
    "name": "houdunwang/uploader",
    "authors": [
        {
            "name": "houdunwang",
            "email": "2300071698@qq.com"
        }
    ],
    "require": {},
}

Do you confirm generation [yes]?

说明

# houdunwang为我Github帐号,你要填写你的Github库名称
Author [houdunwang <2300071698@qq.com>, n to skip]: 你Github帐号
剩下一直回车即可

# 文件结构

├── config			# 配置文件目录
├── composer.json
├── phpunit.xml		# 单元测试配置文件
├── src				# 软件代码
│   └── Exceptions  # 异常处理
│   └── Services  # 处理服务类
│   └── ServiceProvider  # Provider
└── tests			# 测试代码

设置单元测试配置文件

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"
>
    <testsuites>
        <testsuite name="Prettus Repository Test Suite">
            <directory suffix=".php">./tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>

# 配置站点

下面是我的 homestead 的配置,如果你使用 wamp 等集成开发环境可以省略这一步。

sites:
    - map: uploader.test
      to: /home/vagrant/code/components/uploader

配置 host 文件

192.168.10.10   uploader.test

# 安装依赖包

安装以下两个依赖包用于软件的单元测试

$ composer require phpunit/phpunit mockery/mockery
$ composer require aliyuncs/oss-sdk-php
  • phpunit/phpunit mockery/mockery 用于单元测试的组件
  • aliyuncs/oss-sdk-php 阿里云提供的 OSS 上传库

# 注册

下面提供两种方式将扩展包注册到系统

# autoload

下面使用 autoload 自动加载进行注册,修改 composer.json 设置自动加载

...
"autoload": {
	"psr-4": {
		"Houdunwang\\Uploader\\": "src/"
	}
}
...

然后访问 config/app.php 注册扩展包的 provider

'providers' => [
    Houdunwang\\Uploader\\ServiceProvider::class,
]

# repositories

在系统的 composer.json 中定义本地仓库,具体使用也可参考composer (opens new window) 文档

  • 不需要在config/app.php 中注册 provider
{
  ...
  "repositories": {
      "uploader": {
          "type": "path",
          "url": "packages/uploader",
          "options": {
              "syslink": true
          }
      }
  },
  "require": {
        ...
        "houdunwang/uploader": "@dev"
    },
  ...
}

然后执行命令安装本地扩展

composer install

# 业务实现

# 软件配置

配置文件定义在 config/uploader.php,内容如下:

<?php
return [
    'oss' => [
        'accessKeyId' => '',
        'accessKeySecret' => '',
        'bucket' => '',
        'endpoint' => '',
    ],
];

# 异常处理

异常处理用于处理运行错误,比如用户参数错误、Http 处理错误等。下面是我们定义的异常处理类

src
│   ├── Exceptions
│   │   ├── Exception.php	# 基础异常
│   │   ├── InvalidParamException.php # 参数错误异常
│   │   └── ServerDisposeException.php # 服务网关异常(如调用阿里oss服务异常)

下面是各个异常类

namespace Houdunwang\Uploader\Exceptions;
class Exception extends \Exception
{

}
namespace Houdunwang\Uploader\Exceptions;
class InvalidParamException extends Exception
{

}
namespace Houdunwang\Uploader\Exceptions;
class ServerDisposeException extends Exception
{

}

# 业务代码

扩展包入口类

因为上传支持多个服务,我们使用 Uploader 类统一调用处理。

<?php
/** .-------------------------------------------------------------------
 * |    Author: 向军 <www.aoxiangjun.com>
 * | Copyright (c) 2012-2019, www.houdunren.com. All Rights Reserved.
 * '-------------------------------------------------------------------*/

namespace Houdunwang\Uploader;

use Houdunwang\Uploader\Exceptions\InvalidParamException;
use Houdunwang\Uploader\Services\OssServer;

class Uploader
{
    protected $config;

    /**
     * 服务列表
     * @var array
     */
    protected $servers = [
        'oss' => OssServer::class,
    ];

    public function config(array $config): Uploader
    {
        $this->config = $config;
        return $this;
    }

    /**
     * 上传处理
     * @param string $file
     * @param string $service
     * @return string 文件
     * @throws InvalidParamException
     */
    public function upload(string $file, string $service='oss'): string
    {
        if (!is_string($file) || !is_file($file)) {
            throw new InvalidParamException('invalid file param');
        }
        if (!in_array($service, ['oss', 'local'])) {
            throw new ServerDisposeException('service dones not exists' . $service);
        }
        try {
            $serverInstance = new $this->servers[$service];
            return $serverInstance->config($this->config[$service])->upload($file);
        } catch (\Exception $e) {
            throw new \Exception($e->getMessage(), $e->getCode(), $e);
        }
    }
}

OSS 服务

<?php
/** .-------------------------------------------------------------------
 * |  Software: [hdcms framework]
 * |      Site: www.hdcms.com
 * |-------------------------------------------------------------------
 * |    Author: 向军 <www.aoxiangjun.com>
 * |    WeChat: houdunren2018
 * |      Date: 2018/11/12
 * | Copyright (c) 2012-2019, www.houdunren.com. All Rights Reserved.
 * '-------------------------------------------------------------------*/

namespace Houdunwang\Uploader\Services;

use Houdunwang\Uploader\Exceptions\HttpException;
use Houdunwang\Uploader\Exceptions\InvalidParamException;
use OSS\OssClient;

class OssServer implements ServerInterface
{
    protected $config;

    /**
     * 设置配置
     * @param array $config
     * @return OssServer
     * @throws InvalidParamException
     */
    public function config(array $config): ServerInterface
    {
        if (empty($config['accessKeyId']) || empty($config['accessKeySecret']) || empty($config['bucket']) || empty($config['endpoint'])) {
            throw new InvalidParamException('server param invalid');
        }
        $this->config = $config;
        return $this;
    }

    /**
     * OSS服务
     * @return OssClient
     * @throws \OSS\Core\OssException
     */
    public function getHttpClient()
    {
        return new OssClient($this->config['accessKeyId'], $this->config['accessKeySecret'], $this->config['endpoint']);
    }

    /**
     * 执行上传
     * @param string $file
     * @return string
     * @throws HttpException
     * @throws InvalidParamException
     */
    public function upload(string $file): string
    {
        if (!is_string($file) || !is_file($file)) {
            throw new InvalidParamException($file . ' is not a file');
        }
        try {
            $res = $this->getHttpClient()->uploadFile($this->config['bucket'], $this->getFileName($file), $file);
            return $res['oss-request-url'];
        } catch (\Exception $e) {
            throw new HttpException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * 随机文件名
     * @param string $file
     * @return string
     */
    public function getFileName(string $file): string
    {
        $extension = substr($file, strrpos($file, '.'));
        return md5($file) . time() . $extension;
    }
}

# Laravel 集成

为了更好在 Laravel 框架中使用,我们需要添加 providerFacade 支持,并在 Laravel 框架的 config 目录中生成扩展包配置文件。

# composer.json

首先需要在 composer.json 文件中定义 providerfacade 配置项

{
   ...
    "extra": {
        "laravel": {
            "providers": [
                "Houdunwang\\Uploader\\ServiceProvider"
            ],
            "aliases": {
                "Uploader": "Houdunwang\\Uploader\\Facade"
            }
        }
    },
    ...
}

# Provider

创建 src/ServiceProvider 服务文件

<?php
/** .-------------------------------------------------------------------
 * |    Author: 向军 <www.aoxiangjun.com>
 * |    WeChat: houdunren2018
 * |      Date: 2018/11/12
 * | Copyright (c) 2012-2019, www.houdunren.com. All Rights Reserved.
 * '-------------------------------------------------------------------*/

namespace Houdunwang\Uploader;

class ServiceProvider extends \Illuminate\Support\ServiceProvider
{
    protected $defer = true;

    /**
     * 服务引导方法
     *
     * @return void
     */
    public function boot(): void
    {
        //发布配置文件到项目的 config 目录中
        $this->publishes([
            __DIR__ . '/config/uploader.php' => config_path('uploader.php'),
        ]);
    }

    /**
     * 注册服务
     */
    public function register(): void
    {
        $this->app->singleton(Uploader::class, function () {
            return new Uploader();
        });
    }
}

# Facade

创建 src/Facade外观文件(方便在 Laravel 中以像 DB::table()形式使用扩展包)

<?php
/** .-------------------------------------------------------------------
 * |    Author: 向军 <www.aoxiangjun.com>
 * |    WeChat: houdunren2018
 * |      Date: 2018/11/12
 * | Copyright (c) 2012-2019, www.houdunren.com. All Rights Reserved.
 * '-------------------------------------------------------------------*/

namespace Houdunwang\Uploader;

use Illuminate\Support\Facades\Facade as LaravelFacade;

class Facade extends LaravelFacade
{
    /**
     * 获取组件的注册名称。
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return Uploader::class;
    }
}

# 单元测试

使用单元测试程序的稳定性是软件开发中必不可少的环节。

# 命名规范

  • 测试用例目录要与项目目录结构一致
  • 测试用户类名为:项目类名+Test.php
  • 测试用例要继承 PHPUnit\Framework\TestCase
  • 测试用户方法为:test+项目类方法

# 单元测试

对 OssServer 服务类测试

<?php
/** .-------------------------------------------------------------------
 * |    Author: 向军 <www.aoxiangjun.com>
 * |    WeChat: houdunren2018
 * |      Date: 2018/11/12
 * | Copyright (c) 2012-2019, www.houdunren.com. All Rights Reserved.
 * '-------------------------------------------------------------------*/

use PHPUnit\Framework\TestCase;
use Houdunwang\Uploader\Services\OssServer;
use Mockery\Matcher\AnyArgs;
use Houdunwang\Uploader\Exceptions\InvalidParamException;

class OssServerTest extends TestCase
{
    public function testConfig()
    {
        $this->expectException(InvalidParamException::class);
        $this->expectExceptionMessage('server param invalid');
        $oss = new OssServer();
        $oss->config([
            'accessKeyIda' => 'test',
            'accessKeySecret' => 'test',
            'bucket' => 'test',
            'endpoint' => 'test',
        ]);
        $this->fail('config param exception fail');
    }

    public function testGetFileName()
    {
        $oss = Mockery::mock(OssServer::class)->makePartial();
        $this->assertStringEndsWith('.jpeg', $oss->getFileName('a.jpeg'));
    }

    public function testUploadParamFile()
    {
        $oss = \Mockery::mock(OssServer::class)->makePartial();
        $this->expectException(InvalidParamException::class);
        $this->expectExceptionMessage('a.jpeg is not a file');
        $oss->upload('a.jpeg');
        $this->fail('ossClient request param invalid');
    }

    public function testUpload()
    {
        $client = \Mockery::mock(\OSS\OssClient::class);
        $client->allows()->uploadFile(new AnyArgs())->andReturn([
            'oss-request-url' => __FILE__,
        ]);
        $oss = \Mockery::mock(OssServer::class)->makePartial();
        $oss->allows()->getHttpClient()->andReturn($client);
        return $this->assertSame(__FILE__, $oss->upload(__FILE__));
    }
}

对扩展包入口类测试

<?php
/** .-------------------------------------------------------------------
 * |    Author: 向军 <www.aoxiangjun.com>
 * |    WeChat: houdunren2018
 * |      Date: 2018/11/12
 * | Copyright (c) 2012-2019, www.houdunren.com. All Rights Reserved.
 * '-------------------------------------------------------------------*/

use PHPUnit\Framework\TestCase;
use Houdunwang\Uploader\Exceptions\InvalidParamException;

class UploaderTest extends TestCase
{
    public function testUploadParamException()
    {
        $uploader = new \Houdunwang\Uploader\Uploader([]);
        $this->expectException(InvalidParamException::class);
        $this->expectExceptionMessage('invalid file param');
        $uploader->upload('test.php', 'oss');

        $this->fail('server param exception');

    }
}

测试结果:

➜  uploader git:(master) ✗ phpunit
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.....                                                               5 / 5 (100%)

Time: 98 ms, Memory: 10.00MB

OK (5 tests, 8 assertions)
➜  uploader git:(master)

# 项目测试

下面我们在 Laravel 项目中进行测试,首先使用以下命令安装 laravel 项目。

composer create-project --prefer-dist laravel/laravel

安装我们的本地扩展包

$ composer config repositories.uploader path ../components/uploader
$ composer require houdunwang/uploader:dev-master

# 生成配置文件

组件会自动发布配置文件 uploader.php 到项目的 config 目录中,需要先进行相应配置。

也可以使用以下方式手动发布配置:

$ laravel php artisan vendor:publish

Which provider or tag's files would you like to publish?:
  [0 ] Publish files from all providers and tags listed below
  [1 ] Provider: BeyondCode\DumpServer\DumpServerServiceProvider
  [2 ] Provider: Fideloper\Proxy\TrustedProxyServiceProvider
  [3 ] Provider: Illuminate\Mail\MailServiceProvider
  [4 ] Provider: Illuminate\Notifications\NotificationServiceProvider
  [5 ] Provider: Illuminate\Pagination\PaginationServiceProvider
  [6 ] Provider: Laravel\Tinker\TinkerServiceProvider
  [7 ] Tag: config
  [8 ] Tag: laravel-mail
  [9 ] Tag: laravel-notifications
  [10] Tag: laravel-pagination
 > 7

Publishing complete.

设置 config/uploader.php 文件中的上传配置项。

# 阿里云

  1. 访问控制 中添加一个新帐号
  2. 获得帐号的 accessKeyIdaccessKeySecret资料设置到配置文件中
  3. 赋予新增的帐号 oss 使用权限。
  4. oss 服务中新增 bucket
  5. 为新增的 bucket 块配置跨域访问权限
  6. 设置块为 公共读
  7. 外网访问 配置项中的 EndPoint 设置到配置文件中的 endpoint

# 测试扩展包

使用Facade 调用

Route::get('/', function () {
	return Uploader::config(config('uploader'))->upload('index.php');
});

使用 provider 服务调用

Route::get('/', function () {
	return app(\Houdunwang\Uploader\Uploader::class)->config(config('uploader'))->upload('index.php');
});

# 开源发布

# GitHub

在 Github 新建项目并执行以下命令提交代码到版本库。

git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/houdunwang/uploader.git
git push -u origin master

别忘记改成自己的 github 库地址

# Packagist

将软件发布到 https://packagist.org/ (opens new window) 用户就可以使用 composer进行安装或更新了。

  1. 使用 github 帐号登录 packagist

  2. 点击 Submit (opens new window) 提交软件包,从 github 复制 https 的地址

  3. 点击 https://packagist.org/profile/ (opens new window) 页面的 https://packagist.org/trigger-github-sync/ (opens new window) 与 github 同步,同步后当 github 代码提交时会自动通知 packagist。

# Version

版本号由含义指 重构或不向下兼容版本号.新功能.修复版本 上面我以最直白的方式进行的版本号的说明,严格定义版本号是对使用你开源项目作者的基本责任,乱定义版本号可能造成使用者在升级后无法运行,这方便知识需要了解一下 composer 中版本号的说明。

下面我们为软件添加每一个版本

$ git tag v1.0.0 # 添加版本号
$ git push --tags # 向github发布

发布后登录 https://packagist.org/packages/houdunwang/uploader (opens new window)就可以看到版本号了,我们软件的使用者可以使用 composer update 更新了。

# 图标

https://poser.pugx.org/ (opens new window) 搜索你的项目,可生成展示下载量、协议等信息的图标。