laravel中路由、缓存等常常有Rote::get()、Cache::get()这样的写法,引入对应门面类就可以使用所定义方法,然而打开所引用门面类,并没有这些方法呀,那么它是怎样实现的呢?以及为何要这么写呢?
涉及知识点:重载、延迟静态绑定、外观模式。
我以非常常用的Cache门面为例追一下代码。
打开Cache门面类,发现只有一个方法,返回了一个名字。
class Cache extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cache';
}
}
不知所云吗?别急,它继承了Facade,打开看看,翻一下发现有魔术方法
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
当我们调用Cache::get(),很显然Cache门面和Facade都不存在静态的get方法,由此此魔术方法被触发,这是非常经典的使用场景,static::又是延迟静态绑定的写法,是要用调用域的getFacadeRoot()方法,追一下。
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
发现它用到了static::getFacadeAccessor(),也是延迟静态绑定的用法,咱们现在是Cache门面,返回的是cache,可以查看一下路由门面返回的是router。
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
Facade类定义的属性包括:
/**
* The application instance being facaded.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected static $app;
/**
* The resolved object instances.
*
* @var array
*/
protected static $resolvedInstance;
这种统筹管理实例的方式使用了注册树模式,laravel服务容器也用到了注册树模式。
再回过头就清楚了,Cache等具体门面返回的是服务容器中核心类的别名,门面类中重载方法是去对应核心类中调用指定方法,所传参数直接传递。还拿Cache举例
public function registerCoreContainerAliases()
{
foreach ([
'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
...
'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
...
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
cache核心类别名注册了\Illuminate\Cache\CacheManager::class()它对应的核心类和\Illuminate\Contracts\Cache\Factory::class(它对应的契约),所以继承了\Illuminate\Contracts\Cache\Factory::class的也都可以用Cache门面,包括Lock、store等,当然实际使用的时候不用一个一个去找,对应门面有所代理类和方法的注释。
这种使用门面来代理一大堆关联类,而不用一个一个引入的方式可以使代码非常简洁,这就是外观模式。不仅如此,门面类引入了Mockery,可以模拟无法复现的条件以方便做单元测试。
门面缺点
但是,过多的门面可能造成类范围蠕变,过多调用门面可能让使用者在不知不觉中已经调用了过多的类,使得当前类变得膨胀,使用依赖注入会让用户清晰地感知到类变大的过程从而更注意此类问题。
重载
php重载是指动态地创建类属性和方法,换一种说法,php的重载是指访问/设置一些原本没有权限看见或不存在的属性或方法时,去现场创建一个的过程。这与大多数语言的重载实现多态是完全不一样的。
重载需要借助这些魔术方法:
__call()、__callStatic()用以做方法重载,其他几个用以做属性重载。
| __call() | 在对象中调用一个不可访问方法时调用 |
| __callStatic() | 在静态上下文中调用一个不可访问方法时调用 |
| __get() | 读取不可访问属性的值时调用,以获取私有属性的值 |
| __set() | 设置不可访问属性的值时调用,以设置私有属性的值,__set() 的返回值会被忽略 |
| __isset() | 对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用以检测私有属性是否被设定。 |
| __unset() | 对不可访问属性调用 unset() 时,__unset() 会被调用以删除私有属性 |
表格中的不可访问包含:不存在、访问范围不可见两种情况。
例1:__get()访问未设置/不可见属性
class Test{
private $year = '17';
public function __get($key){
return $key.' is empty';
}
}
//不存在
echo (new Test)->name."</br>";
//不可见
echo (new Test)->year;

例2:__set() 设置 未设置/不可见属性并访问
class Test{
//private $year = '17';
public function __get($key){
return $key." is empty </br>";
}
public function __set($key,$value){
$this->$key = $value;
}
public function index(){
echo $this->year."</br>";
}
}
$a = new Test;
//未设置前访问
echo $a->year;
//设置
$a->year = '2020';
//设置后访问
$a->index();
//设置后再通过私有属性访问
echo $a->year;

解开private $year=17注释:

在private $year=17被注释后,set不仅给year赋值,还把其访问范围设为了public,在打开注释后,set依然能赋值,只是不能把private改为public。在正常使用时一般是会预先定义私有属性的。
例3.isset() 可以动态地检测一个属性是否被定义
class Test{
private $year = '17';
public function __set($key,$value){
$this->$key = $value;
}
public function __isset($key){
echo $key."设置了吗:".isset($this->$key);
}
}
$a = new Test;
$a->year = '2020';
echo isset($a->year);
接例2,当private $year = '17',此时$a->year = '2020'只是赋值,$year依然是私有,isset($a->year)需要走Test类的__isset()方法,并且因为在类中有year属性,所以输出

private $year=17被注释后:$a->year被设置为了2020,且默认是public,当前是可以访问的,不需要调用__isset()
![]()
例4:__unset()动态销毁属性
class Test{
private $year = '17';
public function __get($key){
echo $key." is empty </br>";
}
public function __set($key,$value){
$this->$key = $value;
echo "set a key </br>";
}
public function __unset($key){
echo "I will unset it </br>";
unset($this->$key);
$this->$key;
}
}
$a = new Test;
$a->year = '2020';
unset($a->year);

由于Test{}中定义了private $year,因此类外给其赋值触发了__set(),又因为类外无法访问$year,因此unset()触发了__unset()。unset后,在类内部访问该属性,也会触发__get()
例5:__call() __callStatic()重载方法、重载静态调用的方法
class Test{
private function mySecret($arg){
var_dump($arg);
}
public function __call($name,$arg){
echo 'my secret:';
return $this->mySecret(...$arg);
}
public static function __callStatic($name,$arg){
echo 'my static secret:';
$data = new Test();
return $data->mySecret(...$arg);
}
}
$a = new Test;
echo '<pre>';
$a->mySecret(['k1','k2']);
$a::mySecret(['k3','k4']);

需要注意的是 __callStatic()方法本身需要是静态方法,而它内部不限于调用静态方法或属性。
静态方法和普通方法区别
静态方法和普通方法有何区别?laravel为什么要用__callStatic()而不用__call()实现重载?
静态方法属于当前类,它不需要实例化就可以直接通过类调用,也可以通过类的对象使用,它不能自动销毁,它被创建后就使用同一块内存,类的所有实例也共享此静态方法。
laravel用静态方式实现门面,个人认为是为了减少内存开销。
参考:
https://www.php.net/manual/zh/language.oop5.overloading.php

本文探讨了Laravel中门面的实现原理,如Cache::get()是如何工作的,涉及到重载、延迟静态绑定和外观模式。通过示例解释了门面如何利用服务容器进行注册树模式管理,提供简洁的代码。同时,文章提到了门面可能导致的类膨胀问题以及PHP的重载机制,包括__callStatic()和__call()在静态方法和普通方法中的应用,强调静态方法的内存效率优势。
571

被折叠的 条评论
为什么被折叠?



