使用 IoC 容器进行代码优化
PHP语言的灵活性
段誉奇道:“什么?只这么一会儿,便使了一十七种不同的武功?” 小说里面的慕容复博知天下武学,只一会儿功夫便使出了十七种不同的武功,php可以称之为编程界的慕容复,看看php一段Hello World。
<?php
/**
*php7.4 需要安装swoole扩展
*/
class Test{
public static function main() : void{
go(function() {
$arr = ['H', 'e', 'l', 'l', 'o'];
echo array_reduce($arr, fn($s, $e) => $s .= $e);
});
}
}
Test::main();
你是不是看到了Java、Golang、JavaScript的影子,php吸取了很多语言的特性,可函数式,可OOP,作为一门脚本语言当然也可以面向过程。因为PHP如此灵活再加上没有JavaEE这样企业级的标准导致代码风格”五花八门”。比如项目早期是这么创建对象的,每个方法前面一堆的new对象,文件头部一长串 use namespace。
<?php
namespace Model\Food;
use Psr\KLogg\Logger;
use Model\User\User;
use Model\Shop\Vip;
use Model\Pay\Pay;
use App\Libs\Http;
... 此处略去一万行
class Food{
public function createOrder(){
//日志类构造函数第一个参数日志目录,第二个参数调试
$logger = new Logger('order', 'debug');
$user = new User();
$vip = new Vip();
$pay = new Pay();
$http = new Http();
... 此处略去一万行
}
- 使用工厂模式优化
某一天项目上了分布式服务,原来的日志类不好用了,得换另外一个开源的日志类库,leader一声令下,一个个改,所有写日志的地方全得修改,程序猿痛苦不跌,而且每次写业务逻辑都得手动创建对象,既然是创建对象,那么就用对象创建型的工厂设计模式来生产对象。
<?php
class Model{
public static $_model = [];
/**
*魔术静态方法可以减少很多工厂方法
*/
public static function __callStatic($name, $argv) {
if (isset(self::$_model[$name])) {
return self::$_model[$name];
}
$name = "\\App\\libs\\" . $name;
self::$_model[$name] = new $name(...$argv);
return self::$_model[$name];
}
public static function logger($path, $level){
if(isset(self::$_model['logger'])){
self::$_model['logger'] = new Psr\KLogg\Logger();
}
return self::$_model['logger'];
}
public static function User(){
if(isset(self::$_model['user'])){
self::$_model['user'] = new \Model\User\User();
}
return self::$_model['user'];
}
......
}
好景不长,当项目不断迭代,工厂里的方法会越来越多,每次新增一个对象类型就得在工厂增加一个方法,违背了solid原则里的开闭原则,其实这个倒是影响不是很大,关键是这个工厂类会越来越膨胀,那么如何把对象的创建和工厂进行解耦呢?本文的主角IOC容器登场了,Laravel的核心就是一个IOC容器。
IOC容器
laravel容器的作用是管理类的依赖和注入的工具, 原来类对外部的依赖需要在本类创建对象,使用了容器后,使得类把创建对象的工作反转到了容器。
laravel源码分析
public/public/index.php入口
bootstrap/app.php
vendor/laravel/framework/src/Illuminate/Foundation/Application.php
这一步会注册基础的服务提供者vendor/laravel/framework/src/Illuminate/Routing/RoutingServiceProvider.php
我们看到注册路由方法 执行了容器实例的singleton方法 vendor/laravel/framework/src/Illuminate/Foundation/Application.php
bind方法重点看这一行,容器的属性bindings数组 以绑定的字符为key,闭包为值,闭包里有创建对象的方法。那这个闭包是什么时候会执行呢?
由于控制器不是在我们的应用层,这里我们以Redis为例子来分析下。首先你当然得配置好redis连接配置。
- 自动注入源码分析
index.php会首先实例化kernel并执行handle方法, vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
vendor\laravel\framework\src\Illuminate\Foundation\Application.php
bootstrppers数组里的服务提供者都会被实例化,并且执行bootstrap方法。
vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterFacades.php
vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php
config/app.php里的provoders的创建方法会被自动注入到容器。
config/app.php里的alials会执行class_alias
vendor\laravel\framework\src\Illuminate\Foundation\AliasLoader.php
实例真正被创建是在调用的时候
我们写一个redis存取的例子,这里我把config/app.php里的Redis别名改成MyRedis<?php namespace App\Http\Controllers; class IndexController extends Controller { public function show() { /**@var $redis \Redis **/ /**我们可以直接使用MyRedis别名,不需要引入任何**/ $key = 'test_laravel'; $redis = \MyRedis::connection(); $redis->set($key, 2000000); echo $redis->get($key); } }
那么redis实例到底是如何进行的呢?
自动执行实例对象源码分析
这个类只有一个方法 getFaceeAccessor,我们调用的set方法其实是执行了Faced类的__callstaic方法
static::getFacedAccessor()执行的就是 (使用了静态延迟绑定)重点来了。。。
我们知道这个name的值是MyRedis。。。MyRedis是怎么实例化的呢? 谜底马上揭开了。。。
- containner类实现了ArrayAcess接口
原因就在于容器类实现了ArrayAceess接口,使得$staic::$app[$name],调用了offsetGet 然后执行了make方法,一切水落石出
知道了laravel的实现,我们就可以自己实现一个简陋的容器。
- 使用IOC容器优化
容器类
namespace sdk\container; class Container implements \ArrayAccess { /** * @var array 模型实例数组 */ private $instances = []; /** * @var array 绑定的闭包 */ private $bindings = []; public function isBinded($abstract){ return isset($this->bindings[$abstract]); } public function bind($abstract, $concrete){ if(!$this->isBinded($abstract)){ $this->bindings[$abstract] = $concrete; } return $this; } public function make($abstract){ if($this->bindings[$abstract] instanceof \Closure){ return call_user_func($this->bindings[$abstract]); }else{ return $this->bindings[$abstract]; } } public function setInstances($alias, $instance){ $this->instances[$alias] = $instance; return $instance; } public function getInstances(){ return $this->instances; } /** * @inheritDoc */ public function offsetExists($alias) { return isset($this->instances[$alias]); } /** * @inheritDoc */ public function offsetGet($alias) { if(isset($this->instances[$alias])){ return $this->instances[$alias]; } return $this->make($alias); } /** * @inheritDoc */ public function offsetSet($alias, $value) { $this->instances[$alias] = $value; return $this; } /** * @inheritDoc */ public function offsetUnset($alias) { unset($this->instances[$alias]); return $this; } }
模型类
namespace sdk\container; /** * @method static \sdk\cater\Order caterOrder() * @method static \sdk\cater\Waimai waimai() * @method static \Katzgrau\KLogger\Logger logger() */ class Model { /** * @var array */ private static $cfg = [ 'caterOrder'=>[ \sdk\cater\Order::class, ], 'waimai'=>[ \sdk\cater\Waimai::class, ], 'logger'=>[ \Katzgrau\KLogger\Logger::class, [ SDK_PATH . '/../log/', \Psr\Log\LogLevel::DEBUG, ] ], ]; /** * 是否已经绑定过了 * @var bool */ private static $isBind = false; /** * @var \sdk\container\Container */ private static $app; public static function bind(){ foreach(static::$cfg as $alias=>$value){ $class = $value[0] ?? ''; if(empty($class)){ continue; } $args = $value[1] ?? []; static::$app->bind($alias, function() use($alias, $class, $args){ $ref = new \ReflectionClass($class); return static::$app->setInstances($alias, $ref->newInstanceArgs($args)); }); } } public static function __callStatic($name, $args) { if(static::$app == null){ static::$app = new Container(); } if(static::$isBind == false){ static::bind(); } if(static::$app != null){ return static::$app[$name]; } } }
以后加新的模型方法只需要增加配置以及使IDE具备提示功能的注释就可以了。
本作品采用《CC 协议》,转载必须注明作者和本文链接