开发自己的 composer 包

composer 是 PHP 用来管理依赖(dependency)关系的工具, 使用 composer 在业务中是非常常见的,比如使用 阿里 oss 的 sdk ,短信 sdk ,非常好用的微信sdk EasyWeChat 都是使用composer 安装, 一个业务的形成就如同搭建积木一样,通过composer 引入各种组件完成,但很多 phper 依然不会开发自己的 composer 包(我也只是会用一下),下面是记录自己开发 composer 的过程。

1. 创建一个开发目录

mkdir weather
cd weather

2. 利用composer生成一个composer.json

composer init
> Welcome to the Composer config generator
> This command will guide you through creating your composer.json config.

// 1. 输入项目命名空间
// 注意<vendor>/<name> 必须要符合 [a-z0-9_.-]+/[a-z0-9_.-]+
Package name (<vendor>/<name>) [dell/htdocs]: yourname/projectname

// 2. 项目描述
Description []: 这是一个测试

// 3. 输入作者信息,可以直接回车
Author [maopanfeng <[email protected]>, n to skip]:

// 4. 输入最低稳定版本,stable, RC, beta, alpha, dev
Minimum Stability []: dev

// 5. 输入项目类型,
Package Type (e.g. library, project, metapackage, composer-plugin) []: library

// 6. 输入授权类型
License []:
> Define your dependencies.

// 7. 输入依赖信息
Would you like to define your dependencies (require) interactively [yes]?

// 如果需要依赖,则输入要安装的依赖
Search for a package: php

// 输入版本号
Enter the version constraint to require (or leave blank to use the latest version): >=5.4.0

// 如需多个,则重复以上两个步骤

// 8. 是否需要require-dev,
Would you like to define your dev dependencies (require-dev) interactively [yes]?

// 操作同上

/*
{
"name": "mpf/test",
"description": "这是一个测试",
"type": "library",
"require": {
"php": ">=5.4.0"
},
"license": "MIT",
"authors": [
{
"name": "maopanfeng",
"email": "[email protected]"
}
],
"minimum-stability": "dev"
}
*/

// 9. 是否生成composer.json
Do you confirm generation [yes]? yes

QQ截图20190919145132.png
输出以上内容则初始化完成

3. 创建开发目录

mkdir src
mkdir tests

4. 声明自动加载

接下来我们需要在 composer.json 中声明包自动加载的命名空间

"autoload": {
        "psr-4": {
            "Doghead\\Weather\\": "./src/"
        }
    },

然后执行命令:composer dump-autoload 或者 composer du 「可选步骤」

5. 安装依赖

我们的项目需要请求接口,所以我们选择 guzzle/guzzle 来做为 http client, 其它暂时用不到,后面用到的时候再安装即可:

$ cd weather/
$ composer require guzzlehttp/guzzle      

输出
QQ截图20190919150900.png

最终 composer.json 内容如下:

composer.json:

{
    "name": "doghead/weather",
    "description": "天气查询",
    "type": "library",
    "require": {
        "php": ">=5.6",
        "guzzlehttp/guzzle": "^6.3@dev"
    },
    "license": "Define your dependencies.",
    "authors": [
        {
            "name": "doghead",
            "email": "[email protected]"
        }
    ],
    "autoload": {
        "psr-4": {
            "doghead\\Weather\\": "./src/"
        }
    },
    "minimum-stability": "dev"
}

基本结构搞定,,,终于开始写代码了!

代码编写

1. 从接口获取天气数据

创建文件

touch src/Weather.php

src/Weather.php

<?php

namespace Doghead\Weather;

class Weather
{

}
方法设计

根据之前设计的功能,结合 天气查询接口文档 的参数说明,我们添加几个方法:

注意:方法名通常是 动名词 形式,比如:getUsersupdateProfiledeleteOrderrevertAction 等。

构造函数 __construct($key)

参数说明:

  • $key 为高德开放平台创建的应用 API Key;

我们调用天气 API 需要用到 API Key,所以把它设计在构造函数中。

<?php

namespace Doghead\Weather;

class Weather
{
    protected $key;

    public function __construct(string $key)
    {
        $this->key = $key;
    }
}
HTTP 客户端 getHttpClient()

获取天气需要用到 http 请求,所以需要先创建一个方法用于返回 guzzle 实例:

<?php

namespace Doghead\Weather;

use GuzzleHttp\Client;

class Weather
{
    protected $key;
    protected $guzzleOptions = [];
    .
    .
    .
    public function getHttpClient()
    {
        return new Client($this->guzzleOptions);
    }

    public function setGuzzleOptions(array $options)
    {
        $this->guzzleOptions = $options;
    }
    .
    .
    .

其中我们设计了一个 $guzzleOptions 参数与方法 setGuzzleOptions,旨在用户可以自定义 guzzle 实例的参数,比如超时时间等。

获取天气 getWeather($city, $type = 'base', $format = 'json')

参数说明:

