目的
通過閱讀好的代碼,學習其中的設計思想和對設計模式的運用,來提升自我代碼水平。
代碼版本: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();
Factory
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
每個模塊下都有一個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
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