An example of developing a blog on Zend Framework 2. Part 2. MyBlog module

  • Tutorial
This is the second of a three-part article about developing a simple application using Zend Framework 2. In the first part, I looked at 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 installing a third-party module in the Zend Framework usually consists of about these four steps:
  1. add the appropriate line in composer.json to inform Composer about the new module,
  2. execute the php composer.phar update command , so that Composer downloads a new module and, if necessary, regenerates autoload files,
  3. add the new module to the modules list in the config / application.config.php file ,
  4. if necessary, we 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 changes in it.

Also, I want to emphasize that for all the modules listed below I set the minimum settings necessary for their work, more details about the settings and capabilities of each of the modules can be found on their documentation pages.

Let's start by installing the simple but useful Zend Developer Tools module.

Zend developer tools


Zend Developer Tools is a convenient toolbar that contains information useful for the developer about the created page: the number and list of database queries, the list of current user roles, used by Entity, loaded site configuration, etc. Of course, the toolbar can be expanded 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",

into the composer.json file in the root of the project and then run the php composer.phar update command in the root of the project.

Then, in the config / application.config.php file, 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 to the config / autoload folder of our project and rename it, for example, to zenddevelopertools.local.php (part of the name before local.php by by and large does not matter).

Everything, now, by default, at the bottom of all pages displays information about the resources spent on page generation, project configuration, etc.

I want to pay attention to the fact that by default the toolbar will be available to all visitors to the site, so it is not worth using it in the production environment.

The current version of the application is available on Github in the project repository with the zenddevelopertools tag :github.com/romka/zend-blog-example/tree/zenddevelopertools

Doctrine orm


To integrate with the Doctrine, you will need the DoctrineModule and DoctrineORMModule modules ( https://github.com/doctrine/DoctrineModule and github.com/doctrine/DoctrineORMModule ).

Add the following lines to the require section of the composer.json file:
"doctrine/common": ">=2.1",
"doctrine/doctrine-orm-module": "0.7.*"

and execute the php composer.phar update command in the console .

The DoctrineModule module can be omitted explicitly in our composer.json , since this dependency is registered at the DoctrineORMModule module level.

Now you need to place the doctrine.local.php file in the config / autoload directory with the database access parameters that will be used by the Doctrine, its contents should look something like this:
 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 requests and a list of mapping 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 is time to write the first lines of code for the new module.

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:
 array(
        'namespaces' => array(
          __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
        ),
      ),
    );
  }
  public function getConfig()
  {
    return include __DIR__ . '/config/module.config.php';
  }
}

The file is similar to that used in the Application module, we tell the core of the framework where to look for the module configuration file and source files.

The configuration file for now should return an empty array, we will set the settings for the new module a little later.

The file src / MyBlog / Entity / BlogPost.php is the connection (mapping) between the Doctrine and the database and we need to talk more about it.

BlogPost.php


Each blog post in my example will contain the following fields:
  • title
  • body of the blog post
  • author id (0 for anonymous),
  • status (published / not published)
  • publication date.

For simplicity, I will not bother in this tutorial with tags, comments, and other blog-specific features.

This file declares a BlogPost class that contains descriptions of the blog post fields and methods for accessing them. You can see 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 its part looks:
id;
  }
  /**
   * Set id.
   *
   * @param int $id
   *
   * @return void
   */
  public function setId($id)
  {
    $this->id = (int) $id;
  }
  /**
   * Get title.
   *
   * @return string
   */
  public function getTitle()
  {
    return $this->title;
  }
  /**
   * Set title.
   *
   * @param string $title
   *
   * @return void
   */
  public function setTitle($title)
  {
    $this->title = $title;
  }
}

Each variable in this class will become a field in the database, field parameters are set in annotations that will be read by the Doctrine (like this: php.net/manual/en/reflectionclass.getdoccomment.php , class Doctrine \ Common \ Annotations \ AnnotationReader method getClassAnnotations ( )).

Now in the configuration file of the module config / module.config.php, you can add information about our new Entity, which will be used by the Doctrine:
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 appropriate table in the database, for this we will use the console utility that comes 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 of the form:
Found 1 mapped entities:
[OK]   MyBlog\Entity\BlogPost

After we are convinced that the Doctrine sees our BlogPost object, we will execute the command:
./vendor/bin/doctrine-module orm:validate-schema

As a result, an error of the form 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 --force

Its result will be the following conclusion:
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 refresh the page of our site now, then in the toolbar at the bottom of the page we will see that the Doctrine sees one mapping Myblog \ Entity \ BlogPost.

The source code for the current version of the project can be found in the project repository on Github with the tag blogpost_entity : github.com/romka/zend-blog-example/tree/blogpost_entity .

Now that we have the essence for working with blog posts, we can proceed to writing our first controller that implements the form for adding a blog post.

Adding a blog post


In the src / MyBlog directory of the module, create two new directories with files:
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 template directory:
'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 above settings, all pages of our blog will have urls of the form blog / [action] / [id] (path elements in square brackets are optional).

The BlogPostForm.php file will contain a form that will be used to add / edit a blog post, let's create this form.

BlogPostForm.php


In the simplest case, the form code will look like this (this is not the full 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 required form fields, but so far no filters have been set for them (allowing converting input data) or validators (which will not allow entering data in the wrong format into the form). We will ask them later, and now let's write the controller code, which will display the form for adding a blog post and save the entered data.

BlogController.php


You can see 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 the 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();
                // Redirect to list of blogposts
                return $this->redirect()->toRoute('blog');
            }
        }
        return array('form' => $form);
    }
}

The addAction action code is significant for us (the names of all actions should 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 will use the same form for creating and editing blog posts, so the texts on this button are convenient to have different):
$form = new \MyBlog\Form\BlogPostForm();
$form->get('submit')->setValue('Add');

Then, if the form has passed validation (and validation will now pass anyway, since we don’t have any validators yet), we create an instance of the \ MyBlog \ Entity \ BlogPost () class, which is the connection between our application and the database, and fill out 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();

The current version of the template responsible for displaying the form can be seen 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 rest should be filled. Let's add filters and validators to the form to catch such errors even before trying to save data (at the level of our application, not the database), so that the user has the opportunity to fix the error.

Form validation


In the previously created BlogPostInputFilter.php file, we will 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 assume that the meaning of these lines should be intuitive: for title and text fields, we add input filters that remove all html tags (StripTags) from the text and trim the spaces at the edges of the lines (StringTrim), and add validators that determine the minimum and maximum field lengths (StringLength).

It remains to attach a new filter to the form by adding the line to the form class:
$this->setInputFilter(new \MyBlog\Form\BlogPostInputFilter());

Now the form will not pass validation if incorrect data is entered in it.

View plugins


After the blog post has been successfully saved (or not saved), we redirect the user to the / blog page, where in the future we will have a complete list of blog posts. I would like not only to make a redirect, but also to display a message about the successfully completed action.

You can add such messages using the methods:
$this->flashMessenger()->addMessage($message);
$this->flashMessenger()->addErrorMessage($message);

You can retrieve messages added in this way in the controller or in phtml templates in this 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 on the screen with one line.

To do this, create the following directories and files in the src \ MyBlog directory of the module:
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, format and return the finished html-code to display them.

It remains to do three actions:
  1. register View plugin,
  2. add its use to the template,
  3. and display success / failure to save the form.

To register the plugin, add the following line to the module settings in the view_helper => invokables line:
'view_helpers' => array(
    'invokables' => array(
        'showMessages' => 'MyBlog\View\Helper\ShowMessages',
    ),
),

Add message output 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 system messages to the user.

You can find this version of the application in the git repository with the tag blogpost_form_1 : github.com/romka/zend-blog-example/tree/blogpost_form_1 .

At the current stage, we have:
  1. an entity for connecting an application and a database created using the Doctrine,
  2. controller serving the page for adding a blog post,
  3. form for adding a blog post with input filters and validations,
  4. own custom View plugin for displaying messages on the screen.

Now let's add the pages of one blog post, the list of blog posts and the edit / delete post form.

Blog Post Page


Add a new view action 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 blog post id is specified in the URL, if this is not the case, then we return an error and redirect the user to the page with the list of blog posts. If id is specified, then we extract the post from the database and pass it to the template.

The default controller name is used as the 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:
showMessages();
    print '

' . $post['title'] . '

'; print '
' . $post['text'] . '
';


List of Blog Posts


Update the code of our indexAction to this type:
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 blog posts (state == 1), sort them by publication date and submit them to index.phtml github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/view/my-blog /blog/index.phtml . The template displays the titles of blog posts and links to edit and delete them.

Small digression


Выше, при создании формы я забыл добавить поле userId, в котором хранится айдишник автора блогпоста. Так как сейчас регистрации/авторизации в нашем блоге нет, это поле по умолчанию заполняется нулем, но в будущем оно пригодится, поэтому сейчас я добавил в форму hidden поле userId.

Кроме того, я добавил к форме Csrf-токен (поле security), который должен защитить форму от подделки. По умолчанию этот токен формируется на основании пользовательской сессии и соли и живет 300 секунд (Zend\Form\Element\Csrf.php), но может быть (и по хорошему должен быть) переопределен и к нему как минимум должна быть добавлена зависимость от ip посетителя.

Редактирование блогпоста


To edit the post, we will use the existing form. In the controller, you need to create the editAction () action, which 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 fetching data 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);
    // Redirect to list of blogposts
    return $this->redirect()->toRoute('blog');
}

Here we download a blog post from the database, based on the id that came in the form, update the data:
$blogpost->exchangeArray($form->getData());

and put the updated blog post in the database:
$objectManager->persist($blogpost);
$objectManager->flush();


Removing Blog Posts


Removing a blog post is a trivial task, it’s enough to display a form with a question like “Do you really want to delete a post?” and if the user clicks the “Yes” button, perform the appropriate actions.

The code of the corresponding controller and template can be viewed on Github: github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/src/MyBlog/Controller/BlogController.php#L161 .

Sources with the tag tag blogpost_form_2 (https://github.com/romka/zend-blog-example/tree/blogpost_form_2) contain forms for editing and deleting a blog post, a list of posts and 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 fasten the Twig template engine to the project.

Also popular now: