标签:
So far all we have done is read data from the database. In a real-life application, this won‘t get us very far, as we‘ll often need to support the full range of fullCreate
, Read
, Update
and Delete
operations (CRUD). Typically, new data will arrive via web form submissions.
The zend-form and zend-inputfilter components provide us with the ability to create fully-featured forms and their validation rules. zend-form consumes zend-inputfilter internally, so let‘s take a look at the elements of zend-form that we will use for our application.
Zend\Form\Fieldset
models a reusable set of elements. You will use a Fieldset
to create the various HTML inputs needed to map to your server-side entities. It is considered good practice to have one Fieldset
for every entity in your application.
The Fieldset
component, however, is not a form, meaning you will not be able to use a Fieldset
without attaching it to the Zend\Form\Form
instance. The advantage here is that you have one set of elements that you can re-use for as many forms as you like.
Zend\Form\Form
is a container for all elements of your HTML <form>
. You are able to add both single elements or fieldsets (modeled as Zend\Form\Fieldset
instances).
Explaining how zend-form works is best done by giving you real code to work with. So let‘s jump right into it and create all the forms we need to finish ourBlog
module. We start by creating a Fieldset
that contains all the input elements that we need to work with our blog data:
id
property, which is only needed for editting and deleting data.title
property.text
property.Create the file module/Blog/src/Form/PostFieldset.php
with the following contents:
<?php
namespace Blog\Form;
use Zend\Form\Fieldset;
class PostFieldset extends Fieldset
{
public function init()
{
$this->add([
‘type‘ => ‘hidden‘,
‘name‘ => ‘id‘,
]);
$this->add([
‘type‘ => ‘text‘,
‘name‘ => ‘title‘,
‘options‘ => [
‘label‘ => ‘Post Title‘,
],
]);
$this->add([
‘type‘ => ‘textarea‘,
‘name‘ => ‘text‘,
‘options‘ => [
‘label‘ => ‘Post Text‘,
],
]);
}
}
This new class creates an extension of Zend\Form\Fieldset
that, in an init()
method (more on this later), adds elements for each aspect of our blog post. We can now re-use this fieldset in as many forms as we want. Let‘s create our first form.
Now that we have our PostFieldset
in place, we can use it inside a Form
. The form will use the PostFieldset
, and also include a submit button so that the user can submit the data.
Create the file module/Blog/src/Form/PostForm.php
with the following contents:
<?php
namespace Blog\Form;
use Zend\Form\Form;
class PostForm extends Form
{
public function init()
{
$this->add([
‘name‘ => ‘post‘,
‘type‘ => PostFieldset::class,
]);
$this->add([
‘type‘ => ‘submit‘,
‘name‘ => ‘submit‘,
‘attributes‘ => [
‘value‘ => ‘Insert new Post‘,
],
]);
}
}
And that‘s our form. Nothing special here, we add our PostFieldset
to the form, we add a submit button to the form, and nothing more.
Now that we have the PostForm
written, it‘s time to use it. But there are a few more tasks left:
WriteController
which accepts the following instances via its constructor:PostCommandInterface
instancePostForm
instanceaddAction()
method in the new WriteController
to handle displaying the form and processing it.blog/add
, that routes to the WriteController
and its addAction()
method.While we could re-use our existing controller, it has a different responsibility: it will be writing new blog posts. As such, it will need to emit commands, and thus use the PostCommandInterface
that we have defined previously.
To do that, it needs to accept and process user input, which we have modeled in our PostForm
in a previous section of this chapter.
Let‘s create this new class now. Open a new file,module/Blog/src/Controller/WriteController.php
, and add the following contents:
<?php
namespace Blog\Controller;
use Blog\Form\PostForm;
use Blog\Model\Post;
use Blog\Model\PostCommandInterface;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class WriteController extends AbstractActionController
{
/**
* @var PostCommandInterface
*/
private $command;
/**
* @var PostForm
*/
private $form;
/**
* @param PostCommandInterface $command
* @param PostForm $form
*/
public function __construct(PostCommandInterface $command, PostForm $form)
{
$this->command = $command;
$this->form = $form;
}
public function addAction()
{
}
}
We‘ll now create a factory for this new controller; create a new file,module/Blog/src/Factory/WriteControllerFactory.php
, with the following contents:
<?php
namespace Blog\Factory;
use Blog\Controller\WriteController;
use Blog\Form\PostForm;
use Blog\Model\PostCommandInterface;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class WriteControllerFactory implements FactoryInterface
{
/**
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return WriteController
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$formManager = $container->get(‘FormElementManager‘);
return new WriteController(
$container->get(PostCommandInterface::class),
$formManager->get(PostForm::class)
);
}
}
The above factory introduces something new: the FormElementManager
. This is a plugin manager implementation that is specifically for forms. We don‘t necessarily need to register our forms with it, as it will check to see if a requested instance is a form when attempting to pull one from it. However, it does provide a couple nice features:
init()
method, it invokes that method after instantiation. This is useful, as that way we‘re initializing after we have all our dependencies injected, such as input filters. Our form and fieldset define this method!Finally, we need to configure the new factory; inmodule/Blog/config/module.config.php
, add an entry in the controllers
configuration section:
‘controllers‘ => [
‘factories‘ => [
Controller\ListController::class => Factory\ListControllerFactory::class,
// Add the following line:
Controller\WriteController::class => Factory\WriteControllerFactory::class,
],
],
Now that we have the basics for our controller in place, we can create a route to it:
<?php
// In module/Blog/config/module.config.php:
namespace Blog;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
‘service_manager‘ => [ /* ... */ ],
‘controllers‘ => [ /* ... */ ],
‘router‘ => [
‘routes‘ => [
‘blog‘ => [
‘type‘ => ‘literal‘,
‘options‘ => [
‘route‘ => ‘/blog‘,
‘defaults‘ => [
‘controller‘ => Controller\ListController::class,
‘action‘ => ‘index‘,
]
],
‘may_terminate‘ => true,
‘child_routes‘ => [
‘detail‘ => [
‘type‘ => ‘segment‘,
‘options‘ => [
‘route‘ => ‘/:id‘,
‘defaults‘ => [
‘action‘ => ‘detail‘
],
‘constraints‘ => [
‘id‘ => ‘\d+‘
]
]
],
// Add the following route:
‘add‘ => [
‘type‘ => ‘literal‘,
‘options‘ => [
‘route‘ => ‘/add‘,
‘defaults‘ => [
‘controller‘ => Controller\WriteController::class,
‘action‘ => ‘add‘,
],
],
],
],
],
],
],
‘view_manager‘ => [ /* ... */ ],
];
Finally, we‘ll create a dummy template:
<!-- Filename: module/Blog/view/blog/write/add.phtml -->
<h1>WriteController::addAction()</h1>
If you try to access the new route localhost:8080/blog/add
you‘re supposed to see the following error message:
An error occurred
An error occurred during execution; please try again later.
Additional information:
Zend\ServiceManager\Exception\ServiceNotFoundException
File:
{projectPath}/vendor/zendframework/zend-servicemanager/src/ServiceManager.php:{lineNumber}
Message:
Unable to resolve service "Blog\Model\PostCommandInterface" to a factory; are you certain you provided it during configuration?
If this is not the case, be sure to follow the tutorial correctly and carefully check all your files.
The error is due to the fact that we have not yet defined an implementation of our PostCommandInterface
, much less wired the implementation into our application!
Let‘s create a dummy implementation, as we did when we first started working with repositories. Create the file module/Blog/src/Model/PostCommand.php
with the following contents:
<?php
namespace Blog\Model;
class PostCommand implements PostCommandInterface
{
/**
* {@inheritDoc}
*/
public function insertPost(Post $post)
{
}
/**
* {@inheritDoc}
*/
public function updatePost(Post $post)
{
}
/**
* {@inheritDoc}
*/
public function deletePost(Post $post)
{
}
}
Now add service configuration in module/Blog/config/module.config.php
:
‘service_manager‘ => [
‘aliases‘ => [
/* ... */
// Add the following line:
Model\PostCommandInterface::class => Model\PostCommand::class,
],
‘factories‘ => [
/* ... */
// Add the following line:
Model\PostCommand::class => InvokableFactory::class,
],
],
Reloading your application now will yield you the desired result.
Now that we have new controller working, it‘s time to pass this form to the view and render it. Change your controller so that the form is passed to the view:
// In /module/Blog/src/Controller/WriteController.php:
public function addAction()
{
return new ViewModel([
‘form‘ => $this->form,
]);
}
And then we need to modify our view to render the form:
<!-- Filename: module/Blog/view/blog/write/add.phtml -->
<h1>Add a blog post</h1>
<?php
$form = $this->form;
$form->setAttribute(‘action‘, $this->url());
$form->prepare();
echo $this->form()->openTag($form);
echo $this->formCollection($form);
echo $this->form()->closeTag();
The above does the following:
action
attribute of the form to the current URL.formCollection()
view helper; this is a convenience method with some typically sane default markup. We‘ll be changing it momentarily.Form method
HTML forms can be sent using
POST
andGET
. zend-form defaults toPOST
. If you want to switch toGET
:$form->setAttribute(‘method‘, ‘GET‘);
Refreshing the browser you will now see your form properly displayed. It‘s not pretty, though, as the default markup does not follow semantics for Bootstrap (which is used in the skeleton application by default). Let‘s update it a bit to make it look better; we‘ll do that in the view script itself, as markup-related concerns belong in the view layer:
<!-- Filename: module/Blog/view/blog/write/add.phtml -->
<h1>Add a blog post</h1>
<?php
$form = $this->form;
$form->setAttribute(‘action‘, $this->url());
$fieldset = $form->get(‘post‘);
$title = $fieldset->get(‘title‘);
$title->setAttribute(‘class‘, ‘form-control‘);
$title->setAttribute(‘placeholder‘, ‘Post title‘);
$text = $fieldset->get(‘text‘);
$text->setAttribute(‘class‘, ‘form-control‘);
$text->setAttribute(‘placeholder‘, ‘Post content‘);
$submit = $form->get(‘submit‘);
$submit->setAttribute(‘class‘, ‘btn btn-primary‘);
$form->prepare();
echo $this->form()->openTag($form);
?>
<fieldset>
<div class="form-group">
<?= $this->formLabel($title) ?>
<?= $this->formElement($title) ?>
<?= $this->formElementErrors()->render($title, [‘class‘ => ‘help-block‘]) ?>
</div>
<div class="form-group">
<?= $this->formLabel($text) ?>
<?= $this->formElement($text) ?>
<?=