EasyWechat源码分析
一、组件目录
src
├─BasicService 基础服务
│ ├─... ...
│ ├─Application.php 基础服务入口
├─... 中间都是一些与基础服务相同目录结构结构的服务,比如小程序服务、开放平台服务等等
├─OfficialAccount 公众号
│ ├─Auth
│ │ ├─AccessToken 获取公众号AccessToken类
│ │ ├─ServiceProvider 容器类
│ ├─Application.php 公众号入口
├─Kernel 核心类库
以公众号服务为例对EasyWechat源码分析
二、EasyWeChat\Factory类源码分析
使用组件公众号服务
此操作相当于 $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类源码分析
操作:$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()返回的是一个数组,其主要目的是将公众号的所有服务和组件必须注册的组件合并为一个数组,并传递给注册服务的方法。
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对象添加属性。具体实现看四
$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对象。
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菜单功能为例,这个步骤等同于
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方法
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方法
//何时会调用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();
//$app->menu返回的是EasyWeChat\OfficialAccount\Menu\Client类的一个实例
//EasyWeChat\OfficialAccount\Menu\Client类
httpGet('cgi-bin/menu/get');
}
...
}
实例化步骤:
- 执行了EasyWeChat\Kernel\BaseClient类中的httpGet,最终定位到执行了EasyWeChat\Kernel\BaseClient类的request方法;
EasyWeChat\Kernel\BaseClient类的request方法
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方法
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); // 单位:秒