Magic method và Class aliases trong PHP

Định nghĩa class trong PHP

Các đối tượng trong PHP đều được dựa trên class. Nếu bạn muốn khởi tạo một đối tượng trong PHP, bạn cần phải khai báo class PHP nào được dùng để khởi tạo đối tượng đó. Ví dụ như khi bạn muốn khởi tạo một đối tượng Foo, trước tiên bạn cần có một class Foo tương ứng

<?php    
class Foo
{
}

$object = new Foo;

Có thể bạn đã biết, trong PHP có một loại đối tượng chung( generic object ), bạn có thể ngay lập tức gọi khởi tạo. Tuy nhiên, loại object đó cũng phải được dựa trên class, là stdClass. Nghĩa là khi bạn khai báo

$object = {};

cũng tương đương với

$object = new stdClass;

Việc khai báo class trong PHP là hết sức linh động. Khi bạn gọi keyword class và bắt đầu một block code, PHP sẽ tự động bước vào một trạng thái parsing riêng , trong đó nó sẽ chỉ tìm đọc các thuộc tính, hằng số và phương thức của class. Vì thế, giả sử như nếu ta có một đoạn code PHP như sau, chắc chắn nó sẽ sinh ra lỗi PHP Parse error .

<?php        
class Foo
{
    $time = time();
    protected $_time = $time;
}

Nguyên nhân cũng khá dễ hiểu, nếu như bạn áp dụng nguyên tắc parsing của PHP như đã nói ở trên phải không. Với những ai đã quen thuộc với PHP, đây như là một lẽ tất nhiên vậy. Tuy nhiên, với những lập trình viên của một vài ngôn ngữ khác như ruby hay python, chuyện này khá là cản trở, phiền toái. Trong các ngôn ngữ đó, việc khai báo class cũng chỉ là một block code bình thường, không có nguyên tắc riêng gì áp dụng cho nó. Và rất nhiều những đặc điểm linh hoạt của các ngôn ngữ đó là hệ quả của điều này. Tuy nhiên, thật may mắn cho chúng ta ( đôi khi cũng không hẳn là may mắn đâu xD ), PHP class cũng có những yếu tố "meta-programing" của riêng nó, khiến trong nhiều trường hợp, nó cũng có thể đạt được sự linh hoạt mong muốn. Trong bài viết này, chúng ta cũng tập trung vào một feature cụ thể như thế , đó là các magic methods của PHP.

Magic Methods

Giả sử như, bạn viết một đoạn code

<?php        
class Foo
{
}
$object = new Foo;
$object->someMethod();

chắc chắn, đoạn code này sẽ sinh lỗi ngay PHP Fatal error: Call to undefined method Foo::someMethod() . Cũng dễ hiểu thôi phải không, ở đây ta đang gọi đến phương thức someMethod của class Foo, nhưng trong class Foo lại chưa có khai báo method nào như vậy. Tuy nhiên , nếu như ta sửa lại đoạn code của mình một chút

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

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

Vẫn như thế, ta chưa khai báo cụ thể phương thức someMethod trong class Foo, nhưng khi ta chạy đoạn code này, thay vì error, sẽ cho ta output

Called __call with someMethod

Có vẻ như chương trình của chúng ta đã được thực thi thành công, chuyện gì đã xảy ra vậy. Đó là vì, __call là một magic method của PHP. Trong PHP, khi ta định nghĩa một phương thức với tên __call, và khi chương trình của ta gọi đến một phương thức mà chưa được khai báo ( hoặc gọi đến phương thức mà nó không có quyền truy cập đến), PHP sẽ tự động gọi tới phương thức __call thay vì bắn ra error. Phương thức __call này nhận đầu vào là 2 tham số :

  • Tên của phương thức mà chương trình của ta định gọi tới.
  • Một array chứa các tham số mà chương trình của ta muốn truyền vào phương thức trên.

Đây là một tính năng hết sức hữu ích, cho phép người lập trình quyết định chuyện gì sẽ xảy ra khi lập trình viên hay chương trình khác muốn gọi tới một phương thức trong đối tượng của họ. Nó cũng mang lại một sự linh hoạt nhất định, như ví dụ trong Magento, các getter và setter của framework này được dựa trên magic method __call , qua đó cho phép lập trình viên có thể viết những đoạn code kiểu như

<?php            
$object->setSomeField('value');
echo $object->getSomeField();    

mà không cần phải định nghĩa cụ thể các phương thức setSomeField hay getSomeField. Giá của nó - cũng như mọi tính năng meta-programming khác - là sự rõ ràng và hiệu năng của code. Với những ai không quen / không hiểu, khi đọc một đoạn code gọi đến một phương thức chưa thấy được định nghĩa ở đâu nhưng vẫn thực thi được, có thể gây ra một chút bối rối. Ngoài ra, tuy mỗi lần gọi đến phương thức __call không làm giảm đáng kể về mặt hiệu suất, nhưng nếu quá lạm dụng nó, hoàn toàn có thể khiến chương trình của ta chậm đi thấy rõ.

Static Magic Methods

Ta cùng xem xét một ví dụ khác

<?php            
class Foo
{
    static public function someStaticMethod()
    {
        echo "Called";
    }

    public function __call($method, $args)
    {
        echo "Called __call with $method","\n<br>\n";
    }    
}
Foo::someStaticMethod();

