搞定PHP面試 - PHP魔術方法知識點整理

PHP魔術方法知識點整理

代碼使用PHP7.2語法編寫

一、構造函數和析構函數

__construct()

構造函數

__construct ([ mixed $args [, $... ]] ) : void

具有構造函數的類會在每次創建新對象時先調用此方法,所以非常適合在使用對象之前做一些初始化工作。

如果子類中定義了構造函數則不會隱式調用其父類的構造函數。要執行父類的構造函數,需要在子類的構造函數中調用 parent::__construct()。如果子類沒有定義構造函數則會從父類繼承。
當子類的 __construct() 與父類 __construct() 具有不同參數不同時,PHP 不會產生錯誤信息。這一點與其他的類方法不同。

Code

  • 書籍類
/**
 * 書籍類
 * Class Book
 */
class Book {
    /**
     * 書籍名稱
     * @var string $name
     */
    public $name;

    /**
     * 書籍作者
     * @var string $author
     */
    public $author;

    /**
     * 構造函數
     * @param $name
     * @param $author
     */
    public function __construct(string $name, string $author)
    {
        $this->name = $name;
        $this->author = $author;
    }
}
  • 計算機書籍類繼承自Book類,且增加了一個屬性 $category
/**
 * 計算機書籍類
 * Class ComputerBook
 */
class ComputerBook extends Book {

    /**
     * 計算機書籍多了分類屬性
     * @var string $category
     */
    public $category;

    /**
     * 構造函數
     * 這裏的構造函數幣父類的構造函數多了一個參數,但是PHP並不會報錯。
     * 而其他的類方法在這種情況下會報錯。
     * @param $name
     * @param $author
     * @param $category
     */
    public function __construct(string $name, string $author, string $category)
    {
        parent::__construct($name, $author);
        $this->category = $category;
    }
}
$book = new Book('茶館', '老舍');

echo <<<TXT
書籍名稱: {$book->name}
書籍作者: {$book->author}
---\n
TXT
;

$computerBook = new ComputerBook('高性能MySQL','Baron', '數據庫');

echo <<<TXT
書籍名稱: {$computerBook->name}
書籍作者: {$computerBook->author}
書籍分類: {$computerBook->category}
---\n
TXT
;
書籍名稱: 茶館
書籍作者: 老舍
---
書籍名稱: 高性能MySQL
書籍作者: Baron
書籍分類: 數據庫
---

__destruct()

析構函數

__destruct ( void ) : void

析構函數會在到某個對象的所有引用都被刪除或者當對象被顯式銷燬時執行。

如果子類中定義了析構函數則不會隱式調用其父類的析構函數。要執行父類的析構函數,必須在子類的析構函數體中顯式調用 parent::__destruct()。如果子類沒有定義析構函數則會從父類繼承。
析構函數即使在使用 exit() 終止腳本運行時也會被調用。
在析構函數中調用 exit() 將會中止其餘關閉操作的運行。
析構函數中拋異常會導致致命錯誤。

Code

  • 析構函數即使在使用 exit() 終止腳本運行時也會被調用。
/**
 * 房子類
 * Class House
 */
class House {

    /**
     * 析構函數
     */
    public function __destruct()
    {
        echo "要開始拆房子了\n";
        $this->dismantleRoof();
        $this->demolishWall();
    }

    /**
     * 拆除屋頂
     */
    private function dismantleRoof()
    {
        echo "屋頂被拆除\n";
    }

    /**
     * 推掉牆
     */
    private function demolishWall()
    {
        echo "牆被推到\n";
    }
}

// 新建一座房子
$house = new House();
// 腳本停止運行
exit();
要開始拆房子了
屋頂被拆除
牆被推到
  • 在析構函數中調用 exit() 將會中止其餘關閉操作的運行。
/**
 * 別墅類
 * Class House
 */
class Villa extends House {

    /**
     * 析構函數
     */
    public function __destruct()
    {
        $this->demolishSwimmingPool();
        // 拆掉游泳池後後悔了,不想繼續拆了
        exit();
        parent::__destruct();
    }

    /**
     * 拆掉游泳池
     */
    private function demolishSwimmingPool()
    {
        echo "拆掉游泳池\n";
    }
}

// 新建一棟別墅
$villa = new Villa();
// 腳本停止運行
exit();
拆掉游泳池
  • 析構函數中拋異常會導致致命錯誤。
