导航
开始使用 Laminas
表单和操作
添加一个新Album
我们现在可以编写添加新Album的功能了。这里将会有两部分去完成:
- 为用户显示一个提交信息的表单。
- 提交表单信息并且储存到数据库中。
我们将使用 laminas-form 来完成这个工作。laminas-form 管理表单的输入信息,同时也负责表单数据
的验证,验证工作将会由 zend-inputfilter 组件来完成。我们将创建一个集成自
 Laminas\Form\Form 的类 Album\Form\AlbumForm。在文件 
 module/Album/src/Form/AlbumForm.php 内添加如下信息:
namespace Album\Form;
use Laminas\Form\Form;
class AlbumForm extends Form
{
    public function __construct($name = null)
    {
        // We will ignore the name provided to the constructor
        parent::__construct('album');
        $this->add([
            'name' => 'id',
            'type' => 'hidden',
        ]);
        $this->add([
            'name' => 'title',
            'type' => 'text',
            'options' => [
                'label' => 'Title',
            ],
        ]);
        $this->add([
            'name' => 'artist',
            'type' => 'text',
            'options' => [
                'label' => 'Artist',
            ],
        ]);
        $this->add([
            'name' => 'submit',
            'type' => 'submit',
            'attributes' => [
                'value' => 'Go',
                'id'    => 'submitbutton',
            ],
        ]);
    }
}在 AlbumForm 的构造函数内我们将会完成几个步骤。首先,我们将会调用父类的构造函数,
并且设置一个表单名称。接下来我们创建四个表单元素:id,title,artist,以及一个submit
按钮。每项我们都将设置属性和选项,包括显示的标签。
表单方法
HTML表单提供
POST和GET的方式,laminas-form 默认使用POST; 因此我们不必要明确的制定其属性。 如果您想要制定GET方式,我们需要在构造函数中明确的制定:$this->setAttribute('method', 'GET');
我们同时需要为当前表单设置验证方法,参见 laminas-inputfilter
为输入框提供的通用验证机制。同样也提供了一个接口 InputFilterAwareInterface,
为了给表单绑定输入验证 zend-form 将需要这个接口的实现。 我们将在 Album 类中添加。
// module/Album/src/Model/Album.php:
namespace Album\Model;
// Add the following import statements:
use DomainException;
use Laminas\Filter\StringTrim;
use Laminas\Filter\StripTags;
use Laminas\Filter\ToInt;
use Laminas\InputFilter\InputFilter;
use Laminas\InputFilter\InputFilterAwareInterface;
use Laminas\InputFilter\InputFilterInterface;
use Laminas\Validator\StringLength;
class Album implements InputFilterAwareInterface
{
    public $id;
    public $artist;
    public $title;
    // Add this property:
    private $inputFilter;
    public function exchangeArray(array $data)
    {
        $this->id     = !empty($data['id']) ? $data['id'] : null;
        $this->artist = !empty($data['artist']) ? $data['artist'] : null;
        $this->title  = !empty($data['title']) ? $data['title'] : null;
    }
    /* Add the following methods: */
    public function setInputFilter(InputFilterInterface $inputFilter)
    {
        throw new DomainException(sprintf(
            '%s does not allow injection of an alternate input filter',
            __CLASS__
        ));
    }
    public function getInputFilter()
    {
        if ($this->inputFilter) {
            return $this->inputFilter;
        }
        $inputFilter = new InputFilter();
        $inputFilter->add([
            'name' => 'id',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],
            ],
        ]);
        $inputFilter->add([
            'name' => 'artist',
            'required' => true,
            'filters' => [
                ['name' => StripTags::class],
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 1,
                        'max' => 100,
                    ],
                ],
            ],
        ]);
        $inputFilter->add([
            'name' => 'title',
            'required' => true,
            'filters' => [
                ['name' => StripTags::class],
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 1,
                        'max' => 100,
                    ],
                ],
            ],
        ]);
        $this->inputFilter = $inputFilter;
        return $this->inputFilter;
    }
}InputFilterAwareInterface 顶一个 setInputFilter() 和 getInputFilter()
两个方法。我们只需要时实现 getInputFilter(),所以我们为 setInputFilter()
提供了一个异常抛出。
在 getInputFilter() 内,我们使用 InputFilter 来添加我们需要的输入验证。
我们依次为每个表单项添加希望验证的属性信息。id 项添加一个 int 验证,以确保
只接收整数。对于文本元素,万能添加两个验证信息, StripTags 以及 StringTrim 
删除不需要的 HTML 以及必须要的空格。同时需要 required 以及 StringLength 
验证确保用户输入指定范围内的字符数,以便存入数据库。
现在我们需要获取表单并显示然后处理提交的信息。这些都将在 AlbumController::addAction()
内完成:
// module/Album/src/Controller/AlbumController.php:
// Add the following import statements at the top of the file:
use Album\Form\AlbumForm;
use Album\Model\Album;
class AlbumController extends AbstractActionController
{
    /* ... */
    /* Update the following method to read as follows: */
    public function addAction()
    {
        $form = new AlbumForm();
        $form->get('submit')->setValue('Add');
        $request = $this->getRequest();
        if (! $request->isPost()) {
            return ['form' => $form];
        }
        $album = new Album();
        $form->setInputFilter($album->getInputFilter());
        $form->setData($request->getPost());
        if (! $form->isValid()) {
            return ['form' => $form];
        }
        $album->exchangeArray($form->getData());
        $this->table->saveAlbum($album);
        return $this->redirect()->toRoute('album');
    }
    /* ... */
}向 Album 以及 AlbumForm 类中添加信息后,我们回到 addAction()。让我们详细的
查看 addAction() 中的代码:
$form = new AlbumForm();
$form->get('submit')->setValue('Add');我们实例化了 AlbumForm 并且设置 submit 按钮的值为 "Add"。我们这样做是为了在编辑专辑
的时候可以重用表单的配置信息,以便可以设置不同的标签。
$request = $this->getRequest();
if (! $request->isPost()) {
    return ['form' => $form];
}如果非 `POST 请求,表单值将不会被提交,而是展示一个表单,laminas-mvc允许我们在需要的时候 返回一个数组以替代视图模型,如果你这样做它将会使用这个数组自动创建一个视图模型:
$album = new Album();
$form->setInputFilter($album->getInputFilter());
$form->setData($request->getPost());此时,我们获取一个提交的表单,我们将创建一个 Album 实例,并且通过其来进行表单验证,
另外我们也通过其来向表单中传递请求的实例。
if (! $form->isValid()) {
    return ['form' => $form];
}如果验证失败,我们重新展示表单。此时的表单组件将会指出验证失败项,以及为什么验证失败, 这些信息都将被传递给视图。
$album->exchangeArray($form->getData());
$this->table->saveAlbum($album);如果表单验证通过,我们将从表单中抓取数据并且使用模型的 saveAlbum() 方法将数据储存到
数据库中。
return $this->redirect()->toRoute('album');一旦我们保存了新的专辑行,我们将使用控制器插件 Redirect 跳转到专辑列表页。
现在我需要在 add.phtml 视图脚本中渲染表单:
<?php
// module/Album/view/album/album/add.phtml:
$title = 'Add new album';
$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>
<?php
$form->setAttribute('action', $this->url('album', ['action' => 'add']));
$form->prepare();
echo $this->form()->openTag($form);
echo $this->formHidden($form->get('id'));
echo $this->formRow($form->get('title'));
echo $this->formRow($form->get('artist'));
echo $this->formSubmit($form->get('submit'));
echo $this->form()->closeTag();首先我们显示一个标题,接下来开始渲染表单。zend-form 提供了一些视图助手使得这项工作变得
异常轻松。form() 视图助手包含 openTag() 以及 closeTag() 两个方法,用来数据表单
头和尾。每个项都具有一个说明标签,我们使用 formRow()  来渲染这些标签、输入框以及验证
失败后的错误信息。另有隐藏以及提交这两个独立的没有验证信息的项,我们使用 formHidden() 
以及 formSubmit() 来渲染。
另外,渲染视表单的过程中我们可以直接只用绑定的 formCollection 视图脚本。例如,上面
所有的表单项渲染都可以使用下面代码代替:
echo $this->formCollection($form);这边遍历所有的表单元素,调用适当的标签,元素以及每项中中的错误信息,但是 formCollection($form)
中仍然不包括表单的头尾信息。这将帮助您在接受默认HTML呈现方式的情况下减少视图脚本的复杂性。
您现在可以使用首页的 "Add new album" 链接,去添加一个新专辑,显示情况如下:

这看起来不是好!我们可以使用了骨架中 Bootstrap 这个 CSS 框架,用专门的标记信息用来 呈现表单!我们可以使用下面的方法来完善我们的视图脚本:
- 为元素添加标记信息.
- 分别渲染 labels, elements, 以及错误信息.
- 为元素添加属性.
按照如下方式更新 add.phtml 视图脚本:
<?php
$title = 'Add new album';
$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>
<?php
// This provides a default CSS class and placeholder text for the title element:
$album = $form->get('title');
$album->setAttribute('class', 'form-control');
$album->setAttribute('placeholder', 'Album title');
// This provides a default CSS class and placeholder text for the artist element:
$artist = $form->get('artist');
$artist->setAttribute('class', 'form-control');
$artist->setAttribute('placeholder', 'Artist');
// This provides CSS classes for the submit button:
$submit = $form->get('submit');
$submit->setAttribute('class', 'btn btn-primary');
$form->setAttribute('action', $this->url('album', ['action' => 'add']));
$form->prepare();
echo $this->form()->openTag($form);
?>
<?php // Wrap the elements in divs marked as form groups, and render the
      // label, element, and errors separately within ?>
<div class="form-group">
    <?= $this->formLabel($album) ?>
    <?= $this->formElement($album) ?>
    <?= $this->formElementErrors()->render($album, ['class' => 'help-block']) ?>
</div>
<div class="form-group">
    <?= $this->formLabel($artist) ?>
    <?= $this->formElement($artist) ?>
    <?= $this->formElementErrors()->render($artist, ['class' => 'help-block']) ?>
</div>
<?php
echo $this->formSubmit($submit);
echo $this->formHidden($form->get('id'));
echo $this->form()->closeTag();结果将会如下呈现:

以上是为了展示表单渲染中一些默认的特征以及一些自定义的方式。你可以为您的站点设置 任何想要的标记信息。
编辑专辑
编辑专辑有点类似于添加一个,代码也是比较接近的,现在我们使用 AlbumController 中的
editAction() :
// module/Album/src/Controller/AlbumController.php:
// ...
    public function editAction()
    {
        $id = (int) $this->params()->fromRoute('id', 0);
        if (0 === $id) {
            return $this->redirect()->toRoute('album', ['action' => 'add']);
        }
        // Retrieve the album with the specified id. Doing so raises
        // an exception if the album is not found, which should result
        // in redirecting to the landing page.
        try {
            $album = $this->table->getAlbum($id);
        } catch (\Exception $e) {
            return $this->redirect()->toRoute('album', ['action' => 'index']);
        }
        $form = new AlbumForm();
        $form->bind($album);
        $form->get('submit')->setAttribute('value', 'Edit');
        $request = $this->getRequest();
        $viewData = ['id' => $id, 'form' => $form];
        if (! $request->isPost()) {
            return $viewData;
        }
        $form->setInputFilter($album->getInputFilter());
        $form->setData($request->getPost());
        if (! $form->isValid()) {
            return $viewData;
        }
        $this->table->saveAlbum($album);
        // Redirect to album list
        return $this->redirect()->toRoute('album', ['action' => 'index']);
    }这段代码看起来如此熟悉。接下来让我们看看和添加专辑的不同之处。首先, id 是从路由中
匹配待的并且使用其来加载专辑并且编辑:
$id = (int) $this->params()->fromRoute('id', 0);
if (0 === $id) {
    return $this->redirect()->toRoute('album', ['action' => 'add']);
}
// Retrieve the album with the specified id. Doing so raises
// an exception if the album is not found, which should result
// in redirecting to the landing page.
try {
    $album = $this->table->getAlbum($id);
} catch (\Exception $e) {
    return $this->redirect()->toRoute('album', ['action' => 'index']);
}params 是一个控制器插件,可以很方便的从路由中取出匹配值。我们使用它取出从我们创建的
Album 模块中 module.config.php 文件里的 id 值。如果 id 为0,将会跳转到添加操作中,
否则,我们将从数据库中获取 album 实体。
我们必须确保对应 id 的专辑能被找到。如果不能方法将会抛出一个异常。我们可以捕获异常
并且将用户重定向到首页。
$form = new AlbumForm();
$form->bind($album);
$form->get('submit')->setAttribute('value', 'Edit');表单的 bind() 方法将模型赋值给表单。这将会有两种用途:
- 当现实表单的时候,可以为每个元素绑定初始值。
- 通过 isValid()验证通过后, 表单数据将会被传递会模型。
这些操作都是使用 hydrator 对象类完成的。有几种 hydrators,默认使用的是 
Zend\Hydrator\ArraySerializable,它会事先在模型中查找 getArrayCopy() 
以及 exchangeArray() 这两个方法。我们已经在 Album 实体中添加了 exchangeArray() 
现在我们需要去添加 getArrayCopy():
// module/Album/src/Model/Album.php:
// ...
    public function exchangeArray($data)
    {
        $this->id     = isset($data['id']) ? $data['id'] : null;
        $this->artist = isset($data['artist']) ? $data['artist'] : null;
        $this->title  = isset($data['title']) ? $data['title'] : null;
    }
    // Add the following method:
    public function getArrayCopy()
    {
        return [
            'id'     => $this->id,
            'artist' => $this->artist,
            'title'  => $this->title,
        ];
    }
// ...由于使用 bind()来实现 hydrator,我们不需要自己填充表单数据到 $album 中,就可以直接
调用 saveAlbum() 方法来将数据储存到数据库中。
视图模板 edit.phtml 同样和添加表单的视图类似:
<?php
// module/Album/view/album/album/edit.phtml:
$title = 'Edit album';
$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>
<?php
$album = $form->get('title');
$album->setAttribute('class', 'form-control');
$album->setAttribute('placeholder', 'Album title');
$artist = $form->get('artist');
$artist->setAttribute('class', 'form-control');
$artist->setAttribute('placeholder', 'Artist');
$submit = $form->get('submit');
$submit->setAttribute('class', 'btn btn-primary');
$form->setAttribute('action', $this->url('album', [
    'action' => 'edit',
    'id'     => $id,
]));
$form->prepare();
echo $this->form()->openTag($form);
?>
<div class="form-group">
    <?= $this->formLabel($album) ?>
    <?= $this->formElement($album) ?>
    <?= $this->formElementErrors()->render($album, ['class' => 'help-block']) ?>
</div>
<div class="form-group">
    <?= $this->formLabel($artist) ?>
    <?= $this->formElement($artist) ?>
    <?= $this->formElementErrors()->render($artist, ['class' => 'help-block']) ?>
</div>
<?php
echo $this->formSubmit($submit);
echo $this->formHidden($form->get('id'));
echo $this->form()->closeTag();唯一的改变就是我们使用了 Edit Album 的标题,并且设置了表单的 action 到 'edit' 方法上。.
现在您就可以去编辑专辑了。
删除一个 album
为了完善我们的专辑应用,我们需要去添加删除操作,我们已经在每个专辑列表的后天添加了删除 链接。实际情况应该是我们点击链接就会去删除专辑,但是这样容易出现误操作。请记住我们的 HTTP 规范,我们不应该使用GET方法来实现一个不可逆的操作,应该使用 POST 方法来代替。 我们最好在点击删除链接后显示一个确认的表单,如果我们点击 "yes",我们将会删除信息。 由于我们的表单很简单,我们将直接将代码写到我们的视图中(zend-form可以作为一个可选项!)。
让我们开始在 AlbumController::deleteAction() 中编写代码:
// module/Album/src/Controller/AlbumController.php:
//...
    // Add content to the following method:
    public function deleteAction()
    {
        $id = (int) $this->params()->fromRoute('id', 0);
        if (!$id) {
            return $this->redirect()->toRoute('album');
        }
        $request = $this->getRequest();
        if ($request->isPost()) {
            $del = $request->getPost('del', 'No');
            if ($del == 'Yes') {
                $id = (int) $request->getPost('id');
                $this->table->deleteAlbum($id);
            }
            // Redirect to list of albums
            return $this->redirect()->toRoute('album');
        }
        return [
            'id'    => $id,
            'album' => $this->table->getAlbum($id),
        ];
    }
//...首先,我们从路由中获取 id,并且检查请求对象是够 isPost(),以确保我们显示确认页还是进行
删除操作。我们使用表单对象的 deleteAlbum() 方法来删除专辑,并且在删除后跳转到专辑列表页
如果非 POST 请求,我们根据 id 从数据库中取回专辑信息,并在视图中显示。
视图脚本是一个非常简单的表单:
<?php
// module/Album/view/album/album/delete.phtml:
$title = 'Delete album';
$url   = $this->url('album', ['action' => 'delete', 'id' => $id]);
$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>
<p>
    Are you sure that you want to delete
    "<?= $this->escapeHtml($album->title) ?>" by
    "<?= $this->escapeHtml($album->artist) ?>"?
</p>
<form action="<?= $url ?>" method="post">
<div class="form-group">
    <input type="hidden" name="id" value="<?= (int) $album->id ?>" />
    <input type="submit" class="btn btn-danger" name="del" value="Yes" />
    <input type="submit" class="btn btn-success" name="del" value="No" />
</div>
</form>在视图脚本中我们为用户显示一个确认信息的表单,并且提供 "Yes" 和 "No" 两个按钮, 在操作方法中,我们检查是否为 "Yes",如果是则执行删除操作。
Ensuring that the home page displays the list of albums
确保首页显示 albums 列表
最后一点。当前主页 http://laminas-mvc-tutorial.localhost/ 不会显示专辑列表。
这是由于我们在模块的 module.config.php 文件中设置了 Application 为主页。
打开 module/Application/config/module.config.php 找到首页路由:
'home' => [
    'type' => \Laminas\Router\Http\Literal::class,
    'options' => [
        'route'    => '/',
        'defaults' => [
            'controller' => Controller\IndexController::class,
            'action'     => 'index',
        ],
    ],
],在文件顶部引入 Album\Controller\AlbumController:
use Album\Controller\AlbumController;and change the controller from Controller\IndexController::class to AlbumController::class:
'home' => [
    'type' => \Laminas\Router\Http\Literal::class,
    'options' => [
        'route'    => '/',
        'defaults' => [
            'controller' => AlbumController::class, // <-- change here
            'action'     => 'index',
        ],
    ],
],现在,我们拥有了一个完整的可正常工作的应用了!
发现错误或者想为此文档做贡献? 来 GitHub 编辑!