Пример разработки блога на Zend Framework 2. Часть 2. Модуль MyBlog

  • Tutorial
Это вторая из трех частей статьи, посвященной разработке простого приложения при помощи Zend Framework 2. В первой части я рассмотрел структуру ZendSkeletonApplication, а в этой части приведу пример разработки простого модуля. Третья часть будет посвящена работе с пользователями и шаблонизатором Twig.

Установка и настройка дополнительных модулей


Первым делом хочу отметить, что установка стороннего модуля в Zend Framework обычно состоит из примерно таких четырех шагов:
  1. добавляем соответствующую строчку в composer.json, чтобы сообщить Композеру о новом модуле,
  2. выполняем команду php composer.phar update, чтобы Композер загрузил новый модуль и при необходимости перегенерировал автолоад файлы,
  3. добавляем новый модуль в список modules в файле config/application.config.php,
  4. при необходимости, размещаем конфигурационный файл модуля (обычно пример такого файла находится в папке config модуля) в config/autoload и делаем в нем необходимые правки.

Также, хочу подчеркнуть, что для всех модулей, перечисленных далее я задаю минимально необходимые для их работы настройки, более подробно о настройках и возможностях каждого из модулей можно узнать на их страницах документации.

Давайте начнем с установки простого, но полезного модуля Zend Developer Tools.

Zend Developer Tools


Zend Developer Tools — это удобный тулбар, содержащий полезную для разработчика информацию о созданной странице: число и список запросов к БД, список ролей текущего пользователя, использованные Entity, загруженная конфигурация сайта и т.д. Разумеется, тулбар может быть расширен любой другой вспомогательной информацией. Найти его можно тут: github.com/zendframework/ZendDeveloperTools.

Чтобы установить тулбар сначала добавим строчку:
"zendframework/zend-developer-tools": "dev-master",

в файл composer.json в корне проекта и затем выполним команду php composer.phar update в корне проекта.

Затем в файл config/application.config.php в массив modules нужно добавить элемент ZendDeveloperTools:
'modules' => array(
    'Application',
    'ZendDeveloperTools',
),

Теперь осталось скопировать файл vendor/zendframework/zend-developer-tools/config/zenddevelopertools.local.php.dist в папку config/autoload нашего проекта и переименовать его, например, в zenddevelopertools.local.php (часть имени до local.php по большому счету значения не имеет).

Всё, теперь, по умолчанию, внизу всех страниц выводится информация о затраченных на генерацию страницы ресурсах, конфигурация проекта и т.п.

Хочу обратить внимание на то, что по умолчанию тулбар будет доступен всем посетителям сайта поэтому в production окружении его использовать не стоит.

Текущая версия приложения доступна на Гитхабе в репозитории проекта с тэгом zenddevelopertools: github.com/romka/zend-blog-example/tree/zenddevelopertools

Doctrine ORM


Для интеграции с Доктриной понадобятся модули DoctrineModule и DoctrineORMModule (https://github.com/doctrine/DoctrineModule и github.com/doctrine/DoctrineORMModule).

Добавим в секцию require файла composer.json строчки:
"doctrine/common": ">=2.1",
"doctrine/doctrine-orm-module": "0.7.*"

и выполним в консоли команду php composer.phar update.

Модуль DoctrineModule можно не указывать явно в нашем composer.json, так как эта зависимость прописана на уровне модуля DoctrineORMModule.

Теперь нужно в директории config/autoload разместить файл doctrine.local.php с параметрами доступа к БД, который будет использоваться Доктриной, его содержимое должно быть примерно таким:
<?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',
                )
            )
        ),
    ),
);

Теперь если мы перезагрузим страницу нашего сайта, то внизу страницы в Zend девлопер тулбаре увидим два новых блока, показывающих число выполненных запросов и список маппингов к БД. Оба значения равны нулю, так как маппинги мы пока не сделали и, как следствие, запросов к базе нет.

В этом туториале я хочу разработать простейший блог и сейчас настало время написать первые строчки кода нового модуля.

Модуль MyBlog


В каталоге modules создадим следующие директории и файлы:
MyBlog/
    config/
        module.config.php
    src/
        MyBlog/
            Entity/
                BlogPost.php
    Module.php

Содержимое файла Module.php должно быть таким:
<?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';
  }
}

Файл аналогичен тому, что используется в модуле Application, мы говорим ядру фреймворка где искать конфигурационный файл модуля и файлы исходников.

Конфигурационный файл пока должен возвращать пустой массив, настройки нового модуля мы зададим чуть позже.

Файл src/MyBlog/Entity/BlogPost.php является связью (маппингом) между Доктриной и базой данных и о нем нужно поговорить подробнее.

BlogPost.php


Каждый блогпост в моем примере будет содержать следующие поля:
  • заголовок,
  • тело блогпоста,
  • id автора (0 для анонимов),
  • статус (опубликовано/не опубликовано)
  • дата публикации.

Для простоты я не стану в этом туториале заморачиваться с тэгами, комментариями и другими свойственными блогам фичами.

Этот файл объявляет класс BlogPost, который содержит описания полей блогпоста и методов доступа к ним. Полную версию файла вы можете посмотреть на Гитхабе (https://github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/Entity/BlogPost.php), вот так выглядит его часть:
<?php
namespace MyBlog\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

class BlogPost
{
  /**
   * @var int
   * @ORM\Id
   * @ORM\Column(type="integer")
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;

  /**
   * @var string
   * @ORM\Column(type="string", length=255, nullable=false)
   */
  protected $title;

  /**
   * Get id.
   *
   * @return int
   */
  public function getId()
  {
    return $this->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;
  }

}

Каждая переменная в этом классе станет полем в БД, параметры полей задаются в аннотациях, которые будут прочитаны Доктриной (примерно вот так: php.net/manual/en/reflectionclass.getdoccomment.php, класс Doctrine\Common\Annotations\AnnotationReader метод getClassAnnotations()).

Теперь в конфигурационный файл модуля config/module.config.php можно добавить информацию о нашей новой Entity, которая будет использована Доктриной:
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',
                )
            )
        )
    ),
);

И осталось добавить модуль MyBlog в список активных модулей в application.config.php.

Мы закончили настройку сущности BlogPost и сейчас нужно создать соответствующую таблицу в базе данных, для этого воспользуемся консольной утилитой, поставляющейся в комплекте с Доктриной. В корне проекта выполним команду:
./vendor/bin/doctrine-module orm:info

И результатом должно быть сообщение вида:
Found 1 mapped entities:
[OK]   MyBlog\Entity\BlogPost

После того как мы убедились в том, что Доктрина видит наш объект BlogPost выполним команду:
./vendor/bin/doctrine-module orm:validate-schema

В результате должна вернуться ошибка вида:
[Mapping]  OK - The mapping files are correct.
[Database] FAIL - The database schema is not in sync with the current mapping file.

Это и логично, так как наша база данных все еще пуста и сейчас мы создадим нужную таблицу командой:
./vendor/bin/doctrine-module orm:schema-tool:update --force

Её результатом будет следующий вывод:
Updating database schema...
Database schema updated successfully! "1" queries were executed

И теперь вызов команды:
./vendor/bin/doctrine-module orm:validate-schema

вернет результат:
[Mapping]  OK - The mapping files are correct.
[Database] OK - The database schema is in sync with the mapping files.

Если сейчас обновить страницу нашего сайта, то в тулбаре внизу страницы мы увидим, что Доктрина видит один маппинг Myblog\Entity\BlogPost.

Исходные коды текущей версии проекта можно найти в репозитории проекта на Гитхабе с тэгом blogpost_entity: github.com/romka/zend-blog-example/tree/blogpost_entity.

Теперь, когда у нас есть сущность для работы с блогпостами можно перейти к написанию своего первого контроллера, реализующего форму добавления блогпоста.

Добавление блогпоста