/**
 * 大廈類
 * Class House
 */
class Mansion extends House {

    /**
     * 析構函數
     */
    public function __destruct()
    {
        $this->evacuateCrowd();
        parent::__destruct();
    }

    /**
     * 疏散大廈裏的人
     */
    private function evacuateCrowd()
    {
        throw new Exception('大廈裏還有人,不能拆!');
    }
}

// 新建一座大廈
$mansion = new Mansion();
// 腳本停止運行
exit();
PHP Fatal error:  Uncaught Exception: 大廈裏還有人,不能拆!

二、對象複製

__clone()

__clone ( void ) : void

通過 clone 關鍵字克隆對象,當複製完成時,如果對象定義了 __clone() 方法,則新創建的對象(複製生成的對象)中的 __clone() 方法會被調用。

對象中的 __clone() 方法不能被直接調用。

Code

當對象被複制後,PHP 5 會對對象的所有屬性執行一個淺複製(shallow copy)。所有的引用屬性仍然會是一個指向原來的變量的引用。此時我們需要在 __clone 中強制複製一份對象中的引用屬性。

/**
 * Class MyCloneable
 */
class MyCloneable
{
    public $objectA;
    public $objectB;

    function __clone()
    {
        echo "MyCloneable::__clone\n";
        // 強制複製一份this->objectA, 否則仍然指向同一個對象
        $this->objectA = clone $this->objectA;
    }
}

/**
 * Class SubObject
 */
class SubObject
{
    /**
     * 靜態計數器,對象每被實例化或clone一次,則+1
     * @var int
     */
    public static $counter = 0;

    public $instance;

    public function __construct()
    {
        echo "SubObject::__construct\n\n";
        $this->instance = ++ self::$counter;
    }

    public function __clone()
    {
        echo "SubObject::__clone\n\n";
        $this->instance = ++ self::$counter;
    }
}
$myCloneable = new MyCloneable();

echo "實例化對象SubObject,並複製給 \$myCloneable->objectA :\n";
$myCloneable->objectA = new SubObject();

echo "實例化對象SubObject,並複製給 \$myCloneable->objectB :\n";
$myCloneable->objectB = new SubObject();

echo "複製對象 \$myCloneable \n";
$myCloneable2 = clone $myCloneable;

echo "-------------------\n";
echo "原始對象:\n";
print_r($myCloneable);

echo "複製的對象:\n";
print_r($myCloneable2);
實例化對象SubObject,並複製給 $myCloneable->objectA :
SubObject::__construct

實例化對象SubObject,並複製給 $myCloneable->objectB :
SubObject::__construct

複製對象 $myCloneable
MyCloneable::__clone
SubObject::__clone

-------------------
原始對象:
MyCloneable Object
(
    [objectA] => SubObject Object
        (
            [instance] => 1
        )

    [objectB] => SubObject Object
        (
            [instance] => 2
        )

)
複製的對象:
MyCloneable Object
(
    [objectA] => SubObject Object
        (
            [instance] => 3
        )

    [objectB] => SubObject Object
        (
            [instance] => 2
        )

)

三、重載魔術方法

PHP中的重載與其它絕大多數面嚮對象語言不同。傳統的重載是用於提供多個同名的類方法,但各方法的參數類型和個數不同。
PHP的重載(overloading)是指動態地創建類屬性和方法。是通過魔術方法(magic methods)來實現的。

當調用當前環境下不可訪問(未定義或不可見)的類屬性或方法時,重載方法會被調用。

所有的重載方法都必須被聲明爲 public,且參數都不能通過引用傳遞。

屬性重載的魔術方法有:__set(), __get(), __isset(), __unset()
方法重載的魔術方法有:__call(), __callStatic()

屬性重載

__set()

在給不可訪問屬性賦值時,__set() 會被調用。

public __set ( string $name , mixed $value ) : void

$name 爲要賦值的屬性名,$value 爲屬性值。

__get()

讀取不可訪問屬性的值時,__get() 會被調用。

public __get ( string $name ) : mixed

$name 爲要訪問的屬性名。

__isset()

當對不可訪問屬性調用 isset()empty() 時,__isset() 會被調用。

public __isset ( string $name ) : bool

$name 爲屬性名。

