PHP 接口声明了一个类承诺提供的方法,但不包含任何代码:interface Shape { public function area(): float; }。写了 implements Shape 的类必须定义 area(),任何以 Shape 做类型声明的函数都会接受这个类。
<?php
interface Shape
{
public function area(): float;
}
class Square implements Shape
{
public function __construct(private float $side) {}
public function area(): float
{
return $this->side ** 2;
}
}
class Circle implements Shape
{
public function __construct(private float $r) {}
public function area(): float
{
return round(M_PI * $this->r ** 2, 2);
}
}
function describe(Shape $s): string
{
return get_class($s) . ' with area ' . $s->area();
}
echo describe(new Square(3)), "\n";
echo describe(new Circle(2)), "\n";
var_dump(new Circle(1) instanceof Shape);Master PHP the hands-on way
Interactive lessons, real projects, and instant feedback.
START NOW
describe() 不知道也不关心它收到的是哪个类。它只依赖接口做出的承诺,所以新写一个 Triangle implements Shape,不用改一行代码就能和它一起使用。
面向接口编程
编写接口最常见的原因,是让代码依赖一种能力,而不是某个具体的类。下面的订单服务接受任何 Notifier,因此同一段代码在生产环境中可以发送邮件,在测试中则可以把消息收集起来:
<?php
interface Notifier
{
public function send(string $to, string $message): bool;
}
class EmailNotifier implements Notifier
{
public function send(string $to, string $message): bool
{
echo "Email to $to: $message\n";
return true;
}
}
class FakeNotifier implements Notifier
{
public array $sent = [];
public function send(string $to, string $message): bool
{
$this->sent[] = [$to, $message];
return true;
}
}
class OrderService
{
public function __construct(private Notifier $notifier) {}
public function place(string $customer, int $orderId): void
{
$this->notifier->send($customer, "Order #$orderId confirmed");
}
}
(new OrderService(new EmailNotifier()))->place('ada@example.com', 1001);
$fake = new FakeNotifier();
(new OrderService($fake))->place('linus@example.com', 1002);
print_r($fake->sent);
实现多个接口
一个类最多继承一个父类,但可以按需实现任意多个接口。把它们写在 implements 后面,用逗号分隔,并定义所有接口的方法:
<?php
interface HasName
{
public function name(): string;
}
interface Exportable
{
public function toArray(): array;
}
class Product implements HasName, Exportable, JsonSerializable
{
public function __construct(private string $title, private int $priceCents) {}
public function name(): string
{
return $this->title;
}
public function toArray(): array
{
return ['title' => $this->title, 'price' => $this->priceCents / 100];
}
public function jsonSerialize(): array
{
return $this->toArray();
}
}
$p = new Product('Desk lamp', 3490);
echo $p->name(), "\n";
print_r($p->toArray());
echo json_encode($p), "\n";
print_r(class_implements($p));
JsonSerializable 是 PHP 的内置接口之一:实现它就能控制 json_encode() 为该对象生成什么内容。
接口常量
接口可以声明常量。每个实现它的类都会继承这些常量,你可以通过接口名或类名来读取:
<?php
interface HttpStatus
{
const OK = 200;
const NOT_FOUND = 404;
}
class Response implements HttpStatus
{
public function __construct(public int $status = self::OK) {}
public function isError(): bool
{
return $this->status >= 400;
}
}
echo HttpStatus::NOT_FOUND, "\n";
echo Response::OK, "\n";
$r = new Response(Response::NOT_FOUND);
var_dump($r->isError());
在 PHP 8.3 中,接口不能有属性,只能有方法和常量。方法可以是静态的:在接口中写 public static function fromArray(array $data): static;,每个类就必须定义这个静态方法。对于一组固定的相关值,枚举 通常比一串接口常量更合适。
接口可以继承其他接口
接口也使用 extends,而且和类不同,它可以同时继承多个接口。实现子接口的类必须定义整条继承链上的所有方法:
<?php
interface Readable
{
public function read(): string;
}
interface Writable
{
public function write(string $data): void;
}
interface Stream extends Readable, Writable
{
public function close(): void;
}
class MemoryStream implements Stream
{
private string $buffer = '';
public function read(): string { return $this->buffer; }
public function write(string $data): void { $this->buffer .= $data; }
public function close(): void { $this->buffer = ''; }
}
$s = new MemoryStream();
$s->write('Hello, ');
$s->write('stream');
echo $s->read(), "\n";
var_dump($s instanceof Readable, $s instanceof Writable);
内置接口:Countable、IteratorAggregate、Stringable
PHP 自带一些接口,让对象能接入语言本身的功能。Countable 让 count($obj) 可用,IteratorAggregate 让 foreach 可用,而任何带有 __toString() 方法的类都会自动实现 Stringable:
<?php
class Playlist implements Countable, IteratorAggregate
{
private array $songs = [];
public function add(string $song): static
{
$this->songs[] = $song;
return $this;
}
public function count(): int
{
return count($this->songs);
}
public function getIterator(): Iterator
{
return new ArrayIterator($this->songs);
}
public function __toString(): string
{
return implode(' / ', $this->songs);
}
}
$list = (new Playlist())->add('Intro')->add('Blue')->add('Outro');
echo count($list), " songs\n";
foreach ($list as $i => $song) {
echo "$i: $song\n";
}
var_dump($list instanceof Stringable);
echo "$list\n";
漏掉一个方法
如果一个类声称实现了某个接口,却漏掉了其中一个方法,PHP 会以致命错误停止运行,并指出缺少的方法:
<?php
// This fails on purpose: Invoice does not define total()
interface Billable
{
public function total(): float;
public function currency(): string;
}
class Invoice implements Billable
{
public function currency(): string
{
return 'EUR';
}
}
echo (new Invoice())->currency();
加上 total() 方法后,脚本就能运行了。抽象类 可以不定义接口方法,这正是错误信息中提示的另一条出路。
接口与抽象类
两者都定义了一个类必须拥有的方法,也都可以用作类型声明。区别在于它们还能包含什么,以及一个类能使用几个:
接口 抽象类 方法体 无(只有签名) 抽象方法和已实现的方法 属性 不能 可以 构造函数 可以声明,但很少有用 可以,并带有代码 方法的可见性 只能是 public public、protected、private 常量 可以 可以 每个类能用几个 任意多个(implements A, B, C) 一个(extends Base) 典型用途 一种能力:Exportable、Countable 一组类共享的代码:Model、Controller
两者可以很好地结合:定义接口用于类型声明,再提供一个实现了该接口、包含公共代码的抽象基类。需要不同基类的类仍然可以直接实现这个接口。