说明:Laravel中经常使用PHP的反射特性来设计代码,本文主要学习PHP的反射特性,来提高写代码时的设计质量。PHP提供一套检测class, interface, trait, property, method
的两个工具包:Introspection Functions
和Reflection API
,类似于探针一样的东西来探测这些一等公民。本文先看下Introspection Functions
的使用。
开发环境: Laravel5.3 PHP7
Introspection Functions
Introspection Functions
是用来操作object class的一些函数,PHP提供了大量的Introspection Functions
来操作class, interface, trait, method, property
:
- class_exists()
- interface_exists()
- method_exists()
- property_exists()
- trait_exists()
- class_alias()
- get_class()
- get_parent_class()
- get_called_class()
- get_class_methods()
- get_class_vars()
- get_object_vars()
- is_subclass_of()
- is_a()
class_exists()
Laravel源码中好多个地方使用到class_exists()方法来判断指定类是否存在,如IlluminateDatabaseConnection::isDoctrineAvailable()的源码:
代码语言:javascript复制 public function isDoctrineAvailable()
{
return class_exists('DoctrineDBALConnection'); // DoctrineDBALConnection::class类是否存在,大小写不敏感
}
写个PHPUnit测试下(爆绿灯,说明是正确的,这里不截图了。后面所有Introspection的测试都放在IntrospectionTest这个单元测试里):
代码语言:javascript复制namespace MyRightCapitalContainerTests;
class IntrospectionTest extends PHPUnit_Framework_TestCase
{
public function testClassExists()
{
// Arrange
// Actual
$class_exists = class_exists(TestClassExists::class);
// Assert
$this->assertTrue($class_exists);
}
}
class TestClassExists
{
}
interface_exists()
interface_exists()是用来检查接口是否存在,写个PHPUnit测试下,爆绿灯:
代码语言:javascript复制namespace MyRightCapitalContainerTests;
class IntrospectionTest extends PHPUnit_Framework_TestCase
{
public function testInterfaceExists()
{
// Arrange
// Actual
$interface_exists = interface_exists(TestInterfaceExists::class);
// Assert
$this->assertTrue($interface_exists);
}
}
interface TestInterfaceExists
{
}
method_exists()
检查类的方法(private,protected,public
)是否存在于指定的类对象或类名中,Laravel中很多处用到了这个函数,如Application中的register()
检查service provider中register
是否存在,和bootProvider()
中检查service provider中boot()
方法是否存在:
public function register($provider, $options = [], $force = false)
{
...
if (method_exists($provider, 'register')) {
$provider->register();
}
...
}
protected function bootProvider(ServiceProvider $provider)
{
if (method_exists($provider, 'boot')) {
return $this->call([$provider, 'boot']);
}
}
这里写个PHPUnit测试下,爆绿灯:
代码语言:javascript复制 public function testMethodExists()
{
// Arrange
$test_class_exists = new TestClassExists();
// Actual
$object_method_exists1 = method_exists($test_class_exists, 'testPrivateMethodExists');
$object_method_exists2 = method_exists($test_class_exists, 'testProtectedMethodExists');
$object_method_exists3 = method_exists($test_class_exists, 'testPublicMethodExists');
$classname_method_exists1 = method_exists(TestClassExists::class, 'testPrivateMethodExists');
$classname_method_exists2 = method_exists(TestClassExists::class, 'testProtectedMethodExists');
$classname_method_exists3 = method_exists(TestClassExists::class, 'testPublicMethodExists');
// Assert
$this->assertTrue($object_method_exists1);
$this->assertTrue($object_method_exists2);
$this->assertTrue($object_method_exists3);
$this->assertTrue($classname_method_exists1);
$this->assertTrue($classname_method_exists2);
$this->assertTrue($classname_method_exists3);
}
class TestClassExists
{
private function testPrivateMethodExists()
{
}
protected function testProtectedMethodExists()
{
}
public function testPublicMethodExists()
{
}
}
property_exists()
检查该属性(private, protected, public
)是否存在于类对象或类名中,Laravel很多地方用到了该函数,如IlluminateFoundationAuthRedirectsUsers::redirectPath()源码:
public function redirectPath()
{
return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';
}
写个PHPUnit测试下该函数,爆绿灯:
代码语言:javascript复制 // class IntrospectionTest
public function testPropertyExists()
{
// Arrange
$test_class_exists = new TestClassExists();
// Actual
$private_property1 = property_exists($test_class_exists, 'testPrivatePropertyExists');
$private_property2 = property_exists(TestClassExists::class, 'testPrivatePropertyExists');
$protected_property1 = property_exists($test_class_exists, 'testProtectedPropertyExists');
$protected_property2 = property_exists(TestClassExists::class, 'testProtectedPropertyExists');
$public_property1 = property_exists($test_class_exists, 'testPublicPropertyExists');
$public_property2 = property_exists(TestClassExists::class, 'testPublicPropertyExists');
// Assert
$this->assertTrue($private_property1);
$this->assertTrue($private_property2);
$this->assertTrue($protected_property1);
$this->assertTrue($protected_property2);
$this->assertTrue($public_property1);
$this->assertTrue($public_property2);
}
class TestClassExists
{
private $testPrivatePropertyExists;
protected $testProtectedPropertyExists;
public $testPublicPropertyExists;
}
trait_exists()
检查trait是否存在,写下PHPUnit测试,爆绿灯:
代码语言:javascript复制 // class IntrospectionTest
public function testTraitExists()
{
// Arrange
// Actual
$test_trait_exists = trait_exists(TestTraitExists::class);
// Assert
$this->assertTrue($test_trait_exists);
}
trait TestTraitExists
{
}
class_alias()
给指定类取别名,Laravel中只有一处使用了class_alias(),用来给config/app.php中$aliases[ ]注册别名,可看下Laravel5.3之bootstrap源码解析,看下Laravel中如何使用的:
代码语言:javascript复制 public function load($alias)
{
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
写个PHPUnit测试,爆绿灯:
代码语言:javascript复制 public function testClassAlias()
{
// Arrange
class_alias(TestClassExists::class, 'MyRightCapitalContainerTestsAliasTestClassExists');
$test_class_exists = new TestClassExists();
// Actual
$actual = new AliasTestClassExists();
//Assert
$this->assertInstanceOf(TestClassExists::class, $actual);
$this->assertInstanceOf(AliasTestClassExists::class, $test_class_exists);
}
get_class()
get_class()获取对象的类名,这个函数在Laravel中大量地方在用了,如Application::getProvider($provider)方法,是个很好用的方法:
代码语言:javascript复制 public function getProvider($provider)
{
$name = is_string($provider) ? $provider : get_class($provider);
return Arr::first($this->serviceProviders, function ($value) use ($name) {
return $value instanceof $name;
});
}
写个PHPUnit测试,爆绿灯:
代码语言:javascript复制 public function testGetClass()
{
// Arrange
$test_class_exists = new TestClassExists();
// Actual
$class_name = get_class($test_class_exists);
// Assert
$this->assertSame(TestClassExists::class, $class_name);
}
get_parent_class()
get_parent_class()是用来获取类的父类名,目前Laravel中还没用到这个函数,传入的可以是子类对象或者子类名,写个PHPUnit测试下:
代码语言:javascript复制 // namespace MyRightCapitalContainerTests;
// class IntrospectionTest extends PHPUnit_Framework_TestCase
public function testGetParentClass()
{
// Arrange
$child_class = new ChildClass();
// Actual
$parent_class1 = get_parent_class($child_class);
$parent_class2 = get_parent_class(ChildClass::class);
// Assert
$this->assertSame(ParentClass::class, $parent_class1);
$this->assertSame(ParentClass::class, $parent_class2);
}
class ChildClass extends ParentClass
{
}
class ParentClass
{
}
get_called_class()
get_called_class()获取后期静态绑定类即实际调用类的名称,Laravel中还没使用到该函数,不妨写个测试看下如何使用:
代码语言:javascript复制 // namespace MyRightCapitalContainerTests;
// class IntrospectionTest extends PHPUnit_Framework_TestCase
public function testGetCalledClass()
{
// Arrange
$child_class = new ChildClass();
$parent_class = new ParentClass();
// Actual
$child_called_class = $child_class->testGetCalledClass();
$parent_called_class = $parent_class->testGetCalledClass();
// Assert
$this->assertSame(ChildClass::class, $child_called_class);
$this->assertSame(ParentClass::class, $parent_called_class);
}
class ChildClass extends ParentClass
{
}
class ParentClass
{
public function testGetCalledClass()
{
return get_called_class();
}
}
get_class_methods()
get_class_methods()用来获取类的方法名组成一个数组(测试只能是public)
,Laravel只有一处用到了该方法IlluminateDatabaseEloquentModel::cacheMutatedAttributes() :line 3397,这里写个PHPUnit测试,爆绿灯:
public function testGetClassMethod()
{
// Arrange
$get_class_methods1 = get_class_methods(ChildClass::class);
$get_class_methods2 = get_class_methods(new ChildClass());
// Actual
// Assert
$this->assertFalse(in_array('testPrivateGetClassMethod', $get_class_methods1, true));
$this->assertFalse(in_array('testPrivateGetClassMethod', $get_class_methods2, true));
$this->assertFalse(in_array('testProtectedGetClassMethod', $get_class_methods1, true));
$this->assertFalse(in_array('testProtectedGetClassMethod', $get_class_methods2, true));
$this->assertTrue(in_array('testPublicGetClassMethod', $get_class_methods1, true));
$this->assertTrue(in_array('testPublicGetClassMethod', $get_class_methods2, true));
$this->assertTrue(in_array('testGetCalledClass', $get_class_methods1, true));
$this->assertTrue(in_array('testGetCalledClass', $get_class_methods2, true));
}
class ChildClass extends ParentClass
{
private function testPrivateGetClassMethod()
{
}
protected function testProtectedGetClassMethod()
{
}
public function testPublicGetClassMethod()
{
}
}
get_class_vars()
get_class_vars()只会读取类的public
属性组成一个数组,类似于get_class_methods()
,若属性没有默认值就为null,目前Laravel中还未使用,看下PHPUnit测试:
public function testGetClassVars()
{
// Arrange
// Actual
$class_vars = get_class_vars(ChildClass::class);
// Assert
$this->assertArrayNotHasKey('privateNoDefaultVar', $class_vars);
$this->assertArrayNotHasKey('privateDefaultVar', $class_vars);
$this->assertArrayNotHasKey('protectedNoDefaultVar', $class_vars);
$this->assertArrayNotHasKey('protectedDefaultVar', $class_vars);
$this->assertEmpty($class_vars['publicNoDefaultVar']);
$this->assertEquals('public_laravel', $class_vars['publicDefaultVar']);
}
class ChildClass extends ParentClass
{
private $privateNoDefaultVar;
private $privateDefaultVar = 'private_laravel';
protected $protectedNoDefaultVar;
protected $protectedDefaultVar = 'protected_laravel';
public $publicNoDefaultVar;
public $publicDefaultVar = 'public_laravel';
}
get_object_vars()
get_object_vars()只会读取对象的public
属性组成一个数组,类似于get_class_vars(), get_class_methods()
,且属性没有默认值就是null,Laravel中只有一处使用到IlluminateMailJobsHandleQueuedMessage::__sleep() :line 78,写个PHPUnit测试下,爆绿灯:
public function testGetObjectVars()
{
// Arrange
$get_object_vars = new TestGetObjectVars(1, 2, 3);
// Actual
$object_vars = get_object_vars($get_object_vars);
// Assert
$this->assertArrayNotHasKey('x', $object_vars);
$this->assertArrayNotHasKey('y', $object_vars);
$this->assertEquals(3, $object_vars['z']);
$this->assertArrayNotHasKey('dot1', $object_vars);
$this->assertArrayNotHasKey('dot2', $object_vars);
$this->assertArrayNotHasKey('circle1', $object_vars);
$this->assertArrayNotHasKey('circle2', $object_vars);
$this->assertEquals(10, $object_vars['line1']);
$this->assertEmpty($object_vars['line2']);
}
class TestGetObjectVars
{
private $x;
protected $y;
public $z;
private $dot1 = 10;
private $dot2;
protected $circle1 = 20;
protected $circle2;
public $line1 = 10;
public $line2;
public function __construct($x, $y, $z)
{
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
}
is_subclass_of()
is_subclass_of()用来判断给定类对象是否是另一给定类名的子类,Laravel中有用到,这里写下PHPUnit测试,爆绿灯:
代码语言:javascript复制 public function testIsSubclassOf()
{
// Arrange
$child_class = new ChildClass();
// Actual
$is_subclass = is_subclass_of($child_class, ParentClass::class);
// Assert
$this->assertTrue($is_subclass);
}
is_a()
is_a()用来判定给定类对象是否是另一给定类名的对象或是子类,和is_subclass_of()有点类似,只是is_a()还可以判定是不是该类的对象,is_a()类似于instanceof操作符
,Laravel中还没用到这个方法,这里写个PHPUnit测试,爆绿灯:
public function testIsA()
{
// Arrange
$child_class = new ChildClass();
// Actual
$is_object = is_a($child_class, ChildClass::class);
$is_subclass = is_a($child_class, ParentClass::class);
// Assert
$this->assertTrue($is_object);
$this->assertTrue($is_subclass);
}
最后,给下整个PHPUnit的测试代码:
代码语言:javascript复制<?php
namespace MyRightCapitalContainerTests;
class IntrospectionTest extends PHPUnit_Framework_TestCase
{
public function testClassExists()
{
// Arrange
// Actual
$class_exists = class_exists(TestClassExists::class);
// Assert
$this->assertTrue($class_exists);
}
public function testInterfaceExists()
{
// Arrange
// Actual
$interface_exists = interface_exists(TestInterfaceExists::class);
// Assert
$this->assertTrue($interface_exists);
}
public function testMethodExists()
{
// Arrange
$test_class_exists = new TestClassExists();
// Actual
$object_method_exists1 = method_exists($test_class_exists, 'testPrivateMethodExists');
$object_method_exists2 = method_exists($test_class_exists, 'testProtectedMethodExists');
$object_method_exists3 = method_exists($test_class_exists, 'testPublicMethodExists');
$classname_method_exists1 = method_exists(TestClassExists::class, 'testPrivateMethodExists');
$classname_method_exists2 = method_exists(TestClassExists::class, 'testProtectedMethodExists');
$classname_method_exists3 = method_exists(TestClassExists::class, 'testPublicMethodExists');
// Assert
$this->assertTrue($object_method_exists1);
$this->assertTrue($object_method_exists2);
$this->assertTrue($object_method_exists3);
$this->assertTrue($classname_method_exists1);
$this->assertTrue($classname_method_exists2);
$this->assertTrue($classname_method_exists3);
}
public function testPropertyExists()
{
// Arrange
$test_class_exists = new TestClassExists();
// Actual
$private_property1 = property_exists($test_class_exists, 'testPrivatePropertyExists');
$private_property2 = property_exists(TestClassExists::class, 'testPrivatePropertyExists');
$protected_property1 = property_exists($test_class_exists, 'testProtectedPropertyExists');
$protected_property2 = property_exists(TestClassExists::class, 'testProtectedPropertyExists');
$public_property1 = property_exists($test_class_exists, 'testPublicPropertyExists');
$public_property2 = property_exists(TestClassExists::class, 'testPublicPropertyExists');
// Assert
$this->assertTrue($private_property1);
$this->assertTrue($private_property2);
$this->assertTrue($protected_property1);
$this->assertTrue($protected_property2);
$this->assertTrue($public_property1);
$this->assertTrue($public_property2);
}
public function testTraitExists()
{
// Arrange
// Actual
$test_trait_exists = trait_exists(TestTraitExists::class);
// Assert
$this->assertTrue($test_trait_exists);
}
public function testClassAlias()
{
// Arrange
class_alias(TestClassExists::class, 'MyRightCapitalContainerTestsAliasTestClassExists');
$test_class_exists = new TestClassExists();
// Actual
$actual = new AliasTestClassExists();
//Assert
$this->assertInstanceOf(TestClassExists::class, $actual);
$this->assertInstanceOf(AliasTestClassExists::class, $test_class_exists);
}
public function testGetClass()
{
// Arrange
$test_class_exists = new TestClassExists();
// Actual
$class_name = get_class($test_class_exists);
// Assert
$this->assertSame(TestClassExists::class, $class_name);
}
public function testGetParentClass()
{
// Arrange
$child_class = new ChildClass();
// Actual
$parent_class1 = get_parent_class($child_class);
$parent_class2 = get_parent_class(ChildClass::class);
// Assert
$this->assertSame(ParentClass::class, $parent_class1);
$this->assertSame(ParentClass::class, $parent_class2);
}
public function testGetCalledClass()
{
// Arrange
$child_class = new ChildClass();
$parent_class = new ParentClass();
// Actual
$child_called_class = $child_class->testGetCalledClass();
$parent_called_class = $parent_class->testGetCalledClass();
// Assert
$this->assertSame(ChildClass::class, $child_called_class);
$this->assertSame(ParentClass::class, $parent_called_class);
}
public function testInArray()
{
$this->assertTrue(in_array('a', ['a', 'b', 1], true));
}
public function testGetClassMethod()
{
// Arrange
$get_class_methods1 = get_class_methods(ChildClass::class);
$get_class_methods2 = get_class_methods(new ChildClass());
// Actual
// Assert
$this->assertFalse(in_array('testPrivateGetClassMethod', $get_class_methods1, true));
$this->assertFalse(in_array('testPrivateGetClassMethod', $get_class_methods2, true));
$this->assertFalse(in_array('testProtectedGetClassMethod', $get_class_methods1, true));
$this->assertFalse(in_array('testProtectedGetClassMethod', $get_class_methods2, true));
$this->assertTrue(in_array('testPublicGetClassMethod', $get_class_methods1, true));
$this->assertTrue(in_array('testPublicGetClassMethod', $get_class_methods2, true));
$this->assertTrue(in_array('testGetCalledClass', $get_class_methods1, true));
$this->assertTrue(in_array('testGetCalledClass', $get_class_methods2, true));
}
public function testGetClassVars()
{
// Arrange
// Actual
$class_vars = get_class_vars(ChildClass::class);
// Assert
$this->assertArrayNotHasKey('privateNoDefaultVar', $class_vars);
$this->assertArrayNotHasKey('privateDefaultVar', $class_vars);
$this->assertArrayNotHasKey('protectedNoDefaultVar', $class_vars);
$this->assertArrayNotHasKey('protectedDefaultVar', $class_vars);
$this->assertEmpty($class_vars['publicNoDefaultVar']);
$this->assertEquals('public_laravel', $class_vars['publicDefaultVar']);
}
public function testGetObjectVars()
{
// Arrange
$get_object_vars = new TestGetObjectVars(1, 2, 3);
// Actual
$object_vars = get_object_vars($get_object_vars);
// Assert
$this->assertArrayNotHasKey('x', $object_vars);
$this->assertArrayNotHasKey('y', $object_vars);
$this->assertEquals(3, $object_vars['z']);
$this->assertArrayNotHasKey('dot1', $object_vars);
$this->assertArrayNotHasKey('dot2', $object_vars);
$this->assertArrayNotHasKey('circle1', $object_vars);
$this->assertArrayNotHasKey('circle2', $object_vars);
$this->assertEquals(10, $object_vars['line1']);
$this->assertEmpty($object_vars['line2']);
}
public function testIsSubclassOf()
{
// Arrange
$child_class = new ChildClass();
// Actual
$is_subclass = is_subclass_of($child_class, ParentClass::class);
// Assert
$this->assertTrue($is_subclass);
}
public function testIsA()
{
// Arrange
$child_class = new ChildClass();
// Actual
$is_object = is_a($child_class, ChildClass::class);
$is_subclass = is_a($child_class, ParentClass::class);
// Assert
$this->assertTrue($is_object);
$this->assertTrue($is_subclass);
}
}
class TestGetObjectVars
{
private $x;
protected $y;
public $z;
private $dot1 = 10;
private $dot2;
protected $circle1 = 20;
protected $circle2;
public $line1 = 10;
public $line2;
public function __construct($x, $y, $z)
{
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
}
class ChildClass extends ParentClass
{
private $privateNoDefaultVar;
private $privateDefaultVar = 'private_laravel';
protected $protectedNoDefaultVar;
protected $protectedDefaultVar = 'protected_laravel';
public $publicNoDefaultVar;
public $publicDefaultVar = 'public_laravel';
private function testPrivateGetClassMethod()
{
}
protected function testProtectedGetClassMethod()
{
}
public function testPublicGetClassMethod()
{
}
}
class ParentClass
{
public function testGetCalledClass()
{
return get_called_class();
}
}
class TestClassExists
{
private $testPrivatePropertyExists;
protected $testProtectedPropertyExists;
public $testPublicPropertyExists;
private function testPrivateMethodExists()
{
}
protected function testProtectedMethodExists()
{
}
public function testPublicMethodExists()
{
}
}
interface TestInterfaceExists
{
}
trait TestTraitExists
{
}
PHP不仅提供了检测class, interface, trait, property, method
这些函数Introspection Functions,还提供了一整套的API即反射来检测class, interface, trait, property, method
,这些API是好几个类组成的,提供了很多好用的方法。限于篇幅,下篇再聊下反射API。
总结:本文主要聊了下PHP提供的一套检测class, interface, trait, property, method的两个工具包:Introspection Functions和Reflection API,这里先聊到Introspection Functions。下篇再聊下Reflection API的使用,到时见。