在除 isset() 外的其它語言結構中無法使用重載的屬性,這意味着當對一個重載的屬性使用 empty() 時,重載魔術方法將不會被調用。爲避開此限制,必須將重載屬性賦值到變量再使用 empty()。

__unset()

當對不可訪問屬性調用 unset() 時,__unset() 會被調用。

public __unset ( string $name ) : void

$name 爲屬性名。

Code

class Property {
    /**
     * 被重載的數據保存在該屬性
     * @var array
     */
    private $data = [];

    /**
     * 已經定義的屬性不會調用重載魔術方法
     * @var int
     */
    public $declared = 1;

    /**
     * 只有從類外部訪問屬性時,纔會調用重載魔術方法
     * @var int
     */
    private $hidden = 2;

    /**
     * @param $name
     * @param $value
     */
    public function __set($name, $value)
    {
        echo "Setting '{$name}' to '{$value}'\n";
        $this->data[$name] = $value;
    }

    /**
     * @param $name
     * @return mixed|null
     */
    public function __get($name)
    {
        echo "Getting '$name' ";
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
    }

    /**
     * @param $name
     * @return bool
     */
    public function __isset($name)
    {
        echo "Is '$name' set?\n";
        return isset($this->data[$name]);
    }

    /**
     * @param $name
     */
    public function __unset($name)
    {
        echo "Unsetting '$name'\n";
        unset($this->data[$name]);
    }

    /**
     * 非魔術方法
     * @return int
     */
    public function getHidden()
    {
        return $this->hidden;
    }
}
$obj = new Property;

echo '爲不存在的屬性a賦值:';
$obj->a = 1;
echo '獲取不存在的屬性a的值:', $obj->a . "\n";

echo "----------------------------\n";

echo "使用empty判斷屬性a是否爲空\n";
var_dump(empty($obj->a));

echo "使用isset判斷屬性a是否存在\n";
var_dump(isset($obj->a));

echo "----------------------------\n";

echo "使用unset註銷掉屬性a的值\n";
unset($obj->a);

echo "使用empty判斷屬性a是否爲空\n";
var_dump(empty($obj->a));

echo "使用isset判斷屬性a是否存在\n";
var_dump(isset($obj->a));

echo "----------------------------\n";

echo '獲取可訪問的屬性declared的值:', $obj->declared . "\n";

echo "爲不可訪問的屬性hidden賦值0,此時調用了魔術方法__set()\n";
$obj->hidden = 0;

echo "通過public方法getHidden()從類內部訪問私有屬性hidden的值,不會調用魔術方法:\n", $obj->getHidden() . "\n";

echo "獲取不可訪問的屬性hidden的值,此時調用了魔術方法__get():\n" , $obj->hidden . "\n";
爲不存在的屬性a賦值:Setting 'a' to '1'
獲取不存在的屬性a的值:Getting 'a' 1
----------------------------
使用empty判斷屬性a是否爲空
Is 'a' set?
Getting 'a' /vagrant/magic/overloading.php:119:
bool(false)
使用isset判斷屬性a是否存在
Is 'a' set?
/vagrant/magic/overloading.php:122:
bool(true)
----------------------------
使用unset註銷掉屬性a的值
Unsetting 'a'
使用empty判斷屬性a是否爲空
Is 'a' set?
/vagrant/magic/overloading.php:130:
bool(true)
使用isset判斷屬性a是否存在
Is 'a' set?
/vagrant/magic/overloading.php:133:
bool(false)
----------------------------
獲取可訪問的屬性declared的值:1
爲不可訪問的屬性hidden賦值0,此時調用了魔術方法__set()
Setting 'hidden' to '0'
通過public方法getHidden()從類內部訪問私有屬性hidden的值,不會調用魔術方法:
2
獲取不可訪問的屬性hidden的值,此時調用了魔術方法__get():
Getting 'hidden' 0

方法重載

__call()

在對象中調用一個不可訪問的方法時,__call() 會被調用。

public __call ( string $name , array $arguments ) : mixed

$name 參數是要調用的方法名稱。$arguments 參數是一個枚舉數組,包含着要傳遞給方法 $name 的參數。

__callStatic()

調用一個不可訪問的類靜態方法時,__callStatic() 會被調用。

public static __callStatic ( string $name , array $arguments ) : mixed

$name 參數是要調用的方法名稱。$arguments 參數是一個枚舉數組,包含着要傳遞給方法 $name 的參數。

