关于php:EasyWechat4x源码分析

6次阅读

共计 10676 个字符,预计需要花费 27 分钟才能阅读完成。

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);  // 单位:秒
正文完
 0