2

EasyWechat源码分析

一、组件目录

src
├─BasicService          基础服务
│  ├─...                 ...
│  ├─Application.php    基础服务入口
├─...                     中间都是一些与基础服务相同目录结构结构的服务,比如小程序服务、开放平台服务等等
├─OfficialAccount       公众号
│  ├─Auth
│  │  ├─AccessToken     获取公众号AccessToken类
│  │  ├─ServiceProvider 容器类     
│  ├─Application.php    公众号入口
├─Kernel                 核心类库

以公众号服务为例对EasyWechat源码分析

二、EasyWeChat\Factory类源码分析

<?php
namespace EasyWeChat;

class Factory
{
    public static function make($name, array $config)
    {
        $namespace = Kernel\Support\Str::studly($name);
        $application = "\\EasyWeChat\\{$namespace}\\Application";

        return new $application($config);
    }
    
    public static function __callStatic($name, $arguments)
    {
        return self::make($name, ...$arguments);
    }
}

使用组件公众号服务

<?php
use EasyWeChat\Factory;
$config = [
    ...
];
$app = Factory::officialAccount($config);

此操作相当于 $app=new EasyWeChat\OfficialAccount\Application($config)

实例化过程:

  1. 调用EasyWeChat\Factory类的静态方法officialAccount,由于EasyWeChat\Factory类不存在静态方法officialAccount所以调用了__callStatic,此时的name为officialAccount;
  2. __callStatic方法中调用了EasyWeChat\Factory类的make方法;meke方法返回了new $application($config)

三、EasyWeChat\OfficialAccount\Application类源码分析

<?php
namespace EasyWeChat\OfficialAccount;

use EasyWeChat\BasicService;
use EasyWeChat\Kernel\ServiceContainer;

class Application extends ServiceContainer
{
    protected $providers = [
        Auth\ServiceProvider::class,
        ...
        BasicService\Jssdk\ServiceProvider::class,
    ];
}

操作:$app=new EasyWeChat\OfficialAccount\Application($config),此时的EasyWeChat\OfficialAccount\Application 类中并没有构造函数,但是继承了EasyWeChat\Kernel\ServiceContainer,我们去看EasyWeChat\Kernel\ServiceContainer源码。

==特别注意:由于EasyWeChat\OfficialAccount\Application 继承了 EasyWeChat\Kernel\ServiceContainer ,此时的所有操作都是在执行一个EasyWeChat\OfficialAccount\Application 类的对象。==

实例化过程:

  1. 执行了EasyWeChat\Kernel\ServiceContainer 类的构造方法;
  2. 执行了EasyWeChat\Kernel\ServiceContainer 类的registerProviders方法;$this->getProviders()返回的是一个数组,其主要目的是将公众号的所有服务和组件必须注册的组件合并为一个数组,并传递给注册服务的方法。
<?php
namespace EasyWeChat\Kernel;
...
class ServiceContainer extends Container
{
    ...
    public function __construct(array $config = [], array $prepends = [], string $id = null)
    {//$app=new EasyWeChat\OfficialAccount\Application($config)操作执行了此方法
        $this->userConfig = $config;
        parent::__construct($prepends);//执行了前置服务,当前操作没有,所以没有绑定任何服务
        $this->id = $id;
        $this->registerProviders($this->getProviders());
        $this->aggregate();
        $this->events->dispatch(new Events\ApplicationInitialized($this));
    }
    public function getProviders()
    {
        return array_merge([
            ConfigServiceProvider::class,
            LogServiceProvider::class,
            RequestServiceProvider::class,
            HttpClientServiceProvider::class,
            ExtensionServiceProvider::class,
            EventDispatcherServiceProvider::class,
        ], $this->providers);//返回所有需要注册的服务
    }
    public function __get($id)
    {//这个方法在使用$app->property语法的时候调用
        if ($this->shouldDelegate($id)) {
            return $this->delegateTo($id);
        }
        return $this->offsetGet($id);
    }
    public function __set($id, $value)
    {//这个方法在使用$app->property=$value语法的时候调用
        $this->offsetSet($id, $value);
    }