Code

class Method
{
    public function __call($name, $arguments)
    {
        // 注意: $name 的值區分大小寫
        echo "調用對象不可訪問的方法 '$name' ", implode(', ', $arguments). "\n";
    }

    public static function __callStatic($name, $arguments)
    {
        // 注意: $name 的值區分大小寫
        echo "調用不可訪問的類靜態方法 '$name' ", implode(', ', $arguments). "\n";
    }
}
$obj = new Method;
$obj->run('參數1', '參數2', '參數3');

Method::run('參數1', '參數2', '參數3');
調用對象不可訪問的方法 'run' 參數1, 參數2, 參數3
調用不可訪問的類靜態方法 'run' 參數1, 參數2, 參數3

四、序列化魔術方法

__sleep()

public __sleep ( void ) : array

使用 serialize() 函數對對象進行序列化時,若類中存在魔術方法 __sleep() ,則會先被調用 __sleep(),然後才執行序列化操作。
此功能可以用於清理對象,並返回一個包含對象中所有應被序列化的變量名稱的數組。如果該方法未返回任何內容,則 NULL 被序列化,併產生一個 E_NOTICE 級別的錯誤。
__sleep() 方法常用於提交未提交的數據,或類似的清理操作。同時,如果有一些很大的對象,但不需要全部保存,這個功能就很好用。

__sleep() 不能返回父類的私有成員的名字。這樣做會產生一個 E_NOTICE 級別的錯誤。可以用 Serializable 接口來替代。

__wakeup()

__wakeup ( void ) : void

使用 unserialize() 函數對對象進行反序列化時,若類中存在魔術方法 __wakeup() ,則會先被調用 __wakeup() ,然後才執行反序列化操作。
在反序列化操作中,__wakeup() 方法常用於重新建立數據庫連接,或執行其它初始化操作。

Code

class Connection
{
    /**
     * 數據庫連接資源
     * @var
     */
    protected $link;

    /**
     * 連接數據庫所需要的屬性
     * @var
     */
    private $server, $username, $password, $db;

    /**
     * 構造函數,建立數據庫連接
     * unserialize 執行反序列化時不會調用構造函數
     * @param $server
     * @param $username
     * @param $password
     * @param $db
     */
    public function __construct($server, $username, $password, $db)
    {
        $this->server = $server;
        $this->username = $username;
        $this->password = $password;
        $this->db = $db;
        $this->connect();
    }

    /**
     * 連接數據庫
     */
    private function connect()
    {
        $this->link = mysqli_connect($this->server, $this->username, $this->password);
        mysqli_select_db($this->db, $this->link);
    }

    /**
     * 使用serialize序列化對象時,只保存以下四個對象屬性
     * @return array
     */
    public function __sleep()
    {
        return ['server', 'username', 'password', 'db'];
    }

    /**
     * unserialize 執行反序列化時不會調用構造函數
     * 因此使用 __wakeup 函數重新進行數據庫連接
     */
    public function __wakeup()
    {
        $this->connect();
    }
}

五、其他魔術方法

__toString()

public __toString ( void ) : string

當對象被當做字符串使用時,調用 __toString() 方法,該方法必須返回一個字符串。
若對象未定義 __toString() 方法,或 __toString() 方法返回值不是字符串,則會產生 E_RECOVERABLE_ERROR 級別的致命錯誤。

不能在 __toString() 方法中拋出異常。這麼做會導致致命錯誤。

Code

class ToString
{
    public $foo;

    public function __construct($foo)
    {
        $this->foo = $foo;
    }

    public function __toString()
    {
        return $this->foo;
    }
}

$class = new ToString('Hello World!');
echo $class;
Hello World!

__invoke()

__invoke ([ $... ] ) : mixed

當嘗試以調用函數的方式調用一個對象時,__invoke() 方法會被自動調用。

Code

class CallableClass
{
    function __invoke($foo)
    {
        return $foo;
    }
}

$callable = new CallableClass();
echo $callable("Hello World!\n");
var_dump(is_callable($callable));
Hello World!
/vagrant/magic/__invoke.php:19:
bool(true)
class UncallableClass
{
}
$uncallable = new UncallableClass();
var_dump(is_callable($uncallable));
/vagrant/magic/__invoke.php:27:
bool(false)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章