2024 日本 PHP 研討會

物件介面

物件介面允許您建立程式碼來指定類別必須實作的方法和屬性,而不必定義這些方法或屬性是如何實作的。介面與類別、特性和列舉共用一個命名空間,因此它們不能使用相同的名稱。

介面的定義方式與類別相同,但使用關鍵字 interface 取代 class 關鍵字,且任何方法都沒有定義其內容。

所有在介面中宣告的方法都必須是公開的;這是介面的本質。

實際應用上,介面有兩個相輔相成的目的:

  • 允許開發者建立不同類別的物件,這些物件因為實作了相同的介面而可以互換使用。常見的例子像是多個資料庫存取服務、多個支付閘道器,或不同的快取策略。不同的實作可以互相替換,而不需要修改使用它們的程式碼。
  • 允許函式或方法接受並操作符合介面的參數,而不關心物件的其他行為或如何實作。這些介面通常命名為像是 IterableCacheableRenderable 等等,用以描述其行為的意義。

介面可以定義魔術方法,要求實作的類別也必須實作這些方法。

注意:

雖然允許在介面中包含建構子,但強烈建議不要這樣做。這樣做會顯著降低實作介面的物件的彈性。此外,建構子不受繼承規則的約束,這可能會導致不一致且難以預料的行為。

implements(實作)

要實作介面,可以使用 implements 運算子。介面中的所有方法都必須在類別中實作;若未實作,將會導致致命錯誤。如果需要,類別可以透過逗號分隔來實作多個介面。

警告

實作介面的類別可以使用與介面不同的參數名稱。然而,從 PHP 8.0 開始,該語言支援具名引數,這意味著呼叫者可能會依賴介面中的參數名稱。因此,強烈建議開發者使用與所實作介面相同的參數名稱。

注意:

介面可以像類別一樣使用 extends 運算子來繼承。

注意:

實作介面的類別必須宣告介面中所有方法,並具有相容的簽章。一個類別可以實作多個宣告相同名稱方法的介面。在這種情況下,實作必須遵循所有介面的簽章相容性規則。因此可以應用共變數與反變數

常數

介面可以擁有常數。介面常數的運作方式與類別常數完全相同。在 PHP 8.1.0 之前,繼承它們的類別/介面無法覆寫它們。

屬性(Properties)

從 PHP 8.4.0 開始,介面也可以宣告屬性。如果要宣告屬性,則必須指定該屬性是可讀的、可寫的,或是兩者皆可。介面宣告僅適用於公開讀取和寫入的權限。

一個類別可以透過多種方式滿足介面屬性。它可以定義一個公開屬性。它可以定義一個公開的虛擬屬性,只實作對應的鉤子(hook)。或者,唯讀屬性可以滿足唯讀屬性。然而,可設定的介面屬性不能是 readonly

範例 #1 介面屬性範例

<?php
interface I
{
// An implementing class MUST have a publicly-readable property,
// but whether or not it's publicly settable is unrestricted.
public string $readable { get; }

// An implementing class MUST have a publicly-writeable property,
// but whether or not it's publicly readable is unrestricted.
public string $writeable { set; }

// An implementing class MUST have a property that is both publicly
// readable and publicly writeable.
public string $both { get; set; }
}

// This class implements all three properties as traditional, un-hooked
// properties. That's entirely valid.
class C1 implements I
{
public
string $readable;

public
string $writeable;

public
string $both;
}

// This class implements all three properties using just the hooks
// that are requested. This is also entirely valid.
class C2 implements I
{
private
string $written = '';
private
string $all = '';

// Uses only a get hook to create a virtual property.
// This satisfies the "public get" requirement.
// It is not writeable, but that is not required by the interface.
public string $readable { get => strtoupper($this->writeable); }

// The interface only requires the property be settable,
// but also including get operations is entirely valid.
// This example creates a virtual property, which is fine.
public string $writeable {
get => $this->written;
set {
$this->written = $value;
}
}

// This property requires both read and write be possible,
// so we need to either implement both, or allow it to have
// the default behavior.
public string $both {
get => $this->all;
set {
$this->all = strtoupper($value);
}
}
}
?>

範例

範例 #2 介面範例

<?php

// 宣告介面 'Template'
interface Template
{
public function
setVariable($name, $var);
public function
getHtml($template);
}

// 實作介面
// 這個會正常運作
class WorkingTemplate implements Template
{
private
$vars = [];

public function
setVariable($name, $var)
{
$this->vars[$name] = $var;
}

public function
getHtml($template)
{
foreach(
$this->vars as $name => $value) {
$template = str_replace('{' . $name . '}', $value, $template);
}

return
$template;
}
}

// 這個會失敗
// 致命錯誤:類別 BadTemplate 包含 1 個抽象方法
// 因此必須宣告為抽象類別 (Template::getHtml)
class BadTemplate implements Template
{
private
$vars = [];

public function
setVariable($name, $var)
{
$this->vars[$name] = $var;
}
}
?>

範例 #3 可擴展的介面

<?php
介面 A
{
public function
foo();
}

介面
B 延伸 A
{
public function
baz(Baz $baz);
}

// 這個會正常運作
類別 C 實作 B
{
public function
foo()
{
}

public function
baz(Baz $baz)
{
}
}

// 這個不會正常運作,會導致致命錯誤
類別 D 實作 B
{
public function
foo()
{
}

public function
baz(Foo $foo)
{
}
}
?>

範例 #4 變異數與多個介面的相容性

<?php
類別 Foo {}
類別
Bar 延伸 Foo {}

介面
A {
public function
myfunc(Foo $arg): Foo;
}

介面
B {
public function
myfunc(Bar $arg): Bar;
}

類別
MyClass 實作 A, B
{
public function
myfunc(Foo $arg): Bar
{
return new
Bar();
}
}
?>

範例 #5 多重介面繼承

<?php
介面 A
{
public function
foo();
}

介面
B
{
public function
bar();
}

介面
C 延伸 A, B
{
public function
baz();
}

類別
D 實作 C
{
public function
foo()
{
}

public function
bar()
{
}

public function
baz()
{
}
}
?>

範例 #6 具有常數的介面

<?php
interface A
{
const
B = 'Interface constant';
}

// 顯示:Interface constant
echo A::B;


class
B implements A
{
const
B = 'Class constant';
}

// 顯示:Class constant
// 在 PHP 8.1.0 之前,這是不允許的,因為不允許覆寫常數。
echo B::B;
?>

範例 #7 具有抽象類別的介面

<?php
interface A
{
public function
foo(string $s): string;

public function
bar(int $i): int;
}

// 抽象類別可以只實作介面的一部分。
// 繼承抽象類別的類別必須實作其餘部分。
abstract class B implements A
{
public function
foo(string $s): string
{
return
$s . PHP_EOL;
}
}

class
C extends B
{
public function
bar(int $i): int
{
return
$i * 2;
}
}
?>

範例 #8 同時繼承和實作

<?php

class One
{
/* ... */
}

interface
Usable
{
/* ... */
}

interface
Updatable
{
/* ... */
}

// 這裡的關鍵字順序很重要。「extends」必須放在前面。
class Two extends One implements Usable, Updatable
{
/* ... */
}
?>

介面與類型聲明一起,提供了一個很好的方法來確保特定物件包含特定的方法。請參閱 instanceof 運算子和 類型聲明

新增筆記

使用者貢獻的筆記 4 則筆記

thanhn2001 at gmail dot com
13 年前
PHP 會阻止介面中的常數被直接繼承它的類別/介面覆蓋。然而,進一步的繼承允許這樣做。這意味著介面常數並不像先前評論中提到的那樣是 final。這是一個錯誤還是一個功能?

<?php

interface a
{
const
b = '介面常數';
}

// 顯示:介面常數
echo a::b;

class
b implements a
{
}

// 這個可以運作!!!
class c extends b
{
const
b = '類別常數';
}

echo
c::b;
?>
vcnbianchi
2 年前
就像所有介面的方法都是公開的一樣,所有介面的方法也都是抽象的。
williebegoode at att dot net
10 年前
在 Erich Gamma 和他的夥伴(又稱「四人幫」)關於設計模式的書中,他們交替使用「介面」和「抽象類別」這兩個術語。在使用 PHP 和設計模式時,介面雖然清楚地是一份關於實作中應包含內容的「合約」,但它也是一個有助於重複使用和進行更改的指南。只要實作的更改遵循介面(無論它是介面還是具有抽象方法的抽象類別),就可以安全地更新大型複雜程式,而無需重新編寫整個程式或模組。

在使用物件介面(作為關鍵字)的 PHP 編碼中,以及在更廣泛的上下文中包含物件介面和抽象類別的「介面」中,為了易於更改和重複使用而「鬆散綁定」(鬆散綁定的物件)的目的,是思考「介面」這兩個用法的有用方式。重點從「合約」轉移到「鬆散綁定」,以促進合作開發和重複使用。
xedin dot unknown at gmail dot com
3 年前
這個頁面提到,如果繼承多個具有相同方法的介面,則簽章必須相容。但这並非全部:`extends` 的順序很重要。這是一個已知的問題,雖然它是否是一個錯誤是有爭議的,但開發者應該意識到它,並在編寫介面程式碼時考慮到這一點。

https://bugs.php.net/bug.php?id=67270
https://bugs.php.net/bug.php?id=76361
https://bugs.php.net/bug.php?id=80785
To Top