    public function registerProviders(array $providers)
    {
        foreach ($providers as $provider) {
            parent::register(new $provider());
        }
    }
}

EasyWeChat\Kernel\ServiceContainer 类的registerProviders方法分析:

  1. registerProviders方法中的变量$providers
  2. 循环$providers变量注册服务到容器中;此操作相当于给$app对象添加属性。具体实现看四

    $providers = [
        ConfigServiceProvider::class,
        LogServiceProvider::class,
        Menu\ServiceProvider::class,
        ...
        BasicService\Url\ServiceProvider::class,
        BasicService\Jssdk\ServiceProvider::class,
    ];
    //$providers变量合并了EasyWeChat\OfficialAccount\Application类中的$providers属性和EasyWeChat\Kernel\ServiceContainer类中的getProviders

四、Pimple\Container类源码分析

EasyWeChat\OfficialAccount\Application 类继承 EasyWeChat\Kernel\ServiceContainer 类继承 Pimple\Container 所以 EasyWeChat\OfficialAccount\Application 类的对象$app拥有ServiceContainerContainer类的方法和属性,在ServiceContainerContainer 类中的操作都等同于作用$app对象。

<?php
namespace Pimple;
...
class Container implements \ArrayAccess
{
    private $values = [];
    private $factories;
    private $protected;
    private $frozen = [];
    private $raw = [];
    private $keys = [];
    
    public function __construct(array $values = [])
    {
        $this->factories = new \SplObjectStorage();
        $this->protected = new \SplObjectStorage();

        foreach ($values as $key => $value) {
            $this->offsetSet($key, $value);
        }
    }
    public function offsetSet($id, $value)
    {
        if (isset($this->frozen[$id])) {
            throw new FrozenServiceException($id);
        }
        $this->values[$id] = $value;
        $this->keys[$id] = true;
    }
    public function offsetGet($id)
    {
        if (!isset($this->keys[$id])) {
            throw new UnknownIdentifierException($id);
        }

        if (
            isset($this->raw[$id])
            || !\is_object($this->values[$id])
            || isset($this->protected[$this->values[$id]])
            || !\method_exists($this->values[$id], '__invoke')
        ) {
            return $this->values[$id];
        }

        if (isset($this->factories[$this->values[$id]])) {
            return $this->values[$id]($this);
        }

        $raw = $this->values[$id];
        $val = $this->values[$id] = $raw($this);
        $this->raw[$id] = $raw;

        $this->frozen[$id] = true;

        return $val;
    }
    public function register(ServiceProviderInterface $provider, array $values = [])
    {
        $provider->register($this);

        foreach ($values as $key => $value) {
            $this[$key] = $value;
        }

        return $this;
    }
}

实例化过程:

  1. EasyWeChat\Kernel\ServiceContainer 类的 registerProviders 方法调用了 Container 类的 register方法;
  2. $provider->register($this) ,此时的$this 为$app对象,使用Menu菜单功能为例,这个步骤等同于

    <?php
    namespace EasyWeChat\OfficialAccount\Menu;
    use Pimple\Container;
    use Pimple\ServiceProviderInterface;
    
    class ServiceProvider implements ServiceProviderInterface
    {
        public function register(Container $app)
        {
            $app['menu'] = function ($app) {
                return new Client($app);
            };
        }
    }

