Không gian tên (namespace) có mục đích rõ ràng nhất là giải quyết vấn đề trùng tên. Trong PHP không cho phép tồn tại hai hàm hoặc lớp có cùng tên, nếu không sẽ phát sinh lỗi nghiêm trọng. Trường hợp này chỉ cần tránh đặt tên trùng nhau là có thể giải quyết được, cách phổ biến nhất là quy ước một tiền tố chung.
Ví dụ: Trong dự án có hai mô-đun: bài viết và bảng tin nhắn, mỗi mô-đun đều có một lớp xử lý bình luận người dùng là Comment. Sau đó tôi muốn thêm tính năng thống kê thông tin các bình luận của tất cả người dùng, ví dụ như muốn lấy tổng số lượng bình luận. Lúc này việc gọi phương thức do lớp Comment cung cấp là cách làm rất tốt, nhưng việc đồng thời đưa vào hai lớp Comment riêng biệt là điều không thể thực hiện được, mã nguồn sẽ bị lỗi, việc viết lại lại một trong hai lớp Comment ở nơi khác cũng làm giảm khả năng bảo trì. Lúc này chỉ còn cách tái cấu trúc tên lớp, tôi quy ước một quy tắc đặt tên, thêm tên mô-đun vào phía trước tên lớp, như sau: Article_Coment, MessageBoard_Comment
Có thể thấy, tên trở nên dài hơn nhiều, điều đó có nghĩa là sau này khi sử dụng Comment sẽ phải viết nhiều mã hơn (ít nhất là số ký tự tăng lên). Và sau này nếu muốn thêm nhiều chức năng tích hợp hơn nữa cho các mô-đun, hoặc gọi chéo giữa các mô-đun, khi xảy ra trùng tên lại cần phải tái cấu trúc tên. Tất nhiên nếu từ đầu dự án đã chú ý đến vấn đề này và quy định quy tắc đặt tên thì có thể tránh tốt vấn đề này. Một giải pháp khác có thể cân nhắc là sử dụng không gian tên.
Lưu ý: Hằng số được đề cập trong bài: Từ PHP 5.3 trở đi, từ khóa const có thể được sử dụng bên ngoài lớp. Cả const và define đều dùng để khai báo hằng số (không trình bày chi tiết sự khác biệt giữa chúng), nhưng trong không gian tên, define có tác dụng toàn cục, trong khi const có tác dụng trong không gian hiện tại. Các hằng số tôi đề cập trong bài là các hằng số được khai báo bằng const.
Cơ bản
Không gian tên phân chia mã nguồn thành các khu vực khác nhau, tên của các hằng số, hàm, lớp (để tiện, tôi sẽ gọi chung là các phần tử) trong mỗi khu vực không ảnh hưởng lẫn nhau, điều này tương tự với khái niệm 'đóng gói' mà chúng ta thường nói đến.
Để tạo một không gian tên cần sử dụng từ khóa namespace, như sau:
<?php
// Tạo không gian tên 'Article'
namespace Article;
?>Lưu ý rằng trước không gian tên đầu tiên trong tệp hiện tại không được có bất kỳ mã nào, những cách viết dưới đây đều sai:
Ví dụ một
// Viết một số logic phía trước tệp
<?php
$path = "/";
class BinhLuan { }
namespace Article;
?>
Ví dụ hai
</html>
<?php
namespace Article;
?>Tại sao lại nói về không gian tên đầu tiên? Vì trong cùng một tệp có thể tạo nhiều không gian tên khác nhau.
Dưới đây tôi tạo hai không gian tên, đồng thời thêm một lớp BinhLuan cho từng không gian:
<?php
// Tạo không gian tên 'Article'
namespace Article;
// Lớp này thuộc phần tử của không gian Article
class BinhLuan { }
// Tạo không gian tên 'MessageBoard'
namespace MessageBoard;
// Lớp này thuộc phần tử của không gian MessageBoard
class BinhLuan { }
?>Giữa các không gian khác nhau không thể trực tiếp gọi các phần tử khác, cần sử dụng cú pháp không gian tên:
<?php
namespace Article;
class BinhLuan { }
namespace MessageBoard;
class BinhLuan { }
// Gọi lớp BinhLuan của không gian hiện tại (MessageBoard)
$binhluan = new BinhLuan();
// Gọi lớp BinhLuan của không gian Article
$article_binhluan = new \Article\BinhLuan();
?>Có thể thấy, khi gọi lớp BinhLuan trong không gian article từ không gian MessageBoard, sử dụng cú pháp giống đường dẫn tệp: \tên_không_gian\tên_phần_tử
Ngoài lớp, cách dùng cho hàm và hằng số cũng giống nhau, dưới đây tôi tạo các phần tử mới cho hai không gian, và xuất giá trị của chúng trong không gian MessageBoard.
<?php
namespace Article;
const DUONGDAN = '/article';
function layTongSoBinhLuan() {
return 100;
}
class BinhLuan { }
namespace MessageBoard;
const DUONGDAN = '/message_board';
function layTongSoBinhLuan() {
return 300;
}
class BinhLuan { }
// Gọi hằng số, hàm và lớp của không gian hiện tại
echo DUONGDAN; ///message_board
echo layTongSoBinhLuan(); //300
$binhluan = new BinhLuan();
// Gọi hằng số, hàm và lớp của không gian Article
echo \Article\DUONGDAN; ///article
echo \Article\layTongSoBinhLuan(); //100
$article_binhluan = new \Article\BinhLuan();
?>Sau đó tôi thực sự nhận được dữ liệu phần tử của không gian Article.
Không gian con
Cú pháp gọi không gian tên giống như đường dẫn tệp là có lý do, nó cho phép chúng ta tự định nghĩa không gian con để mô tả mối quan hệ giữa các không gian.
Xin lỗi vì tôi quên nói, hai mô-đun article và message board thực chất đều nằm trong cùng một dự án blog. Nếu dùng không gian tên để biểu thị mối quan hệ của chúng, sẽ như sau:
<?php
// Tôi dùng không gian tên như thế này để biểu thị mô-đun article trong blog
namespace Blog\Article;
class BinhLuan { }
// Tôi dùng không gian tên như thế này để biểu thị mô-đun message board trong blog
namespace Blog\MessageBoard;
class BinhLuan { }
// Gọi lớp của không gian hiện tại
$binhluan = new BinhLuan();
// Gọi lớp của không gian Blog\Article
$article_binhluan = new \Blog\Article\BinhLuan();
?>Và không gian con có thể định nghĩa nhiều cấp độ, ví dụ như Blog\Article\Archive\Date
Không gian công cộng
Tôi có một tệp script common_inc.php, bên trong chứa một số hàm và lớp hữu ích:
<?php
function layIP() { }
class LocXSS { }
?>Khi đưa tệp script này vào một không gian tên, các phần tử trong tệp không thuộc không gian đó. Nếu tệp này không định nghĩa không gian tên khác, các phần tử của nó luôn nằm trong không gian công cộng:
<?php
namespace Blog\Article;
// Nhập tệp script
include './common_inc.php';
$loc_XSS = new LocXSS(); // Xảy ra lỗi nghiêm trọng: không tìm thấy lớp Blog\Article\LocXSS
$loc_XSS = new \LocXSS(); // Đúng
?>Cách gọi không gian công cộng là thêm \ trực tiếp trước tên phần tử, nếu không trình phân tích PHP sẽ cho rằng tôi muốn gọi phần tử trong không gian hiện tại. Ngoài các phần tử do người dùng định nghĩa, bao gồm cả các phần tử tích hợp sẵn của PHP, đều thuộc không gian công cộng.
Phải nói thêm, thực tế các hàm và hằng số trong không gian công cộng không cần thêm \ vẫn có thể gọi bình thường (không hiểu tại sao PHP lại làm như vậy), nhưng để phân biệt đúng phần tử, vẫn khuyên dùng thêm \ khi gọi hàm
Thuật ngữ tên gọi
Trước khi nói về bí danh và nhập khẩu, cần biết ba thuật ngữ về tên không gian và cách PHP phân tích chúng. Tài liệu chính thức viết rất hay, tôi sẽ mượn luôn.
- 1. Tên không giới hạn, hoặc tên lớp không có tiền tố, ví dụ $binhluan = new BinhLuan();. Nếu không gian tên hiện tại là Blog\Article, BinhLuan sẽ được phân tích thành Blog\Article\BinhLuan. Nếu mã sử dụng BinhLuan không nằm trong bất kỳ không gian tên nào (trong không gian toàn cục), thì BinhLuan sẽ được phân tích thành BinhLuan.
- 2. Tên giới hạn, hoặc tên có tiền tố, ví dụ $binhluan = new Article\BinhLuan();. Nếu không gian hiện tại là Blog, thì BinhLuan sẽ được phân tích thành Blog\Article\BinhLuan. Nếu mã sử dụng BinhLuan không nằm trong bất kỳ không gian tên nào (trong không gian toàn cục), thì BinhLuan sẽ được phân tích thành BinhLuan.
- 3. Tên hoàn toàn giới hạn, hoặc tên có toán tử tiền tố toàn cục, ví dụ $binhluan = new \Article\BinhLuan();. Trong trường hợp này, BinhLuan luôn được phân tích thành tên văn bản (literal name) Article\BinhLuan trong mã.
Thực tế có thể so sánh ba loại tên này với tên tệp (ví dụ binhluan.php), tên đường dẫn tương đối (ví dụ ./article/binhluan.php), tên đường dẫn tuyệt đối (ví dụ /blog/article/binhluan.php), như vậy có thể dễ hiểu hơn.
Tôi dùng vài ví dụ để biểu thị chúng:
<?php
// Tạo không gian Blog
namespace Blog;
class BinhLuan { }
// Tên không giới hạn, biểu thị không gian Blog hiện tại
// Cuộc gọi này sẽ được phân tích thành Blog\BinhLuan();
$blog_binhluan = new BinhLuan();
// Tên giới hạn, biểu thị liên quan đến không gian Blog
// Cuộc gọi này sẽ được phân tích thành Blog\Article\BinhLuan();
$article_binhluan = new Article\BinhLuan(); // Không có dấu gạch chéo \ phía trước lớp
// Tên hoàn toàn giới hạn, biểu thị tuyệt đối so với không gian Blog
// Cuộc gọi này sẽ được phân tích thành Blog\BinhLuan();
$article_binhluan = new \Blog\BinhLuan(); // Có dấu gạch chéo \ phía trước lớp
// Tên hoàn toàn giới hạn, biểu thị tuyệt đối so với không gian Blog
// Cuộc gọi này sẽ được phân tích thành Blog\Article\BinhLuan();
$article_binhluan = new \Blog\Article\BinhLuan(); // Có dấu gạch chéo \ phía trước lớp
// Tạo không gian con Article của Blog
namespace Blog\Article;
class BinhLuan { }
?>Thực tế trước giờ tôi luôn sử dụng tên không giới hạn và tên hoàn toàn giới hạn, bây giờ cuối cùng có thể gọi đúng tên chúng.
Bí danh và nhập khẩu
Bí danh và nhập khẩu có thể coi là cách rút gọn khi gọi các phần tử không gian tên. PHP không hỗ trợ nhập khẩu hàm hoặc hằng số.
Chúng đều được thực hiện thông qua toán tử use:
<?php
namespace Blog\Article;
class BinhLuan { }
// Tạo không gian BBS (tôi có ý định mở diễn đàn)
namespace BBS;
// Nhập khẩu một không gian tên
use Blog\Article;
// Sau khi nhập khẩu không gian tên có thể dùng tên giới hạn để gọi phần tử
$article_binhluan = new Article\BinhLuan();
// Dùng bí danh cho không gian tên
use Blog\Article as BaiViet;
// Dùng bí danh thay thế tên không gian
$article_binhluan = new BaiViet\BinhLuan();
// Nhập khẩu một lớp
use Blog\Article\BinhLuan;
// Sau khi nhập khẩu lớp có thể dùng tên không giới hạn để gọi phần tử
$article_binhluan = new BinhLuan();
// Dùng bí danh cho lớp
use Blog\Article\BinhLuan as BL;
// Dùng bí danh thay thế tên không gian
$article_binhluan = new BL();
?>Tôi nhận thấy, nếu nhập khẩu phần tử khi không gian hiện tại có phần tử cùng tên sẽ thế nào? Rõ ràng kết quả sẽ là lỗi nghiêm trọng.
<?php
namespace Blog\Article;
class BinhLuan { }
namespace BBS;
class BinhLuan { }
Class BL { }
// Nhập khẩu một lớp
use Blog\Article\BinhLuan;
$article_binhluan = new BinhLuan(); // Xung đột với BinhLuan trong không gian hiện tại, chương trình tạo lỗi nghiêm trọng
// Dùng bí danh cho lớp
use Blog\Article\BinhLuan as BL;
$article_binhluan = new BL(); // Xung đột với BL trong không gian hiện tại, chương trình tạo lỗi nghiêm trọng
?>Gọi động
PHP cung cấp từ khóa namespace và hằng số ma thuật __NAMESPACE__ để truy cập động phần tử, __NAMESPACE__ có thể được sử dụng theo dạng kết hợp chuỗi để truy cập động:
<?php
namespace Blog\Article;
const DUONGDAN = '/Blog/article';
class BinhLuan { }
// Từ khóa namespace biểu thị không gian hiện tại
echo namespace\DUONGDAN; ///Blog/article
$binhluan = new namespace\BinhLuan();
// Giá trị của hằng số ma thuật __NAMESPACE__ là tên không gian hiện tại
echo __NAMESPACE__; //Blog\Article
// Có thể kết hợp thành chuỗi và gọi
$ten_lop_binhluan = __NAMESPACE__ . '\BinhLuan';
$binhluan = new $ten_lop_binhluan();
?>Vấn đề gọi dạng chuỗi
Trong ví dụ gọi động ở trên, chúng ta thấy cách gọi động dạng chuỗi, nếu muốn dùng cách này cần lưu ý hai vấn đề.
1. Khi dùng dấu ngoặc kép ký tự đặc biệt có thể bị escape
<?php
namespace Blog\Article;
class ten { }
// Tôi muốn gọi Blog\Article\ten
$ten_lop = __NAMESPACE__ . "\ten"; // Nhưng \t sẽ bị escape thành ký tự tab
$ten = new $ten_lop(); // Xảy ra lỗi nghiêm trọng
?>2. Không được coi là tên giới hạn
PHP xác định không gian chứa phần tử và tình trạng nhập khẩu khi biên dịch script. Trong khi phân tích script, cách gọi dạng chuỗi chỉ có thể được coi là tên không giới hạn hoặc tên hoàn toàn giới hạn, mà không bao giờ là tên giới hạn.
<?php
namespace Blog;
// Nhập khẩu lớp Common
use Blog\Article\Common;
// Tôi muốn dùng tên không giới hạn để gọi Blog\Article\Common
$ten_lop_common = 'Common';
// Thực tế sẽ được coi là tên không giới hạn, tức là biểu thị lớp Common trong không gian hiện tại, nhưng tôi chưa tạo lớp Common trong không gian này
$common = new $ten_lop_common(); // Xảy ra lỗi nghiêm trọng: lớp Common không tồn tại
// Tôi muốn dùng tên giới hạn để gọi Blog\Article\Common
$ten_lop_common = 'Article\Common';
// Thực tế sẽ được coi là tên hoàn toàn giới hạn, tức là biểu thị lớp Common trong không gian Article, nhưng tôi chỉ định nghĩa không gian Blog\Article chứ không phải không gian Article
$common = new $ten_lop_common(); // Xảy ra lỗi nghiêm trọng: lớp Article\Common không tồn tại
namespace Blog\Article;
class Common { }
?>Tổng kết
Tôi vừa tiếp xúc với không gian tên trong PHP, cũng không nên đưa ra những lời khuyên chưa trải nghiệm thực tế. Cá nhân tôi cho rằng vai trò và chức năng của không gian tên rất mạnh mẽ, khi viết plugin hoặc thư viện chung sẽ không còn lo lắng về vấn đề trùng tên. Tuy nhiên nếu dự án đã tiến hành được một phần, muốn giải quyết vấn đề trùng tên bằng cách thêm không gian tên, tôi nghĩ lượng công việc không ít hơn tái cấu trúc tên. Cũng phải thừa nhận cú pháp của nó sẽ làm tăng độ phức tạp nhất định cho dự án, do đó từ đầu dự án nên lập kế hoạch kỹ lưỡng và thiết lập quy tắc đặt tên.