В директории src/MyBlog модуля создадим два новых каталога с файлами:
Controller/
    BlogController.php
Form/
    BlogPostForm.php
    BlogPostInputFilter.php

Далее в конфигурационный файл модуля нужно добавить элементы, объявляющие список контроллеров модуля, маршруты и путь к директории с шаблонами:
'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',
    ),
),


Исходя из заданных выше настроек, все страницы нашего блога будут иметь урлы вида blog/[action]/[id] (элементы пути в квадратных скобках не обязательны).

Файл BlogPostForm.php будет содержать форму, которая будет использоваться для добавления/редактирования блогпоста, давайте создадим эту форму.

BlogPostForm.php


В самом простом случае код формы будет выглядеть вот так (это не полный исходный код формы, целиком его можно увидеть тут: 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',
            ),
        ));
    }
}

Очевидно, что этот код объявляет необходимые нам поля формы, но пока для них не заданы ни фильтры (позволяющие преобразовать входящие данные), ни валидаторы (которые не позволят ввести в форму данные в неправильном формате). Их мы зададим позже, а сейчас давайте напишем код контроллера, который будет выводить форму добавления блогпоста и сохранять введенные данные.

BlogController.php


Полный код контроллера вы можете посмотреть в репозитории (https://github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/Controller/BlogController.php), ниже его ключевая часть:
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);
    }
}

Значимым для нас является код экшена addAction (имена всех экшенов должны создаваться по маске nameAction()). В нем мы сначала создаем объект формы и заменяем текст кнопки submit на ней (у нас одна и та же форма будет использоваться и для создания, и для редактирования блогпостов, поэтому тексты на этой кнопке удобно иметь разные):
$form = new \MyBlog\Form\BlogPostForm();
$form->get('submit')->setValue('Add');

Затем, в случае если форма прошла валидацию (а валидацию сейчас она пройдет в любом случае, так как валидаторов у нас пока нет) мы создаем экземпляр класса \MyBlog\Entity\BlogPost(), который является связью между нашим приложением и БД, наполняем созданный объект данными и сохраняем их в БД:
$blogpost->exchangeArray($form->getData());

$blogpost->setCreated(time());
$blogpost->setUserId(0);

$objectManager->persist($blogpost);
$objectManager->flush();

Текущую версию шаблона, отвечающего за отображение формы, можно увидеть по ссылке github.com/romka/zend-blog-example/blob/blogpost_form_1/module/MyBlog/view/my-blog/blog/add.phtml.

Если cейчас попробовать сохранить пустую форму, то Доктрина вернет сообщение об ошибке вида:
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

Это правильно, ведь у нас есть только одно поле, помеченное как nullable=«true» — это поле state, а все остальные должны быть заполнены. Давайте добавим к форме инпут фильтры и валидаторы, чтобы перехватывать подобные ошибки еще до попытки сохранить данные (на уровне нашего приложения, а не БД), чтобы у пользователя была возможность исправить ошибку.

Валидация форм


В ранее созданном файле BlogPostInputFilter.php разместим такой код (полная версия на Гитхабе: 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,
        ));
    }
}

Предполагаю, что смысл этих строк должен быть интуитивно понятным: для полей title и text мы добавляем инпут фильтры, которые удалят из текстов все html тэги (фльтр StripTags) и обрежут пробелы по краям строк (StringTrim), а также добавляем валидаторы, определяющие значения минимальной и максимальной длины полей (StringLength).

Осталось присоединить новый фильтр к форме, добавив в класс формы строчку:
$this->setInputFilter(new \MyBlog\Form\BlogPostInputFilter());

Теперь форма не пройдет валидацию, если в нее введены некорректные данные.

View плагины


После того как блогпост успешно сохранен (или не сохранен) мы перенаправляем пользователя на страницу /blog, на которой в будущем у нас будет полный список блогпостов. Хотелось бы не просто сделать редирект, но и вывести на экран сообщение об успешно выполненном действии.

Добавить такие сообщения можно с помощью методов:
$this->flashMessenger()->addMessage($message);
$this->flashMessenger()->addErrorMessage($message);

