news 2026/10/1 4:42:23

PHP面向对象进阶:从类与对象到依赖注入、序列化与安全

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP面向对象进阶:从类与对象到依赖注入、序列化与安全

写PHP的人早晚会遇到一道坎:单个脚本写得很顺,一到系统级需求就卡壳。我也见过不少同学交期末大作业,明明用了class Order {},里面却全是MySQL拼接、foreach嵌套和到处echo,说白了就是给过程式代码披了一件对象的外套。问题不出在语法,而出在对"面向对象"四个字的理解还停留在背定义。庖丁解牛这个故事其实把这件事讲透了——庖丁不看整头牛,他看见的是骨节、经络、缝隙,刀顺着空隙走,所以一把刀用了十九年还像新的一样。今天这篇就照着这个思路,把PHP面向对象拆开:类与对象是什么关系,三大特性到底在解决什么,依赖和接口怎么搭才不别扭,以及在序列化、反序列化、文件包含这些暗骨地带怎么稳稳地不翻车。适合刚接触框架的PHP后端新人、准备期末大作业的学生,以及想真正读懂各种开源PHP源码的人。

1. 先看清"全牛":类与对象不是名词游戏

1.1 类是一张图纸,对象才是盖好的房子

很多教材把类和对象的关系说成"类是模板,对象是实例",道理对,但太干,学完照样不会用。换个角度看:类是一张建筑图纸,图纸上写"客厅30平方米、主卧15平方米",图纸本身不占土地,也不会有人住进去;对象是根据图纸盖出来的那套房,每套房的结构一样,里面的住户却各不相同。

对应到PHP里,代码里写一个class User {},只是定义了一张图纸;只有当通过new User()创建对象后,才真正在内存里分配了这块数据空间,每次创建出来的对象,属性值都是独立互不干扰的。

刚入门的人容易犯一个认知错误:以为class里面定义的属性和方法会"常驻"在某个全局空间,多个请求之间可以共享。实际完全不是这样。PHP的传统模型是"一次请求一次生命周期",同一个类可以在每个请求里反复实例化,但对象从new开始活到请求结束,一旦脚本跑完,对象随之销毁。想跨请求保存数据,得靠数据库、缓存、文件或者session,而不是幻想某个对象会自己在内存里等你回来。

1.2 二维数组到对象的转换:先治"数据结构松散"的病

PHP的数组几乎是万能的,查数据库默认返回二维数组,前端传参数也是数组,甚至在很多老代码里连配置都扔在一个大数组里。数组的优点是真香,缺点也很明显:结构太松散。

举个例子,从数据库查用户表,你得到的是:

$rows = [ ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'], ['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com'], ];

这种结构看着方便,但它没有"强制约束"。假如某处拼错了字段名,写成$row['emial'],PHP不会在编译期报错,只会悄悄返回一个"未定义索引"的警告,然后继续跑。等bug浮出水面,你往往要找半天。

面向对象的第一个实际动作,就是把这类松散数组收敛成有结构的数据类型:

class User { public function __construct( public readonly int $id, public readonly string $name, public readonly string $email ) {} } $users = array_map( fn(array $row) => new User((int)$row['id'], $row['name'], $row['email']), $rows );

这样一改,字段名写错会在IDE和静态检查阶段就被发现,而且后续处理时可以直接调用对象方法,比如$user->getProfileUrl(),不用到处写一串数组套数组。把二维数组转成对象数组,是很多PHP项目从"能跑"迈向"好维护"的第一步,也是搜索里"php二维数组改变键值"这一类问题的最佳解法——你不再需要小心翼翼操作数组键名,对象自己带着访问入口。

1.3 从"金额函数"到"金额对象":把行为绑在数据上

再拿一个网上高频问题举例:"PHP把小写数字金额转为大写"。过程式做法是写一个全局函数:

function amountToChinese(float $amount): string { /* ... */ }

功能能用,但只要金额这个数字出现在不同地方,你就得不停把这个函数到处引用,而且金额的计算逻辑、检验逻辑、格式化逻辑全散落在各处。面向对象更推荐的做法是做一个"值对象":

class Money { public function __construct(private readonly float|int $amount) {} public function toChineseUpper(): string { $digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; $units = ['', '拾', '佰', '仟']; // 这里省略完整转换实现细节,核心是数据和行为绑定在同一个类里 return '转换结果'; } public function add(Money $other): Money { return new Money($this->amount + $other->amount); } public function getAmount(): float|int { return $this->amount; } }

区别在哪?过程式函数里,金额只是函数的参数;值对象里,金额是对象的灵魂。调用方把$money传来传去,所有和金额相关的规则都能挂在这个对象上,谁也不会绕开对象直接操作底层数值。这就是面向对象最开始的一点点"骨感":数据和行为不分离。

2. 三刀落在骨缝上:封装、继承、多态的真实分寸

2.1 封装:private不是摆设,是状态守卫

网上最常见的晒代码写法是:属性全公开,方法全公开,谁都可以直接改。在只有一个开发者的玩具项目里,这确实无所谓。但只要项目进入协作阶段,public字段就是灾难。

搜"模拟炒股php"能发现很多示例代码,恰好能用来说明这个问题。假设你写了一个交易账户类:

class TradingAccount { public float $balance = 0; }

看起来简洁,但问题很大。任何一处代码都能随手$account->balance = 999999;,风控模块根本不知道余额是被谁改的、通过什么流程改的、是否合法。如果某天需求变成"卖出股票要校验持仓足够"、"提现要记录流水",你只能满项目搜索->balance的赋值点,挨个补逻辑。

封装的正确姿势不是把字段藏起来故弄玄虚,而是把状态的修改收口到方法里,让每次变更都可以附带校验和观察点:

class TradingAccount { private float $balance = 0; public function deposit(float $amount): void { if ($amount <= 0) { throw new InvalidArgumentException('存款金额必须大于0'); } $this->balance += $amount; } public function withdraw(float $amount): bool { if ($amount > $this->balance) { return false; } $this->balance -= $amount; return true; } public function getBalance(): float { return $this->balance; } }

我把balance设成private,余额只能通过deposit()和withdraw()修改。将来要加流水记录、加风控阈值、加消息通知,只需在这两个方法里插入对应代码,所有调用方自动获得新行为。这才是封装的意义:你不是在限制别人,而是在保护未来的自己。

很多新人还有个误区,以为封装就是"属性私有、提供getter/setter",并且两者一一对应。如果只是private $name; getName(){...} setName($v){...},那跟public没有任何区别。封装的精髓在于:能对外暴露的操作越少越好,暴露出来的操作必须带有业务语义。setName这种毫无语义的方法,很多时候不如直接在构造函数里一次性定死。

2.2 继承:先搭骨架,再谈复用

继承是OOP里最容易被滥用的特性。一说到复用,很多人第一反应就是extends,结果硬造出一堆"订单继承用户""苹果继承香蕉"的歪门关系。

踩得最多的坑是构造函数。PHP中子类不会自动调用父类的构造函数,这一点经常被忽略。看这个例子:

class Account { public function __construct( protected string $accountNumber, protected float $balance = 0 ) {} } class SavingsAccount extends Account { public function __construct( string $accountNumber, private float $interestRate = 0.03 ) { // 如果忘了 parent::__construct($accountNumber),$accountNumber 就没有被初始化 } }

一旦子类重写了构造函数却忘记调用parent::__construct(),父类的属性就处于未初始化状态,后续任何方法一用到就报错。这种错误在框架里非常隐蔽,因为IDE不一定会给红色波浪线,得运行到特定路径才炸。

所以我的个人经验是:继承前先问自己,父子之间是不是真正的"is-a"关系。SavingsAccount是Account吗?是,因为储蓄账户本来就是账户的一种,可以继承。Order是User吗?明显不是,订单和用户是"has-a"关系——订单里有一个归属用户。后者应该通过属性组合:

class Order { public function __construct( private string $orderNo, private User $buyer ) {} }

组合优于继承,这句话在工程里比大多数设计原则都实用。另外,如果一个类设计出来就没打算让别人继承,直接加上final修饰,编译器会帮你拦住所有不合理的继承行为,这比靠文档约束可靠得多。

2.3 多态:同一个动作,多种实现

多态听起来玄,实际就是一句话:调用方只依赖一个抽象接口,具体行为由实现类各自决定。

还是用交易场景。股票、基金、债券都可以折算成"日收益",但计算方式完全不同。如果你写一堆if ($type === 'stock') {...} elseif ($type === 'fund') {...},每加一种新资产,就得改一遍所有业务代码。

面向对象的做法是先把"日收益"定义成接口:

interface Asset { public function dailyReturn(): float; } class Stock implements Asset { public function __construct(private float $openPrice, private float $closePrice) {} public function dailyReturn(): float { return ($this->closePrice - $this->openPrice) / $this->openPrice; } } class Fund implements Asset { public function __construct(private float $netValueToday, private float $netValueYesterday) {} public function dailyReturn(): float { return ($this->netValueToday - $this->netValueYesterday) / $this->netValueYesterday; } } function formatPortfolio(array $assets): array { // 调用方根本不管具体是什么资产,只认 Asset 接口 return array_map(fn(Asset $asset) => $asset->dailyReturn(), $assets); }

新增一种资产,只要实现Asset接口就行,formatPortfolio()一行不用改。这种扩展性不是花架子,当你的项目从3种资产长到30种资产时,多态能救你命。

3. 游刃有余的技术:依赖注入、接口与轻量容器

3.1 "到处new"是在拿刀砍骨头

庖丁说自己的刀之所以十九年不坏,是因为刀刃从来不碰硬骨头,只走骨节之间那道缝。PHP里最硬的骨头,就是类与类之间那层硬编码依赖。

控制器里普遍有这样的写法:

class BookController { public function list(): void { $db = new PDO('mysql:host=127.0.0.1;dbname=library', 'root', ''); // 查询逻辑... } }

每个方法里都new PDO一遍,看起来没问题,但数据库连接配置被复制了无数次,换库、加日志、做测试全都寸步难行。更麻烦的是,控制器和数据库连接"焊死"在一起,想mock一个假数据库来做单元测试根本无从下手。

解决办法是把依赖从外部传入,这就是依赖注入。具体表现就是构造函数注入:

class BookController { public function __construct(private BookRepository $repository) {} public function list(): void { $books = $this->repository->findAll(); // 输出逻辑... } }

BookController不再关心BookRepository是怎么连数据库的,它只知道自己需要一个能查书的仓库。将来要换数据源、加缓存层,改的是组装BookController的地方,而不是改控制器本身。

3.2 接口与抽象类的分工

依赖注入解决了"谁创建谁"的问题,但还有一个前置问题:依赖的类型应该声明成什么。你可以直接依赖一个具体类BookRepository,也可以依赖一个接口BookRepositoryInterface。后者更灵活。

接口和抽象类的选择,很多新手搞不清楚。我习惯用一个粗俗的类比:接口是一份合同,要求签约方必须提供哪些服务,但完全不干涉内部长什么样;抽象类是一个半成品模具,已经把一部分共用逻辑做好了,子类只需要补全剩下的部分。

比如日志系统可以定义接口:

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

文件日志、数据库日志、发送到远程日志服务的类,各自实现Logger接口即可。调用方只要Logger,不需要知道具体是写到文件还是写到数据库。而抽象类更适合有明显"骨架"的场景,比如所有数据库驱动都要处理连接、超时这些公共步骤,就可以在抽象类里写一遍公共代码,子类只实现特定的SQL方言差异。

实际工程建议是:优先依赖接口,而不是依赖具体类。你依赖接口,你就逼着自己去思考对方"能做什么"而不是"是什么",设计出来的协作关系会清爽很多。

3.3 接口、数组、对象与JSON的统一出口

搜"php接口数组对象"的人,多半是在前后端联调时被数据格式绕晕了。PHP后端返回给前端的数据,经常一会儿是数组,一会儿是对象,一会儿又嵌套得乱七八糟,前端解析起来得写一堆防御性判断。

我踩过这个坑后,定了一条规矩:凡是给外部系统的接口,统一用DTO加JSON,绝不直接吐数据库行。所谓DTO,就是专门用来传输数据的对象,它不承担业务逻辑,只负责组织数据结构。

class BookDto implements JsonSerializable { public function __construct( public readonly int $id, public readonly string $title, public readonly string $author ) {} public function jsonSerialize(): array { return [ 'id' => $this->id, 'title' => $this->title, 'author' => $this->author, ]; } }

接口出口处固定包装成统一结构:

function success(mixed $data): string { return json_encode(['code' => 0, 'message' => 'ok', 'data' => $data], JSON_UNESCAPED_UNICODE); }

这样前端永远面对的是{code, message, data}这个固定外壳,data到底是什么由接口文档约定,而不是由后端今天的数组心情决定。

另外,搜索词里还有"php跨域+jsonp"。如果是被JSONP和跨域折磨过的后端,最直接的方案是用CORS替代JSONP,现代浏览器对JSONP的兼容性需求已经很低了,CORS更干净也更安全:

// 跨域响应头,按需收紧来源白名单 header('Access-Control-Allow-Origin: https://allowed.example.com'); header('Access-Control-Allow-Headers: Content-Type, X-Requested-With'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');

如果你确实是在维护老系统,JSONP的PHP端一般约定一个callback参数,注意只允许白名单内的回调名字,防止被注入,然后输出echo $callback . '(' . json_encode($data) . ');'。但新项目我基本不会再碰JSONP,CORS省心得多。

4. 暗骨遍布之处:生命周期、魔术方法与对象序列化

4.1 生命周期和请求绑得死死的

这一节聊的是对象看不见的"生死时刻"。前面说过,传统PHP模型里对象生命周期以一个请求为界限,请求结束,对象树全部销毁。这意味着你在这个请求里创建的一切对象,都没办法留在下一次请求里直接被读取。

在实际开发中,这个性质带来两个直接后果。第一,面向对象代码里不要试图"保存状态"到对象字段上等下一次请求再用,那是白费力气,要保存就保存到数据库、缓存或session。第二,想提升性能,重点不在"让对象活着",而在"让对象尽快创建、尽快用完"。

很多框架类为什么设计成"路由收到请求后才动态加载"?就是为了减少无谓的对象创建。这些设计背后,全是同一个生命周期认知在支撑。

4.2 魔术方法要用在刀刃上

PHP的魔术方法不少,常见的有__construct、__destruct、__get、__set、__call、__toString、__clone、__sleep、__wakeup等。它们本身不神秘,就是在特定时机被PHP自动调用的钩子。

初学者最爱滥用的是__get/__set:

class User { private array $data = []; public function __get(string $name): mixed { return $this->data[$name] ?? null; } public function __set(string $name, mixed $value): void { $this->data[$name] = $value; } }

这种写法让$user->email能访问到,但IDE完全给不了自动补全提示,属性名拼错了也不会在开发期暴露,等到运行时才发现返回了null。一旦项目变大,这类"魔术"就是埋雷。我的经验是:__get/__set尽量少用,就算要用,也得在类里用@property注解把可访问的属性明确声明出来,让IDE和静态分析工具能兜底。

__toString是另一个容易被忽视的坑。只要对象被当作字符串使用,比如拼接、echo,它就会自动触发。如果里面写了重逻辑,你排查几层都想不到性能问题出在自动字符串转换上。所以__toString永远只做一件事:快速返回一个有意义的字符串表示,别干重活。

__clone则和对象复制有关。PHP的对象默认是引用赋值,$b = $a其实是让$b指向同一个对象,而不是复制。要真正复制对象,必须用clone。如果这个对象内部还持有另一个对象,浅复制会把内部对象原封不动地共享过去,这时候就需要在__clone里手动把内部对象也复制一遍。这是"深拷贝"和"浅拷贝"的根源。

4.3 序列化长什么样,中文为什么总被说有问题

PHP的serialize()把对象变成字符串,格式本身是PHP私有的。一个User对象序列化出来大致长这样:

O:4:"User":2:{s:8:"userName";s:6:"张三";s:8:"userPass";s:6:"123456";}

O代表对象,4是类名长度,User是类名,2是属性数量,后面跟着属性名和属性值。字符串格式里,中文不会被serialize()直接破坏,因为它保存的是长度和字节内容,重新unserialize()回来仍然能还原中文。

那为什么大家总觉得序列化和中文扯上关系?因为很多人把serialize()和json_encode()混在一起用。json_encode默认会把中文转成\uXXXX这种转义形式,要显示中文需要再加JSON_UNESCAPED_UNICODE参数。两者根本不是一回事。我的建议是:跨语言、跨系统的数据交换统一用JSON,不要用PHP序列化字符串。JSON是人可读的、跨语言的、安全的;PHP序列化字符串只适合PHP自己内部临时存储。

4.4 反序列化与文件包含的防守姿势

搜索里频繁出现"php反序列化漏洞原理""php文件包含漏洞",说明这是很多人在安全题目和实际排查中会遇到的主题。我在这里只讲防守思路,不讲攻击利用。

unserialize()的杀伤力在于:反序列化的时候,PHP会在后台重建对象,并自动触发某些魔术方法,比如__wakeup(),对象销毁时可能触发__destruct()。如果攻击者能控制你传入的序列化字符串,他就可能利用项目里某个类的方法,实现非预期的代码执行。很多CTF题目和真实漏洞,都是从这里切入的。

防守要点三条:第一条,永远不要unserialize()不可信数据。用户提交的、外部接口传过来的字符串,只要不是你自己系统生成的序列化结果,就不该走反序列化。第二条,如果某个老业务必须反序列化,用allowed_classes白名单:

$obj = unserialize($rawString, ['allowed_classes' => [User::class, Book::class]]);

不在白名单里的类直接变成__PHP_Incomplete_Class,攻击面会被大幅压缩。第三条,业务数据尽量选择JSON而不是序列化,JSON只是纯数据解析,不会触发PHP类的魔术方法。

文件包含也是同样的防守逻辑。所谓"文件包含漏洞",常见成因是把用户输入直接拼到include语句的路径里,比如include './pages/' . $_GET['page'] . '.php';。最稳妥的修法是根本不让用户输入碰路径:

$allowedPages = ['home', 'books', 'users']; $page = $_GET['page'] ?? 'home'; if (!in_array($page, $allowedPages, true)) { $page = 'home'; } include __DIR__ . '/../pages/' . $page . '.php';

白名单+固定目录前缀,用户的输入再花哨也出不了圈。

5. 刀要常磨:PHP 8.3 的类型系统与开发调试环境

5.1 从PHP7到PHP8.3,面向对象越来越稳

很多老开发嫌弃PHP,多半停留在PHP5、PHP7早期的印象。实际上PHP 8之后,语言层面已经补上了面向对象最需要的东西,特别适合把"刀"磨得更锋。

首先是严格类型。在文件头部声明declare(strict_types=1);后再写类型约束,函数传参和返回值就不允许静默转换。比如function sum(int $a, int $b): int {}你传字符串进去直接抛TypeError,而不是悄悄转成0,很多低级bug在入口就现形了。

然后是构造器属性提升,这是PHP 8最提升幸福感的语法。以前写一个包含初始化的类,要先声明属性、再写构造函数参数、再在构造函数里一个个赋值,三个步骤冗余又啰嗦。现在一行搞定:

class Book { public function __construct( public readonly int $id, public readonly string $title, public readonly string $author, ) {} }

readonly表示只读属性,初始化之后就不能再改,天然表达了"这条记录的数据不可变"这一层语义,省下去一堆写setter的冲动。搭配联合类型int|float、match表达式和enum枚举,很多以前要靠注释和约定来维持的规则,现在都能写进语法里。

这就是我对"面向对象"的进阶理解:面向对象不是在class里堆方法,而是让你能把业务约束表达进类型系统,让语言帮你守住边界。

5.2 Windows下PHP版本选择与运行环境

搜索里有"php 8.3下载""phpstudy升级php版本""windows server php环境搭建",说明很多人卡在第一步:环境跑不起来,后面全是白搭。

Windows下PHP分两种包,TS(Thread Safety)和NTS(None Thread Safe)。用Apache或IIS跑PHP时,一般选TS版本,因为Apache在Windows下需要线程安全模式;用PHP内置开发服务器或FastCGI模式,可以选NTS。很多人随便下载一个版本,扩展装不上、运行报错,往往就是TS/NTS和服务器模式不匹配导致的。所以第一件事就是确认你用的是哪一档。

用phpstudy这类集成环境升级PHP版本时,别只顾着下载新版本,还要确认扩展是不是配套的。PHP 8.3和PHP 8.2在扩展目录上是分开的,ext文件夹里没有对应扩展,启动就会提示找不到DLL。升级完成后重点检查三样:php -m能列出预期扩展、php -v版本号正确、CLI和Web端PHP版本一致。这两个版本不一致,是很多诡异报错的元凶。

5.3 在VS Code里把PHP跑起来(含调试)

VS Code写PHP其实体验相当不错,关键是把两个东西配好:语言服务插件和调试器。

第一步,装插件PHP Intelephense,然后打开设置,把php.validate.executablePath指向你的php.exe路径。这样类名跳转、方法提示、参数提示全都活了,写面向对象代码时体验和Java/Python比完全不落下风。

第二步,配调试。下载对应版本的Xdebug扩展,在php.ini里加:

zend_extension=xdebug xdebug.mode=debug xdebug.start_with_request=yes xdebug.client_host=127.0.0.1 xdebug.client_port=9003

然后在VS Code里创建launch.json,用PHP类型的调试配置,监听9003端口。之后在PHP代码里打上断点,F5启动调试,浏览器访问对应页面,程序就会停在断点上。这里有个容易出错的点是:Xdebug版本必须和PHP版本严格匹配,很多人安装了却不生效,排查到最后都是版本不匹配。

怎么用PHP在浏览器控制台里输出变量?这个需求其实不用控制台,调试时用Xdebug打断点看变量最直观。如果是临时打印,推荐error_log(json_encode($data))写到日志文件,或者var_dump后加die(),而不是乱丢echo污染页面输出。正规项目里把调试输出打印到响应主体里是非常不推荐的做法,会破坏接口数据格式。

6. 这头牛怎么下锅:图书管理系统过程式到面向对象重构记录

最后拿一个出现率极高的需求完整走一遍:图书管理系统,带数据库,很多学校的期末大作业就是这个题目。我用它演示一次真正的面向对象重构路线。

6.1 原始过程式代码的病根

期末作业常见的过程式写法大概长这样:

$conn = mysqli_connect('127.0.0.1', 'root', '', 'library'); $action = $_POST['action'] ?? 'list'; if ($action === 'add') { $title = $_POST['title']; $author = $_POST['author']; mysqli_query($conn, "INSERT INTO books (title, author) VALUES ('$title', '$author')"); } elseif ($action === 'delete') { $id = (int)$_POST['id']; mysqli_query($conn, "DELETE FROM books WHERE id = $id"); }

这段代码功能上没毛病,但它把所有事都揉在一锅粥里:数据库连接、请求解析、SQL拼接、状态流转全在一个文件里。它的病根不是"没用class",而是职责没有分离。一旦要加"借阅记录""用户登录""分类管理",这个文件就会膨胀成一个没人敢动的怪兽。

6.2 第一步:把"数据行"变成Book模型

重构的第一刀,先定义数据的样子。没有模型的情况下,一本书就是一个关联数组;有了模型,书就是一个有行为约束的对象:

class Book { public function __construct( public readonly int $id, public readonly string $title, public readonly string $author, private bool $available = true ) {} public function isAvailable(): bool { return $this->available; } public function markBorrowed(): void { $this->available = false; } }

available是私有状态,只借出和归还方法才能修改它,这就是前面说的封装。以后想在借书时记录时间、推消息,都只需要改这一个方法。

6.3 第二步:仓库与服务分离

模型定义好后,数据库访问应该单独封装成一个仓库类:

class BookRepository { public function __construct(private PDO $pdo) {} public function findAll(): array { $stmt = $this->pdo->query('SELECT id, title, author FROM books'); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); return array_map( fn(array $row) => new Book((int)$row['id'], $row['title'], $row['author']), $rows ); } public function save(string $title, string $author): int { $stmt = $this->pdo->prepare('INSERT INTO books (title, author) VALUES (?, ?)'); $stmt->execute([$title, $author]); return (int)$this->pdo->lastInsertId(); } }

PDO + 预处理语句,这一步也顺便解决了SQL注入问题,比原来直接拼接字符串安全得多。BookRepository只负责和数据库打交道,业务判断、计算、编排逻辑则放到Service层:

class BookService { public function __construct(private BookRepository $repository) {} public function createBook(string $title, string $author): Book { if (mb_strlen($title) < 1 || mb_strlen($title) > 100) { throw new InvalidArgumentException('书名长度不合法'); } $id = $this->repository->save($title, $author); return new Book($id, $title, $author); } }

到这里,控制器不再需要知道任何SQL和业务规则,它只负责拿到参数、调用服务、把结果返回出去。

6.4 第三步:控制器、路由与统一JSON出口

控制器本身不需要做太多事情,这也是我一直强调的:控制器薄一点,Service厚一点,Model轻一点。

class BookController { public function __construct( private BookService $service, private BookRepository $repository ) {} public function list(): string { $books = $this->repository->findAll(); return json_encode(['code' => 0, 'data' => $books], JSON_UNESCAPED_UNICODE); } public function add(array $request): string { try { $book = $this->service->createBook($request['title'], $request['author']); return json_encode(['code' => 0, 'data' => $book], JSON_UNESCAPED_UNICODE); } catch (InvalidArgumentException $e) { return json_encode(['code' => 1, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE); } } }

入口文件里只做两件事:解析请求、分发给对应的方法。这就是最简路由:

$route = $_GET['route'] ?? 'book/list'; $controller = new BookController( new BookService(new BookRepository(new PDO('mysql:host=127.0.0.1;dbname=library', 'root', ''))) ); if ($route === 'book/list') { header('Content-Type: application/json; charset=utf-8'); echo $controller->list(); } elseif ($route === 'book/add') { header('Content-Type: application/json; charset=utf-8'); echo $controller->add($_POST); }

你看,这里就把依赖"装配"集中在入口处。以后想换数据库、加日志、加缓存,只改这个组装点,控制器、服务、仓库三个层次的内部代码完全不用动。这就是依赖注入和分层带来的直观收益。

6.5 重构之后的体会

从过程式改造成面向对象,代码行数并没有变少,可能还更多。但三个变化是实打实的:第一,每个类只看名字就知道它是干什么的;第二,改业务规则时不用全项目搜索,只需要改对应类里那一个方法;第三,想测试时可以分别mock掉仓库层和服务层,单独验证某一段逻辑。

我实际带人重构过好几次类似的作业,最深的体会是:面向对象最难的不是语法,而是"定对象边界"这件事。同样一个图书系统,有人会把用户、图书、借阅记录全塞进一个大类,有人会拆成五六个小类互相协作。后者一开始麻烦,但越往后维护越轻松。庖丁解牛的"目无全牛",说的就是这个意思——你眼里不再是一头完整的牛,而是一块块筋膜、一条条骨缝,刀自然就游刃有余了。写PHP也一样,看得清类与类之间的接缝,代码的刀口就不会崩。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:41:53

C# WinForms超市管理系统实战:收银+库存闭环开发

简介&#xff1a;本资源是一个基于C#开发的超市管理系统完整项目源码包&#xff0c;面向C#初学者与Windows桌面应用开发者&#xff0c;聚焦收银结算与库存管理两大核心业务场景&#xff0c;助力理解企业级POS系统架构设计与数据库交互实践。压缩包共134个文件&#xff0c;含97个…

作者头像 李华
网站建设 2026/10/1 4:41:51

YOLOv11实战:罐装饮料识别从数据集到部署的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 4:41:27

OpenCV答题卡识别:鲁棒定位与结构化判卷实战

简介&#xff1a;本资源是一套基于OpenCV与Python实现的答题卡自动识别与智能判卷实战项目&#xff0c;面向计算机视觉初学者、图像处理爱好者及高校课程设计学生&#xff0c;解决标准化考试中人工阅卷效率低、易出错等实际问题。压缩包共7个文件&#xff0c;含6张不同样本的答…

作者头像 李华
网站建设 2026/10/1 4:41:21

STM32开发资源全攻略:官方资料、开源项目与社区实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 4:41:21

12G显存跑27B MoE模型:128K上下文与50+ tokens/s调优实战

上个月我把翻出来的RTX 3060 12G从游戏机房里重新装好&#xff0c;给自己定了个有点离谱的目标&#xff1a;跑一个27B量级的MoE模型&#xff0c;开满128K上下文&#xff0c;生成速度稳住50 tokens/s。当时群里几个朋友的第一反应都是“12G显存跑27B&#xff1f;你在想什么”&am…

作者头像 李华
网站建设 2026/10/1 4:40:50

CBAM注意力机制如何提升CNN图像分类预测精度

做深度学习项目这几年&#xff0c;我越来越有一个感觉&#xff1a;结构上多一两个模块&#xff0c;往往比调十天参管用。CBAM&#xff08;Convolutional Block Attention Module&#xff0c;卷积块注意力模块&#xff09;就是这么一个"投入小、回报稳"的组件&#xf…

作者头像 李华