EasyWechat源码剖析

一、组件目录

src├─BasicService          根底服务│  ├─...                 ...│  ├─Application.php    根底服务入口├─...                     两头都是一些与根底服务雷同目录构造构造的服务,比方小程序服务、开放平台服务等等├─OfficialAccount       公众号│  ├─Auth│  │  ├─AccessToken     获取公众号AccessToken类│  │  ├─ServiceProvider 容器类     │  ├─Application.php    公众号入口├─Kernel                 外围类库

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

二、EasyWeChat\Factory类源码剖析

<?phpnamespace 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);    }}

应用组件公众号服务

<?phpuse 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类源码剖析

<?phpnamespace 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()返回的是一个数组,其次要目标是将公众号的所有服务和组件必须注册的组件合并为一个数组,并传递给注册服务的办法。
<?phpnamespace 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对象。

<?phpnamespace 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菜单性能为例,这个步骤等同于

    <?phpnamespace 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类的一个实例<?phpnamespace 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类<?phpnamespace 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办法

    <?phpnamespace 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办法

<?phpnamespace 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);  // 单位:秒