A、此时的$provider实际等于 $provider = new EasyWeChat\OfficialAccount\Menu\ServiceProvider();
B、执行了register方法,由于EasyWeChat\OfficialAccount\Application类继承EasyWeChat\Kernel\ServiceContainer类继承Pimple\Container,Pimple\Container类实现了\ArrayAccess接口,所以使用$app['menu']语法的赋值行为会执行Pimple\Container类的offsetSet方法。

  1. Pimple\Container类的offsetSet方法

    public function offsetSet($id, $value)
    {
        if (isset($this->frozen[$id])) {
            throw new FrozenServiceException($id);
        }
        $this->values[$id] = $value;
        $this->keys[$id] = true;
    }
    //使用$app['menu']语法的赋值,使得程序执行offsetSet方法,此时的$id=menu, $value=function ($app) {return new Client($app);};
    //至于为什么id跟value会如此,可以去看接口ArrayAccess源码分析
  2. Pimple\Container类的offsetGet方法

    //何时会调用offsetGet方法,具体调用过程:
    //1、在需要使用某个功能的时候,比如使用菜单功能,使用语法$app->menu;
    //2、$app->menu会调用EasyWeChat\Kernel\ServiceContainer类__get魔术方法;
    //3、EasyWeChat\Kernel\ServiceContainer类__get魔术方法调用了offsetGet方法;
    //4、所以此时的$app->menu其实等同于调用了$app->__get('menu'),如果我们没有设置shouldDelegate代理其实$app->menu可以等同于$app->offsetGet('menu')
    public function offsetGet($id)
    {
        if (!isset($this->keys[$id])) {//在offsetSet设置过了此时为true
            throw new UnknownIdentifierException($id);
        }
    
        if (
            isset($this->raw[$id])//第一次获取,由于offsetSet方法中没有设置此时为false
            || !\is_object($this->values[$id])
            || isset($this->protected[$this->values[$id]])//第一次获取,由于offsetSet方法中没有设置此时为false
            || !\method_exists($this->values[$id], '__invoke')
        ) {
            return $this->values[$id];
        }
    
        if (isset($this->factories[$this->values[$id]])) {//第一次获取,由于offsetSet方法中没有设置此时为false
            return $this->values[$id]($this);
        }
    
        $raw = $this->values[$id];
        $val = $this->values[$id] = $raw($this);
        $this->raw[$id] = $raw;
    
        $this->frozen[$id] = true;
    
        return $val;
    }

特别注意: 由于赋值的时候都是使用闭包的方式也就是匿名函数的方式,匿名函数是一个对象,且存在__invoke方法,所以在使用 offsetGet 方法的获取值的时候!\is_object($this->values[$id]), !\method_exists($this->values[$id], '__invoke') 都为 false;

  1. Pimple\Container类的offsetGet方法中的$this->values[$id] = $raw($this)

    以menu为例,此时的$this->values[$id] 等同于$this->values['menu']。$raw($this) 等同于执行了function ($app) {return new Client($app);}。

    $this->values['menu']实际可以看作为:$this->values['menu'] = new Client($app); 为什么使用闭包,到获取的时候才实例化,因为这样子可以减少不必要的开销,因为执行某一个操作不是所有注册的功能都需要使用到,比如我们执行$app->menu->list();这个操作,他只是使用到了menu功能,像user功能等等都没有使用到,此时如果我们都实例化的是完全没有必要的。

五、关于AccessToken何时获取,在哪里获取的问题

以menu菜单功能为例

调用 $list = $app->menu->list();

//$app->menu返回的是EasyWeChat\OfficialAccount\Menu\Client类的一个实例
<?php
namespace EasyWeChat\OfficialAccount\Menu;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
class ServiceProvider implements ServiceProviderInterface
{
    public function register(Container $app)
    {
        $app['menu'] = function ($app) {
            return new Client($app);
        };
    }
}
//EasyWeChat\OfficialAccount\Menu\Client类
<?php
namespace EasyWeChat\OfficialAccount\Menu;

use EasyWeChat\Kernel\BaseClient;

class Client extends BaseClient
{
    public function list()
    {
        return $this->httpGet('cgi-bin/menu/get');
    }
    ...
}

