最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • EasyWechat4.x源码分析

    正文概述 转载于:掘金(Pumpkin1002)   2021-06-08   602

    EasyWechat4.x源码分析

    一、组件目录

    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(application(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变量注册服务到容器中;此操作相当于给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拥有ServiceContainerContainer类的方法和属性,在ServiceContainerContainer类中的操作都等同于作用app拥有`ServiceContainer`和`Container`类的方法和属性,在`ServiceContainer`和`Container` 类中的操作都等同于作用app拥有‘ServiceContainer‘和‘Container‘类的方法和属性,在‘ServiceContainer‘和‘Container‘类中的操作都等同于作用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) ,此时的thisthis 为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实际等于 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方法。

    3. 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源码分析
      
    4. 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;

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

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

      this>values[menu]实际可以看作为:this->values['menu']实际可以看作为:this−>values[′menu′]实际可以看作为:this->values['menu'] = new Client(app);为什么使用闭包,到获取的时候才实例化,因为这样子可以减少不必要的开销,因为执行某一个操作不是所有注册的功能都需要使用到,比如我们执行app); 为什么使用闭包,到获取的时候才实例化,因为这样子可以减少不必要的开销,因为执行某一个操作不是所有注册的功能都需要使用到,比如我们执行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);  // 单位:秒
    

    起源地下载网 » EasyWechat4.x源码分析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元