函数驱动型应用(Drupal 7)中PHP依赖注入容器的适配问题
这确实是Drupal 7这类老旧过程式架构项目里很常见的痛点——一边想用上现代DI的好处,一边又没法摆脱大量现存的函数代码。我来分享几个实用的方案,帮你把DI容器和这些函数打通:
方案1:用静态持有类让全局可访问容器(兼顾测试性)
既然Drupal 7的函数无法通过构造函数注入依赖,我们可以把容器做成全局可访问但可控的实例,避免直接用全局变量(太乱),而是用一个静态类来持有它:
首先在你的自定义模块初始化时创建并注入容器:
// 在你的自定义模块的 .module 文件中 function mymodule_init() { // 初始化Pimple容器(换成PHP-DI逻辑也一样) $container = new Pimple\Container(); $container['my_service'] = function($c) { return new MyService($c['core_dependency']); }; $container['core_dependency'] = function() { return new CoreDependency(); }; // 用静态类持有容器,比全局变量更易控制 MyAppContainer::set($container); } // 自定义一个极简的容器持有类 class MyAppContainer { private static $container; public static function set(Pimple\Container $container) { self::$container = $container; } public static function get(): Pimple\Container { return self::$container; } // 可选:封装get方法,直接获取服务,减少重复代码 public static function service(string $id) { return self::$container[$id]; } }
然后在任意Drupal函数里就可以这样用:
function my_drupal_user_function($uid) { // 直接获取容器里的服务 $userService = MyAppContainer::service('user_processing_service'); return $userService->handleUserLogic($uid); }
测试时的优势:你可以在测试的setUp阶段替换容器里的模拟对象,完全隔离依赖:
public function testMyDrupalUserFunction() { // 用Prophecy创建mock服务 $mockUserService = Prophecy::prophesize(UserProcessingService::class)->reveal(); $mockUserService->handleUserLogic(123)->willReturn('mocked_result'); // 临时替换容器里的服务 $testContainer = new Pimple\Container(); $testContainer['user_processing_service'] = fn() => $mockUserService; MyAppContainer::set($testContainer); // 调用函数并断言结果 $result = my_drupal_user_function(123); $this->assertEquals('mocked_result', $result); }
方案2:逐步重构,让函数成为服务的“薄入口”
如果不想让函数直接依赖容器,更优雅的方式是把函数里的核心逻辑迁移到服务类,只留函数作为调用服务的入口层:
比如原来的函数是这样的:
function old_drupal_node_save_function($node_data) { // 一堆复杂的业务逻辑,混杂Drupal API调用 $processed_data = do_complex_processing($node_data); node_save($processed_data); send_notification($node_data['uid']); }
重构后:
function old_drupal_node_save_function($node_data) { // 函数只做一件事:获取服务并调用方法 $nodeService = MyAppContainer::service('node_management_service'); return $nodeService->saveAndNotify($node_data); } // 对应的服务类(完全可通过构造函数注入依赖) class NodeManagementService { private $notificationService; public function __construct(NotificationService $notificationService) { $this->notificationService = $notificationService; } public function saveAndNotify(array $node_data) { $processed_data = $this->processNodeData($node_data); node_save($processed_data); // 保留必要的Drupal API调用 $this->notificationService->sendToUser($node_data['uid']); } private function processNodeData(array $data) { // 原来的复杂逻辑移到这里,可单独测试 return $data; } }
这种方式的好处是:
- 核心业务逻辑完全在可测试的类里,函数只是无逻辑的入口
- 可以逐步重构,不用一次性改完所有函数
- 保持了DI容器的规范使用,避免函数里到处取服务
方案3:解决PHP-DI的自动注入问题(如果你想换回它)
你之前说PHP-DI初次尝试没成功解析依赖,大概率是初始化或配置的问题。试试在Drupal初始化时正确构建容器:
function mymodule_init() { $builder = new DI\ContainerBuilder(); // 加载DI配置文件(可以是数组或PHP文件) $builder->addDefinitions(__DIR__ . '/di-config.php'); // 构建容器 $container = $builder->build(); MyAppContainer::set($container); }
di-config.php里的配置示例:
return [ MyService::class => DI\autowire(), Dependency::class => DI\create(), // 如果需要依赖Drupal的全局对象,可以用工厂方法 'drupal_database' => DI\factory(fn() => Database::getConnection()), ];
这样你就可以用MyAppContainer::get()->get(MyService::class)来获取自动注入的实例,类型更安全。
关键注意事项
- 不要把容器当成“万能工具”滥用:尽量让类通过构造函数注入依赖,函数只作为入口获取服务
- 封装Drupal核心依赖:比如把
node_load()、user_load()这类函数封装成服务,注入到业务类里,方便测试时mock - 避免在函数里直接实例化类:所有实例化都交给容器,保持依赖的可控性
内容的提问来源于stack exchange,提问作者artfulrobot
相关产品推荐
相关产品推荐