Извлечь сообщения, добавленные таким способом, можно в контроллере или в phtml-шаблонах таким образом:
$this->flashMessenger()->getMessages();
$this->flashMessenger()->getErrorMessages();

Проблема в том, что неудобно (а в Twig-шаблонах, которые мы будем использовать позже, и вовсе невозможно) вызывать PHP-код для вывода сообщений. Поэтому мы напишем небольшой View-плагин, который сможет одной строчкой выводить на экран все сообщения.

Для этого в директории src\MyBlog модуля создадим такие директории и файлы:
View\
    Helper\
        ShowMessages.php

Содержимое ShowMessages.php можно посмотреть тут: github.com/romka/zend-blog-example/blob/master/module/MyBlog/src/MyBlog/View/Helper/ShowMessages.php, оно не очень интересно, я здесь просто получаю список сообщений, форматирую и возвращаю готовый html-код для их отображения.

Осталось сделать три действия:
  1. зарегистрировать View-плагин,
  2. добавить его использование в шаблон,
  3. и вывести сообщения о успешном/неудачном сохранении формы.

Чтобы зарегистрировать плагин добавим в настройки модуля в сецкию view_helper => invokables строчку:
'view_helpers' => array(
    'invokables' => array(
        'showMessages' => 'MyBlog\View\Helper\ShowMessages',
    ),
),

В шаблоны добавим вывод сообщений:
print $this->showMessages();

Для вывода сообщений на экран добавим в контроллер такие строчки:
$message = 'Blogpost succesfully saved!';
$this->flashMessenger()->addMessage($message);

Теперь у нас есть возможность выводить пользователю системные сообщения.

Эту версию приложения вы можете найти в гит-репозитории с тэгом blogpost_form_1: github.com/romka/zend-blog-example/tree/blogpost_form_1.

На текущем этапе у нас есть:
  1. сущность для связи приложения и БД, созданная при помощи Доктрины,
  2. контроллер обслуживающий страницу добавления блогпоста,
  3. форма добавления блогпоста с инпут фильтрами и валидаций,
  4. свой кастомный View-плагин для вывода сообщений на экран.

Теперь давайте добавим страницы одного блогпоста, списка блогпостов и формы редактирования/удаления поста.

Страница блогпоста


Добавим в контроллер BlogpostController новое действие view:
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;
}

Этот экшен доступен по адресу blog/view/ID. В нем мы сначала мы проверяем, что в URL’е задан id блогпоста, если это не так, то возвращаем ошибку и редиректим пользователя на страницу со списком блогпостов. Если id указан, то мы извлекаем пост из базы и передаем его в шаблон.

В качестве имени шаблона по умолчанию используется имя контроллера, поэтому теперь в директории модуля view/my-blog/blog нужно создать файл view.phtml с примерно вот таким содержимым:
<?php
    print $this->showMessages();
    print '<h1>' . $post['title'] . '</h1>';
    print '<div>' . $post['text'] . '</div>';


Список блогпостов


Обновим код нашего indexAction до такого вида:
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;
}

Здесь мы выбираем все опубликованные блогпосты (state == 1), сортируем их по дате публикации и передаем в шаблон index.phtml github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/view/my-blog/blog/index.phtml. В шаблоне выводятся заголовки блогпостов и ссылки на их редактирование и удаление.

Небольшое отступление


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

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

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


Для редактирования поста мы будем использовать уже существующую форму. В контроллере необходимо создать действие editAction(), которое будет создавать форму, наполнять её существующими данными и отдавать пользователю. Этот экшен является смесью addAction(), в части работы с формой, и viewAction(), в части выборки данных github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/src/MyBlog/Controller/BlogController.php#L95.

Вот самая интересная часть этого контроллера:
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');
}

Здесь мы загружаем из БД блогпост, основываясь на id, который пришел в форме, обновляем данные:
$blogpost->exchangeArray($form->getData());

