通过阅读好的代码,学习其中的设计思想和对设计模式的运用,来提升自我代码水平。
代码版本:https://github.com/overtrue/wechat/archive/4.2.8.zip
├── Factory.php // 库入口,需要初始化哪个组件就生成哪个组件,例如 Factory::officialAccount($config)
├── BasicService // 基础服务
│ ├── Application.php // 注册了基础服务的容器
│ ├── ContentSecurity
│ ├── Jssdk
│ ├── Media
│ ├── QrCode
│ └── Url
├── Kernel // 核心,包含模块的基础类,以及运行模块所需要的支持
│ ├── AccessToken.php
│ ├── BaseClient.php
│ ├── Clauses
│ ├── Config.php
│ ├── Contracts
│ ├── ...
├── OfficialAccount // 公众号模块
│ ├── Application.php // 调用本公众号模块中所有服务下的ServiceProvider.php,通过服务提供者的方式将整个公众号模块的所有服务注册到服务容器。
│ ├── Auth // auth服务,每个服务下都有一个ServiceProvider.php,用于将自己的类绑定到容器。
│ ├── AutoReply
│ ├── Base
│ ├── Broadcasting
│ ├── Card
│ ├── Comment
│ ├── ...
├── OpenPlatform // 开放平台模块,与公众号模块同理
│ ├── Application.php
│ ├── Auth
│ ├── Authorizer
│ ├── ...
├── ...
使用示例:
$config = [
'app_id' => 'xxx',
'secret' => 'xxxxxxxxxx',
'response_type' => 'array',
//...
];
$app = Factory::officialAccount($config);
$current = $app->menu->current();
class Factory
{
/**
* @param string $name
* @param array $config
*
* @return \EasyWeChat\Kernel\ServiceContainer
*/
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);
}
}
整个组件的入口,利用魔术方法初始化对应模块,例如 Factory::officialAccount($config)
是初始化公众号模块,
会返回\EasyWeChat\OfficialAccount\Application
这个类的实例。
这种设计可以很优雅的初始化各个模块,否则的话,使用的时候就得根据模块路径不同而use不同模块。
每个模块下都有一个Application容器类,并负责将本模块下所有服务注册到服务容器中。
例如EasyWeChat\OfficialAccount\Application
class Application extends ServiceContainer
{
/**
* @var array
*/
protected $providers = [
Auth\ServiceProvider::class,
Server\ServiceProvider::class,
User\ServiceProvider::class,
...
// Base services
BasicService\QrCode\ServiceProvider::class,
...
];
}
这样设计的好处是,可以很方便的管理该模块的所有服务。
假设新增一个服务的话,只需要在模块下创建该服务的文件夹,然后写该服务的一些类,以及一个服务提供者,用于将类注册到服务容器。然后再在Application中增加注册这个新服务,就完成了。
举例公众号模块中Auth服务的服务提供者类Auth\ServiceProvider::class
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
!isset($app['access_token']) && $app['access_token'] = function ($app) {
return new AccessToken($app);
};
}
}
在容器类的构造方法中,会通过 registerProviders()
方法,最终调用Pimple\Container容器的register方法,将定义的服务注册到服务容器中。
可见是调用了服务提供者的register方法,并将容器本身传递给服务提供者,和上面的代码片段相呼应。
public function register(ServiceProviderInterface $provider, array $values = array())
{
$provider->register($this);
foreach ($values as $key => $value) {
$this[$key] = $value;
}
return $this;
}
比如这段代码:
$app->menu->current();
$app
是公众号模块的容器实例,menu
是公众号模块下的一个服务,而该容器中并没有提供menu
方法,这里就利用到了 __get()
对调用menu方法的这个行为进行重载。
/**
* Magic get access.
*
* @param string $id
*
* @return mixed
*/
public function __get($id)
{
if ($this->shouldDelegate($id)) {
return $this->delegateTo($id);
}
return $this->offsetGet($id);
}
这里的offsetGet()
方法最终会返回menu这个服务的对象,然后调用该对象的current()
方法来获取公众号的当前菜单。
ArrayAccess是一组PHP内置接口,实现该接口的类需要实现offsetExists、offsetGet、offsetSet、offsetUnset四个方法。
当以数组方式调用类时,PHP就会引导到以上实现的方法中,这样就实现了以数组的方式操作类。
例如:
在ServiceContainer中use了WithAggregator这个trait类,其中有一段代码:
/**
* Aggregate.
*/
protected function aggregate()
{
foreach (EasyWeChat::config() as $key => $value) {
$this['config']->set($key, $value);
}
}
/**
* @return bool
*/
public function shouldDelegate($id)
{
return $this['config']->get('delegation.enabled')
&& $this->offsetGet($id) instanceof BaseClient;
}
ServiceContainer的继承关系:ServiceContainer extends Pimple\Container extends \ArrayAccess
所以这个trait类的this指的就是Container这个类的实例,因为Container类实现了ArrayAccess接口,所以这个实例支持数组方式调用。
https://symfony.com/components
symfony的组件被广泛使用,这里easywechat使用了其中的三个组件,分别是:
symfony/cache
缓存组件,提供了多种缓存方式的实现,遵循了php-fig组织制定的psr-6缓存标准。
symfony/event-dispatcher
Mediator模式的实现
symfony/http-foundation
在PHP中,所述请求是由一些全局变量表示($_GET, $_POST,$_FILES,$_COOKIE,$_SESSION,...
)并且通过一些功能生成的响应(echo,header(),setcookie(),...
)。
HttpFoundation组件通过面向对象层替换了这些默认的PHP全局变量和函数。
然后还使用了其他的一些组件,如下:
GuzzleHttp\Client
封装了curl请求
Monolog\Logger
写log的组件,遵循了psr-3日志标准。
pimple/pimple
ioc服务容器
…
TODO
TODO