  • $city - 城市名 / 高德地址位置 adcode,比如:“深圳” 或者(adcode:440300);
  • $type - 返回内容类型:base: 返回实况天气 / all: 返回预报天气;
  • $format - 输出的数据格式,默认为 json 格式,当 output 设置为 “xml” 时,输出的为 XML 格式的数据。

获取天气的写法非常简单,获取 http client,组装参数,返回请求结果:

<?php

namespace Doghead\Weather;

use GuzzleHttp\Client;

class Weather
{
    protected $key;
    protected $guzzleOptions = [];
    .
    .
    .
    public function getWeather($city, string $type = 'base', string $format = 'json')
    {
        $url = 'https://restapi.amap.com/v3/weather/weatherInfo';

        $query = array_filter([
            'key' => $this->key,
            'city' => $city,
            'output' => $format,
            'extensions' =>  $type,
        ]);

        $response = $this->getHttpClient()->get($url, [
            'query' => $query,
        ])->getBody()->getContents();

        return 'json' === $format ? \json_decode($response, true) : $response;
    }

那基本上这个类就已经完成了,最终的样子:

src/Weather.php

<?php
namespace Doghead\Weather;

use GuzzleHttp\Client;
use Doghead\Weather\Exceptions\HttpException;
use Doghead\Weather\Exceptions\InvalidArgumentException;

class Weather
{
    protected $key;
    protected $guzzleOptions = [];

    public function __construct(string $key)
    {
        $this->key = $key;
    }

    public function getHttpClient()
    {
        return new Client($this->guzzleOptions);
    }

    public function setGuzzleOptions(array $options)
    {
        $this->guzzleOptions = $options;
    }

    public function getWeather($city, string $type = 'base', string $format = 'json')
    {
        $url = 'https://restapi.amap.com/v3/weather/weatherInfo';

        $query = array_filter([
            'key' => $this->key,
            'city' => $city,
            'output' => $format,
            'extensions' =>  $type,
        ]);

        $response = $this->getHttpClient()->get($url, [
            'query' => $query,
        ])->getBody()->getContents();

        return 'json' === $format ? \json_decode($response, true) : $response;
    }
}

由于 getWeather$type 参数支持多样性的参数格式,所以满足了我们前面需求分析时的三点要求:

  • 按地名查询实时天气;
  • 获取最近的天气预报。

2. 异常处理

创建目录&文件

mkdir src/Exceptions
touch src/Exceptions/Exception.php

src/Exceptions/Exception.php

<?php

namespace Overtrue\Weather\Exceptions;

class Exception extends \Exception
{

}

自定义的异常需要继承 PHP 内置的异常类 Exception,且不需要包含任何方法,后面我会告诉你为什么。
另外,当调用方传递的 $format 不是 xml 也不是 json 时需要抛出参数异常,所以还需要创建一个类:
创建文件

touch src/Exceptions/InvalidArgumentException.php

src/Exceptions/InvalidArgumentException.php

<?php

namespace Doghead\Weather\Exceptions;

class InvalidArgumentException extends Exception
{

}

当请求接口失败的时候,需要抛出异常类:
创建文件

touch src/Exceptions/HttpException.php

src/Exceptions/HttpException.php

<?php

namespace Doghead\Weather\Exceptions;

class HttpException extends Exception
{

}

接下来将天气获取方法加上异常处理:

src/Weather/Weather.php

<?php

namespace Doghead\Weather;

use GuzzleHttp\Client;
use Doghead\Weather\Exceptions\HttpException;
use Doghead\Weather\Exceptions\InvalidArgumentException;

class Weather
{
    .
    .
    .
    public function getWeather($city, string $type = 'base', string $format = 'json')
    {
        $url = 'https://restapi.amap.com/v3/weather/weatherInfo';

        if (!\in_array(\strtolower($format), ['xml', 'json'])) {
            throw new InvalidArgumentException('Invalid response format: '.$format);
        }

        if (!\in_array(\strtolower($type), ['base', 'all'])) {
            throw new InvalidArgumentException('Invalid type value(base/all): '.$type);
        }

        $query = array_filter([
            'key' => $this->key,
            'city' => $city,
            'output' => \strtolower($format),
            'extensions' =>  \strtolower($type),
        ]);

        try {
            $response = $this->getHttpClient()->get($url, [
                'query' => $query,
            ])->getBody()->getContents();

            return 'json' === $format ? \json_decode($response, true) : $response;
        } catch (\Exception $e) {
            throw new HttpException($e->getMessage(), $e->getCode(), $e);
        }
    }

当传递错误的 $format$type 参数时将会抛出 \Overtrue\Weather\Exceptions\InvalidArgumentException 异常,当请求接口失败时将会抛出 \Overtrue\Weather\Exceptions\HttpException 异常,注意不要忘记引入类名哦。

6. 测试扩展包

mkdir weather-test
cd weather-test

然后在这个测试项目根目录使用 composer 引入我们的包:

# 需要先初始化 composer.json, 一路回车即可
$ composer init  

# 配置包路径,注意,这里 `../weather` 为相对路径,不要弄错了
$ composer config repositories.weather path ../weather    

# 安装扩展包  这里  `dev-master`  中的 dev 指该分支下最新的提交,master 是指定的包中的分支名
$ composer require doghead/weather:dev-master

安装完成后,在 weather-test 根目录创建一个 index.php 来测试:

index.php

<?php

require __DIR__ .'/vendor/autoload.php';

use Doghead\Weather\Weather;

// 高德开放平台应用 API Key
$key = 'bb5e3bd493d1f29f52f9d8ee4bf47049';
$w = new Weather($key);

echo "获取实时天气:\n";

$response = $w->getWeather('深圳');
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

echo "\n\n获取天气预报:\n";

$response = $w->getWeather('深圳', 'all');
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

echo "\n\n获取实时天气(XML):\n";

echo $w->getWeather('深圳', 'base', 'XML');

然后测试一下:

$ php index.php

结果如下:

获取实时天气:
{
    "status": "1",
    "count": "1",
    "info": "OK",
    "infocode": "10000",
    "lives": [
        {
            "province": "广东",
            "city": "深圳市",
            "adcode": "440300",
            "weather": "中雨",
            "temperature": "27",
            "winddirection": "南",
            "windpower": "6",
            "humidity": "94",
            "reporttime": "2018-08-21 16:00:00"
        }
    ]
}

获取天气预报:
{
    "status": "1",
    "count": "1",
    "info": "OK",
    "infocode": "10000",
    "forecasts": [
        {
            "city": "深圳市",
            "adcode": "440300",
            "province": "广东",
            "reporttime": "2018-08-21 11:00:00",
            "casts": [
                {
                    "date": "2018-08-21",
                    "week": "2",
                    "dayweather": "雷阵雨",
                    "nightweather": "雷阵雨",
                    "daytemp": "31",
                    "nighttemp": "26",
                    "daywind": "无风向",
                    "nightwind": "无风向",
                    "daypower": "≤3",
                    "nightpower": "≤3"
                },
                {
                    "date": "2018-08-22",
                    "week": "3",
                    "dayweather": "雷阵雨",
                    "nightweather": "雷阵雨",
                    "daytemp": "32",
                    "nighttemp": "27",
                    "daywind": "无风向",
                    "nightwind": "无风向",
                    "daypower": "≤3",
                    "nightpower": "≤3"
                },
                {
                    "date": "2018-08-23",
                    "week": "4",
                    "dayweather": "雷阵雨",
                    "nightweather": "雷阵雨",
                    "daytemp": "32",
                    "nighttemp": "26",
                    "daywind": "无风向",
                    "nightwind": "无风向",
                    "daypower": "≤3",
                    "nightpower": "≤3"
                },
                {
                    "date": "2018-08-24",
                    "week": "5",
                    "dayweather": "雷阵雨",
                    "nightweather": "雷阵雨",
                    "daytemp": "31",
                    "nighttemp": "26",
                    "daywind": "无风向",
                    "nightwind": "无风向",
                    "daypower": "≤3",
                    "nightpower": "≤3"
                }
            ]
        }
    ]
}

获取实时天气(XML):
<?xml version="1.0" encoding="UTF-8"?>
<response>
    <status>1</status>
    <count>1</count>
    <info>OK</info>
    <infocode>10000</infocode>
    <lives type="list">
        <live>
            <province>广东</province>
            <city>深圳市</city>
            <adcode>440300</adcode>
            <weather>中雨</weather>
            <temperature>27</temperature>
            <winddirection>南</winddirection>
            <windpower>6</windpower>
            <humidity>94</humidity>
            <reporttime>2018-08-21 16:00:00</reporttime>
        </live>
    </lives>
</response>⏎

就可以正常拿到返回的内容了。

原理说明

你可以打开 composer.json 看一下,你会发现上面我们执行的:

$ composer config repositories.weather path ../weather 

它在 composer.json 中添加了如下部分:

composer.json

    .
    .
    .
     "repositories": {
        "weather": {
            "type": "path",
            "url": "../weather"
        }
      }
    .
    .
    .

这样我们在安装的时候 composer 会创建一个软链接 vendor/overtrue/weather 到包所在目录 ../weather,这样一来,你可以直接在测试项目的 vendor/overtrue/weather 下修改文件,包里的文件也会跟着变了,是不是对于开发过程中来讲非常的方便?

留下你的脚步
推荐阅读