Skip to content

设计模式在 PHP 中的应用

设计模式是面向对象编程中经过验证的解决方案模板,用于解决特定上下文中反复出现的设计问题。在 PHP 项目中合理运用设计模式可以提高代码的可复用性、可扩展性和可维护性。本节结合 PHP 8.x 的新特性,详解最常用的设计模式及其在 PHP 中的实现方式。

前置知识

阅读本节前,建议先了解:命名规范项目目录结构

基础概念

设计模式分类

分类模式目的
创建型单例、工厂、抽象工厂、建造者、原型对象创建机制
结构型适配器、装饰器、代理、外观、桥接、组合、享元类和对象的组合
行为型策略、观察者、命令、模板方法、迭代器、状态、责任链对象间的通信

单例模式(Singleton)

基本实现

php
<?php
declare(strict_types=1);

namespace App\Core;

final class DatabaseConnection
{
    private static ?self $instance = null;

    private function __construct(
        private readonly string $dsn,
        private readonly string $username,
        private readonly string $password,
    ) {
        // 私有构造函数,禁止外部实例化
    }

    public static function getInstance(
        string $dsn = '',
        string $username = '',
        string $password = '',
    ): self {
        if (self::$instance === null) {
            self::$instance = new self($dsn, $username, $password);
        }

        return self::$instance;
    }

    // 禁止克隆
    private function __clone(): void {}

    // 禁止反序列化
    public function __wakeup(): void
    {
        throw new \RuntimeException('Cannot unserialize singleton');
    }

    public function query(string $sql, array $params = []): array
    {
        // 数据库查询实现
        return [];
    }
}

使用枚举实现单例(PHP 8.1+)

php
<?php
declare(strict_types=1);

namespace App\Core;

/**
 * PHP 8.1+ 枚举天然支持单例特性
 * 枚举的 case 本身就是单例实例
 */
enum DatabaseConfig: string
{
    case Development = 'mysql://localhost/dev_db';
    case Staging = 'mysql://staging.example.com/staging_db';
    case Production = 'mysql://prod.example.com/prod_db';

    public function getDsn(): string
    {
        return $this->value;
    }

    public function getHost(): string
    {
        return parse_url($this->value, PHP_URL_HOST);
    }

    public function getDatabase(): string
    {
        $path = parse_url($this->value, PHP_URL_PATH);

        return str_replace('/', '', $path);
    }
}

// 使用
$config = DatabaseConfig::Production;
echo $config->getHost();     // prod.example.com
echo $config->getDatabase();  // prod_db

单例模式的使用建议

单例模式会引入全局状态,使代码难以测试。推荐通过依赖注入容器管理单例生命周期,而非手动实现单例模式。

工厂模式(Factory)

简单工厂

php
<?php
declare(strict_types=1);

namespace App\Factory;

use App\Notification\EmailNotification;
use App\Notification\SmsNotification;
use App\Notification\PushNotification;
use App\Notification\NotificationInterface;

enum NotificationType: string
{
    case Email = 'email';
    case Sms = 'sms';
    case Push = 'push';
}

final class NotificationFactory
{
    public static function create(NotificationType $type): NotificationInterface
    {
        return match ($type) {
            NotificationType::Email => new EmailNotification(),
            NotificationType::Sms => new SmsNotification(),
            NotificationType::Push => new PushNotification(),
        };
    }

    /**
     * 使用命名参数创建带配置的通知实例
     */
    public static function createWithConfig(
        NotificationType $type,
        array $config = [],
    ): NotificationInterface {
        $notification = self::create($type);

        if (isset($config['recipient'])) {
            $notification->setRecipient($config['recipient']);
        }

        return $notification;
    }
}

抽象工厂

php
<?php
declare(strict_types=1);

namespace App\Factory\Logging;

interface LoggerFactoryInterface
{
    public function createLogger(string $channel): LoggerInterface;
    public function createHandler(string $type): HandlerInterface;
    public function createFormatter(string $format): FormatterInterface;
}

class MonologFactory implements LoggerFactoryInterface
{
    public function createLogger(string $channel): LoggerInterface
    {
        return new \Monolog\Logger($channel);
    }

    public function createHandler(string $type): HandlerInterface
    {
        return match ($type) {
            'stream' => new \Monolog\Handler\StreamHandler(
                'php://stdout',
                \Monolog\Level::Debug,
            ),
            'file' => new \Monolog\Handler\RotatingFileHandler(
                storage_path('logs/app.log'),
                7,
            ),
            default => throw new \InvalidArgumentException("Unknown handler: {$type}"),
        };
    }

    public function createFormatter(string $format): FormatterInterface
    {
        return match ($format) {
            'json' => new \Monolog\Formatter\JsonFormatter(),
            'line' => new \Monolog\Formatter\LineFormatter(),
            default => throw new \InvalidArgumentException("Unknown format: {$format}"),
        };
    }
}

策略模式(Strategy)

基本实现

php
<?php
declare(strict_types=1);

namespace App\Pricing;

interface PricingStrategyInterface
{
    /**
     * 计算价格
     *
     * @param float $basePrice 原价
     * @param array<string, mixed> $context 上下文信息
     */
    public function calculate(float $basePrice, array $context): float;
}

class RegularPricingStrategy implements PricingStrategyInterface
{
    public function calculate(float $basePrice, array $context): float
    {
        return $basePrice;
    }
}

class MemberDiscountStrategy implements PricingStrategyInterface
{
    public function __construct(
        private readonly float $discountRate = 0.1,
    ) {}

    public function calculate(float $basePrice, array $context): float
    {
        return $basePrice * (1 - $this->discountRate);
    }
}

class FlashSaleStrategy implements PricingStrategyInterface
{
    public function __construct(
        private readonly float $salePrice,
    ) {}

    public function calculate(float $basePrice, array $context): float
    {
        return min($basePrice, $this->salePrice);
    }
}

final class PricingContext
{
    public function __construct(
        private PricingStrategyInterface $strategy,
    ) {}

    public function setStrategy(PricingStrategyInterface $strategy): void
    {
        $this->strategy = $strategy;
    }

    public function getPrice(float $basePrice, array $context = []): float
    {
        return $this->strategy->calculate($basePrice, $context);
    }
}

使用枚举实现策略选择

php
<?php
declare(strict_types=1);

namespace App\Pricing;

enum PricingType: string
{
    case Regular = 'regular';
    case MemberDiscount = 'member';
    case FlashSale = 'flash_sale';

    public function createStrategy(): PricingStrategyInterface
    {
        return match ($this) {
            self::Regular => new RegularPricingStrategy(),
            self::MemberDiscount => new MemberDiscountStrategy(),
            self::FlashSale => new FlashSaleStrategy(salePrice: 99.00),
        };
    }
}

// 使用
$type = PricingType::from('member');
$context = new PricingContext($type->createStrategy());
$price = $context->getPrice(100.00);

观察者模式(Observer)

SPL 接口实现

php
<?php
declare(strict_types=1);

namespace App\Event;

/**
 * 主题(被观察者)
 */
final class UserEventSubject implements \SplSubject
{
    /** @var array<int, \SplObserver> */
    private array $observers = [];

    private array $eventData = [];

    public function attach(\SplObserver $observer): void
    {
        $id = spl_object_id($observer);
        $this->observers[$id] = $observer;
    }

    public function detach(\SplObserver $observer): void
    {
        $id = spl_object_id($observer);
        unset($this->observers[$id]);
    }

    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function emit(string $eventName, array $data): void
    {
        $this->eventData = [
            'name' => $eventName,
            'data' => $data,
            'timestamp' => new \DateTimeImmutable(),
        ];

        $this->notify();
    }

    public function getEventData(): array
    {
        return $this->eventData;
    }
}

自定义事件调度器

php
<?php
declare(strict_types=1);

namespace App\Event;

interface EventListenerInterface
{
    public function handle(array $event): void;
}

interface EventDispatcherInterface
{
    public function listen(string $event, EventListenerInterface $listener): void;
    public function dispatch(string $event, array $data = []): void;
    public function removeListener(string $event, EventListenerInterface $listener): void;
}

final class EventDispatcher implements EventDispatcherInterface
{
    /** @var array<string, array<int, EventListenerInterface>> */
    private array $listeners = [];

    public function listen(string $event, EventListenerInterface $listener): void
    {
        $this->listeners[$event][] = $listener;
    }

    public function dispatch(string $event, array $data = []): void
    {
        if (!isset($this->listeners[$event])) {
            return;
        }

        foreach ($this->listeners[$event] as $listener) {
            $listener->handle(array_merge(['event' => $event], $data));
        }
    }

    public function removeListener(string $event, EventListenerInterface $listener): void
    {
        if (!isset($this->listeners[$event])) {
            return;
        }

        $this->listeners[$event] = array_filter(
            $this->listeners[$event],
            fn (EventListenerInterface $l) => $l !== $listener,
        );
    }
}

// 具体监听器
final class SendWelcomeEmailListener implements EventListenerInterface
{
    public function __construct(
        private readonly \App\Mail\MailerInterface $mailer,
    ) {}

    public function handle(array $event): void
    {
        $email = $event['data']['email'] ?? '';
        $this->mailer->send($email, 'Welcome!', 'Welcome to our platform!');
    }
}

final class CreateUserLogListener implements EventListenerInterface
{
    public function __construct(
        private readonly \Psr\Log\LoggerInterface $logger,
    ) {}

    public function handle(array $event): void
    {
        $this->logger->info('User registered', $event['data']);
    }
}

装饰器模式(Decorator)

基本实现

php
<?php
declare(strict_types=1);

namespace App\Log;

interface LogHandlerInterface
{
    public function handle(string $message, array $context = []): void;
}

class FileLogHandler implements LogHandlerInterface
{
    public function __construct(
        private readonly string $logPath,
    ) {}

    public function handle(string $message, array $context = []): void
    {
        $line = sprintf(
            '[%s] %s %s',
            date('Y-m-d H:i:s'),
            $message,
            json_encode($context, JSON_UNESCAPED_UNICODE),
        );

        file_put_contents($this->logPath, $line . PHP_EOL, FILE_APPEND);
    }
}

abstract class LogHandlerDecorator implements LogHandlerInterface
{
    public function __construct(
        protected readonly LogHandlerInterface $handler,
    ) {}
}

class TimestampDecorator extends LogHandlerDecorator
{
    public function handle(string $message, array $context = []): void
    {
        $context['timestamp'] = (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.u');
        $this->handler->handle($message, $context);
    }
}

class ContextDecorator extends LogHandlerDecorator
{
    public function __construct(
        LogHandlerInterface $handler,
        private readonly array $globalContext,
    ) {
        parent::__construct($handler);
    }

    public function handle(string $message, array $context = []): void
    {
        $mergedContext = array_merge($this->globalContext, $context);
        $this->handler->handle($message, $mergedContext);
    }
}

class FilterDecorator extends LogHandlerDecorator
{
    public function __construct(
        LogHandlerInterface $handler,
        private readonly string $minLevel = 'debug',
    ) {
        parent::__construct($handler);
    }

    public function handle(string $message, array $context = []): void
    {
        $level = $context['level'] ?? 'debug';

        if ($this->shouldLog($level)) {
            $this->handler->handle($message, $context);
        }
    }

    private function shouldLog(string $level): bool
    {
        $levels = ['debug', 'info', 'warning', 'error', 'critical'];
        $currentIdx = array_search($this->minLevel, $levels, true);
        $messageIdx = array_search($level, $levels, true);

        return $messageIdx >= $currentIdx;
    }
}

// 使用 - 装饰器可以任意组合
$baseHandler = new FileLogHandler('/var/log/app.log');
$timestamped = new TimestampDecorator($baseHandler);
$withContext = new ContextDecorator($timestamped, [
    'app' => 'my-app',
    'env' => 'production',
]);
$filtered = new FilterDecorator($withContext, 'info');

$filtered->handle('User login successful', ['level' => 'info', 'user_id' => 123]);

仓储模式(Repository)

接口与实现

php
<?php
declare(strict_types=1);

namespace App\Domain\User;

use App\Domain\User\UserId;

interface UserRepositoryInterface
{
    public function findById(UserId $id): ?User;
    public function findByEmail(string $email): ?User;
    public function save(User $user): void;
    public function remove(User $user): void;
    public function findActive(int $limit = 20, int $offset = 0): array;
    public function count(): int;
}
php
<?php
declare(strict_types=1);

namespace App\Infrastructure\Persistence;

use App\Domain\User\User;
use App\Domain\User\UserId;
use App\Domain\User\UserRepositoryInterface;
use PDO;

final class PdoUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private readonly PDO $db,
    ) {}

    public function findById(UserId $id): ?User
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id->value()]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($data === false) {
            return null;
        }

        return $this->hydrate($data);
    }

    public function findByEmail(string $email): ?User
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE email = :email');
        $stmt->execute(['email' => $email]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($data === false) {
            return null;
        }

        return $this->hydrate($data);
    }

    public function save(User $user): void
    {
        $stmt = $this->db->prepare(
            'INSERT INTO users (id, email, name, created_at)
             VALUES (:id, :email, :name, :created_at)
             ON DUPLICATE KEY UPDATE
             email = VALUES(email), name = VALUES(name)'
        );

        $stmt->execute([
            'id' => $user->getId()->value(),
            'email' => $user->getEmail(),
            'name' => $user->getName(),
            'created_at' => $user->getCreatedAt()->format('Y-m-d H:i:s'),
        ]);
    }

    public function remove(User $user): void
    {
        $stmt = $this->db->prepare('DELETE FROM users WHERE id = :id');
        $stmt->execute(['id' => $user->getId()->value()]);
    }

    public function findActive(int $limit = 20, int $offset = 0): array
    {
        $stmt = $this->db->prepare(
            'SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC LIMIT :limit OFFSET :offset'
        );
        $stmt->bindValue('limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue('offset', $offset, PDO::PARAM_INT);
        $stmt->execute();

        return array_map(fn (array $row): User => $this->hydrate($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
    }

    public function count(): int
    {
        $stmt = $this->db->query('SELECT COUNT(*) FROM users');

        return (int) $stmt->fetchColumn();
    }

    private function hydrate(array $data): User
    {
        return User::reconstitute(
            id: UserId::fromString($data['id']),
            email: $data['email'],
            name: $data['name'],
            createdAt: new \DateTimeImmutable($data['created_at']),
        );
    }
}

适配器模式(Adapter)

基本实现

php
<?php
declare(strict_types=1);

namespace App\Payment;

interface PaymentGatewayInterface
{
    public function charge(float $amount, array $params): PaymentResult;
    public function refund(string $transactionId, float $amount): RefundResult;
    public function getTransactionStatus(string $transactionId): TransactionStatus;
}

final class StripePaymentGateway implements PaymentGatewayInterface
{
    public function __construct(
        private readonly string $apiKey,
    ) {}

    public function charge(float $amount, array $params): PaymentResult
    {
        // Stripe API 调用实现
        return new PaymentResult(
            transactionId: 'txn_' . bin2hex(random_bytes(16)),
            status: TransactionStatus::Success,
            amount: $amount,
        );
    }

    public function refund(string $transactionId, float $amount): RefundResult
    {
        // Stripe 退款实现
        return new RefundResult(refundId: 'ref_' . bin2hex(random_bytes(8)));
    }

    public function getTransactionStatus(string $transactionId): TransactionStatus
    {
        return TransactionStatus::Success;
    }
}

/**
 * 适配器:将旧的支付系统适配到新接口
 */
final class LegacyPaymentAdapter implements PaymentGatewayInterface
{
    public function __construct(
        private readonly \App\Legacy\OldPaymentSystem $legacySystem,
    ) {}

    public function charge(float $amount, array $params): PaymentResult
    {
        $oldResult = $this->legacySystem->processPayment(
            $amount * 100,  // 旧系统使用分
            $params['customer_id'] ?? 0,
            $params['description'] ?? '',
        );

        return new PaymentResult(
            transactionId: (string) $oldResult['payment_id'],
            status: $oldResult['success'] ? TransactionStatus::Success : TransactionStatus::Failed,
            amount: $amount,
        );
    }

    public function refund(string $transactionId, float $amount): RefundResult
    {
        $result = $this->legacySystem->processRefund((int) $transactionId, $amount * 100);

        return new RefundResult(refundId: (string) $result['refund_id']);
    }

    public function getTransactionStatus(string $transactionId): TransactionStatus
    {
        $status = $this->legacySystem->checkStatus((int) $transactionId);

        return match ($status) {
            'completed' => TransactionStatus::Success,
            'failed' => TransactionStatus::Failed,
            'pending' => TransactionStatus::Pending,
            default => TransactionStatus::Unknown,
        };
    }
}

注意事项

避免过度设计

设计模式不是银弹

不要为了使用设计模式而使用设计模式。过度设计会增加不必要的复杂性。仅在确实能解决当前问题或预见明确需求时使用。

  • 单例模式:在现代 PHP 中,推荐通过 DI 容器管理单例
  • 工厂模式:简单对象创建不需要工厂,直接使用构造函数即可
  • 策略模式:当策略只有两种且不会扩展时,简单的 if/else 或 match 更合适
  • 观察者模式:现代框架通常内置事件系统,无需自行实现

最佳实践

  1. 优先使用 PHP 8 特性:枚举、命名参数、match 表达式使模式实现更简洁
  2. 面向接口编程:依赖注入接口而非具体实现
  3. 组合优于继承:装饰器和策略模式是组合的典型应用
  4. 遵循 SOLID 原则:设计模式是 SOLID 原则的具体体现
  5. 保持简洁:模式实现应尽量简洁,避免不必要的抽象

下一节

继续学习:依赖管理

参考链接