This is the second of three parts of an article devoted to the development of a simple application using Zend Framework 2.
In the first part I reviewed the structure of ZendSkeletonApplication, and in this part I will give an example of developing a simple module.
The third part will focus on working with users and the Twig template engine.
Installation and configuration of additional modules
First of all I want to note that the installation of a third-party module in the Zend Framework usually consists of about these four steps:
- add the appropriate line to composer.json to inform Composer about the new module,
- execute the php composer.phar update command so that Composer loads the new module and, if necessary, regenerates the autoload files,
- add a new module to the modules list in the config / application.config.php file,
- if necessary, place the module configuration file (usually an example of such a file is located in the module config folder) in config / autoload and make the necessary edits.
Also, I want to emphasize that for all the modules listed below I set the minimum necessary settings for their work, for more information about the settings and the capabilities of each of the modules, see their documentation pages.
Let's start by installing the simple but useful Zend Developer Tools module.
Zend Developer Tools
Zend Developer Tools is a handy toolbar containing useful information for the developer about the created page: the number and list of database requests, the list of current user roles used by the Entity, the loaded site configuration, etc. Of course, the toolbar can be extended with any other supporting information. You can find it here:
github.com/zendframework/ZendDeveloperTools .
')
To install the toolbar, first add the line:
"zendframework/zend-developer-tools": "dev-master",
in the
composer.json file in the project root and then run the
php composer.phar update command in the project root.
Then in the modules
/ application.config.php file you need to add the ZendDeveloperTools element to the modules array:
'modules' => array( 'Application', 'ZendDeveloperTools', ),
Now it remains to copy the file
vendor / zendframework / zend-developer-tools / config / zenddevelopertools.local.php.dist into our project’s
config / autoload folder and rename it, for example, to
zenddevelopertools.local.php (part of the name to local.php by to a large account does not matter).
Everything, now, by default, at the bottom of all pages displays information about resources spent on page generation, project configuration, etc.
I want to draw attention to the fact that by default the toolbar will be available to all site visitors, so you should not use it in the production environment.
The current version of the application is available on the github in the project repository with the
zenddevelopertools tag:
github.com/romka/zend-blog-example/tree/zenddevelopertoolsDoctrine orm
DoctrineModule and DoctrineORMModule modules (
https://github.com/doctrine/DoctrineModule and
github.com/doctrine/DoctrineORMModule ) will be needed to integrate with the Doctrine.
Add the following lines to the require section of the composer.json file:
"doctrine/common": ">=2.1", "doctrine/doctrine-orm-module": "0.7.*"
and run the
php command
composer.phar update in the console.
The DoctrineModule module can be omitted explicitly in our
composer.json , since this dependency is registered at the level of the DoctrineORMModule module.
Now, in the
config / autoload directory, place the
doctrine.local.php file with the database access parameters to be used by the Doctrine, its contents should be approximately like this:
<?php return array( 'doctrine' => array( 'connection' => array( 'orm_default' => array( 'driverClass' =>'Doctrine\DBAL\Driver\PDOMySql\Driver', 'params' => array( 'host' => 'localhost', 'port' => '3306', 'user' => 'username', 'password' => 'pass', 'dbname' => 'dbname', ) ) ), ), );
Now if we reload the page of our site, then at the bottom of the page in the Zend devloper toolbar we will see two new blocks showing the number of completed queries and the list of mappings to the database. Both values ​​are zero, since we have not done the mapping yet and, as a result, there are no queries to the database.
In this tutorial, I want to develop a simple blog and now it’s time to write the first lines of the new module code.
MyBlog module
In the
modules directory, create the following directories and files:
MyBlog/ config/ module.config.php src/ MyBlog/ Entity/ BlogPost.php Module.php
The contents of the
Module.php file should be like this:
<?php namespace MyBlog; class Module { public function getAutoloaderConfig() { return array( 'Zend\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, ), ), ); } public function getConfig() { return include __DIR__ . '/config/module.config.php'; } }
The file is similar to the one used in the Application module, we tell the framework framework where to look for the module configuration file and source files.
The configuration file should now return an empty array, we will set the settings for the new module a little later.
The
src / MyBlog / Entity / BlogPost.php file is a connection (mapping) between the Doctrine and the database and you need to talk about it in more detail.
BlogPost.php
Each blog post in my example will contain the following fields:
- headline
- blogpost body,
- author's id (0 for anonymous),
- status (published / unpublished)
- publication date.
For simplicity, I will not bother with tags, comments and other blog features inherent in this tutorial.
This file declares the BlogPost class, which contains descriptions of the blogpost fields and methods for accessing them. You can look at the full version of the file on Github (
https://github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/Entity/BlogPost.php ), this is how it looks like:
<?php namespace MyBlog\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; class BlogPost { protected $id; protected $title; public function getId() { return $this->id; } public function setId($id) { $this->id = (int) $id; } public function getTitle() { return $this->title; } public function setTitle($title) { $this->title = $title; } }
Each variable in this class will become a field in the database, field parameters are specified in the annotations that will be read by the Doctrine (something like this:
php.net/manual/en/reflectionclass.getdoccomment.php ,
Doctrine \ Common \ Annotations \ AnnotationReader class method getClassAnnotations ( )).
Now you can add information about our new Entity, which will be used by the Doctrine, to the
config / module.config.php module configuration file:
return array( 'doctrine' => array( 'driver' => array( 'myblog_entity' => array( 'class' =>'Doctrine\ORM\Mapping\Driver\AnnotationDriver', 'paths' => array(__DIR__ . '/../src/MyBlog/Entity') ), 'orm_default' => array( 'drivers' => array( 'MyBlog\Entity' => 'myblog_entity', ) ) ) ), );
And it remains to add the MyBlog module to the list of active modules in
application.config.php .
We have finished setting up the BlogPost entity and now we need to create the corresponding table in the database, for this we will use the console utility supplied with the Doctrine. At the root of the project, execute the command:
./vendor/bin/doctrine-module orm:info
And the result should be a message like:
Found 1 mapped entities: [OK] MyBlog\Entity\BlogPost
Once we have verified that the Doctrine sees our BlogPost object, we will execute the command:
./vendor/bin/doctrine-module orm:validate-schema
As a result, the following error should return:
[Mapping] OK - The mapping files are correct. [Database] FAIL - The database schema is not in sync with the current mapping file.
This is logical, since our database is still empty and now we will create the desired table with the command:
./vendor/bin/doctrine-module orm:schema-tool:update
Its result will be the following output:
Updating database schema... Database schema updated successfully! "1" queries were executed
And now the command call:
./vendor/bin/doctrine-module orm:validate-schema
will return the result:
[Mapping] OK - The mapping files are correct. [Database] OK - The database schema is in sync with the mapping files.
If we now update the page of our site, then in the toolbar at the bottom of the page we will see that the Doctrine is seen by one mapping Myblog \ Entity \ BlogPost.
Source codes of the current version of the project can be found in the project repository on Github with the
blogpost_entity tag:
github.com/romka/zend-blog-example/tree/blogpost_entity .
Now that we have an entity for working with blogposts, we can proceed to writing our first controller that implements the form for adding a blogpost.
Adding a blog post
In the src / MyBlog directory of the module, create two new file directories:
Controller/ BlogController.php Form/ BlogPostForm.php BlogPostInputFilter.php
Next, in the module configuration file you need to add elements that declare the list of module controllers, routes and the path to the directory with templates:
'controllers' => array( 'invokables' => array( 'MyBlog\Controller\BlogPost' => 'MyBlog\Controller\BlogController', ), ), 'router' => array( 'routes' => array( 'blog' => array( 'type' => 'segment', 'options' => array( 'route' => '/blog[/][:action][/:id]', 'constraints' => array( 'action' => '[a-zA-Z][a-zA-Z0-9_-]*', 'id' => '[0-9]+', ), 'defaults' => array( 'controller' => 'MyBlog\Controller\BlogPost', 'action' => 'index', ), ), ), ), ), 'view_manager' => array( 'template_path_stack' => array( __DIR__ . '/../view', ), ),
Based on the settings specified above, all pages of our blog will have URLs like
blog / [action] / [id] (path elements in square brackets are optional).
The
BlogPostForm.php file will contain the form that will be used to add / edit the blogpost, let's create this form.
BlogPostForm.php
In the simplest case, the form code will look like this (this is not the complete source code of the form, you can see it in its entirety here:
github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/Form/ BlogPostForm.php ):
class BlogPostForm extends Form { public function __construct($name = null) { parent::__construct('blogpost'); $this->setAttribute('method', 'post'); $this->add(array( 'name' => 'id', 'type' => 'Hidden', )); $this->add(array( 'name' => 'title', 'type' => 'Text', 'options' => array( 'label' => 'Title', ), 'options' => array( 'min' => 3, 'max' => 25 ), )); $this->add(array( 'name' => 'text', 'type' => 'Textarea', 'options' => array( 'label' => 'Text', ), )); $this->add(array( 'name' => 'state', 'type' => 'Checkbox', )); $this->add(array( 'name' => 'submit', 'type' => 'Submit', 'attributes' => array( 'value' => 'Save', 'id' => 'submitbutton', ), )); } }
Obviously, this code declares the form fields we need, but so far neither filters (allowing the incoming data to be converted) or validators (which will not allow entering the data in the wrong format) into the form are set for them. We will set them later, but now let's write the controller code that will display the blogpost addition form and save the entered data.
BlogController.php
You can view the full controller code in the repository (
https://github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/Controller/BlogController.php ), below is its key part:
class BlogController extends AbstractActionController { public function indexAction() { return new ViewModel(); } public function addAction() { $form = new \MyBlog\Form\BlogPostForm(); $form->get('submit')->setValue('Add'); $request = $this->getRequest(); if ($request->isPost()) { $form->setData($request->getPost()); if ($form->isValid()) { $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager'); $blogpost = new \MyBlog\Entity\BlogPost(); $blogpost->exchangeArray($form->getData()); $blogpost->setCreated(time()); $blogpost->setUserId(0); $objectManager->persist($blogpost); $objectManager->flush();
The addAction action code is significant for us (the names of all actions must be created using the nameAction () mask). In it, we first create a form object and replace the text of the submit button on it (we have the same form used to create and edit blog posts, so the text on this button is convenient to have different):
$form = new \MyBlog\Form\BlogPostForm(); $form->get('submit')->setValue('Add');
Then, if the form is validated (and validation now it will pass anyway, since we still do not have validators), we create an instance of the \ MyBlog \ Entity \ BlogPost () class, which is the connection between our application and the database, fill in the created object data and save them in the database:
$blogpost->exchangeArray($form->getData()); $blogpost->setCreated(time()); $blogpost->setUserId(0); $objectManager->persist($blogpost); $objectManager->flush();
You can see the current version of the template responsible for displaying the form at
github.com/romka/zend-blog-example/blob/blogpost_form_1/module/MyBlog/view/my-blog/blog/add.phtml .
If now try to save an empty form, the Doctrine will return an error message of the form:
An exception occurred while executing 'INSERT INTO blogposts (title, text, userId, created, state) VALUES (?, ?, ?, ?, ?)' with params [null, null, 0, 1377086855, null]: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'title' cannot be null
This is correct, because we have only one field, marked as nullable = “true” - this is the state field, and all the others must be filled. Let's add filters and validators to the form to intercept such errors before attempting to save data (at the level of our application, and not the database), so that the user has the opportunity to correct the error.
Validation of forms
In the previously created
BlogPostInputFilter.php file
we place the following code (full version on GitHub:
github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/Form/BlogPostInputFilter.php ):
class BlogPostInputFilter extends InputFilter { public function __construct() { $this->add(array( 'name' => 'title', 'required' => true, 'validators' => array( array( 'name' => 'StringLength', 'options' => array( 'min' => 3, 'max' => 100, ), ), ), 'filters' => array( array('name' => 'StripTags'), array('name' => 'StringTrim'), ), )); $this->add(array( 'name' => 'text', 'required' => true, 'validators' => array( array( 'name' => 'StringLength', 'options' => array( 'min' => 50, ), ), ), 'filters' => array( array('name' => 'StripTags'), array('name' => 'StringTrim'), ), )); $this->add(array( 'name' => 'state', 'required' => false, )); } }
I suppose that the meaning of these lines should be intuitive: for the title and text fields we add filters that remove all html tags from the text (StripTags) and trim the spaces around the edges of the strings (StringTrim), and also add validators that define the minimum values and maximum field lengths (StringLength).
It remains to add a new filter to the form, adding a line to the form class:
$this->setInputFilter(new \MyBlog\Form\BlogPostInputFilter());
Now the form will not pass validation if incorrect data is entered into it.
View plugins
After the blogpost is successfully saved (or not saved), we redirect the user to the page / blog, where in the future we will have a full list of blogposts. I would like not only to make a redirect, but also to display a message about a successful action.
You can add such messages using the methods:
$this->flashMessenger()->addMessage($message); $this->flashMessenger()->addErrorMessage($message);
You can extract messages added this way in the controller or in phtml templates in the following way:
$this->flashMessenger()->getMessages(); $this->flashMessenger()->getErrorMessages();
The problem is that it is inconvenient (and in the Twig-templates, which we will use later, it is completely impossible) to call the PHP-code to display messages. Therefore, we will write a small View-plugin that can display all messages in one line.
To do this, in the src \ MyBlog directory of the module, create the following directories and files:
View\ Helper\ ShowMessages.php
The contents of
ShowMessages.php can be viewed here:
github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/View/Helper/ShowMessages.php , it’s not very interesting, I’m just getting a list here messages, formatting and returning ready html-code to display them.
It remains to do three things:
- register a view plugin,
- add its use to the template,
- and display messages about successful / unsuccessful saving of the form.
In order to register the plugin, add the line to the settings of the module in the viewkit => invokables:
'view_helpers' => array( 'invokables' => array( 'showMessages' => 'MyBlog\View\Helper\ShowMessages', ), ),
Add templates to the templates:
print $this->showMessages();
To display messages on the screen, add the following lines to the controller:
$message = 'Blogpost succesfully saved!'; $this->flashMessenger()->addMessage($message);
Now we have the ability to display user system messages.
You can find this version of the application in the git repository with the
blogpost_form_1 tag:
github.com/romka/zend-blog-example/tree/blogpost_form_1 .
At the current stage, we have:
- an entity for communication between an application and a database, created with the help of the Doctrine,
- controller service page add blogpost,
- Add a blog post with filters and validations
- custom view plugin for displaying messages on the screen.
Now let's add the pages of one blog post, a list of blog posts and a post edit / delete form.
Blogpost page
Add a new action view to the BlogpostController controller:
public function viewAction() { $id = (int) $this->params()->fromRoute('id', 0); if (!$id) { $this->flashMessenger()->addErrorMessage('Blogpost id doesn\'t set'); return $this->redirect()->toRoute('blog'); } $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager'); $post = $objectManager ->getRepository('\MyBlog\Entity\BlogPost') ->findOneBy(array('id' => $id)); if (!$post) { $this->flashMessenger()->addErrorMessage(sprintf('Blogpost with id %s doesn\'t exists', $id)); return $this->redirect()->toRoute('blog'); } $view = new ViewModel(array( 'post' => $post->getArrayCopy(), )); return $view; }
This action is available at blog / view / ID. In it, we first check that the blogpost id is set in the URL, if this is not the case, we return an error and redirect the user to the page with the list of blogposts. If id is specified, then we retrieve the post from the database and transfer it to the template.
The name of the controller is used as the default template name, so now in the directory of the view / my-blog / blog module you need to create a view.phtml file with something like this:
<?php print $this->showMessages(); print '<h1>' . $post['title'] . '</h1>'; print '<div>' . $post['text'] . '</div>';
Blogpost list
Update our indexAction code to this:
public function indexAction() { $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager'); $posts = $objectManager ->getRepository('\MyBlog\Entity\BlogPost') ->findBy(array('state' => 1), array('created' => 'DESC')); $view = new ViewModel(array( 'posts' => $posts, )); return $view; }
Here we select all published blogposts (state == 1), sort them by publication date and transfer them to the
index.phtml template
github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/view/my-blog /blog/index.phtml . The template displays blogpost headers and links to edit and delete them.
Small retreat
Above, when creating a form, I forgot to add the userId field, which stores the ID of the blogpost's author. Since there is no registration / authorization in our blog now, this field is filled with zero by default, but in the future it will come in handy, so now I have added the userId field to the hidden form.
In addition, I added a Csrf token to the form (security field), which should protect the form from falsification. By default, this token is formed based on the user session and salt and lasts 300 seconds (
Zend \ Form \ Element \ Csrf.php ), but it can be (and should be good) redefined and at least the visitor ip dependency should be added to it .
Editing a blog post
To edit the post, we will use the already existing form. In the controller, you need to create an editAction () action that will create the form, fill it with existing data and give it to the user. This action is a mixture of addAction (), in terms of working with the form, and viewAction (), in terms of data sampling
github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/src/MyBlog/Controller/BlogController .php # L95 .
Here is the most interesting part of this controller:
if ($form->isValid()) { $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager'); $data = $form->getData(); $id = $data['id']; try { $blogpost = $objectManager->find('\MyBlog\Entity\BlogPost', $id); } catch (\Exception $ex) { return $this->redirect()->toRoute('blog', array( 'action' => 'index' )); } $blogpost->exchangeArray($form->getData()); $objectManager->persist($blogpost); $objectManager->flush(); $message = 'Blogpost succesfully saved!'; $this->flashMessenger()->addMessage($message);
Here we load a blog post from the database, based on the id that came in the form, we update the data:
$blogpost->exchangeArray($form->getData());
and put the updated blogpost in the database:
$objectManager->persist($blogpost); $objectManager->flush();
Deleting blog posts
Deleting a blog post is a trivial task; it is enough to display a form to the user with a question like “Do you really want to delete the post?” And if the user clicks the “Yes” button, perform the appropriate actions.
The code for the corresponding controller and template can be viewed at Github:
github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/src/MyBlog/Controller/BlogController.php#L161 .
The sources with the
blogpost_form_2 tag (https://github.com/romka/zend-blog-example/tree/blogpost_form_2) contain the
blogpost editing and deleting forms, the list of posts and the corresponding templates.
On this I would like to complete the second part of the article.
In the third part, we will work with users and attach Twig template engine to the project.