EasyWechat源码剖析
一、组件目录
src ├─BasicService 根底服务 │ ├─... ... │ ├─Application.php 根底服务入口 ├─... 两头都是一些与根底服务雷同目录构造构造的服务,比方小程序服务、开放平台服务等等 ├─OfficialAccount 公众号 │ ├─Auth │ │ ├─AccessToken 获取公众号AccessToken类 │ │ ├─ServiceProvider 容器类 │ ├─Application.php 公众号入口 ├─Kernel 外围类库
以公众号服务为例对EasyWechat源码剖析
二、EasyWeChat\Factory类源码剖析
<code class="php"><?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); } }
应用组件公众号服务
<code class="php"><?php use EasyWeChat\Factory; $config = [ ... ]; $app = Factory::officialAccount($config);
此操作相当于 $app=new EasyWeChat\OfficialAccount\Application($config)
实例化过程:
- 调用
EasyWeChat\Factory
类的静态方法officialAccount,因为EasyWeChat\Factory
类不存在静态方法officialAccount所以调用了__callStatic,此时的name为officialAccount; - __callStatic办法中调用了
EasyWeChat\Factory
类的make办法;meke办法返回了new $application($config)
三、EasyWeChat\OfficialAccount\Application类源码剖析
<code class="php"><?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
类的对象。==
实例化过程:
- 执行了
EasyWeChat\Kernel\ServiceContainer
类的构造方法; - 执行了
EasyWeChat\Kernel\ServiceContainer
类的registerProviders办法;$this->getProviders()返回的是一个数组,其次要目标是将公众号的所有服务和组件必须注册的组件合并为一个数组,并传递给注册服务的办法。
<code class="php"><?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办法剖析:
- registerProviders办法中的变量$providers
-
循环$providers变量注册服务到容器中;此操作相当于给$app对象增加属性。具体实现看四
<code class="php">$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领有ServiceContainer
和Container
类的办法和属性,在ServiceContainer
和Container
类中的操作都等同于作用$app对象。
<code class="php"><?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; } }
实例化过程:
EasyWeChat\Kernel\ServiceContainer
类的 registerProviders 办法调用了Container
类的 register办法;-
$provider->register($this)
,此时的$this 为$app对象,应用Menu菜单性能为例,这个步骤等同于<code class="php"><?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办法。
-
Pimple\Container类的offsetSet办法
<code class="php">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源码剖析
-
Pimple\Container类的offsetGet办法
<code class="php">//何时会调用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
;
-
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();
<code class="php">//$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); }; } }
<code class="php">//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'); } ... }
实例化步骤:
- 执行了EasyWeChat\Kernel\BaseClient类中的httpGet,最终定位到执行了EasyWeChat\Kernel\BaseClient类的request办法;
-
EasyWeChat\Kernel\BaseClient类的request办法
<code class="php"><?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办法
<code class="php"><?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就行
<code class="php">$app = Factory::officialAccount($config); $app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675'); // 或者指定过期工夫 $app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675', 3600); // 单位:秒