PHP 构造函数怎么写?实际项目中初始化对象的常见方法
在 PHP 项目中,只要使用面向对象编程,就很难绕开构造函数。它看起来只是一个简单的方法,但在实际项目里,构造函数承担的任务远不只是给几个属性赋值。一个对象应该在什么状态下被创建、哪些依赖应该由外部传入、哪些参数必须验证、数据库连接该不该放进去,这些问题最终都会落到构造函数的设计上。
PHP 的构造函数写法其实很明确。在现代 PHP 中,标准写法是 __construct(),注意是两个下划线,而不是 construct()。例如:
class Product
{
private string $name;
private float $price;
public function __construct(string $name, float $price)
{
$this->name = $name;
$this->price = $price;
}
}
$product = new Product('笔记本电脑', 1299.99);
当执行 new Product() 时,PHP 会自动调用 __construct()。构造函数最基本的作用,就是确保对象创建完成以后处于一个可以正常使用的状态。
这也是判断构造函数应该放什么逻辑的一个重要标准:这段逻辑是不是对象建立时就必须完成。
例如商品对象必须有商品名称和价格,那么这些内容放在构造函数中非常自然。反过来,如果创建商品对象以后,还需要查询数据库、调用第三方支付接口、计算复杂促销方案,通常就不应该把所有工作都塞进构造函数。
很多初学者容易把“初始化对象”和“执行业务流程”混在一起。
例如:
class Product
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
}
这种写法很合理。商品对象需要数据库连接作为依赖,因此由外部把 PDO 对象传进来。
但如果写成:
public function __construct()
{
$this->db = new PDO(...);
}
问题就出现了。Product 类自己决定数据库服务器、账号、密码以及连接方式以后,它和具体数据库环境产生了直接耦合。测试时也很麻烦,因为你很难轻易换成测试数据库或者模拟对象。
因此,实际项目中非常常见的一种做法就是依赖注入。
class ProductRepository
{
public function __construct(private PDO $db)
{
}
}
创建对象时:
$repository = new ProductRepository($pdo);
这里的重点并不是“构造函数可以传参数”这么简单,而是依赖由外部管理。ProductRepository 不负责决定数据库连接怎么建立,它只负责使用已经提供的数据库连接。
这会直接影响代码的可测试性、可维护性和模块之间的耦合关系。
当然,并不是所有属性都需要通过构造函数传入。如果一个对象非常简单,例如配置对象、值对象或者普通的数据封装类,构造函数直接初始化属性往往是最清晰的方案。
PHP 8 以后还可以利用构造函数属性提升进一步简化代码:
class Product
{
public function __construct(
private string $name,
private float $price
) {
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): float
{
return $this->price;
}
}
这比先声明属性、再在构造函数中重复赋值更加简洁。
但类型声明并不能解决所有问题。
例如:
public function __construct(
private string $name,
private float $price
) {
}
虽然 $price 必须是 float,但 -100 依然可能是一个合法的 float。PHP 知道它是数字,却不知道负数是否符合你的商品业务规则。
所以,如果商品价格必须大于零,可以在构造函数中进行不变量检查:
public function __construct(
private string $name,
private float $price
) {
if (trim($name) === '') {
throw new InvalidArgumentException('商品名称不能为空');
}
if ($price <= 0) {
throw new InvalidArgumentException('商品价格必须大于零');
}
}
这里需要纠正一个很常见的说法:参数验证并不是“不能放在构造函数里”。
恰恰相反,如果某个条件是对象能够成立的基本条件,那么在构造函数中检查非常合适。比如价格不能为负数、邮箱对象必须包含合法邮箱地址、金额对象不能出现非法数值。如果不满足这些条件,对象本身就不应该被创建出来。
但是,业务流程验证是另一回事。
例如“这个用户今天是否已经购买过这个商品”“当前会员是否有资格享受这个折扣”“库存是否足够完成这次订单”,这些都可能需要访问数据库或者调用其他业务服务。它们属于业务流程,而不是简单的对象自身状态检查。
这种逻辑放进构造函数,就容易让构造函数变得非常沉重。
例如:
class Order
{
public function __construct(PDO $db, int $userId, int $productId)
{
// 查询用户
// 查询库存
// 查询会员等级
// 查询优惠券
// 计算价格
// 写入数据库
}
}
这样的设计会让 new Order() 不再只是创建对象,而变成一次复杂的业务操作。测试、异常处理、事务管理都会变得更加困难。
更合理的方式通常是让服务层负责业务流程:
class OrderService
{
public function __construct(
private ProductRepository $products,
private UserRepository $users
) {
}
public function createOrder(int $userId, int $productId): Order
{
// 查询用户
// 查询商品
// 检查库存
// 执行业务规则
return new Order($userId, $productId);
}
}
这样职责就比较清楚:Order 负责表达一个订单对象,OrderService 负责“如何创建一个符合业务规则的订单”。
那么工厂方法又是什么?
工厂方法适合处理“创建对象本身需要选择不同构造方式”的情况。例如同一个领域对象可能根据不同来源、不同配置或者不同业务场景采用不同的创建过程。
class Product
{
private function __construct(
private string $name,
private float $price
) {
}
public static function create(string $name, float $price): self
{
if (trim($name) === '') {
throw new InvalidArgumentException('商品名称不能为空');
}
if ($price <= 0) {
throw new InvalidArgumentException('商品价格必须大于零');
}
return new self($name, $price);
}
}
此时外部不能直接调用 new Product(),而是:
$product = Product::create('笔记本电脑', 1299.99);
工厂方法并不是构造函数的替代品。new 最终仍然会调用构造函数,只不过对象创建过程被工厂方法封装起来了。
如果一个类有多种合理的创建方式,工厂方法尤其有价值:
$product = Product::fromDatabase($row);
$product = Product::fromApi($data);
$product = Product::create('键盘', 99.99);
这样调用代码可以直接表达对象的来源和创建语义。
不过,工厂方法也不能被滥用。如果一个类只需要两个参数就可以正常创建,没有复杂创建过程,直接使用构造函数往往更加简单。为了“设计模式而设计模式”,反而会增加代码量。
大型 PHP 项目中还有一个经常出现的问题,就是构造函数参数越来越多。
例如:
public function __construct(
PDO $db,
CacheInterface $cache,
LoggerInterface $logger,
MailerInterface $mailer,
PaymentInterface $payment,
Config $config
) {
// ...
}
如果一个类需要这么多依赖,不能简单认为“依赖注入太多,所以不好”。依赖注入本身没有问题,真正应该检查的是这个类是不是承担了太多职责。
一个类同时操作数据库、缓存、邮件、支付、日志,很可能已经违反了单一职责原则。
依赖注入的价值就在这里体现出来:它不仅让依赖可以替换,也会把类的结构问题暴露出来。
现代 PHP 项目通常会使用依赖注入容器自动创建对象。开发者不需要在每个地方手动 new 出所有依赖,而是让容器根据类型和配置完成对象组装。
例如:
class ProductService
{
public function __construct(
private ProductRepository $repository,
private CacheInterface $cache
) {
}
}
ProductService 不需要自己创建 ProductRepository,也不需要自己决定使用哪一种缓存实现。具体对象由应用程序的依赖注入容器负责组装。
这也是为什么现代 PHP 框架和大型应用普遍强调依赖注入。
还有一个经常被误解的概念,就是“延迟加载”。
如果商品对象只有在调用 getStock() 时才查询数据库,那么这并不是简单地“把属性访问器放进构造函数”。更合理的设计通常是把需要延迟加载的资源交给专门的 Repository、Service 或 Lazy 对象处理。
例如:
class ProductService
{
public function __construct(
private StockRepository $stocks
) {
}
public function getStock(int $productId): int
{
return $this->stocks->getStock($productId);
}
}
这样商品实体本身不需要知道数据库库存表怎么查询。
这也涉及一个重要的架构原则:对象应该尽量只管理自己真正需要管理的状态,而不是把整个应用程序的业务流程都塞进一个类。
对于构造函数,可以把实际项目中的判断方法概括成几个问题。
第一,这个数据是不是对象成立所必需的?如果是,可以考虑放入构造函数。
第二,这个依赖是不是由类自己创建的?如果是数据库、缓存、日志等外部服务,通常应该通过依赖注入传入,而不是在构造函数里直接 new。
第三,这个验证是在保证对象本身合法,还是在执行一项业务流程?前者可以放在构造函数,后者通常应该交给 Service 等业务层。
第四,对象是不是存在多种合理的创建方式?如果存在,可以考虑工厂方法或命名构造方法。
第五,构造函数是不是已经塞入了大量依赖和复杂逻辑?如果是,与其继续优化构造函数,不如重新检查类的职责划分。
构造函数最重要的作用,不是“把属性赋值进去”,而是建立对象的有效状态和明确依赖关系。简单对象直接使用 __construct() 完成初始化即可;需要外部资源时使用依赖注入;存在复杂创建流程时考虑工厂方法;涉及跨对象、数据库和业务规则的流程,则应该交给服务层。
真正好的 PHP 面向对象设计,并不是让每个类都使用某一种设计模式,而是让对象的创建过程、依赖关系和业务职责保持清楚。构造函数越容易理解,通常也意味着整个系统的职责边界越清晰。
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
分享 Facebook | X | WhatsApp | LinkedIn
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP