前言
物件導向程式設計是當代的主流範式,大部分主流的程式語言皆支援此種範式。本文說明在 PHP 中建立類別和物件的方式,適用於 PHP 5 之後的版本。
使用內建類別建立物件
在開始撰寫類別前,本節以內建類別來展示如何使用物件。本範例使用了 Directory 物件:
<?php
# Create a Directory object.
$dir = dir("/home/user");
# Get a property of the object.
echo $dir->path, "\n";
# Iterate over the object.
while (($entry = $dir->read()))
echo $entry, "\n";
# Close the object.
$dir->close();
建立 Directory
物件的方式是使用 dir 函式傳入代表路徑的字串,而非使用 new
。這是少數的例外情境。
變數 $dir
為 Directory
物件。物件是電腦程式中的抽象物體。在物件中,資料 (data) 和行為 (behavior) 是連動的。使用 ->
可以取得物件的屬性 (property) 或方法 (method)。屬性為物件的資料。方法則是和物件連動的函式。
$dir->path
是 $dir
物件的屬性。該屬性儲存 Directory
物件的路徑。
$dir->read()
是 $dir
物件的方法。該方法回傳傳入路徑內的檔案或子目錄,回傳的資料型態是字串。注意呼叫 $dir->read()
時,會搭配 while
迴圈,逐一走訪該路徑下所有的檔案和子目錄。
由於 $dir
物件內部代表 Directory
物件所在的資源,最後要用 $dir->close()
方法將此資源關閉。
雖然這個例子很短,我們可以從此範例看到使用物件的方式。
建立第一個類別
藉由撰寫類別,程式設計者可視需求擴充新的資料型態。前一節使用 PHP 內建的類別來建立物件。本節要開始撰寫新的類別,以建立物件。
本範例撰寫及使用了 Point
類別,該類別用來表示平面座標上的點:
<?php
# Create a `Point` object with default values.
$p = new Point();
# Create a `Point` object.
$q = new Point(5.0, 12.0);
# Call a static function.
echo Point::distance($p, $q), "\n";
# A point class.
class Point
{
# Private fields
private float $x;
private float $y;
# The constructor of this class.
public function __construct(
float $x = 0.0,
float $y = 0.0
) {
$this->setX($x);
$this->setY($y);
}
# The getter of `x`.
public function x(): float {
return $this->x;
}
# The setter of `x`.
private function setX(float $x): void {
$this->x = $x;
}
# The getter of `y`.
public function y(): float {
return $this->y;
}
# The setter of `y`.
private function setY(float $y): void {
$this->y = $y;
}
# A static method.
public static function distance(
self $p, self $q
): float {
$dx = $p->x() - $q->x();
$dy = $p->y() - $q->y();
return sqrt($dx * $dx + $dy * $dy);
}
}
如同函式,使用類別的程式碼可以寫在類別宣告之前。這樣的撰碼模式易於追蹤程式碼,故此處保留此慣例。
建立新物件會使用保留字 new
。建立物件 $p
時,使用預設的參數,建立位於座標原點的點。建立物件 $q
時,則傳入該點所在的 x
、y
座標。
接著,使用 Point::distance()
靜態方法來計算 $p
和 $q
的距離。靜態方法是和類別連動的方法,不屬於任何物件。
類別是建立物件的藍圖。撰寫物件導向程式時,會建立一至多個類別,再由這些類別來建立物件。建立類別會使用保留字 class
。按照慣例,類別名稱使用 PascalCase
的形式來命名。
我們在前一節提過,類別包括屬性和方法。前者是物件的資料,後者是物件的行為。一般來說,屬性會保持私有 (private) 狀態,方法則視需求採用公開 (public) 或私有。適當地控制屬性和方法的狀態,對日後撰寫及重構程式碼相當重要。
建構子 (constructor) 是特殊的方法,在物件建立時會呼叫建構子。PHP 類別的建構函式的名稱固定為 __construct
,而且每個類別只會有一個建構子。後文會繼續探討建構子相關的議題。
$this
是類別中特殊的變數。該變數用來表示此類別日後建立的物件。所以 $this
可用 ->
來呼叫連動的屬性和方法。
此 Point
類別在建立 Point
物件後就不能修改座標點所在的位置,故此處使用 private
來修飾 setX()
及 setY()
方法。若希望座標點在建立後仍可修改位置,則將其改用 public
來修飾。
在類別外部,使用 private
修飾的屬性和方法是不可視的 (invisible)。所以,類別實作者可視需求撰寫任意個私有屬性和私有方法,只要控制好公開屬性及公開方法即可。
使用 static
修飾的方法是靜態方法。靜態方法會和類別而非物件連動,所以在靜態方法中無法使用 $this
。
self
是類別特有的型態宣告,代表該類別的資料型態。使用 self
會比直接撰寫 Point
要好,因為前者在類別名稱更動時會自動代入新的類別名稱。
解構子 (Destructor)
除了建構子以外,在 PHP 類別中也可宣告解構子。當物件沒被變數指向時,會自動呼叫解構子。以下 PHP 虛擬碼宣告建構子和解構子:
<?php
class Klass
{
public function __construct() {
# Implement this constructor here.
}
public function __destruct() {
# Implement this destructor here.
}
# Implement other methods.
}
在 PHP 中,解構子不常使用。因為 PHP 會自動管理記憶體。其他的系統資源需要精準地控制釋放的時機。
使用靜態方法宣告多個建構子
PHP 類別只能撰寫一個建構子,但有時候會有撰寫多個建構子的需求。PHP 官方文件展示了一個使用靜態方法做為替代性建構子的範例如下:
<?php
# Some examples.
$p1 = Product::fromBasicData(5, 'Widget');
$p2 = Product::fromJson($someJsonString);
$p3 = Product::fromXml($someXmlString);
class Product
{
# Private fields
private ?int $id;
private ?string $name;
# Hide our traditional constructor.
private function __construct(?int $id = null, ?string $name = null) {
$this->id = $id;
$this->name = $name;
}
public static function fromBasicData(int $id, string $name): static {
$new = new static($id, $name);
return $new;
}
public static function fromJson(string $json): static {
$data = json_decode($json);
return new static($data['id'], $data['name']);
}
public static function fromXml(string $xml): static {
# Implement custom logic here.
$data = convert_xml_to_array($xml);
$new = new static();
$new->id = $data['id'];
$new->name = $data['name'];
return $new;
}
}
靜態方法和類別連動,不能呼叫物件方法。PHP 提供 static
保留字,在靜態方法內用來建立物件。
由於這個範例出現在 PHP 官方文件中,代表這個模式是 PHP 對於宣告多個建構子的官方解答。不過,本文提供另一個替代性的模式。詳見下一節的內容。
(選擇性) 不使用靜態方法宣告多個建構子
PHP 提供 func_num_args 和 func_get_args 兩個函式來取得函式參數的數量和元素。也就是說,其實 PHP 函式可以設計成同名的多重參數函式,只要實作上有支援即可。
本小節展示一個稍長的範例虛擬碼。這個範例用上了本小節所提到的概念:
<?php
# Some examples.
$c1 = RGBColor("orange");
$c2 = RGBColor("FFA500");
$c3 = RGBColor(255, 165, 0);
$c4 = RGBColor(1, 0.647, 0);
class RGBColor
{
private int $red;
private int $green;
private int $blue;
public function __construct()
{
$argc = func_num_args();
$argv = func_get_args();
if (1 == $argc) {
if ("string" == gettype($argv[0])) {
if (ctype_xdigit($argv[0])) {
createFromHexCode($argv[0]);
}
else {
createFromName($argv[0]);
}
}
else {
die("Invalid argument");
}
}
elseif (3 == $argc) {
if (isIntArray($argv)) {
createFromIntArray($argv);
}
else if (isFloatArray($argv)) {
createFromFloatArray($argv);
}
else {
die("Invalid arguments");
}
}
else {
die("Invalid argument(s)");
}
}
private function createFromHexCode(string $code) {
# Implement the constructor here.
}
private function createFromName(string $name) {
# Implement the constrctor here.
}
private function createFromIntArray($arr) {
# Implement the constructor here.
}
private function createFromFloatArray($arr) {
# Implement the constructor here.
}
private function isIntArray($arr): bool {
return count($arr)
== count(
array_filter($arr,
fn($n) => is_int($n)));
}
private function isFloatArray($arr): bool {
return count($arr)
== count(
array_filter($arr,
fn($n) => is_float($n)));
}
}
本範例的類別 RGBColor
表面上看來有四種建立物件的建構子,但實際上共用同一個建構子。在建構子中根據參數形式呼叫不同的私有方法。藉由此模式來模擬多個建構子。
這個模式的缺點在於 PHP 無法協助程式設計者檢查建構子的參數,要由程式設計者在程式中自行檢查。這模式非官方文件提供,請自行考慮是否要用在自己的程式碼中。