面向对象编程(OOP),是PHP开发中非常重要的编程范式。从PHP 5开始,PHP的面向对象功能就已经非常完善了。类、对象、继承、接口、抽象类、命名空间、 traits……PHP的面向对象功能,和Java、C#等语言已经非常接近了。

在之前的文章中,我们介绍了PHP面向对象的基础。今天,我们来深入学习PHP面向对象的进阶知识,包括命名空间、自动加载、Traits、魔术方法、设计模式等,帮你写出更加规范、优雅、可维护的面向对象代码。

命名空间(Namespace)

什么是命名空间

命名空间,是PHP 5.3引入的特性,用来解决类名、函数名、常量名的冲突问题。

在大型项目中,可能会有很多类,不同的开发者、不同的库,可能会定义同名的类,导致冲突。命名空间,就是给类、函数、常量加上一个"前缀",避免命名冲突。

比如,两个库都有一个User类,一个在App\Model命名空间下,一个在Vendor\Lib命名空间下,就不会冲突了。

定义命名空间

<?php
namespace App\Model;

class User {
    // ...
}

这个User类的完整类名是App\Model\User

命名空间的分隔符是反斜杠\,命名空间名通常用大写开头,和目录结构对应。

一个文件中只能定义一个命名空间(虽然PHP允许多个,但不推荐)。命名空间声明必须是文件的第一个语句(除了declare和注释)。

使用命名空间

<?php
namespace App\Controller;

use App\Model\User; // 引入其他命名空间的类
use App\Model\User as UserModel; // 别名

class UserController {
    public function index() {
        $user = new User(); // 使用引入的类
        $user2 = new UserModel(); // 使用别名
        
        // 也可以用完整类名
        $user3 = new \App\Model\User();
    }
}

use关键字用来引入其他命名空间的类,可以用as设置别名。

命名空间中的函数和常量

命名空间不仅可以用于类,也可以用于函数和常量:

namespace App\Utils;

function helper() {
    // ...
}

const MAX_COUNT = 100;

使用:

use function App\Utils\helper;
use const App\Utils\MAX_COUNT;

helper();
echo MAX_COUNT;

全局命名空间

没有定义命名空间的类、函数、常量,都在全局命名空间下。在命名空间中访问全局的类、函数、常量,需要加\前缀:

namespace App;

class MyClass {
    public function test() {
        $exception = new \Exception(); // 全局的Exception类
        $len = \strlen('hello'); // 全局的strlen函数
    }
}

自动加载(Autoload)

什么是自动加载

在PHP中,使用一个类之前,需要先requireinclude这个类的文件。如果类很多,手动引入会很麻烦。

自动加载,就是当使用一个未定义的类时,PHP会自动调用注册的加载函数,去加载对应的类文件。这样,就不需要手动引入每个类文件了。

__autoload函数

PHP提供了__autoload魔术函数,当使用未定义的类时,会自动调用:

function __autoload($className) {
    $file = __DIR__ . '/' . str_replace('\\', '/', $className) . '.php';
    if (file_exists($file)) {
        require $file;
    }
}

注意:__autoload只能定义一个,已经不推荐使用了,推荐用splautoloadregister

splautoloadregister

splautoloadregister可以注册多个自动加载函数,更灵活:

spl_autoload_register(function($className) {
    $file = __DIR__ . '/' . str_replace('\\', '/', $className) . '.php';
    if (file_exists($file)) {
        require $file;
    }
});

可以注册多个加载函数:

spl_autoload_register('autoloadModel');
spl_autoload_register('autoloadController');

PSR-4自动加载规范

PSR-4是PHP-FIG制定的自动加载规范,规定了命名空间和目录的对应关系。现在大部分PHP框架和库都遵循PSR-4规范。

PSR-4规范:

  • 命名空间前缀对应一个基础目录
  • 命名空间分隔符转换为目录分隔符
  • 类名后面加.php

比如,命名空间App\Model对应目录src/Model/,类App\Model\User对应文件src/Model/User.php

Composer就是基于PSR-4规范实现自动加载的,用Composer管理依赖,自动加载非常方便。

Traits

什么是Traits

Traits(性状),是PHP 5.4引入的特性,用来解决PHP单继承的问题。

PHP的类只能继承一个父类(单继承),但有时候,一个类需要复用多个类的方法,这时候就可以用Traits。

Traits类似于类,但不能实例化,只能被其他类使用。一个类可以使用多个Traits,从而实现代码的复用。

定义和使用Traits

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

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

class UserService {
    use Logger, Cache; // 使用多个Traits
    
    public function createUser($name) {
        $this->log("创建用户:$name");
        $this->setCache('user_' . $name, $name);
        // ...
    }
}

$service = new UserService();
$service->createUser('张三');

Traits的冲突处理

如果多个Traits中有同名的方法,会产生冲突,需要用insteadofas来解决:

trait A {
    public function test() {
        echo 'A::test';
    }
}

trait B {
    public function test() {
        echo 'B::test';
    }
}

class MyClass {
    use A, B {
        B::test insteadof A; // 用B的test方法,而不是A的
        A::test as testA; // 把A的test方法别名为testA
    }
}

$obj = new MyClass();
$obj->test(); // 输出B::test
$obj->testA(); // 输出A::test

Traits的抽象方法和静态方法

Traits中可以定义抽象方法,要求使用它的类实现:

trait Loggable {
    abstract public function getLogName();
    
    public function log($message) {
        echo $this->getLogName() . ': ' . $message . "\n";
    }
}

Traits中也可以定义静态方法和静态属性:

trait Singleton {
    private static $instance;
    
    public static function getInstance() {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

魔术方法(Magic Methods)

PHP提供了一些魔术方法,在特定的时机自动调用。魔术方法以开头,前面我们已经介绍了constructdestructautoload等,这里介绍更多的魔术方法。

get和set

访问不存在或不可访问的属性时,getset会被调用:

class User {
    private $data = [];
    
    public function __get($name) {
        return $this->data[$name] ?? null;
    }
    
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }
    
    public function __isset($name) {
        return isset($this->data[$name]);
    }
    
    public function __unset($name) {
        unset($this->data[$name]);
    }
}

$user = new User();
$user->name = '张三'; // 调用__set
echo $user->name; // 调用__get
isset($user->name); // 调用__isset
unset($user->name); // 调用__unset

call和callStatic

调用不存在或不可访问的方法时,callcallStatic会被调用:

class DB {
    public function __call($name, $arguments) {
        echo "调用方法:$name,参数:" . implode(', ', $arguments) . "\n";
    }
    
    public static function __callStatic($name, $arguments) {
        echo "调用静态方法:$name,参数:" . implode(', ', $arguments) . "\n";
    }
}

$db = new DB();
$db->where('id = 1'); // 调用__call
DB::table('users'); // 调用__callStatic

__toString

把对象当作字符串使用时,__toString会被调用:

class User {
    public function __construct($name) {
        $this->name = $name;
    }
    
    public function __toString() {
        return 'User: ' . $this->name;
    }
}

$user = new User('张三');
echo $user; // 输出User: 张三

__invoke

把对象当作函数调用时,__invoke会被调用:

class Calculator {
    public function __invoke($a, $b) {
        return $a + $b;
    }
}

$calc = new Calculator();
echo $calc(1, 2); // 输出3

__clone

克隆对象时,__clone会被调用:

class User {
    public $id;
    public $name;
    
    public function __clone() {
        $this->id = null; // 克隆时清空id
    }
}

$user1 = new User();
$user1->id = 1;
$user1->name = '张三';

$user2 = clone $user1;
echo $user2->id; // 输出null(因为__clone中清空了)
echo $user2->name; // 输出张三

sleep和wakeup

序列化对象时,sleep会被调用;反序列化时,wakeup会被调用:

class User {
    public $name;
    private $password;
    
    public function __sleep() {
        return ['name']; // 只序列化name,不序列化password
    }
    
    public function __wakeup() {
        // 反序列化时的初始化操作
    }
}

__debuginfo

var_dump()打印对象时,__debuginfo会被调用(PHP 7.0+):

class User {
    private $data = ['name' => '张三', 'password' => '123456'];
    
    public function __debuginfo() {
        return ['name' => $this->data['name']]; // 只显示name,不显示password
    }
}

设计模式入门

设计模式,是解决常见问题的最佳实践。面向对象编程中,有很多经典的设计模式,这里介绍几个PHP中常用的设计模式。

单例模式(Singleton)

单例模式,保证一个类只有一个实例,并提供一个全局访问点。

class Database {
    private static $instance;
    private $pdo;
    
    private function __construct() {
        $this->pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    }
    
    private function __clone() {} // 禁止克隆
    private function __wakeup() {} // 禁止反序列化
    
    public static function getInstance() {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    public function getConnection() {
        return $this->pdo;
    }
}

// 使用
$db = Database::getInstance();
$pdo = $db->getConnection();

工厂模式(Factory)

工厂模式,用工厂方法创建对象,而不是直接用new

interface Logger {
    public function log($message);
}

class FileLogger implements Logger {
    public function log($message) {
        file_put_contents('log.txt', $message . "\n", FILE_APPEND);
    }
}

class DatabaseLogger implements Logger {
    public function log($message) {
        // 写入数据库
    }
}

class LoggerFactory {
    public static function create($type = 'file') {
        switch ($type) {
            case 'file':
                return new FileLogger();
            case 'database':
                return new DatabaseLogger();
            default:
                throw new InvalidArgumentException("未知的日志类型:$type");
        }
    }
}

// 使用
$logger = LoggerFactory::create('file');
$logger->log('这是一条日志');

观察者模式(Observer)

观察者模式,定义了对象间的一对多依赖,当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。

interface Observer {
    public function update($event, $data);
}

interface Subject {
    public function attach(Observer $observer);
    public function detach(Observer $observer);
    public function notify($event, $data);
}

class User implements Subject {
    private $observers = [];
    
    public function attach(Observer $observer) {
        $this->observers[] = $observer;
    }
    
    public function detach(Observer $observer) {
        $key = array_search($observer, $this->observers);
        if ($key !== false) {
            unset($this->observers[$key]);
        }
    }
    
    public function notify($event, $data) {
        foreach ($this->observers as $observer) {
            $observer->update($event, $data);
        }
    }
    
    public function register($name, $email) {
        // 注册逻辑
        $this->notify('user.registered', ['name' => $name, 'email' => $email]);
    }
}

class EmailObserver implements Observer {
    public function update($event, $data) {
        if ($event == 'user.registered') {
            echo "发送欢迎邮件到:" . $data['email'] . "\n";
        }
    }
}

class LogObserver implements Observer {
    public function update($event, $data) {
        echo "记录日志:$event\n";
    }
}

// 使用
$user = new User();
$user->attach(new EmailObserver());
$user->attach(new LogObserver());
$user->register('张三', 'zhangsan@example.com');

依赖注入(Dependency Injection)

依赖注入,是指对象的依赖由外部注入,而不是对象自己创建。这能降低耦合,方便测试和维护。

class UserService {
    private $db;
    private $logger;
    
    // 依赖通过构造函数注入
    public function __construct(Database $db, Logger $logger) {
        $this->db = $db;
        $this->logger = $logger;
    }
    
    public function createUser($name) {
        $this->logger->log("创建用户:$name");
        // 用$this->db操作数据库
    }
}

// 使用
$db = Database::getInstance();
$logger = new FileLogger();
$userService = new UserService($db, $logger); // 注入依赖
$userService->createUser('张三');

依赖注入容器(DI Container)可以自动管理依赖的创建和注入,如Pimple、PHP-DI等。

面向对象最佳实践

  1. 单一职责原则:一个类只做一件事,只有一个引起它变化的原因。不要把不相关的功能放到一个类里。
  2. 开闭原则:对扩展开放,对修改关闭。用继承、接口、多态来扩展功能,而不是修改已有代码。
  3. 里氏替换原则:子类可以替换父类,而不影响程序的正确性。子类不要重写父类的方法改变其行为。
  4. 接口隔离原则:接口要小而专,不要大而全。一个类不需要实现它用不到的接口方法。
  5. 依赖倒置原则:依赖抽象,不依赖具体。面向接口编程,而不是面向实现编程。
  6. 组合优于继承:优先用组合(has-a)而不是继承(is-a)来复用代码。继承是静态的、紧耦合的,组合是动态的、松耦合的。
  7. 封装变化:把会变化的部分封装起来,和不变的部分分离。这样变化时,只需要修改封装的部分。
  8. 面向接口编程:依赖接口,而不是具体实现。这样可以方便地替换实现,降低耦合。
  9. 合理使用命名空间:用命名空间组织代码,避免命名冲突,和目录结构对应。
  10. 合理使用Traits:Traits用来复用横切关注点的代码(如日志、缓存),不要滥用Traits来模拟多继承。
  11. 避免上帝类:不要创建什么都做的"上帝类",一个类的方法和属性不要太多,职责要单一。
  12. 合理使用魔术方法:魔术方法虽然方便,但会降低代码的可读性和性能,不要滥用。

总结

PHP面向对象进阶,包括命名空间、自动加载、Traits、魔术方法、设计模式等内容。

核心要点:

  1. 命名空间:解决命名冲突,用namespace定义,use引入,和目录结构对应
  2. 自动加载:用splautoloadregister注册自动加载函数,遵循PSR-4规范,Composer自动加载
  3. Traits:解决单继承问题,用trait定义,use使用,支持冲突处理、抽象方法、静态方法
  4. 魔术方法get/set/isset/unset(属性访问)、call/callStatic(方法调用)、toString(字符串转换)、invoke(函数调用)、clone(克隆)、sleep/wakeup(序列化)、debuginfo(调试信息)
  5. 设计模式:单例模式、工厂模式、观察者模式、依赖注入等
  6. SOLID原则:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置
  7. 最佳实践:组合优于继承、封装变化、面向接口编程、合理使用命名空间和Traits、避免上帝类、合理使用魔术方法

掌握这些面向对象的进阶知识,能帮你写出更加规范、优雅、可维护、可扩展的PHP代码。面向对象编程,不仅仅是语法,更是一种思维方式。希望这篇文章能帮你更好地理解和使用PHP的面向对象特性。