PHP Magic Methods là gì?

PHP Magic Methods là gì?

Nếu bạn từng đọc code của một project PHP mã nguồn mở, bạn có thể chú ý tới các object methods đượt bắt đầu bằng hai dấu gạch dưới (__).
Chúng là Magic Methods, cho phép bạn phản ứng lại các events nhất định khi sử dụng các objects cụ thể. Điều đó có nghĩa là khi có điều gì đó xảy ra đối với object của bạn, bạn có thể định nghĩa phản ứng như thế nào trong trường hợp đó.

Bạn có lẽ đã bắt gặp một số magic methods của PHP rồi vì chúng khá là phổ biến. Tuy nhiên với một PHP developer tôi tin rằng bạn cần nắm vững các công cụ như thế khi làm việc với Object Oriented PHP.

Bài viết hôm nay sẽ giúp bạn hiểu thêm về PHP Magic Methods

Working within a context (tạm dịch: làm việc trong một ngữ cảnh)

Tưởng tượng rằng chúng ta đang kéo các tweets từ Twitter API. Chúng ta lấy về một JSON payload của người dùng hiện tại và chúng ta muốn chuyển mỗi tweet vào một object để làm việc với chúng. Dưới đây là một class Tweet đơn giản:

class Tweet {
}

Bây giờ chúng ta đã thiết lập một bối cảnh rồi. Tôi sẽ điểm qua từng Magic Methods để cho bạn thấy được cách áp dụng của chúng trong hệ thống.

Construct và Destruct

Trong PHP, Magic Method phổ biến nhất chính là __construct() method. Method này dùng để tự động gọi khi một object được khởi tạo. Điều đó có nghĩa là bạn có thể chèn ác parameters và dependancies khi khởi tạo object. Ví dụ:

public function __construct($id, $text)
{
    $this->id = $id;
    $this->text = $text;
}

$tweet = new Tweet(123, 'Hello world');

khi bạn tạo mới một object của Tweet, bạn có thể truyền vào parameters, những params này sẽ được chèn vào trong __construct() method. Như bạn có thể thấy, thật ra bạn không cần gọi method này mà nó sẽ tự động gọi cho bạn.

Khi bạn mở rộng một object, object cha sẽ đôi khi cũng có một __construct() method yêu cầu chèn cái gì đó cào trong object. Khi đó bạn cần gọi __construct() method của lớp cha:

class Entity {

    protected $meta;

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

}

class Tweet extends Entity {

    protected $id;
    protected $text;

    public function __construct($id, $text, array $meta)
    {
        $this->id = $id;
        $this->text = $text;

        parent::__construct($meta);
    }

}

Khi một object bị destroy bởi __destruct() method. Một lần nữa với __construct() method sẽ tự động kích hoạt giống như là PHP sẽ xử lý nó giúp bạn.

Destruct method sẽ cho phép bạn xóa sạch bất cứ thứ gì không cần thiết một lần đối với object bị destroy. Ví dụ bạn có thể ngắt kết nối tới một dịch vụ bên ngoài hoặc database:

public function __destruct()
{
    $this->connection->destroy();
}

Thành thật mà nói thì __destruct() method ít khi được sử dụng. Vòng đời của 1 request PHP quá ngắn nên method này ít có giá trị nên ít được sử dụng.

Getting và Setting

Khi làm việc với objects trong PHP, bạn đôi khi muốn truy cập tới thuộc tính của object giống như:

$tweet = new Tweet(123, 'hello world');
echo $tweet->text; // 'hello world'

Tuy vậy, khi các thuộc tính của một object được set protected và cách truy cập như trên sẽ dẫn đến lỗi. Thật may là PHP có __get() method sẽ lắng nghe requests cho các thuộc tính mà không được public:

public function __get($property)
{
    if (property_exists($this, $property)) {
        return $this->$property;
    }
}

__get() method nhận tên của thuộc tính bạn đang tìm kiếm như một argement. Ở đoạn code bên trên, đầu tiền tôi kiểm tra nếu tồn tạo thuộc tính trên object hiện tại thì nó sẽ tự động trả về từ object.

Nếu bạn không gọi __get() method, PHP sẽ tự động gọi nó cho bạn khi bạn cố gắng truy cập một thuộc tính mà không phải là thuộc tính public của đối tượng đó.

Nếu bạn cố gắng set một thuộc tính không thể truy cập, __set() method hân hạnh tài trợ chương trình này. Method này lấy thuộc tính mà bạn đang cố gắng truy cập và giá trị mà bạn muốn thiết lập như hai đối số (arguments).

Nếu bạn muốn cho phép chức năng này trong object, có thể làm như sau:


public function __set($property, $value)
{
    if (property_exists($this, $property)) {
        $this->$property = $value;
    }
}

$tweet->text = 'Setting up my twttr';
echo $tweet->text; // 'Setting up my twttr'

Trên đây là hai ví dụ nói về cách get và set thuộc tính cho một object khi chúng không được set public. Việc truy cập các thuộc tính giống như vậy không phải lúc nào cũng là ý tưởng tốt. Tốt hơn cả là ta định nghĩa getter và setter methods để tạo thành một API nhất quán. Điều đó có nghĩa là nếu các thuộc tính của bạn thay đổi, bất kỳ đoạn code nào được sử dụng object này sẽ không bị lỗi ngay lập tức.

Bạn cũng thường xuyên thấy __get()__set() methods được gọi tự động một getter hoặc setter method trên một object. Đây là một giải pháp tốt nếu bạn muốn biến đổi hoặc thêm vào những logic trong process của bạn.

Checking to see if a property is set (kiểm tra xem thuộc tính đã được set hay chưa)

Nếu bạn quen thuộc với code PHP, bạn có lẽ đã gặp isset() function khi làm việc với array rồi. Và bạn cũng có thể dùng function này trên objects để kiểm tra xem một thuộc tính public có thể truy cập được đã được set hay chưa.

Nếu bạn thử dùng function này để check xem một thuộc tính có được truy cập public hay không, bạn có thể sử dụng __isset() magic method:


public function  __isset($property)
{
    return isset($this->$property);
}

isset($tweet->text); // true

Như bạn có thể thấy ở trên, __isset() method sẽ bắt được sự kiện gọi tới một thuộc tính không được phép truy cập và nhận nó như một argument. Bạn có thể dùng isset() function ở bên trong object để trả về giá trị boolean một cách chính xác.

Unsetting a property (bỏ set một thuộc tính)

Tương tự với isset() function, unset() function cũng thường được sử dụng với array và bạn cũng có thể chạy nó trên một object để unset các thuộc tính public. Nếu bạn cố gắng unset thuộc tính không public, __unset() method sẽ bắt request và cho phép bạn triển khai nó bên trong class:


public function __unset($property)
{
    unset($this->$property);
}

To String

__toString() method cho phép bạn trả về object như một string:

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

$tweet = new Tweet(1, 'hello world');
echo $tweet; // 'hello world'

Điều này có nghĩa là bất cứ khi nào bạn cố gắng gọi ra object như một string, như là cố gắng sử dụng echo, object sẽ trả về theo cách mà bạn định nghĩa nó trong __toString() method.

Một ví dụ cho trường hợp này đó là bất cứ khi nào bạn trả về một trong những Laravel Eloquent Models trong một controller, bạn sẽ trả về một object như json. Việc này có thể thực hiện trong một __toString() method. Để biết cách mà Laravel triển khai nó, bạn có thể tham khảo link sau Laravel Eloquent Model.

Sleep và Wakeup

Function serialize() là một cách thông dụng để lưu trữ một đại diện (representation) của một object. Ví dụ, nếu bạn muốn lưu một object vào trong database, đầu tiên bạn cần serialize nó, lưu trữ nó và tiếp theo khi bạn một sử dụng nó từ trong database, bạn sẽ phải unserialize nó.

__sleep() method cho phép bạn định nghĩa những thuộc tính nào của object nên serialize vì có thể bạn không muốn serialize bất kỳ loại nào bên ngoài object có thể sẽ không phù hợp khi bạn unserialize nó.

Ví dụ, tưởng tượng rằng nếu khi bạn tạo mới một thực thể, chúng ta cũng cần cung cấp một cơ chế lưu trữ:


$tweet = new Tweet(123, 'Hello world', new PDO ('mysql:host=localhost;dbname=twttr', 'root'));

Khi chúng ta serialize tweet này, chúng ta không muốn serialize database connection bởi vì nó không liên quan trong tương lai.

__sleep() method chỉ đơn giản là một mảng các thuộc tính mà cần serialize:

public function __sleep()
{
    return array('id', 'text');
}

Khi nó đến với unserialize object, chúng ta cần thiết lập object trong nó với đúng trạng thái. Trong ví dụ này tôi cần thiết lập lại database connection, nhwnh trong thực tế điều này có thể có nghĩa là thiết lập bất cứ điều gì mà object cần phải nhận thức. Bạn có thể làm điều đó với __wakeup() magic method:

public function __wakeup()
{
    $this->storage->connect();
}

Call

__call() method sẽ nhận khi bạn cố gắng gọi một method không cho phép truy cập ở dạng không public trên một object. Ví dụ bạn có một mảng data trên object mà bạn muốn biến đổi trước khi được trả về:

class Tweet extends {
 
    protected $id;
    protected $text;
    protected $meta;
 
	public function __construct($id, $text, array $meta)
	{
		$this->id = $id;
		$this->text = $text;
		$this->meta = $meta;
	}

	protected function retweet()
	{
		$this->meta['retweets']++;
	}

	protected function favourite()
	{
		$this->meta['favourites']++;
	}

	public function __get($property)
	{
		var_dump($this->$property);
	}

	public function __call($method, $parameters)
	{
		if (in_array($method, array('retweet', 'favourite'))) {
			return call_user_func_array(array($this, $method), $parameters);
		}
	}
}
 
$tweet = new Tweet(123, 'hello world', array('retweets' => 23, 'favourites' => 17));
 
$tweet->retweet();
$tweet->meta; // array(2) { ["retweets"]=> int(24) ["favourites"]=> int(17) }

Cloning

Khi bạn tạo một bản sao của một object trong PHP, nó vẫn liên kết tới object gốc. Điều này có nghĩa là nếu bạn tạo một thay đổi trong object gốc thì object bản sao cũng sẽ thay đổi theo:

$sheep1 = new stdClass;
$sheep2 = $sheep1;
 
$sheep2->name = "Polly";
$sheep1->name = "Dolly";
 
echo $sheep1->name; // Dolly
echo $sheep2->name; // Dolly

Điều này xảy ra khi bạn cope một object trong PHP, nó là sự thông qua một tham chiếu, có nghĩa là nó vẫn duy trì một đường dẫn đến object gốc.

Để giải quyết vấn đề này, chúng ta phải sử dụng từ khóa clone:

$sheep1 = new stdClass;
$sheep2 = clone $sheep1;
 
$sheep2->name = "Polly";
$sheep1->name = "Dolly";
 
echo $sheep1->name; // Dolly
echo $sheep2->name; // Polly

Tuy nhiên, nếu chúng ta có những objects được inject vào trong một object khác thì những phụ thuộc đó sẽ thông qua một tham chiếu:

class Notification {
 
    protected $read = false;
 
    public function markAsRead()
    {
         $this->read = true;
    }
 
    public function isRead()
    {
         return $this->read == true;
    }
 
}
 
class Tweet {
 
    protected $id;
    protected $text;
    protected $notification;
 
    public function __construct($id, $text, Notification $notification)
    {
        $this->id = $id;
        $this->text = $text;
        $this->notification = $notification;
    }
 
    public function  __call($method, $parameters)
    {
        if(method_exists($this->notification, $method)) {
            return call_user_func_array(array($this->notification, $method), $parameters);
        }
    }
}
 
$tweet1 = new Tweet(123, 'Hello world', new Notification);
$tweet2 = clone $tweet1;
 
$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // true

Để sửa vấn đề này chúng ta có thể dùng __clone() method để nhân bản bất kỳ injedted objects bất cứ khi nào clone event xảy ra trên object cha:

class Tweet {
 
    protected $id;
    protected $text;
    protected $notification;
 
    public function __construct($id, $text, Notification $notification)
    {
        $this->id = $id;
        $this->text = $text;
        $this->notification = $notification;
    }
 
    public function  __call($method, $parameters)
    {
         if(method_exists($this->notification, $method)) {
             return call_user_func_array(array($this->notification, $method), $parameters);
         }
    }
 
    public function  __clone()
    {
        $this->notification = clone $this->notification;
    }
 
}
 
$tweet1 = new Tweet(123, 'Hello world', new Notification);
$tweet2 = clone $tweet1;
 
$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // false

Invoke

__invoke() magic method cho phép bạn sử dụng một object như thể nó là một function:

class User {
 
	protected $name;
	protected $timeline = array();

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

	public function addTweet(Tweet $tweet)
	{
		$this->timeline[] = $tweet;
	}
}

class Tweet {

	protected $id;
	protected $text;
	protected $read;

	public function __construct($id, $text)
	{
		$this->id = $id;
		$this->text = $text;
		$this->read = false;
	}

	public function __invoke($user)
	{
		$user->addTweet($this);
		return $user;
	}

}
 
$users = array(new User('Ev'), new User('Jack'), new User('Biz'));
$tweet = new Tweet(123, 'Hello world');
$users = array_map($tweet, $users);
 
var_dump($users);

Trong ví dụ này, tôi có thể sử dụng $tweet object như một callback trong array_map function. array_map sẽ duyệt qua mảng các users và sử dụng $tweet object như một function. Trong trường hợp này, mỗi Tweet sẽ được add vào timeline của users.

Tổng kết

Như bạn đã thấy từ mỗi method ở trên, PHP Magic Method được sử dụng để phản ứng với các sự kiện và kịch bản khác nhau mà object của bạn có thể tìm thấy trong chính nó. Mỗi magic method sẽ được kích hoạt một cách tự động, bởi vậy về bản chất, bạn chỉ cần xác định những gì nên xảy ra dưới những trường hợp như trên.

Hy vọng qua các ví dụ, bạn có thể hiểu thêm về PHP magic method và hiểu thêm về những kiến thức cơ bản của PHP.