Cùng mổ xẻ đoạn code này một chút nào. Ở đây, ta đã gọi đến một phương thức static là someStaticMethod. Như ta đã biết, trong PHP, các class của ta có thể định nghĩa các phương thức static, là những phương thức thuộc về class. Có nghĩa, đó là một cách để ta định nghĩa các hàm của class mà trong đó, nó không cần biết đến giá trị của object, và có thể được gọi mà không cần phải khởi tạo object. Các phương thức static không thể truy cập tới các thuộc tính bình thường của object, nhưng có thể truy cập tới thuộc tính của các static object.

Các phương thức static vốn khá bị "mang tiếng" với những ai theo đuổi lập trình hướng đối tượng, một phần vì trong những ngày đầu, các lập trình viên java thường dùng cách này để viết code mang hơi hướng lập trình C ( vốn quen thuộc hơn với họ ) hơn là java. Chúng cũng, theo một cách nào đó, cho phép bạn đưa global state vào bên trong đối tượng object. Tuy nhiên, trong bài viết naỳ, tạm thời ta không đi quá sâu vào vấn đề đó.

Thêm vào đó, static method của PHP càng hay thường gây khó hiểu bởi trước phiên bản 5.0, PHP còn cho phép bạn gọi tới bất kì phương thức nào của class với syntax giống như là gọi static. Ví dụ như đoạn code sau là hoàn toàn hợp lệ trong PHP 4

<?php            
//valid PHP 4
class Foo
{
    function someMethod()
    {
        echo "Called","\n";
    }
}
Foo::someMethod();

Ý tưởng của PHP 4 ở đây là cho phép developer có khả năng gọi đến mọi phương thức của object - bất kể trạng thái của nó - mà không cần phải khởi tạo đối tượng. Việc viết code như này trong các phiên bản mới hơn của PHP sẽ làm xảy ra lỗi PHP Strict standards error, tuy nhiên, với nguyên tắc duy trì tương thích ngược bằng mọi giá của PHP core team, đoạn code này vẫn cần phải có thể thực thi trong các phiên bản PHP mới ( tuy nhiên, việc gọi static kiểu naỳ ra rất tồi, bạn nên tránh nó bằng mọi giá )

Nói loanh quanh một hồi để làm gì ? Giờ ta cùng nhìn lại đoạn code ví dụ ở đầu mục, bạn nghĩ nó sẽ cho output như thế nào . Câu trả lời là

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

Thế có nghĩa là sao, như ở trên ta đã đc biết, khi gọi đến một method chưa được khai báo, nhưng nếu ta đã có khai báo phương thức __call, PHP sẽ tự động gọi tới đó. Ở đây, ta được biết thêm rằng, __call chỉ hiệu qủa với các instance method, nghĩa là các method mà object được quyền sử dụng, hay nói cách khác là các method không có khai báo static. Quay trở lại vấn đề tương thích ngược, và để giải bài toán này, PHP có một magic method khác

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

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

}

Foo::someStaticMethod();

Vâng, __callStatic hoạt động gần như giống hệt __call với khác biệt ở đây là __callStatic chỉ hoạt động với các phép gọi static method.

Tóm lại, bài hhọc rút ra là nếu bạn đọc code xong truy ngược lên tất cả các kế thừa của class mà vẫn không hiểu phương thức mà nó gọi được định nghĩa ở đâu, thì khả năng lớn là ở đây, nó đã sử dụng magic method đó.

Class Aliases

Ta cùng xem một ví dụ thực luôn, có thể tìm thấy trong framework Magento

<?php            
class Enterprise_SalesArchive_Block_Adminhtml_Sales_Archive_Order_Container extends Mage_Adminhtml_Block_Widget_Grid_Container
{
}

Vâng, cái tên dài một cách hài hước. Tuy nhiên, trước phiên bản PHP 5.3, với sự ra đời của namespace, thì những đoạn code như thế này cũng không phải là quá hiếm. Và phương thức class_alias của PHP ra đời là để đối phó với những tình huống như thế. Ví dụ, ta chỉ cần thêm một đoạn như kiểu naỳ vào trong đoạn code bootstrap của ứng dụng của mình

<?php
class_alias('Enterprise_SalesArchive_Block_Adminhtml_Sales_Archive_Order_Container','ArchiveOrderContainer');

từ đó về sau, chẳng hạn ta chỉ cần viết

<?php            
$object = new ArchiveOrderContainer;

PHP sẽ có khả năng hiểu , ở đây ta đang gọi đến class Enterprise_SalesArchive_Block_Adminhtml_Sales_Archive_Order_Container. Cách làm này khổ một nỗi là ta sẽ khó có thể biết được, rốt cuộc khi ta gọi đến một class có alias như này, thì rốt cuộc là ta đang gọi đến class thật sự nào. Nếu bạn "may mắn" làm việc với một dự án kiểu như đã qua 50 đời dev, đến một đoạn gặp cảnh này, ta có thể dùng Reflection, ít ra nó cũng khá hơn là Ctrl + F toàn bộ source code =))

<?php            
class A
{
}

class_alias('A', 'B');

$object = new B;

$r = new ReflectionClass('B');
var_dump($r->getName());

Nói chung , với các phiên bản PHP mới, đặc biệt là từ sau 5.3 ( khi có namespace ) thì những tình huống như này là rất ít gặp, nhưng nếu lỡ như bạn có diễm phúc làm việc với các source code cũ hơn, class_alias cũng là một kiến thức bạn nên để mắt tới.