+8

Metaprogramming in PHP

Bạn vào Viblo, bạn search từ khóa Metaprogramming, các bạn sẽ thấy kết quả chỉ có Ruby. Hôm nay mình sẽ giới thiệu về Metaprogramming (MP) trong PHP.

Metaprogramming là gì?

Trước hết, đây là 1 kỹ thuật được áp dụng khi chúng ta cốt, nó đại khái là dùng code để cổt ra 1 code khác, mà cái code này mới là cái chúng ta cần chạy nó để ra kết quả mong muốn. Mình thì không biết trong Ruby thì metaprogramming nó kỳ diệu như thế nào, nhưng trong PHP, chúng ta có 1 thứ gọi mà magic method khiến cho việc triển khai MP rất là thú vị. Chúng ta có thể thay đổi các dòng code ngay trong khi chúng đang chạy, có thể thêm mới, thay đổi các phương thức của 1 class, có thể xử lý được cả những method không tồn tại trong class luôn. Chúng ta sẽ xem qua 1 số ví dụ sau.

Tìm hiểu

Ví dụ 1

function example1($array = [], $add = true, $multiply = true) { 
    foreach ($array as $item) { 
        if ($add) { 
            $item += 1; 
        } 
        if ($multiply) { 
            $item *= 2; 
        } 
        echo $item; 
    }
}

Ở đây, chúng ta có 1 hàm xử lý 1 array, với đầu vào là array, flag $add thì sẽ cộng thêm 1, flag $multiply thì nhân thêm 2 vào mỗi phần tử, đại loại là thế. Như vậy là mỗi lần duyệt qua 1 phần tử, chúng ta phải kiểm tra 2 lần, rồi sau mỗi lần kiểm tra chúng ta sẽ thực hiện phép tính hoặc không. Chúng ta sẽ thay đổi một chút cái hàm này như sau:

function example1($array = [], $add = true, $multiply = true) { 
    $code = 'foreach ($array as $item) {'; 
    if ($add) { 
        $code .= '$item += 1;'; 
    }
    if ($multiply) { 
        $code .= '$item *= 2;'; 
    }
    $code .= 'echo $item;'; 
    $code .= '}'; 
 
    // Execute the value of $code variable 
    eval($code); 
} 

Các bạn có thể thấy là, chúng ta sẽ check các flag trước, nếu có thì chúng ta sẽ thêm 1 string code vào biến $code, và sau đó, trước khi tới eval() thì đoạn code của chúng ta sẽ như thế này

foreach ($array as $item) { 
    $item += 1; 
    $item *= 2; 
    echo $item; 
}

Đoạn code trên chỉ có phép cộng hoặc nhân hoặc cả cộng và nhân (trong trường hợp này, các flag đều là true), và hàm eval() sẽ thực hiện đoạn code trên, thay vì chúng ta phải chạy hàm example1 ban đầu.

** eval() trong PHP có chức năng xem 1 chuỗi string đầu vào là 1 đoạn code php. Tuy nhiên, PHP có cảnh báo trước với chúng ta:

The eval() language construct is very dangerous because it allows execution of arbitrary PHP code. 

Sử dụng metaprogramming có khiến code của chúng ta nhanh hơn không?

Chúng ta thay đổi ví dụ trên một chút, thay đổi cách tính toán một chút và tăng vòng lặp lên 100 triệu lần nhé:

function example1($add = true, $multiply = true) {
	$results = 1;
    for ($item = 1; $item <= 100000000; $item++) {
        if ($add) {
            $results += $item;
        }
        if ($multiply) {
            $results /= 2;
        }
    }
    echo 'Results: ' . $results . "\n";
}

Và đây là kết quả:

Thấy nó nhanh hơn đúng không ạ? và chúng ta kết luận code theo MP khiến code chạy nhanh hơn cách thông thường? Không phải đâu ạ, đừng hiểu nhầm, đây là 1 trong số ít trường hợp mà code của chúng ta chạy nhanh hơn, và trong hầu hết các trường hợp còn lại, code của chúng ta chạy vẫn trong thời gian như nhau hoặc lâu hơn cách thông thường.

Ví dụ 2

$searchCode = 'if ($i == $item) { $found = true; $searchCode = ""; }'; 
// found $item, then no need to check the rest of items

$item = 7;

for ($i = 0; $i < 10000; $i++) { 
    eval($searchCode); 
    echo $i;
}

Ở đây, chúng ta có 1 đoạn code, nhiệm vụ của nó là tìm số 7 trong 10000 số tự nhiên đầu tiên, đồng thời in ra các số đó (ví dụ có vẻ hơi ngu 1 tý, nhưng mà kiểu gì chúng ta chả phải gặp trường hợp như vầy). Thì thông thường, chúng ta cứ code là if then là xong, nhưng khi tìm đc số cần tìm rồi, if then vẫn chạy, thật là lãng phí. Đoạn code trên giúp ta giải quyết vấn đề trên: khi chưa tìm thấy, eval() sẽ thực hiện đoạn code tìm kiếm, khi chạy đoạn code tìm kiếm, nếu tìm thấy, đoạn code tìm kiếm sẽ bị gán thành empty, và ở những vòng lặp sau, eval sẽ không cần phải thực hiện vì đoạn code tìm kiếm bây giờ là rỗng. Thật thú vị.

MP khiến code của chúng ta khó hiểu hơn?

Qua 2 ví dụ vừa rồi, chắc hẳn đây là 1 nhận xét chính xác đúng không? nếu không, chúng ta sẽ đi tiếp qua ví dụ khác nữa, đến bao giờ thật sự khó hiểu thì thôi. Ngoài ra thì, việc thực hiện "code that writing code" như thế kia cũng khiến chúng ta khó debug hơn. Các bạn cứ thử viết sai 1 tý code trong string kia, xem nó báo lỗi ở dòng bao nhiêu là biết ngay. 😄

Ví dụ 3

public function getCategoryLocaleDropList($localeID, $articleFlag = null, $onlyResearch = null)
{
    // Do something with $localeID
    // Call others function
    if ($articleFlag) {
        // Do something when set $articleFlag
    }
    if ($onlyResearch) {
        // Do something when set $onlyResearch 
    }
    // Do something else
}

Chắc hẳn là chúng ta, luôn phải làm việc theo team, và thằng code trước nó có 1 hàm hay hay, mà chỉ cần sửa 1 tý để cho phù hợp với yêu cầu của mình, lại ko muốn copy ra để mà bị lặp code, thì với riêng mình, mình sẽ thêm 1 cái flag, rồi trong hàm check if flag là thêm code của mình. Yeah, thằng tiếp theo nó lại làm như vầy, thêm tiếp 1 cái, và sau lại tiếp như thế đến khi không chịu đựng được thì thôi 😄

Để giải quyết vấn đề trên, chúng ta sẽ sử dụng 1 hàm của php, gọi là func_get_args(). ** chi tiết func_get_args

Hàm này sẽ trả về 1 mảng các phần tử là đầu vào (argument) của hàm chứa nó.

private function getCategoryLocaleDropList()
{
    $args = func_get_args();
    $results = null;
    // Do something
    if ($results = call_user_func_array(
        [$this, $args[0]],
        array_slice($args, 2))
    ){
        return $results;
    }
    // Do something with $args[2], $args[3], …
    $this->_errors[] = $args[1];
    return $results;
}

Chúng ta có thể thấy, hàm có đầu vào rất sạch sẽ, mỗi tội chả hiểu cần phải truyền biến nào theo thứ tự nào nếu không đọc code =)), và khi gọi hàm, chúng ta có thể thoải mái truyền đủ thứ vào:

$this->getCategoryLocaleDropList(
    'methodName',
    'Custom error message.',
    $firstParameter,
    $secondParameter,
    ...
);

Magic Method

Ta có class sau:

class Foo
{
}

$object = new Foo;
$object->someMethod();

Trong class không khai báo gì cả, và ta gọi đến 1 method tên là someMethod, thì chắc hẳn là sẽ bị báo lỗi thế này:

Fatal error: Call to undefined method Foo::someMethod()

Bây giờ, chỉnh sửa class này một chút

class Foo
{
    public function __call($method, $args)
    {
        echo "Called __call with $method\n";
    }
}

$object = new Foo;
$object->someMethod();

Ở đây, chúng ta có 1 magic method tên là __call(), được định nghĩa như sau:

__call() is triggered when invoking inaccessible methods in an object context.

** Tức là khi chúng ta khai báo method này trong class, thì khi chúng ta gọi 1 method nào đó theo kiểu object context, tức là như cách trên đó, thì nó sẽ nhảy vào method này, thay vì báo lỗi như trên. Trở lại ví dụ trên, chúng ta gọi đến 1 method (không tồn tại), thì sẽ được handle bởi cái method __call() này, và kết quả là:

Called __call with someMethod

Ta sửa ví dụ trên, thành như sau:

class Foo
{
    public function __call($method, $args)
    {
        echo "Called __call with $method\n";
    }
}

$object = new Foo;
Foo::someStaticMethod();

Lần này, chúng ta gọi 1 method nào đó theo kiểu static, thì magic __call() của chúng ta sẽ không hoạt động đâu, mà sẽ có lỗi thế này:

PHP Fatal error: Call to undefined method Foo::someStaticMethod()

Bởi vì, __call() chỉ sử dụng cho các method được gọi theo kiểu object context như ví dụ trên. Do đó, chúng ta phải sử dụng thêm __callStatic()

class Foo
{
    public function __call($method, $arguments)
    {
        echo "Called $method\n";
    }

    public static function __callStatic($method, $arguments)
    {
        echo "Called $method\n";
    }
}

Foo::someStaticMethod();

** method __callStatic() được định nghĩa:

__callStatic() is triggered when invoking inaccessible methods in a static context.

Tức là hoạt động của nó y như magic __call() nhưng đối với các method được gọi theo kiểu static.

Magic method trong Laravel

Có bao giờ bạn tự hỏi là tại sao có những method ORM của Laravel lại vừa có thể gọi kiểu static, vừa gọi kiểu non-static không? đến đây thì có thể trả lời được là do Laravel có sử dụng magic method. Có ví dụ:

User::all();

Chúng ta lấy tất cả users, rất đơn giản cho người sử dụng. Nhưng trong Laravel thì phải xử lý nhiều hơn 1 tý:

// File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

public static function __callStatic($method, $parameters) {
    $instance = new static;

    return call_user_func_array(array($instance, $method), $parameters);
}

** call_user_func_array sẽ gọi 1 callback với mảng parameters được truyền vào, ví dụ:

$foo = new foo;
call_user_func_array(array($foo, "bar"), array("three", "four"));
// $foo->bar("three", "four");

Do đó, chúng ta có thể hiểu được là khi gọi all() thì Laravel sẽ gọi:

$model = new User;
$model->all(); 

Tiếp tục với 1 ví dụ khác:

User::callSomeThing();

Lúc trước là chúng ta có method all() trong Model thật, còn giờ thì là method callSomeThing(), và theo như giải thích ở trên, chúng ta có:

$model = new User;
$model->callSomeThing(); 

Nhưng vấn đề là cái instance $model này không có method callSomeThing() nào cả, và lúc này method __call() phát huy tác dụng:

// File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

public function __call($method, $parameters) {
    if (in_array($method, array('increment', 'decrement'))) {
        return call_user_func_array(array($this, $method), $parameters);
    }

    $query = $this->newQuery();

    return call_user_func_array(array($query, $method), $parameters);
}

Bỏ qua đoạn if phía trên, thì chúng ta sẽ thấy ở đây, Laravel sẽ tạo 1 instance Query Builder, và sau đó gọi method callSomeThing() của cái instance vừa tạo này. Và tóm lại sẽ là như thế này:

$model = new User;
$query = new Illuminate\Database\Query\Builder;
$query->setModel($model);
$query->callSomeThing();

Áp dụng Metaprogramming

Ta có 1 trait và 1 class sau, dùng metaprogramming để tự sinh ra method:

trait MetaClass
{
	protected $__classMethods = array();
	
	static protected $__staticMethods = array();
	
	
	public function __call($name, $args)
	{
		if (isset($this->__classMethods[$name]) && $this->__classMethods[$name] instanceof \Closure) {
			return call_user_func_array($this->__classMethods[$name], $args);
		}
		
		if (get_parent_class()) {
			return parent::__call($name, $args);
		}
		
		throw new \BadMethodCallException;
	}
	
	public function addMethod($name, Closure $method)
	{
		$this->__classMethods[$name] = $method;
		$this->__classMethods[$name] = $this->__classMethods[$name]->bindTo($this, $this);
	}
	
	public function removeMethod($name)
	{
		unset($this->__classMethods[$name]);
	}
	
	public static function __callStatic($name, $args)
	{
		if (isset(static::$__staticMethods[$name]) && static::$__staticMethods[$name] instanceof \Closure) {
			return forward_static_call_array(static::$__staticMethods[$name], $args);
		}
		
		if (get_parent_class()) {
			return parent::__call($name, $args);
		}
		
		throw new \BadMethodCallException;
	}
	
	public static function addStaticMethod($name, Closure $method) 
	{
		static::$__staticMethods[$name] = $method;
	}
	
	public static function removeStaticMethod($name)
	{
		unset(static::$__staticMethods[$name]);
	}
}
class Person
{
	use MetaClass;
	
	public static $colours = array('red', 'green', 'blue');
	
	public function __construct($name)
	{
		$this->name = $name;
		
		// Create class methods on the fly.
		foreach (static::$colours as $colour) {
			$method = 'say' . ucwords($colour);
			$this->addMethod($method, function() use ($colour) {
				echo '<p>' . ucwords($this->name) . ' likes the colour ' . ucwords($colour) . '!</p>';
			});
		}
		
		// Create instance methods on the fly.
		static::addStaticMethod('showColours', function() {
			echo 'Colours: ' . ucwords(implode(', ', static::$colours));
		});
	}
	
	public function getPandaName()
	{
		echo '<p>' . ucwords($this->name) . ' the happy panda!</p>';
	}
	
	public static function iLikePandas()
	{
		echo '<p>Everybody loves pandas</p>';
	}
}

Có 1 class con kế thừa class cha Person

class SuperHero extends Person
{
}

Sử dụng:

// Create a person.
$person = new Person('Jamie');
// Class methods created on the fly in the constructor.
$person->sayRed();
$person->sayGreen();
$person->sayBlue();
// Add a new method on the fly.
$person->addMethod('sayColour', function($colour) {
	static::$colours[] = $colour;
	echo '<p>' . ucwords($this->name) . ' loves the colour ' . ucwords($colour) . '!</p>';
});
$person->sayColour('Purple');
// Static method created on the fly in the constructor.
Person::showColours();
// Static method created on the fly.
Person::addStaticMethod('sayHappyThings', function() {
	echo '<p>Happy things, like peanut butter and jelly.</p>';
});
Person::sayHappyThings();
// Call regularly declared class method.
$person->getPandaName();
// Call regularly declared static method.
Person::iLikePandas();
// Remove a class method on the fly.
try {
	$person->removeMethod('sayColour');
	$person->sayColour();	
} catch (BadMethodCallException $exception) {
	echo '<p>Class method sayColour does not exist.</p>';
}
// Remove a static method on the fly.
try {
	Person::removeStaticMethod('sayHappyThings');
	Person::sayHappyThings();
} catch (BadMethodCallException $exception) {
	echo '<p>Static method sayHappyThings does not exist.</p>';
}
// Create a superhero.
$hero = new SuperHero('Batman');
// Class methods created on the fly in the parents constructor.
$hero->sayRed();
$hero->sayGreen();
$hero->sayBlue();
// Class method added to child class on the fly.
$hero->addMethod('sayStuff', function() {
	echo '<p>' . ucwords($this->name) . ' say: I am the Dark Knight!</p>';
});
$hero->sayStuff();
// Class methods not available to their parent classes.
try {
	$person->sayStuff();
} catch (BadMethodCallException $exception) {
	echo '<p>Class method sayStuff does not exist.</p>';
}

** Giải thích: working on it........

Kết luận

  • Trong PHP, MP có thể khiến code của chúng ta chạy nhanh trong 1 số ít trường hợp, còn lại đều khiến chạy chậm
  • Khó debug
  • Giảm khả năng đọc hiểu
  • Dùng để tạo ra các method mới của class trong khi chạy, xử lý các method không tồn tại.
  • Gist
  • Còn gì nữa không?

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí