面向对象编程(Object-Oriented Programming,OOP),是现代编程语言的核心特性,也是写出可维护、可扩展、可复用代码的关键。

PHP,从PHP 5开始,引入了完整的面向对象模型,包括类、对象、继承、接口、抽象类、命名空间、性状(Trait)等特性。PHP 7进一步完善了面向对象模型,增加了返回类型声明、标量类型声明、匿名类等特性。

在之前的文章中,我们学习了PHP的各种功能开发,但对面向对象涉及不多。今天,我们来详细学习PHP面向对象编程,从基础概念到高级特性,从设计原则到实战案例,帮你掌握面向对象编程,写出更优雅、更易维护的PHP代码。

为什么需要面向对象

面向过程 vs 面向对象

面向过程编程:以函数为中心,把程序分解为一系列函数,数据和函数是分离的。适合简单、小规模的程序。

// 面向过程:定义函数操作数据
function getUser($id) {
    global $db;
    return $db->query("SELECT * FROM users WHERE id = $id")->fetch();
}

function updateUser($id, $data) {
    global $db;
    // ...
}

$user = getUser(1);
updateUser(1, ['name' => '张三']);

面向对象编程:以对象为中心,把数据和操作数据的方法封装在一起(类)。适合复杂、中大规模的程序。

// 面向对象:定义类,封装数据和方法
class User {
    private $id;
    private $name;
    private $db;
    
    public function __construct($id, PDO $db) {
        $this->id = $id;
        $this->db = $db;
        $this->load();
    }
    
    private function load() {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$this->id]);
        $data = $stmt->fetch();
        $this->name = $data['name'];
    }
    
    public function updateName($name) {
        $this->name = $name;
        $stmt = $this->db->prepare("UPDATE users SET name = ? WHERE id = ?");
        $stmt->execute([$this->name, $this->id]);
    }
    
    public function getName() {
        return $this->name;
    }
}

$user = new User(1, $db);
$user->updateName('张三');
echo $user->getName();

面向对象的优势

  1. 封装:把数据和操作数据的方法封装在一起,隐藏内部实现细节,只暴露必要的接口。降低了代码的复杂度,提高了安全性。
  2. 继承:子类可以继承父类的属性和方法,实现代码复用,减少重复代码。
  3. 多态:不同的对象,对同一个方法,可以有不同的实现。提高了代码的灵活性和可扩展性。
  4. 抽象:抽象出事物的本质特征,忽略非本质细节。降低了复杂度,提高了代码的可理解性。
  5. 可维护性:面向对象的代码,结构清晰,模块化,易于维护和修改。
  6. 可扩展性:通过继承、接口、多态,可以方便地扩展功能,而不需要修改已有代码。
  7. 可复用性:类可以在多个项目中复用,提高开发效率。

类与对象

定义类

class Person {
    // 属性(成员变量)
    public $name;
    public $age;
    
    // 方法(成员函数)
    public function sayHello() {
        echo "你好,我是 {$this->name},今年 {$this->age} 岁。";
    }
}

创建对象

$person = new Person();
$person->name = '张三';
$person->age = 25;
$person->sayHello(); // 输出:你好,我是 张三,今年 25 岁。

构造函数和析构函数

class Person {
    public $name;
    public $age;
    
    // 构造函数:创建对象时自动调用
    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
        echo "对象被创建了\n";
    }
    
    // 析构函数:对象销毁时自动调用
    public function __destruct() {
        echo "对象被销毁了\n";
    }
}

$person = new Person('张三', 25); // 输出:对象被创建了
// 脚本结束时,输出:对象被销毁了

$this关键字

$this指向当前对象,用于在类的方法中访问当前对象的属性和方法。

class Person {
    public $name;
    
    public function setName($name) {
        $this->name = $name; // $this->name 是对象的属性,$name 是方法参数
    }
    
    public function getName() {
        return $this->name;
    }
}

访问控制(封装)

PHP提供了三种访问控制修饰符:

  • public(公共):可以在任何地方访问,类内部、子类、外部都可以。
  • protected(受保护):只能在类内部和子类中访问,外部不能访问。
  • private(私有):只能在类内部访问,子类和外部都不能访问。
class User {
    public $username;      // 公共,任何地方可访问
    protected $email;      // 受保护,类内部和子类可访问
    private $password;     // 私有,只有类内部可访问
    
    public function __construct($username, $email, $password) {
        $this->username = $username;
        $this->email = $email;
        $this->password = $password;
    }
    
    // 公共方法:外部可以调用
    public function getEmail() {
        return $this->email;
    }
    
    // 受保护方法:只有类内部和子类可以调用
    protected function validatePassword($password) {
        return password_verify($password, $this->password);
    }
    
    // 私有方法:只有类内部可以调用
    private function hashPassword($password) {
        return password_hash($password, PASSWORD_DEFAULT);
    }
}

$user = new User('zhangsan', 'zhangsan@example.com', '123456');
echo $user->username;      // 可以,public
echo $user->getEmail();    // 可以,public方法
// echo $user->email;       // 错误,protected
// echo $user->password;    // 错误,private

Getter和Setter

对于私有属性,通常提供公共的getter和setter方法来访问和修改,这样可以在方法中加入验证逻辑。

class User {
    private $name;
    private $age;
    
    public function getName() {
        return $this->name;
    }
    
    public function setName($name) {
        if (empty($name)) {
            throw new InvalidArgumentException('名字不能为空');
        }
        $this->name = $name;
    }
    
    public function getAge() {
        return $this->age;
    }
    
    public function setAge($age) {
        if (!is_numeric($age) || $age < 0 || $age > 150) {
            throw new InvalidArgumentException('无效的年龄');
        }
        $this->age = $age;
    }
}

继承

继承,是子类继承父类的属性和方法,实现代码复用。

基本继承

// 父类(基类)
class Animal {
    protected $name;
    
    public function __construct($name) {
        $this->name = $name;
    }
    
    public function eat() {
        echo "{$this->name} 在吃东西。\n";
    }
    
    public function sleep() {
        echo "{$this->name} 在睡觉。\n";
    }
}

// 子类(派生类)
class Dog extends Animal {
    public function bark() {
        echo "{$this->name} 在汪汪叫。\n";
    }
}

class Cat extends Animal {
    public function meow() {
        echo "{$this->name} 在喵喵叫。\n";
    }
}

$dog = new Dog('旺财');
$dog->eat();    // 继承自Animal
$dog->sleep();  // 继承自Animal
$dog->bark();   // Dog自己的方法

$cat = new Cat('咪咪');
$cat->eat();
$cat->meow();

方法重写(Override)

子类可以重写父类的方法,提供不同的实现。

class Animal {
    protected $name;
    
    public function __construct($name) {
        $this->name = $name;
    }
    
    public function makeSound() {
        echo "{$this->name} 发出了声音。\n";
    }
}

class Dog extends Animal {
    // 重写父类的makeSound方法
    public function makeSound() {
        echo "{$this->name} 汪汪叫。\n";
    }
}

class Cat extends Animal {
    // 重写父类的makeSound方法
    public function makeSound() {
        echo "{$this->name} 喵喵叫。\n";
    }
}

$animals = [new Dog('旺财'), new Cat('咪咪')];
foreach ($animals as $animal) {
    $animal->makeSound(); // 多态:同一个方法,不同的实现
}

parent关键字

在子类中,可以用parent::调用父类的方法。

class Dog extends Animal {
    public function __construct($name, $breed) {
        parent::__construct($name); // 调用父类的构造函数
        $this->breed = $breed;
    }
    
    public function makeSound() {
        parent::makeSound(); // 调用父类的方法
        echo "{$this->name} 汪汪叫。\n";
    }
}

final关键字

  • final class:不能被继承
  • final method:不能被子类重写
final class Util {
    // 这个类不能被继承
}

class Base {
    final public function cannotOverride() {
        // 这个方法不能被子类重写
    }
}

抽象类

抽象类,是不能被实例化的类,只能被继承。抽象类可以包含抽象方法(没有实现的方法),子类必须实现这些抽象方法。

abstract class Shape {
    protected $name;
    
    public function __construct($name) {
        $this->name = $name;
    }
    
    // 抽象方法:没有实现,子类必须实现
    abstract public function area();
    
    // 普通方法:有实现,子类可以继承或重写
    public function getName() {
        return $this->name;
    }
}

class Circle extends Shape {
    private $radius;
    
    public function __construct($radius) {
        parent::__construct('圆形');
        $this->radius = $radius;
    }
    
    // 必须实现抽象方法
    public function area() {
        return pi() * $this->radius * $this->radius;
    }
}

class Rectangle extends Shape {
    private $width;
    private $height;
    
    public function __construct($width, $height) {
        parent::__construct('矩形');
        $this->width = $width;
        $this->height = $height;
    }
    
    public function area() {
        return $this->width * $this->height;
    }
}

$shapes = [new Circle(5), new Rectangle(4, 6)];
foreach ($shapes as $shape) {
    echo $shape->getName() . "的面积是:" . $shape->area() . "\n";
}

抽象类的作用:

  1. 定义规范:抽象方法定义了子类必须实现的方法,形成了规范。
  2. 代码复用:抽象类可以包含普通方法,子类可以继承复用。
  3. 强制实现:子类必须实现所有抽象方法,否则子类也必须是抽象类。

接口

接口,是一种特殊的抽象类,只包含方法签名,不包含实现。类可以实现(implements)一个或多个接口,必须实现接口中定义的所有方法。

定义接口

interface Logger {
    public function log($message, $level = 'info');
    public function info($message);
    public function error($message);
}

interface Cache {
    public function get($key);
    public function set($key, $value, $ttl = 3600);
    public function delete($key);
    public function clear();
}

实现接口

class FileLogger implements Logger {
    private $filePath;
    
    public function __construct($filePath) {
        $this->filePath = $filePath;
    }
    
    // 必须实现接口的所有方法
    public function log($message, $level = 'info') {
        $log = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message);
        file_put_contents($this->filePath, $log, FILE_APPEND);
    }
    
    public function info($message) {
        $this->log($message, 'info');
    }
    
    public function error($message) {
        $this->log($message, 'error');
    }
}

class DatabaseLogger implements Logger {
    private $db;
    
    public function __construct(PDO $db) {
        $this->db = $db;
    }
    
    public function log($message, $level = 'info') {
        $stmt = $this->db->prepare("INSERT INTO logs (message, level, created_at) VALUES (?, ?, NOW())");
        $stmt->execute([$message, $level]);
    }
    
    public function info($message) {
        $this->log($message, 'info');
    }
    
    public function error($message) {
        $this->log($message, 'error');
    }
}

实现多个接口

PHP支持实现多个接口(接口的多继承):

class RedisCache implements Cache, Logger {
    private $redis;
    
    public function __construct(Redis $redis) {
        $this->redis = $redis;
    }
    
    // Cache接口的方法
    public function get($key) {
        return $this->redis->get($key);
    }
    
    public function set($key, $value, $ttl = 3600) {
        return $this->redis->setex($key, $ttl, $value);
    }
    
    public function delete($key) {
        return $this->redis->del($key);
    }
    
    public function clear() {
        return $this->redis->flushDB();
    }
    
    // Logger接口的方法
    public function log($message, $level = 'info') {
        $this->redis->lPush('logs', sprintf("[%s] [%s] %s", date('Y-m-d H:i:s'), $level, $message));
    }
    
    public function info($message) {
        $this->log($message, 'info');
    }
    
    public function error($message) {
        $this->log($message, 'error');
    }
}

接口继承

接口可以继承其他接口:

interface AdvancedCache extends Cache {
    public function increment($key, $step = 1);
    public function decrement($key, $step = 1);
    public function exists($key);
}

抽象类 vs 接口

特性抽象类接口
实例化不能不能
继承单继承(extends)多实现(implements)
方法可以有抽象方法和普通方法只能有抽象方法(PHP 8之前)
属性可以有属性不能有属性(只能有常量)
构造函数可以有不能有
访问控制可以有public/protected/private只能是public

选择建议:

  • 表示"是什么"(is-a)的关系,用抽象类(如Dog是Animal)
  • 表示"能做什么"(can-do)的关系,用接口(如Logger能记录日志)
  • 需要代码复用,用抽象类
  • 需要多继承,用接口

性状(Trait)

Trait(性状),是PHP 5.4引入的特性,用于解决PHP单继承的限制。Trait可以被多个类复用,类似代码片段的复用。

定义和使用Trait

trait Logger {
    public function log($message) {
        echo "[LOG] " . date('Y-m-d H:i:s') . " - " . $message . "\n";
    }
    
    public function error($message) {
        echo "[ERROR] " . date('Y-m-d H:i:s') . " - " . $message . "\n";
    }
}

trait Cacheable {
    protected $cache = [];
    
    public function setCache($key, $value) {
        $this->cache[$key] = $value;
    }
    
    public function getCache($key) {
        return $this->cache[$key] ?? null;
    }
}

class User {
    use Logger, Cacheable; // 使用多个Trait
    
    public function register($name) {
        $this->log("用户注册:$name");
        $this->setCache('current_user', $name);
    }
}

$user = new User();
$user->register('张三');
echo $user->getCache('current_user');

Trait的冲突解决

如果多个Trait有同名方法,会产生冲突,需要用insteadofas解决。

trait A {
    public function hello() {
        echo "Hello from A\n";
    }
}

trait B {
    public function hello() {
        echo "Hello from B\n";
    }
}

class MyClass {
    use A, B {
        B::hello insteadof A; // 用B的hello方法,代替A的
        A::hello as helloFromA; // 把A的hello方法改名为helloFromA
    }
}

$obj = new MyClass();
$obj->hello();        // 输出:Hello from B
$obj->helloFromA();   // 输出:Hello from A

Trait的使用场景:

  1. 多个类需要复用相同的方法,但这些类不能继承同一个父类
  2. 需要横向复用代码,而不是纵向继承
  3. 组合多个功能模块

注意:Trait虽然强大,但不要滥用。过度使用Trait会导致代码结构混乱,难以维护。如果能用继承或组合解决,就不要用Trait。

命名空间

命名空间(Namespace),是PHP 5.3引入的特性,用于解决类名、函数名、常量名的冲突问题,也用于组织代码结构。

定义命名空间

<?php
namespace App\Models;

class User {
    // ...
}

// 另一个文件
namespace App\Controllers;

use App\Models\User;

class UserController {
    public function show($id) {
        $user = new User();
        // ...
    }
}

命名空间的层次

namespace App\Models\Blog;  // 子命名空间

class Post {
    // ...
}

class Category {
    // ...
}

使用命名空间

// 完全限定名称
$user = new \App\Models\User();

// 导入(use)
use App\Models\User;
use App\Models\Post as BlogPost; // 别名

$user = new User();
$post = new BlogPost();

// 导入多个
use App\Models\{User, Post, Category};

命名空间的作用

  1. 避免命名冲突:不同的包可以有同名的类,通过命名空间区分。
  2. 组织代码结构:按照目录结构组织命名空间,代码结构清晰。
  3. 自动加载:配合PSR-4自动加载规范,根据命名空间自动加载类文件。

现代PHP开发,几乎所有项目都使用命名空间。PSR-4是PHP-FIG制定的自动加载规范,规定了命名空间和目录结构的对应关系。

自动加载

自动加载(Autoloading),是指在使用类时,自动加载对应的类文件,不需要手动require/include。

splautoloadregister

spl_autoload_register(function ($className) {
    // 把命名空间转换为文件路径
    $file = __DIR__ . '/' . str_replace('\\', '/', $className) . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});

// 现在可以直接使用类,不需要手动require
$user = new App\Models\User();

Composer自动加载

现代PHP项目,通常使用Composer的自动加载。在composer.json中配置:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

然后在入口文件中:

require_once __DIR__ . '/vendor/autoload.php';

// 自动加载App命名空间下的类
$user = new App\Models\User();

静态成员

静态成员(Static),属于类本身,不属于某个对象。可以通过类名直接访问,不需要创建对象。

静态属性和方法

class Counter {
    // 静态属性:所有对象共享
    public static $count = 0;
    
    // 静态方法:属于类,不属于对象
    public static function increment() {
        self::$count++;
    }
    
    public static function getCount() {
        return self::$count;
    }
}

// 访问静态成员,不需要创建对象
echo Counter::$count;        // 0
Counter::increment();
echo Counter::getCount();     // 1

self vs static

  • self:指向当前类,静态绑定
  • static:指向调用类,延迟静态绑定(PHP 5.3+)
class Base {
    public static function whoAmI() {
        echo "Base\n";
    }
    
    public static function testSelf() {
        self::whoAmI();  // 静态绑定,总是调用Base的whoAmI
    }
    
    public static function testStatic() {
        static::whoAmI();  // 延迟静态绑定,调用实际类的whoAmI
    }
}

class Child extends Base {
    public static function whoAmI() {
        echo "Child\n";
    }
}

Child::testSelf();    // 输出:Base
Child::testStatic();  // 输出:Child

静态成员的使用场景:

  1. 计数器:统计类被实例化的次数
  2. 工具类:不需要保存状态的工具方法(如字符串处理、数学计算)
  3. 单例模式:控制类只能有一个实例
  4. 工厂方法:创建对象的静态方法

注意:静态成员虽然方便,但不要过度使用。静态成员会增加耦合,降低可测试性。

类型声明

PHP 7引入了标量类型声明和返回类型声明,PHP 7.4引入了属性类型声明,让PHP的类型系统更完善。

参数类型声明

// 标量类型声明(PHP 7+)
function add(int $a, int $b): int {
    return $a + $b;
}

function greet(string $name): string {
    return "Hello, $name!";
}

// 类类型声明
function saveUser(User $user): bool {
    // ...
}

// 数组类型声明
function sum(array $numbers): int {
    return array_sum($numbers);
}

// 可空类型(PHP 7.1+)
function findUser(?int $id): ?User {
    if ($id === null) {
        return null;
    }
    // ...
}

返回类型声明

function getUser(int $id): User {
    // 必须返回User类型
}

function findUser(int $id): ?User {
    // 可以返回User或null
}

// void返回类型(PHP 7.1+)
function log(string $message): void {
    // 不返回任何值
}

属性类型声明(PHP 7.4+)

class User {
    public int $id;
    public string $name;
    public ?string $email;
    protected array $roles = [];
    private bool $active = true;
}

类型声明的好处:

  1. 代码更清晰:一看就知道参数和返回值的类型
  2. 提前发现错误:类型不匹配时,会抛出TypeError,提前发现bug
  3. IDE支持更好:IDE可以根据类型声明提供更好的代码提示和补全
  4. 代码更可靠:减少了类型相关的bug

魔术方法

魔术方法(Magic Methods),是PHP中以__开头的特殊方法,在特定的时机自动调用。

常用魔术方法

class MyClass {
    // 构造函数:创建对象时调用
    public function __construct() {
        echo "构造函数\n";
    }
    
    // 析构函数:对象销毁时调用
    public function __destruct() {
        echo "析构函数\n";
    }
    
    // __get:访问不存在的属性时调用
    public function __get($name) {
        echo "获取不存在的属性:$name\n";
        return null;
    }
    
    // __set:设置不存在的属性时调用
    public function __set($name, $value) {
        echo "设置不存在的属性:$name = $value\n";
    }
    
    // __isset:对不存在的属性调用isset()时调用
    public function __isset($name) {
        echo "检查不存在的属性:$name\n";
        return false;
    }
    
    // __unset:对不存在的属性调用unset()时调用
    public function __unset($name) {
        echo "销毁不存在的属性:$name\n";
    }
    
    // __call:调用不存在的方法时调用
    public function __call($name, $arguments) {
        echo "调用不存在的方法:$name,参数:" . implode(', ', $arguments) . "\n";
    }
    
    // __callStatic:调用不存在的静态方法时调用
    public static function __callStatic($name, $arguments) {
        echo "调用不存在的静态方法:$name\n";
    }
    
    // __toString:对象被当作字符串时调用
    public function __toString() {
        return "这是MyClass的对象";
    }
    
    // __invoke:对象被当作函数调用时调用
    public function __invoke($arg) {
        echo "对象被当作函数调用,参数:$arg\n";
    }
    
    // __clone:对象被克隆时调用
    public function __clone() {
        echo "对象被克隆\n";
    }
}

魔术方法虽然强大,但不要滥用。过度使用魔术方法会导致代码难以理解和调试。大多数情况下,应该用明确的方法代替魔术方法。

设计原则

面向对象编程,有一些经典的设计原则,指导我们写出更好的面向对象代码。

单一职责原则(SRP)

一个类,应该只有一个引起它变化的原因,即一个类只负责一项职责。

// 不好:User类负责用户数据、验证、数据库操作、邮件发送
class User {
    public function validate() { /* ... */ }
    public function save() { /* ... */ }
    public function sendWelcomeEmail() { /* ... */ }
}

// 好:每个类只负责一项职责
class User { /* 用户数据 */ }
class UserValidator { /* 用户验证 */ }
class UserRepository { /* 数据库操作 */ }
class UserMailer { /* 邮件发送 */ }

开放封闭原则(OCP)

软件实体(类、模块、函数),应该对扩展开放,对修改封闭。即添加新功能时,应该通过扩展而不是修改已有代码。

// 不好:每增加一种形状,都要修改AreaCalculator类
class AreaCalculator {
    public function calculate($shape) {
        if ($shape instanceof Circle) {
            return pi() * $shape->radius * $shape->radius;
        } elseif ($shape instanceof Rectangle) {
            return $shape->width * $shape->height;
        }
        // 每增加一种形状,都要加一个elseif
    }
}

// 好:通过接口扩展,不需要修改已有代码
interface Shape {
    public function area();
}

class Circle implements Shape {
    public function area() { /* ... */ }
}

class Rectangle implements Shape {
    public function area() { /* ... */ }
}

// 增加新形状,只需要新增一个类,不需要修改AreaCalculator
class AreaCalculator {
    public function calculate(Shape $shape) {
        return $shape->area();
    }
}

里氏替换原则(LSP)

子类必须能够替换父类,而不影响程序的正确性。即子类应该遵守父类的行为约定。

// 不好:Square继承Rectangle,但重写了setWidth和setHeight,违反了里氏替换原则
class Rectangle {
    protected $width;
    protected $height;
    
    public function setWidth($width) { $this->width = $width; }
    public function setHeight($height) { $this->height = $height; }
    public function area() { return $this->width * $this->height; }
}

class Square extends Rectangle {
    public function setWidth($width) {
        $this->width = $width;
        $this->height = $width; // 同时设置高
    }
    public function setHeight($height) {
        $this->width = $height; // 同时设置宽
        $this->height = $height;
    }
}

// 测试函数,期望Rectangle的行为
function testRectangle(Rectangle $r) {
    $r->setWidth(5);
    $r->setHeight(4);
    return $r->area(); // 期望20,但传入Square时返回16
}

// 好:Square不继承Rectangle,而是独立实现,或继承自更通用的Shape
interface Shape {
    public function area();
}

接口隔离原则(ISP)

客户端不应该依赖它不需要的接口。即接口应该小而专一,不要把很多方法放在一个大接口里。

// 不好:大接口,实现类必须实现不需要的方法
interface Worker {
    public function work();
    public function eat();
    public function sleep();
}

class HumanWorker implements Worker {
    public function work() { /* ... */ }
    public function eat() { /* ... */ }
    public function sleep() { /* ... */ }
}

class RobotWorker implements Worker {
    public function work() { /* ... */ }
    public function eat() { /* 机器人不需要吃饭,但必须实现 */ }
    public function sleep() { /* 机器人不需要睡觉,但必须实现 */ }
}

// 好:小接口,实现类只实现需要的接口
interface Workable {
    public function work();
}

interface Eatable {
    public function eat();
}

interface Sleepable {
    public function sleep();
}

class HumanWorker implements Workable, Eatable, Sleepable {
    // 实现所有需要的方法
}

class RobotWorker implements Workable {
    // 只需要实现work方法
}

依赖倒置原则(DIP)

高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。

// 不好:高层模块UserService依赖低层模块MySQLDatabase
class MySQLDatabase {
    public function query($sql) { /* ... */ }
}

class UserService {
    private $db;
    
    public function __construct() {
        $this->db = new MySQLDatabase(); // 直接依赖具体实现
    }
}

// 好:依赖抽象(接口)
interface Database {
    public function query($sql);
}

class MySQLDatabase implements Database {
    public function query($sql) { /* ... */ }
}

class PostgreSQLDatabase implements Database {
    public function query($sql) { /* ... */ }
}

class UserService {
    private $db;
    
    public function __construct(Database $db) { // 依赖接口,不依赖具体实现
        $this->db = $db;
    }
}

// 使用时,可以注入不同的实现
$service = new UserService(new MySQLDatabase());
// 或者
$service = new UserService(new PostgreSQLDatabase());

这五个原则,合称为SOLID原则,是面向对象设计的基本原则。遵循这些原则,能让我们的代码更易维护、更易扩展、更易测试。

实战:一个简单的博客系统类设计

下面,用面向对象的方式,设计一个简单的博客系统的核心类。

<?php
namespace App\Models;

// 文章类
class Post {
    private $id;
    private $title;
    private $slug;
    private $content;
    private $excerpt;
    private $categoryId;
    private $cover;
    private $tags;
    private $views;
    private $status;
    private $createdAt;
    private $updatedAt;
    
    public function __construct(array $data = []) {
        foreach ($data as $key => $value) {
            if (property_exists($this, $key)) {
                $this->$key = $value;
            }
        }
    }
    
    // Getter和Setter
    public function getId() { return $this->id; }
    public function getTitle() { return $this->title; }
    public function setTitle($title) {
        if (empty($title)) throw new \InvalidArgumentException('标题不能为空');
        $this->title = $title;
        $this->slug = $this->generateSlug($title);
    }
    // ... 其他Getter和Setter
    
    public function getWordCount() {
        return mb_strlen(strip_tags($this->content));
    }
    
    public function getReadTime() {
        $wordsPerMinute = 300;
        return max(1, ceil($this->getWordCount() / $wordsPerMinute));
    }
    
    public function isPublished() {
        return $this->status === 'published';
    }
    
    private function generateSlug($title) {
        // 生成URL友好的slug
        return strtolower(trim(preg_replace('/[^a-zA-Z0-9]+/', '-', $title), '-'));
    }
}

// 文章仓储接口
interface PostRepository {
    public function findById($id): ?Post;
    public function findBySlug($slug): ?Post;
    public function findAll($page = 1, $perPage = 10): array;
    public function findByCategory($categoryId, $page = 1, $perPage = 10): array;
    public function search($keyword, $page = 1, $perPage = 10): array;
    public function save(Post $post): bool;
    public function delete($id): bool;
    public function count(): int;
}

// MySQL文章仓储实现
class MySQLPostRepository implements PostRepository {
    private $db;
    
    public function __construct(\PDO $db) {
        $this->db = $db;
    }
    
    public function findById($id): ?Post {
        $stmt = $this->db->prepare("SELECT * FROM blog_posts WHERE id = ?");
        $stmt->execute([$id]);
        $data = $stmt->fetch(\PDO::FETCH_ASSOC);
        return $data ? new Post($data) : null;
    }
    
    public function findBySlug($slug): ?Post {
        $stmt = $this->db->prepare("SELECT * FROM blog_posts WHERE slug = ? AND status = 'published'");
        $stmt->execute([$slug]);
        $data = $stmt->fetch(\PDO::FETCH_ASSOC);
        return $data ? new Post($data) : null;
    }
    
    public function findAll($page = 1, $perPage = 10): array {
        $offset = ($page - 1) * $perPage;
        $stmt = $this->db->prepare("SELECT * FROM blog_posts WHERE status = 'published' ORDER BY created_at DESC LIMIT ? OFFSET ?");
        $stmt->bindValue(1, $perPage, \PDO::PARAM_INT);
        $stmt->bindValue(2, $offset, \PDO::PARAM_INT);
        $stmt->execute();
        $posts = [];
        while ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            $posts[] = new Post($data);
        }
        return $posts;
    }
    
    // ... 其他方法的实现
    
    public function save(Post $post): bool {
        // 保存文章到数据库
    }
    
    public function delete($id): bool {
        $stmt = $this->db->prepare("DELETE FROM blog_posts WHERE id = ?");
        return $stmt->execute([$id]);
    }
    
    public function count(): int {
        return (int)$this->db->query("SELECT COUNT(*) FROM blog_posts WHERE status = 'published'")->fetchColumn();
    }
}

// 文章服务类
class PostService {
    private $repository;
    private $cache;
    
    public function __construct(PostRepository $repository, ?Cache $cache = null) {
        $this->repository = $repository;
        $this->cache = $cache;
    }
    
    public function getPost($id): ?Post {
        $cacheKey = "post:$id";
        if ($this->cache && $this->cache->exists($cacheKey)) {
            return $this->cache->get($cacheKey);
        }
        $post = $this->repository->findById($id);
        if ($post && $this->cache) {
            $this->cache->set($cacheKey, $post, 3600);
        }
        return $post;
    }
    
    public function getPosts($page = 1, $perPage = 10): array {
        return $this->repository->findAll($page, $perPage);
    }
    
    public function createPost(array $data): Post {
        $post = new Post();
        $post->setTitle($data['title']);
        // ... 设置其他属性
        $this->repository->save($post);
        if ($this->cache) {
            $this->cache->clear('posts:*'); // 清除列表缓存
        }
        return $post;
    }
    
    public function deletePost($id): bool {
        $result = $this->repository->delete($id);
        if ($result && $this->cache) {
            $this->cache->delete("post:$id");
            $this->cache->clear('posts:*');
        }
        return $result;
    }
}

这个设计,展示了面向对象的核心思想:

  1. 封装:Post类封装了文章的数据和行为
  2. 抽象:PostRepository接口抽象了数据访问层
  3. 多态:MySQLPostRepository实现了PostRepository,可以替换为其他实现
  4. 依赖倒置:PostService依赖PostRepository接口,不依赖具体实现
  5. 单一职责:每个类只负责一项职责

总结

面向对象编程,是现代PHP开发的基础,也是写出可维护、可扩展、可复用代码的关键。

核心要点:

  1. 类与对象:类是模板,对象是实例;属性和方法;构造函数和析构函数;$this关键字
  2. 访问控制:public/protected/private;封装;Getter和Setter
  3. 继承:extends;方法重写;parent关键字;final关键字
  4. 抽象类:abstract;抽象方法;定义规范和代码复用
  5. 接口:interface;implements;多实现;定义能力规范
  6. 性状(Trait):use;代码横向复用;冲突解决
  7. 命名空间:namespace;use;避免命名冲突;组织代码
  8. 自动加载:splautoloadregister;Composer PSR-4
  9. 静态成员:static;self vs static;工具类、单例、计数器
  10. 类型声明:参数类型、返回类型、属性类型、可空类型;代码更清晰更可靠
  11. 魔术方法construct/destruct/get/set/call/toString等;特定时机自动调用
  12. SOLID原则:单一职责、开放封闭、里氏替换、接口隔离、依赖倒置
  13. 实战:Post类、PostRepository接口、MySQLPostRepository实现、PostService服务类

面向对象编程,不仅仅是语法,更是一种思维方式。它让我们从"怎么做"转向"谁来做",从"过程"转向"对象",从"细节"转向"抽象"。

掌握面向对象编程,能让你写出更优雅、更易维护、更易扩展的PHP代码,也能让你更好地理解和使用现代PHP框架(如Laravel、Symfony)。

"面向对象编程,是用代码来模拟现实世界。"希望这篇文章,能帮你更好地理解和使用PHP面向对象编程,写出更优雅的代码。