и кладем обновленный блогпост в базу:
$objectManager->persist($blogpost);
$objectManager->flush();


Удаление блогпостов


Удаление блогпоста задача тривиальная, достаточно вывести пользователю форму с вопросом вида ”Действительно ли вы хотите удалить пост?” и в случае если пользователь нажмет кнопку “Да”, выполнить соответствующие действия.

Код соответствующего контроллера и шаблона можно посмотреть на Гитхабе: github.com/romka/zend-blog-example/blob/blogpost_form_2/module/MyBlog/src/MyBlog/Controller/BlogController.php#L161.

Исходники с тэгом blogpost_form_2 (https://github.com/romka/zend-blog-example/tree/blogpost_form_2) содержат формы редактирования и удаления блогпоста, список постов и соответствующие шаблоны.

На этом я бы хотел завершить вторую часть статьи. В третьей части мы займёмся работой с пользователями и прикрутим к проекту шаблонизатор Twig.
Поделиться публикацией

Похожие публикации

Комментарии 22
    +7
    Чертовски много лишних телодвижений.
      +1
      Это вы о конкретно этом примере или о Zend-фреймворке в целом?
        +2
        После symfony 1.4 я вообще не могу понять курс современных фреймворков: что zf2, что sf2 — столько всего приходится писать руками, что, порой, задумываешься: «а надо ли вообще всё это?»
          0
          Суть статьи — показать пример разработки готового модуля, а не написать полноценный блог.
          Как опытный разработчик, вы должны понимать, что с ростом функциональности системы и объема кода она (система) становиться неуправляемой. При должной структуризации кода этот момент откладывается (почти) на неопределенное время, вполне возможно до конца актуальности самой системы.

          Простой блог как в статье, на чистом PHP, где код написан в хардкорном стиле «здесь и сейчас» можно вложится в несколько экранов, но сопровождение кода в гораздо сложных проектах будет излишне дорого для бизнеса.

          Курс современных фреймворков состоит в понижении сопряжения компонентов, из чего следует минимизация сложности, снижение количества ошибок, уже далее простота разработки и сопровождения ПО.
          Поэтому ответ на ваш (может быть риторический) вопрос: да, надо. Но прежде всего нужно помнить: для каждой задачи должен быть осознанно применен свой инструмент.
            0
            Чего? Вы комментарий мой точно читали? А про symfony 1 что-нибудь слышали? Каким боком моё высказывание коррелирует с тем, что я не признаю и не понимаю назначение фреймворков?
            Я где-то говорил о том, что кода слишком много? Я просто свято верю в то, что кодогенерация решает проблемы (особенно, крупных проектов).
            В симфони 1.4 то, что вы написали на несколько экранов заключается в следующих действиях:
            — описать (декларативно) схему модели
            — сгенерировать модели, формы, фильтры, админку, роуты и sql несколькими консольными тасками
            При этом, получившийся продукт не будет ни в чём уступать Вашему (в плане кастомизации или абстрактности), но ручного ввода текста (довольно подверженного ошибкам, к слову) будет в несколько раз меньше.
            Вообще, если бы Вы внимательно прочли мой вопрос, то могли бы его свести к «где скаффолдинг? Неужели, этот код не мог сгенерировать компьютер?»
              0
              а что, в sf1 сильно лучше скаффолдинг чем стоковая кодогенерация через SensioGeneratorBundle и sonata admin во второй симфони?
                0
                Понятия не имею, если честно: дальше чтения мануалов, подобных этому (с 20 экранами кода, вместо 20 строк) с сф2 у меня не зашло. Но спасибо, что дали направление — обязательно поизучаю эти бандлы.
                0
                Вас интересовал курс современных фреймворков — я описал свое мнение. Прямых утверждений касательно ваших взглядов в моем комментарии нет, к чему негатив?
                Кодогенерация как и в первых версия была, так и во вторых есть. Если вас интересует тема скаффолдинга в ZF2, увы, статья не про это.
                А Ваш продукт на симфони 1.4 будет все же уступать аналогичному на sf2, как и продукт на zf1 будет уступать продукту на zf2 по причинам описанных мною выше.
          0
          Соглашусь, пост читать тяжеловато, хотя тема интересная и полезная. Попробуйте куски кода убрать в раскрывающийся блок, тогда пост по размеру уменьшится на 30-50%. Так же читать просто порядок действий не очень интересно, можно сравнить, как это делается в других фреймворках и ваше субъективное мнение о том, что тут происходит и можно было бы это как то улучшить или вообще не использовать.
            +1
            Вообще, цель статьи не в том, чтобы сравнить Zend с другими фреймворками, а в том, чтобы дать разработчикам, начинающим работать с фреймворком, ответы на вопросы: что такое ServiceManager, как работать с формами, БД и т.п. Соглашусь, читать лучше на свежую голову, но текста примерно с такой структурой мне не хватало когда я начал свое знакомство с этим фреймворком.
            +1
            1) Зачем в каждом экшене делать
             $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
            
            ?
            В зенд же есть хуки перед вызовом экшена?

            2) Почему в мире php не принято следовать REST? Ведь очень некрасиво выглядят эти постоянные проверки на то, что запрос POST?

            3)
            $blogpost->setCreated(time());
            

            Разве это нельзя сделать в хуках доктрины prePersist?

            4)
            $blogpost->setUserId(0);
            

            что это за магия?

            5)
            $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');
                }
            


            можно переписать как
                $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');
                }
            


            Проверка на существование id не имеет особого смысла, обычно это просто 404 страница. Юзер не должен руками править урл.

            6) EntityManager не кидает эксепшн, просто возвращает null. docs.doctrine-project.org/en/2.0.x/reference/working-with-objects.html#querying, если не найдена запись.

            7)
            $posts = $objectManager
                    ->getRepository('\MyBlog\Entity\BlogPost')
                    ->findBy(array('state' => 1), array('created' => 'DESC'));

            Что за магическая 1?
              0
              1) Зачем в каждом экшене делать?.. В зенд же есть хуки перед вызовом экшена?
              3) Разве это нельзя сделать в хуках доктрины prePersist?


              Не готов ответить на вопросы, попробую изучить его и переделать этот кусок.

              2) Почему в мире php не принято следовать REST? Ведь очень некрасиво выглядят эти постоянные проверки на то, что запрос POST?

              Наверняка это возможно, изучу этот вопрос и переделаю этот кусок кода.

              4) что это за магия?

              Пока в проекте нет работы с пользователями, по этому тут хардкодится ноль, в третьей части я прикручу юзеров и эта строчка будет заменена на айдишник текущего пользователя.

              7) Что за магическая 1?

              Это статус. 0 — не опубликовано, 1 — опубликовано. У вас есть предложение как убрать эту магическую константу?
                0
                4) можно было пока вообще не думать о юзере, а добавить его в дальнейшем потому, что магический ноль смущает.
                7)
                //class BlogPost
                const PUBLISHED = 1;
                const NOT_PUBLISHED = 0;
                
                ...
                
                $posts = $objectManager
                        ->getRepository('\MyBlog\Entity\BlogPost')
                        ->findBy(array('state' => BlogPost::PUBLISHED), array('created' => 'DESC'));
                


                Вообще, тут неплохо было бы вынести это в класс-репозиторий для BlogPost, чтобы было вроде:
                $posts = $objectManager
                        ->getRepository('\MyBlog\Entity\BlogPost')
                        ->findPublished();
                
              0
              Привет старым друпалерам :)

              Проблема в том, что неудобно (а в Twig-шаблонах, которые мы будем использовать позже, и вовсе невозможно) вызывать PHP-код для вывода сообщений. По этому мы напишем небольшой View-плагин, который сможет одной строчкой выводить на экран все сообщения.


              не знаю как в zf, а в symfony2 в твиге очень даже возможно вывести сообщения без дополнительных плагинов:
              {% for flashMessage in app.session.flashbag.get('notice') %} <div class="flash-notice"> {{ flashMessage }} </div> {% endfor %}

              symfony.com/doc/2.1/book/controller.html#flash-messages
                0
                Привет, Антон :)

                Проверил, вот такая конструкция в Твиг шаблоне работает:
                {% for flashMessage in flashMessenger().getMessages() %}
                  <div class="flash-notice"> {{ flashMessage }} </div>
                {% endfor %}
                


                Но вариант с view-плагином мне все же нравится больше:
                {{ showMessages() }}
                
                  0
                  Нервным не читать
                  У себя в проектах в скелет страницы вставляю шаблон
                      <span id="top-of-container">
                          {% if flash_error %}
                              {% include 'snippets/alert.danger.html.twig' %}
                          {% endif %}
                          {% if flash_warning %}
                              {% include 'snippets/alert.warning.html.twig' %}
                          {% endif %}
                          {% if flash_success %}
                              {% include 'snippets/alert.success.html.twig' %}
                          {% endif %}
                          {% if flash_console %}
                              <script type="text/javascript">
                                  {% for flash in flash_console %}
                                  console.info({{ flash|json_encode|raw }});
                                  {% endfor %}
                              </script>
                          {% endif %}
                      </span>
                  


                  знаю что *овнокод, но работает и ладно.

                  В коде достаточно сделать так:
                  ТО_ЧТО_УХОДИТ_В_ШАБЛОН_ТВИГА['flash_error'] = 'F*CK';
                  

                  или так:
                  ТО_ЧТО_УХОДИТ_В_ШАБЛОН_ТВИГА['flash_error'][] = 'F*CK';
                  


                  0
                  $blogpost->setCreated(time());
                  Для этого есть Timestampable из DoctrineExtensions.

                  Конфиги в виде многомерного массива подбешивают, ну есть же yaml/xml, не удобно ведь такие конфиги использовать…
                    0
                    Николай, я правильно из вашего комментария понял, что в Zend'е при помощи классов Zend\Config\Reader можно не только читать настройки своих кастомных модулей, но и прикрутить как замену application.config.php, *global.php, *local.php и конфигов модулей?
                    0
                    zend-developer-tools можно и нужно использовать на проде, если нет нормального мониторинга (пинбы например).

                    Для этого достаточно сделать другую среду(набор конфигов, в котором будет zdt) и в public создать еще один скрипт, на который завернуть поддомен с http-авторизацией или доступ по офисному(домашнему) ip (ex. Zend\Mvc\Application::init(require_once 'config/app/dev.config.php')->run();)
                      0
                      Не используйте плз вызовы классов через глобальное пространство имен ($form = new \MyBlog\Form\BlogPostForm())

                      Сделайте импорт перед объявлением кода класса (use MyBlog\Form\BlogPostForm) и уже в коде класса используйте плз $form = new BlogPostForm().
                        0
                        <?php

                        print $this->showMessages();

                        print '<h1>' . $post['title'] . '</h1>';

                        print '<div>' . $post['text'] . '</div>';

                        Что это? То есть либо twig, либо такая жесть?)

                        <?= $this->showMessage(); ?>

                        <h1><?= $post['title']; ?></h1>

                        <div><?= $post['text']; ?></div>

                        Поправьте плз, это же люди читают…
                          0
                          Не понимаю, что это у автора. Жесткая нехватка времени или лютое нежелание ответить кому-либо в лс? Раз 5 писал в ЛС, но человек не слышит.

                          Пока делал, наступил на грабли. В коде нужно кое-что поправить:

                          Когда мы создаем форму мы создаем поле с name = security:
                          Кроме того, я добавил к форме Csrf-токен (поле security), который должен защитить форму от подделки.
                          . В представлении нужном, это поле не выводится.

                          В итоге страница перезагружалась, но форма не отправлялась, так как не проходила валидацию. Сообщение об ошибке удалось получить только после танцев с бубнами. В итоге поправил view, добавив:
                          echo $this->formRow($form->get('security'));

                          Теперь все работает так, как нужно.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое