开发自己的 composer 包

本贴最后更新于 1921 天前,其中的信息可能已经时移世改

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 <861167322@qq.comm>, 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": "1052661052@qq.comm"
}
],
"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": "861167322@qq.com"
        }
    ],
    "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 下修改文件,包里的文件也会跟着变了,是不是对于开发过程中来讲非常的方便?

  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    179 引用 • 407 回帖 • 492 关注
  • Composer
    2 引用 • 15 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...