Laravel5.3之PHP反射(Reflection) (上)

說明:Laravel中常用PHP的反射特性來設計代碼,本文主要學習PHP的反射特性,來提升寫代碼時的設計質量。PHP提供一套檢測class, interface, trait, property, method的兩個工具包:Introspection FunctionsReflection API,相似於探針同樣的東西來探測這些一等公民。本文先看下Introspection Functions的使用。php

開發環境: Laravel5.3 + PHP7laravel

Introspection Functions

Introspection Functions是用來操做object class的一些函數,PHP提供了大量的Introspection Functions來操做class, interface, trait, method, property:bootstrap

  1. class_exists()api

  2. interface_exists()數組

  3. method_exists()app

  4. property_exists()ide

  5. trait_exists()函數

  6. class_alias()工具

  7. get_class()單元測試

  8. get_parent_class()

  9. get_called_class()

  10. get_class_methods()

  11. get_class_vars()

  12. get_object_vars()

  13. is_subclass_of()

  14. is_a()

class_exists()

Laravel源碼中好多個地方使用到class_exists()方法來判斷指定類是否存在,如\Illuminate\Database\Connection::isDoctrineAvailable()的源碼:

public function isDoctrineAvailable()
    {
        return class_exists('Doctrine\DBAL\Connection'); // Doctrine\DBAL\Connection::class類是否存在,大小寫不敏感
    }

寫個PHPUnit測試下(爆綠燈,說明是正確的,這裏不截圖了。後面全部Introspection的測試都放在IntrospectionTest這個單元測試裏):

namespace MyRightCapital\Container\Tests;

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測試下,爆綠燈:

namespace MyRightCapital\Container\Tests;

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測試下,爆綠燈:

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不少地方用到了該函數,如\Illuminate\Foundation\Auth\RedirectsUsers::redirectPath()源碼:

public function redirectPath()
    {
        return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';
    }

寫個PHPUnit測試下該函數,爆綠燈:

// 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測試,爆綠燈:

// 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中如何使用的:

public function load($alias)
    {
        if (isset($this->aliases[$alias])) {
            return class_alias($this->aliases[$alias], $alias);
        }
    }

寫個PHPUnit測試,爆綠燈:

public function testClassAlias()
    {
        // Arrange
        class_alias(TestClassExists::class, 'MyRightCapital\Container\Tests\AliasTestClassExists');
        $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)方法,是個很好用的方法:

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測試,爆綠燈:

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測試下:

// namespace MyRightCapital\Container\Tests;
    // 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中還沒使用到該函數,不妨寫個測試看下如何使用:

// namespace MyRightCapital\Container\Tests;
    // 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只有一處用到了該方法\Illuminate\Database\Eloquent\Model::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中只有一處使用到\Illuminate\Mail\Jobs\HandleQueuedMessage::__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測試,爆綠燈:

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的測試代碼:

<?php

namespace MyRightCapital\Container\Tests;

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, 'MyRightCapital\Container\Tests\AliasTestClassExists');
        $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的使用,到時見。

相關文章
相關標籤/搜索