返回

Zend Framework 2 (ZF2) 表单与模型解耦最佳实践

php

ZF2 表单中创建模型实例 (像 ZF1 一样?)

在使用 Zend Framework 1 (ZF1) 时, 你可能习惯了在表单类中直接创建模型实例并访问其属性。就像文章开头的那段 ZF1 代码展示的,可以直接new一个Model,然后直接调用它的方法。 ZF2 里这么做就不行了, 确实让人头疼。别担心,咱们来好好看看,怎么在 ZF2 里搞定类似的操作。

为什么 ZF2 不一样?

根本原因在于 ZF1 和 ZF2 在架构设计上的差异。ZF1 倾向于更紧密的耦合,你可以很方便地在任何地方创建和使用各种对象。ZF2 更强调依赖注入和服务定位器(Service Locator)的概念,目的是降低组件之间的耦合度,提高可测试性和可维护性。

在 ZF2 里,直接在 Form 类里面 new 一个 Model 对象不是一个好做法。 这样做, Form 就与特定的 Model 紧密绑在一起了。万一以后你想换个 Model,或者这个 Form 需要用在不同的场景下,处理不同的 Model,改起来就麻烦了。

ZF2 的几种解决办法

咱们有几个办法可以解决这个问题,让 Form 和 Model 解耦,同时还能方便地访问 Model 数据:

1. 使用 Service Manager 获取模型实例

这是推荐的做法。 ZF2 的 Service Manager 负责管理应用程序中的各种服务(包括你的模型)。你应该把模型注册到 Service Manager,然后在需要的时候通过 Service Manager 来获取模型的实例。

原理: Service Manager 就像一个“对象工厂”,你可以事先告诉它怎么创建各种对象(比如你的模型),然后在需要的时候,让它帮你创建,而不是你自己直接 new

步骤:

  1. 注册模型到 Service Manager (通常在 Module.php 中):

    //  Module.php (比如:Application\Module.php)
    
    namespace Application;
    
    use Zend\ModuleManager\Feature\ConfigProviderInterface;
    use Application\Model\DbTable\DrydepotModel;
    use Zend\Db\ResultSet\ResultSet;
    use Zend\Db\TableGateway\TableGateway;
    
    class Module implements ConfigProviderInterface
    {
        // ...其他方法...
    
        public function getServiceConfig()
        {
            return [
                'factories' => [
                    //用闭包来生产TableGateway.
                    DrydepotModel::class => function ($container) {
                         $tableGateway = $container->get(DrydepotTableGateway::class);
                         return new DrydepotModel($tableGateway);
                    },
                    DrydepotTableGateway::class => function ($container){
                      $dbAdapter = $container->get(\Zend\Db\Adapter\Adapter::class);
                       $resultSetPrototype = new ResultSet();
                       //$resultSetPrototype->setArrayObjectPrototype( new DrydepotModel());  // 假设你有对应的实体类。直接用数组也可以。
                       return new TableGateway('drydepot', $dbAdapter, null, $resultSetPrototype);
                    },
                ],
            ];
        }
          // ...其他方法...
    }
    

    在这个配置里,当从ServiceManager里请求DrydepotModel::class服务的时候,ServiceManager会按照闭包定义的创建方式,构建该对象返回。这样就可以在整个应用获取这个Model的实例了。这里演示了如何使用 TableGateway, 这是一个好习惯。

  2. 在表单中通过 Service Manager 获取模型实例:
    你可以在 Controller 里通过 $this->getServiceLocator()->get(DrydepotModel::class) 取出模型。然后把它传递给Form.

    // 你的控制器中 (例如:SomeController.php)
    
    use Application\Form\DrydepotForm;
    use Application\Model\DbTable\DrydepotModel; //确保引用的正确.
    
    public function someAction()
    {
        $drydepotModel = $this->getServiceLocator()->get(DrydepotModel::class);
        $form = new DrydepotForm($drydepotModel); // 通过构造函数传入
        // 或 $form->setModel($drydepotModel);  通过setter方法传入.
    
        // ...其他代码...
        return [ 'form'=>$form];
    }
    
  3. 在你的表单中使用这个Model:

    //  DrydepotForm.php (你的表单类)
    
    namespace Application\Form;
    
    use Zend\Form\Form;
    use Application\Model\DbTable\DrydepotModel;
    
    class DrydepotForm extends Form
    {
        protected $drydepotModel;
    
        public function __construct(DrydepotModel $drydepotModel, $name = null, $options = [])
        {
            parent::__construct($name, $options);
            $this->drydepotModel = $drydepotModel;
            $this->addElements(); //建议把添加元素的步骤都统一放在一个函数里.
        }
    
       /* //另外一种实现方法,不用构造函数。
        public function setModel(DrydepotModel $drydepotModel)
        {
           $this->drydepotModel = $drydepotModel;
        }
       */
    
        public function addElements()
        {
            $list = $this->drydepotModel->formationSelect();
    		//array_unshift 这方法容易出问题, 不建议用。最好是用 array + array 方式。
            $defaultOption = ['' => '--Please Select--'];
            $list = $defaultOption + $list;  //将$defaultOption 放到$list数组头部
    
            $id = new \Zend\Form\Element\Hidden('id');
            $id->setAttributes([ //统一用setAttributes.
                'id' => 'id'
            ]);
    
    
            $formation = new \Zend\Form\Element\Select('formation_id');
            $formation->setLabel('Formation Name')
                      ->setAttributes([
                         'id' => 'formation',
                         'class' => 'required',
                      ])
                      ->setValueOptions($list);  // setValueOptions 代替 setMultiOptions
             //  强烈建议不要 addValidator,  改用InputFilter去验证数据。        
    
            $this->add($id);
            $this->add($formation);
    
        }
    }
    

2. 使用 InputFilter (推荐配合第一种方法一起用)

ZF2 的 InputFilter 专门用于验证和过滤表单数据。你可以把数据验证逻辑从表单类中移到 InputFilter 中。

原理: InputFilter 负责定义表单中每个字段的验证规则、过滤器等。这样做可以让表单类更简洁,只负责定义表单元素,而不用管具体的验证逻辑。

代码示例 (配合方法 1):
通常在Form下建一个XXXFilter类专门负责这个表单的数据验证。

// DrydepotFilter.php (单独的文件,跟你的Form放在一起.)

namespace Application\Form;

use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Input;
use Zend\Validator;  //需要引入.
use Zend\Filter;

class DrydepotFilter extends InputFilter
{
     public function __construct()
    {

        $id = new Input('id');
        $id->getFilterChain()->attach(new Filter\ToInt());  //转成整数
        $id->setRequired(false); //根据需求, id 一般是自动生成的. 可选.
        $this->add($id);
        
        $formation = new Input('formation_id');
        $formation->setRequired(true); //根据业务调整
        $formation->getValidatorChain()->attach(new Validator\NotEmpty()); //确认不为空.
         //可以添加更多的验证, 比如确保这个formation_id 真实存在。
        $this->add($formation);

     }
}

//修改DrydepotForm.php。把Filter绑定进去
 class DrydepotForm extends Form
    {
        protected $drydepotModel;
        protected $inputFilter;
          public function __construct(DrydepotModel $drydepotModel, $name = null, $options = [])
        {
            parent::__construct($name, $options);
            $this->drydepotModel = $drydepotModel;
            $this->inputFilter = new DrydepotFilter();  //直接初始化Filter
            $this->addElements();
        }
        //......
    }

//然后在 Controller 里使用:

 public function someAction()
    {
        $drydepotModel = $this->getServiceLocator()->get(DrydepotModel::class);
        $form = new DrydepotForm($drydepotModel);

         $request = $this->getRequest();
        if ($request->isPost()) {
                $form->setInputFilter($form->getInputFilter()); // 把 input filter 设置给 form
               $form->setData($request->getPost());

               if ($form->isValid()) {  //数据通过了InputFilter 验证!
                        $data =  $form->getData(); //拿到的 $data 就是干净的数据了.
                        // 进一步处理数据, 保存到数据库等...
                } else{
                   //处理验证没通过的情况. 重新显示表单和错误提示
                }
         }
        // ...其他代码...
        return [ 'form'=>$form];
    }

3. 使用 Hydrator

Hydrator 用于在对象和数组之间进行数据转换。你可以用它来把表单数据填充到模型对象中,或者反过来,把模型对象的数据填充到表单中。 ZF2 默认提供了几种 Hydrator,你可以根据需要选择使用。

原理: Hydrator 就像一个“数据转换器”,可以把数组数据转换成对象属性,或者把对象属性转换成数组数据。

这里不再提供详细的代码,篇幅有限,主要因为前两个方案就够用了。并且推荐先用ServiceManager和InputFilter。

额外安全建议

  • 始终验证和过滤输入数据: 不要相信任何来自用户的数据,必须进行验证和过滤,防止安全漏洞。InputFilter 可以在这里帮大忙!
  • 防御XSS和CSRF攻击 在输出用户提交的数据到页面的时候记得转义,并且使用FormElement的Csrf()。

小结

记住, ZF2 的核心思想是解耦和依赖注入。 不要在 Form 里直接 new Model! 通过 Service Manager 获取模型实例,并配合 InputFilter 进行数据验证,是更规范、更安全、也更灵活的做法。