EasyWechat4.x source code analysis

EasyWechat4.x source code analysis

EasyWechat4.x source code analysis

1. component catalog

src BasicService basic service ... ... Application.php basic service entrance ... In the middle are some services with the same directory structure as the basic services, such as applet services, open platform services, etc. OfficialAccount Official Account Auth AccessToken Get the public account AccessToken class ServiceProvider container class Application.php official account entrance Kernel core class library Copy code

Take the official account service as an example to analyze the EasyWechat source code

2. EasyWeChat\Factory class source code analysis

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

Use component official account service

<?php use EasyWeChat/Factory ; $config = [ ... ]; App $ = :: officialAccount Factory's ( $ config ); duplicated code

This operation is equivalent to

$app=new EasyWeChat\OfficialAccount\Application($config)

The instantiation process:

  1. transfer
    EasyWeChat\Factory
    The static method officialAccount of the class, due to
    EasyWeChat\Factory
    The class does not have a static method officialAccount, so __callStatic is called, and the name at this time is officialAccount;
  2. Called in the __callStatic method
    EasyWeChat\Factory
    The make method of the class; the meke method returns new application(application( config)

3. EasyWeChat\OfficialAccount\Application class source code analysis

<?php namespace EasyWeChat/OfficialAccount ; use EasyWeChat/BasicService ; use EasyWeChat/Kernel/ServiceContainer ; class Application extends ServiceContainer { protected $providers = [ Auth\ServiceProvider::class, ... BasicService\Jssdk\ServiceProvider::class, ]; } Copy code

operating:

$app=new EasyWeChat\OfficialAccount\Application($config)
, At this time
EasyWeChat\OfficialAccount\Application
There is no constructor in the class, but inherited
EasyWeChat\Kernel\ServiceContainer
, Let's go see
EasyWeChat\Kernel\ServiceContainer
Source code.

==**Special Note: **Because of

EasyWeChat\OfficialAccount\Application
inherit
EasyWeChat\Kernel\ServiceContainer
, All operations at this time are performing a
EasyWeChat\OfficialAccount\Application
Object of the class. ==

The instantiation process:

  1. Executed
    EasyWeChat\Kernel\ServiceContainer
    Class construction method;
  2. Executed
    EasyWeChat\Kernel\ServiceContainer
    The registerProviders method of the class; $this->getProviders() returns an array. Its main purpose is to merge all the services and components of the official account that must be registered into an array and pass it to the method of registering services.
<?php namespace EasyWeChat/Kernel ; ... class ServiceContainer extends Container { ... public function __construct ( array $config = [], array $prepends = [], string $id = null ) { //$app=new EasyWeChat\OfficialAccount\Application($config) operation executed this method $this ->userConfig = $config ; parent ::__construct( $prepends ); //The pre-service is executed, the current operation is not, so no service is bound $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); //Return all services that need to be registered } public function __get ( $id ) { //This method calls if ( $this ->shouldDelegate( $id )) { return $this ->delegateTo( $id ); } return $this ->offsetGet( $id ); } public function __set ( $id , $value ) { //This method is called when using the $app->property=$value syntax $this ->offsetSet( $id , $value ); } public function registerProviders ( array $providers ) { foreach ( $providers as $provider ) { parent ::register( new $provider ()); } } } Copy code

EasyWeChat\Kernel\ServiceContainer
Analysis of the registerProviders method of the class:

  1. Variable $providers in the registerProviders method

  2. cycleprovidersVariable registration service to the container; this operation is equivalent toThe providers variable registers the service to the container; this operation is equivalent to add an attribute app object. See four for specific implementation

    $providers = [ ConfigServiceProvider::class, LogServiceProvider::class, Menu\ServiceProvider::class, ... BasicService\Url\ServiceProvider::class, BasicService\Jssdk\ServiceProvider::class, ]; //$ providers variable merged EasyWeChat/$ providers attribute OfficialAccount/Application class and EasyWeChat/Kernel/ServiceContainer class getProviders copy the code

4. Pimple\Container class source code analysis

EasyWeChat\OfficialAccount\Application
Class inheritance
EasyWeChat\Kernel\ServiceContainer
Class inheritance
Pimple\Container
and so
EasyWeChat\OfficialAccount\Application
Class objectapphave'ServiceContainer'with'Container'Methods and properties of the class, in'ServiceContainer'with'Container'The operations in the class are equivalent to the roleThe app has the methods and properties of the `ServiceContainer` and `Container` classes, and the operations in the `ServiceContainer` and `Container` classes are equivalent to functions app object.

<?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 ; } } Copy code

The instantiation process:

  1. EasyWeChat\Kernel\ServiceContainer
    The registerProviders method of the class is called
    Container
    The register method of the class;

  2. $provider->register($this)
    , At this timethisforthis is app object, using the Menu function as an example, this step is equivalent to

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

    A. At this timeproviderActually equal toprovider is actually equal to provider = new EasyWeChat/OfficialAccount/Menuperforming a register method, since EasyWeChat/OfficialAccount/Application class inherits EasyWeChat/Kernel/ServiceContainer class inherits Pimple/Container , The Pimple\Container class implements the/ArrayAccess interface, so the assignment behavior using the $app['menu'] syntax will execute the offsetSet method of the Pimple\Container class.

  3. The offsetSet method of Pimple\Container class

    public function offsetSet ( $id , $value ) { if ( isset ( $this ->frozen[ $id ])) { throw new FrozenServiceException( $id ); } $this ->values[ $id ] = $value ; $this ->keys[ $id ] = true ; } //Use the assignment of $app['menu'] syntax to make the program execute the offsetSet method, at this time $id=menu, $value=function ($app) {return new Client($app);}; //As for Why id with the value would be so, you can see the interface source code analysis ArrayAccess copy the code
  4. The offsetGet method of Pimple\Container class

    //When will the offsetGet method be called? The specific calling process: //1. When you need to use a certain function, such as using the menu function, use the syntax $app->menu; //2, $app->menu will be called EasyWeChat\Kernel\ServiceContainer class __get magic method; //3, EasyWeChat\Kernel\ServiceContainer class __get magic method calls the offsetGet method; //4, so $app->menu at this time is actually equivalent to calling $app -> __ get ( 'menu' ), if we do not set shouldDelegate agent actually $ app-> menu can be equated to APP- $> offsetGet ( 'the MENU') public function offsetGet ( $ the above mentioned id ) { IF (! isset ( $ the this - >keys[ $id ])) { //When the offsetSet is set, it is true throw new UnknownIdentifierException( $id ); } if ( isset ( $this ->raw[ $id ]) //First acquisition, because there is no setting in the offsetSet method, this is false || !\is_object( $this ->values[ $id ]) || isset ( $this ->protected[ $this ->values[ $id ]]) //First acquisition, because there is no setting in the offsetSet method, it is false at this time || !\method_exists( $this ->values[ $id ], '__invoke' ) ) { return $this ->values[ $id ]; } if ( isset ( $this ->factories[ $this ->values[ $id ]])) { //First acquisition, because the offsetSet method is not set at this time to 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 ; } Copy code

    == Special attention: == Since the assignment is done using closures, that is, anonymous functions, anonymous functions are an object and exist

    __invoke
    Method, so when using the offsetGet method to get the value
    !\is_object($this->values[$id]), !\method_exists($this->values[$id],'__invoke')
    All for
    false
    ;

  5. In the offsetGet method of the Pimple\Container class

    $this->values[$id] = $raw($this)

    Take menu as an example, at this timethis >values[this->values[ id] is equivalent tothis >values[ menu ].this->values['menu'].raw(this)Is equivalent to executedfunction(this) is equivalent to executing function ( App) {return new new Client ($ App);}.

    this >values[ menu ]It can actually be seen as:this->values['menu'] can actually be seen as:this-> values [' MENU '] = new new Client (app);Why use closures and instantiate them when they are obtained, because this can reduce unnecessary overhead, because not all registered functions need to be used to perform a certain operation, for example, we executeapp); Why use closures and instantiate them when they are obtained, because this can reduce unnecessary overhead, because not all registered functions need to be used to perform a certain operation, for example, we execute app-> menu-> list (); this operation, he just to use the menu function, like user function and so on are not used to, at this time if we are instantiated is completely unnecessary.

5. Questions about when and where to obtain AccessToken

Take the menu function as an example

transfer

$list = $app->menu->list();

//$app->menu returns an instance of EasyWeChat\OfficialAccount\Menu\Client class <?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 ); }; } } Copy code
//EasyWeChat\OfficialAccount\Menu\Client class <?php namespace EasyWeChat/OfficialAccount/Menu ; use EasyWeChat/Kernel/BaseClient ; class Client extends BaseClient { public function list () { return $this ->httpGet( 'cgi-bin/menu/get' ); } ... } Copy code

Instantiation steps:

  1. The httpGet in the EasyWeChat\Kernel\BaseClient class is executed, and finally the request method of the EasyWeChat\Kernel\BaseClient class is executed;

  2. The request method of EasyWeChat\Kernel\BaseClient class

    <?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, the current middleware is The empty condition is true $this->registerHttpMiddlewares();//2, register middleware for the GuzzleHttp instance } $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 $ the this -> pushMiddleware ( $ the this -> retryMiddleware (), 'the retry' ); //Access token $ the this -> pushMiddleware ( $ the this -> accessTokenMiddleware (), 'the access_token' ); $ the this - >pushMiddleware( $this ->logMiddleware(), 'log' ); } protected function accessTokenMiddleware () { return function ( callable $handler ) { return function ( RequestInterface $request , array $options ) use ( $handler ) { if ( $this ->accessToken) { //3. The current accessToken is in the constructor of the current class has been assigned $ request = $ the this -> accessToken-> applyToRequest ( $ request , $ Options ); //. 4, will be added to the request 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 )) { $this ->app[ 'logger' ]->debug( 'Retrying with refreshed access token.' );//Special note: When the request fails, the token will be requested again. If the token is set directly, you can set the http.max_retries parameter to cancel the re-acquire token $this ->accessToken->refresh(); return true ; } } return false ; }, function () { return abs( $this ->app->config->get( 'http.retry_delay' , 500 )); }); } } Copy code

6. about directly setting AccessToken

The accesstoken method of the official account is finally called

EasyWeChat\Kernel\AccessToken
Class getToken method

<?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 )) { // Check if there is a token already cached in the file return $result ; } /** @var array $token */ $token = $this ->requestToken( $this ->getCredentials(), true ); //Request to get token $this ->setToken( $token [ $this ->tokenKey], $token [ 'expires_in' ] ?? 7200 ); $this ->app->events->dispatch( new Events\AccessTokenRefreshed( $this )); return $token ; } ... } Copy code

So if you don t want to get the token through appid and secret, or you just need to set the token before using it.

$app = Factory::officialAccount( $config ); $app [ 'access_token' ]->setToken( 'ccfdec35bd7ba359f6101c2da321d675' ); //or specify the expiration time $app [ 'access_token' ]->setToken( 'ccfdec35bd7ba359f6101c2da321d675' , 3600 ); //unit: second copy the code