实例化步骤:

  1. 执行了EasyWeChat\Kernel\BaseClient类中的httpGet,最终定位到执行了EasyWeChat\Kernel\BaseClient类的request方法;
  2. EasyWeChat\Kernel\BaseClient类的request方法

    <?php
    namespace EasyWeChat\Kernel;
    ...
    class BaseClient
    {
        public function __construct(ServiceContainer $app, AccessTokenInterface $accessToken = null)
        {
            $this->app = $app;
            $this->accessToken = $accessToken ?? $this->app['access_token'];
        }
        public function httpGet(string $url, array $query = [])
        {
            return $this->request($url, 'GET', ['query' => $query]);
        }
    
        public function request(string $url, string $method = 'GET', array $options = [], $returnRaw = false)
        {
            if (empty($this->middlewares)) {//1、当前的中间件为空条件为true
                $this->registerHttpMiddlewares();//2、为GuzzleHttp实例注册中间件
            }
    
            $response = $this->performRequest($url, $method, $options);
    
            $this->app->events->dispatch(new Events\HttpResponseCreated($response));
    
            return $returnRaw ? $response : $this->castResponseToType($response, $this->app->config->get('response_type'));
        }
        protected function registerHttpMiddlewares()
        {
            // retry
            $this->pushMiddleware($this->retryMiddleware(), 'retry');
            // access token
            $this->pushMiddleware($this->accessTokenMiddleware(), 'access_token');
            $this->pushMiddleware($this->logMiddleware(), 'log');
        }
    
        protected function accessTokenMiddleware()
        {
            return function (callable $handler) {
                return function (RequestInterface $request, array $options) use ($handler) {
                    if ($this->accessToken) {//3、当前的accessToken,在当前类的构造器中已经赋值
                        $request = $this->accessToken->applyToRequest($request, $options);//4、将AccessToken添加到请求中
                    }
    
                    return $handler($request, $options);
                };
            };
        }
        
        protected function retryMiddleware()
        {
            return Middleware::retry(function (
                $retries,
                RequestInterface $request,
                ResponseInterface $response = null
            ) {   
                if ($retries < $this->app->config->get('http.max_retries', 1) && $response && $body = $response->getBody()) {
                    $response = json_decode($body, true);
                    if (!empty($response['errcode']) && in_array(abs($response['errcode']), [40001, 40014, 42001], true)) {
                        //特别说明:当token失效请求失败会重新求请求token,如果是直接设置token的可以设置http.max_retries参数取消重新获取token
                        $this->accessToken->refresh();
                        $this->app['logger']->debug('Retrying with refreshed access token.');
    
                        return true;
                    }
                }
    
                return false;
            }, function () {
                return abs($this->app->config->get('http.retry_delay', 500));
            });
        }
    }
    

六、关于直接设置AccessToken

公众号的获取accesstoken方法最终调用的是EasyWeChat\Kernel\AccessToken 类的getToken方法

<?php
namespace EasyWeChat\Kernel;
...
abstract class AccessToken implements AccessTokenInterface
{
    ...
    public function getToken(bool $refresh = false): array
    {
        $cacheKey = $this->getCacheKey();
        $cache = $this->getCache();

        if (!$refresh && $cache->has($cacheKey) && $result = $cache->get($cacheKey)) {//先去有没有已经缓存在文件中的token
            return $result;
        }

        /** @var array $token */
        $token = $this->requestToken($this->getCredentials(), true);//请求获取token

        $this->setToken($token[$this->tokenKey], $token['expires_in'] ?? 7200);

        $this->app->events->dispatch(new Events\AccessTokenRefreshed($this));

        return $token;
    }
    ...
}

所以如果说不想通过appid跟secret获取token的或只需要在使用之前设置token就行

$app = Factory::officialAccount($config);
$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675');
// 或者指定过期时间
$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675', 3600);  // 单位:秒

pumpkin
52 声望1 粉丝