Пример разработки блога на Zend Framework 2. Часть 1. ZendSkeletonApplication

  • Tutorial
В последние несколько лет моя работа связана с использованием CMS Drupal, но на досуге я изучал и just for fun запускал проекты на питоновских фреймворках Django, Flask и Twisted. Сейчас я решил освоить основы двух-трех популярных PHP-фреймфорков и первыми я решил изучить Zend Framework 2 и Yii.

В процессе ознакомления с Zend Framework 2 я изучил туториал с официального сайта (http://framework.zend.com/manual/2.2/en/user-guide/overview.html), просмотрел документацию фреймворка (http://framework.zend.com/manual/2.2/en/index.html), прочитал книгу Michael Romer “Web development with Zend Framework 2” и собрал собственное тестовое приложение.

Переварив всю эту информацию, я пришел к мысли, что официальный туториал к фреймворку суховат:
  • в нем не рассказывается о работе с пользователями, сессиями и правами доступа,
  • лишь вскользь рассматривается такая основополагающая часть фреймворка как ServiceManager,
  • в качестве интерфейса с базой данных безальтернативно используется паттерн Table Gateway (и соответствующая его реализация в фреймворке),
  • используется встроенный в фреймворк шаблонизатор, который после питоновского Jinja 2 кажется совершенно неудобным и примитивным,
  • и т.д.

В итоге, более-менее удовлетворительное по функционалу приложение я смог создать после прочтения книги.

В этой статье я хочу привести пример разработки простого блога, в ней будет несколько отличий от официального туториала. В первую очередь я постараюсь заострить внимание на тех вопросах, которые во время изучения показались мне недостаточно раскрытыми в официальном туториале. Кроме того я буду использовать некоторые технологии, альтернативные тем, что используются в Zend фреймворке по умолчанию:
  • в качестве шаблонизатора будет использоваться Twig,
  • для работы с БД — Doctrine ORM,
  • для авторизации/аутентификации и распределения прав доступа я буду использовать существующие модули ZfcUser и BjyAuthorize,
  • также я рассмотрю вопросы разработки собственных валидаторов форм, View плагинов и другие.


Статья написана новичком (в Zend Framework) для новичков, поэтому приветствуется любая критика по делу и советы по усовершенствованию приложения.

Найти код проекта можно на Гитхабе: github.com/romka/zend-blog-example.

Статья будет разбита на 3 части:
  • в первой (текущей) части я рассмотрю структуру ZendSkeletonApplication,
  • во второй части разберу разработку собственного модуля (формы, работа с БД при помощи Doctrine ORM, разработка View-плагина),
  • третья часть будет посвящена работе с пользователями и шаблонизатором Twig.

Итак, приступим.

Окружение


Я предполагаю, что у вас уже есть настроенный веб-сервер (мой доступен по адресу zblog.kece.ru) и в MySQL создана пустая база данных под этот проект. Ввиду того, что для работы с БД я планирую использовать Doctrine, база данных может быть любой, поддерживаемой этой ORM.

Я предполагаю, что у вас под рукой есть исходный код ZendSkeletonApllication или моего туториала. Взять их можно тут: github.com/zendframework/ZendSkeletonApplication и тут: github.com/romka/zend-blog-example. Кроме того, я предполагаю, что вы понимаете паттерн MVC, имеете опыт работы с каким-либо шаблонизатором и валидатором форм.

Zend Framework использует замечательный менеджер зависимостей Composer, который также должен быть установлен в вашей системе. Подробнее о Композере можно прочитать вот в этой статье: habrahabr.ru/post/145946. Если говорить в двух словах, то Composer — это утилита командной строки, которая позволяет быстро и удобно скачать и установить внешние библиотеки, от которых зависит ваш PHP-проект. На входе утилита принимает JSON-файл в интуитивно понятном формате содержащий список имен и версий зависимостей, на выходе она скачивает и устанавливает нужные библиотеки и их зависимости освобождая вас от рутинной работы.

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

ZendSkeletonApplication


Используя Zend Framework вы можете с нуля спроектировать и создать структуру вашего приложения, но для изучения работы системы лучше воспользоваться заготовкой ZendSkeletonApplication. В случае если у вас настроен Composer достаточно выполнить команду:
php composer.phar create-project --repository-url="http://packages.zendframework.com" -s dev zendframework/skeleton-application path/to/install


(при должной настройке, команду php composer.phar можно заменить просто на composer, но далее в статье я буду приводить первый вариант как более универсальный)

После ее выполнения вы получите приложение со следующей структурой (я оставил только самые интересные директории и файлы):
config/
    autoload/
        global.php
    application.config.php
module/
    Application/
        <Исходный код модуля Application будет рассмотрен далее>
public/
    css/
    img/
    js/
    index.php
vendor/
    <Внешние библиотеки, в том числе исходный код Zend Framework>
composer.json
init_autoloader.php


Теперь вы можете открыть созданный проект. Моя версия доступна по адресу zblog.kece.ru.

Давайте детально разберем созданные директории и файлы.

Структура проекта


composer.json


Начнем с файла composer.json в корне проекта. Он имеет следующий вид:
{
    "name": "zendframework/skeleton-application",
    "description": "Skeleton Application for ZF2",
    "license": "BSD-3-Clause",
    "keywords": [
        "framework",
        "zf2"
    ],
    "homepage": "http:// framework.zend.com/",
    "require": {
        "php": ">=5.3.3",
        "zendframework/zendframework": ">2.2.0rc1"
    }
}

В этом файле задаются параметры приложения, которые будут использоваться Композером. Самой интересной частью этого конфига является секция require, которая содержит список внешних библиотек, от которых зависит приложение. Сейчас в списке зависимостей есть только Zend Framework 2, но в будущем, мы добавим сюда еще несколько зависимостей: Доктрину, Твиг и другие. После добавления новой зависимости достаточно будет в консоли исполнить команду:
php composer.phar update

и Композер скачает необходимые библиотеки. Все внешние зависимости складываются в директорию vendor, сейчас мы в ней можем увидеть директории zendframework и composer.

Document root


Document_root нашего приложения находится не в корне проекта, а в директории public, именно здесь находится файл index.php — точка входа в наш проект и именно на работу с этой директорией должен быть настроен веб-сервер.

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

Для подключения внешних библиотек исполняется файл init_autoloader.php из корня проекта, который в свою очередь вызывает vendor/autoload.php и вслед за ним автоматически сгенерированный Композером файл vendor/composer/autoaload_real.php. В этом файле определены автолоад-методы для загруженных Композером внешних библиотек. Таким образом, когда мы в коде наших модулей будем подключать неймспейсы вида Zend\View\Model\ViewModel PHP будет знать в каких файлах искать указанные неймспейсы.

Строчка кода:
Zend\Mvc\Application::init(require 'config/application.config.php')->run();

загружает конфигурационные файлы приложения и запускает его. В процессе инициализации приложения (вызов метода Zend\Mvc\Application::init()) создается ServiceManager — ключевой объект, использующийся во многих частях приложения. По умолчанию СервисМенеджер создает еще один ключевой объект — EventManager. В дальнейшем, когда мы будем создавать свои модули в их настройках мы будем сообщать СервисМенеджеру какие еще объекты необходимо создать для работы нашего модуля.

О СервисМенеджере мы еще поговорим позже, а сейчас давайте подробнее рассмотрим директорию config. В ней находится файл application.config.php, возвращающий массив с конфигурацией приложения, который может рассказать много интересного.

Массив modules содержит список включенных модулей. Сейчас у нас включен только один модуль Application, но в будущем их будет больше.

Массив module_listener_options содержит два интересных элемента — массивы module_paths и config_glob_paths:
  • module_paths подсказывает где искать подключаемые модули, по умолчанию в директориях vendor — внешние зависимости, и module — здесь будут находиться модули разработанные нами для нашего проекта.
  • Config_glob_paths содержит маски путей к конфигурационным файлам модулей. Регулярное выражение 'config/autoload/{,*.}{global,local}.php' означает, что будут загружены все файлы вида module_name.{global или local}.php, global.php, local.php из директории config/autoload.

Таким образом, первым делом загружаются настройки из файла config/application.config.php, затем загружаются настройки из файлов config/autoload/{,*.}{global,local}.php и в последнюю очередь загружаются настройки заданные на уровне модулей (об этом мы поговорим позже).

Если в config/autoload есть два файла настроек для одного и того же модуля: глобальный и локальный (module_name.global.php и module_name.local.php), то первым делом будет загружен глобальный файл, а за ним локальный, то есть локальные настройки имеют приоритет перед глобальными.

Файлы local.php и *.local.php по умолчанию включены в файл .gitignore (если вы используете другую систему контроля версий, то их нужно добавить в исключения вручную) и они должны использоваться только для настроек, соответствующих только текущему окружению. То есть, если у вас проект в том или ином виде запускается на нескольких разных площадках: production, тестовой и девелоперских площадках, то в глобальных настройках приложения нужно хранить данные, актуальные для всех перечисленных площадок, а в локальных — уникальные для каждой площадки, например, данные для доступа к БД.

Кроме глобальных для всего приложения конфигурационных файлов каждый из модулей может иметь свой файл настроек. Такие конфигурационные файлы модулей загружаются в том порядке, в котором определен список активных модулей в файле application.config.php. Таким образом, если вы хотите в своем модуле переопределить настройки другого модуля, то ваш модуль должен быть ниже в этом списке.

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

ServiceManager


В официальной документации (http://framework.zend.com/manual/2.2/en/modules/zend.service-manager.intro.html) сказано, что ServiceManager — это компонент, реализующий паттерн Service Locator, предназначенный для извлечения других объектов. Другими словами, это некоторая точка входа, которая позволяет из любого места приложения получить доступ к любым объектам, зарегистрированным в СервисМенеджере.

Регистрируются объекты в СервисМенеджере либо в кофигурационном файле module.config.php в секции service_manager, либо в Module.php в методе getServiceConfig(), выглядит это примерно так (пример скопирован из документации):
<?php
// a module configuration, "module/SomeModule/config/module.config.php"
return array(
    'service_manager' => array(
        'aliases' => array(
            // Здесь можно задать алиасы для зарегистрированных сервисов или для других алиасов
            'SomeModule\Model\User' => 'User',
        ),
        'factories' => array(
            // Ключ — имя сервиса,
            // Значение — либо имя класса, реализующего интерфейс FactoryInterface,
            // либо экземпляр класса, реализующего FactoryInterface,
            // либо любой PHP коллбэк
            'User'     => 'SomeModule\Service\UserFactory',
            'UserForm' => function ($serviceManager) {
                $form = new SomeModule\Form\User();

                // Retrieve a dependency from the service manager and inject it!
                $form->setInputFilter($serviceManager->get('UserInputFilter'));
                return $form;
            },
        ),
        'invokables' => array(
            // Ключ — имя сервиса,
            // значение — имя класса, экземляр которого должен быть создан.
            'UserInputFiler' => 'SomeModule\InputFilter\User',
        ),
        'services' => array(
            // Ключ — имя сервиса,
            // значение — объект.
            'Auth' => new SomeModule\Authentication\AuthenticationService(),
        ),
    ),
);

Имея приведенную выше конфигурацию СервисМенеджера в любом нашем контроллере теперь мы можем вызвать код вида:
$user_form = $this->getServiceLocator()->get('UserForm');

и объект $user_form будет содержать соответствующую форму.

Теперь настало время вернуться к модулю Application, входящему в ZendSkeletonApplication.

Модуль Application


Дерево каталогов этого модуля выглядит так:
config/
    module.config.php
language/
    <много *.po файлов с переводами>
src/
    Application/
        Controller/
            IndexController.php
view/
    application/
        index/
            index.phtml
    error/
        404.phtml
        index.phtml
    layout/
        layout.phtml
Module.php

Начнем с Module.php. Этот файл объявляет класс Module с 3 методами:
  1. onBootstrap();
  2. getConfig();
  3. getAutoloaderConfig().

Код метода onBootstrap() в этом модуле отвечает за обслуживание маршрутов типа Segment (о типах маршрутов чуть ниже).

Метод getAutoloaderConfig() сообщает ядру фреймворка где искать исходные коды модуля:
public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }

В текущем случае это директория modules/Application/src/Application.

В методе getConfig() возвращается путь к файлу настроек модуля:
return include __DIR__ . '/config/module.config.php';

Этим файлом возвращается массив с настройками, содержащий следующие данные:
  • элемент массива router — это массив содержащий список маршрутов, обслуживаемых модулем и список контроллеров, для каждого из маршрутов,
  • view_manager содержит пути к шаблонам, которые будут использоваться приложением и ряд дополнительных настроек шаблонизатора,
  • service_manager — список объектов, которые должны быть инициализированы СервисМенеджером,
  • ряд других настроек.

Как и в других MVC-фреймворках в Zend Framework используются понятия: маршрут (route), контроллер и действие (action). Маршрут определяет адрес страницы, контроллер и экшен — действия, которые будут выполнены для отображения страницы. Каждый контроллер — это класс, а действия — это методы с именем вида nameAction() в нём. Как следствие, каждый контроллер может содержать несколько действий, например, контроллер BlogPostController(), который мы создадим далее, будет содержать действия addAction(), editAction(), deleteAction(), viewAction() и indexAction().

В настройках модуля Application определяется два маршрута:
return array(
    'router' => array(
        'routes' => array(
            'home' => array(
                'type' => 'Zend\Mvc\Router\Http\Literal',
                'options' => array(
                    'route'    => '/',
                    'defaults' => array(
                        'controller' => 'Application\Controller\Index',
                        'action'     => 'index',
                    ),
                ),
            ),
            'application' => array(
                'type'    => 'Literal',
                'options' => array(
                    'route'    => '/application',
                    'defaults' => array(
                        '__NAMESPACE__' => 'Application\Controller',
                        'controller'    => 'Index',
                        'action'        => 'index',
                    ),
                ),
                'may_terminate' => true,
                'child_routes' => array(
                    'default' => array(
                        'type'    => 'Segment',
                        'options' => array(
                            'route'    => '/[:controller[/:action]]',
                            'constraints' => array(
                                'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                                'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                            ),
                            'defaults' => array(
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
);

  • За маршрут / (корень сайта, тип Literal) отвечает контроллер Application\Controller\Index.
  • за маршруты вида /application/[:controller[/:action]] (тип Segment) — контроллер и экшен переданные в качестве аргументов, то есть внутри модуля достаточно определить новые контроллеры и экшены и они тут же будут доступны по описанному адресу. Благодаря этому, маршрут / здесь идентичен маршруту /application/index/index.

Существует несколько типов маршрутов, их описание можно найти в документации: framework.zend.com/manual/2.2/en/modules/zend.mvc.routing.html.

Модуль Application содержит один единственный контроллер IndexController() с одним единственным экшеном indexAction(), который возвращает страничку с приветственным сообщением. Для отображения этой странички используются шаблоны, находящиеся в директории view модуля.

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

upd:
Вторая и третья части статьи.
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 28
  • +1
    некоторая точка входа, которая позволяет из любого места приложения получить доступ к любым объектам

    $user_form = $this->getServiceLocator()->get('UserForm');

    Не стоит его так использовать. Используйте DI.
    • +1
      Чем плох сервис менеджер в данном случае?
      • +1
        Добавляет еще одну зависимость (себя).
        Усложняет тестирование.
        Делает зависимости неявными.
        Ухудшает навигацию (конечно, можно обойти всякими /** var… */, но тогда засоряется код).
        • +1
          Очень распространенная проблема у новичков — злоупотребляют сервис менеджером.
          Мы сервис менеджер используем только в Module.php, где он выполняет роль DI, а контроллеры/вьюхелперы/сервисы даже не знают о его существовании.

          Тестирование, я б сказал, становится адом.
        • 0
          Кирилл, а вы можете привести практический пример использования DI в этом контексте? Честно говоря, дальше доки, в которой класс А зависит от класса Б я пока не продвинулся.
          • 0
            У вас будет вместо вызова локатора — объект формы как параметр конструктора, либо сеттера (зависит от конфигурации). И соответственно эти зависимости контролера/сервиса должны быть описаны (Zend\Di\Definition) в контексте DI (в zf из коробки есть авто связь (wire) по подсказкам типов, как вариант (RuntimeDefinition) ).
        • 0
          А не могли бы привести пример в виде кода? Меня даже больше интересует не сам DI — с ним более менее ясно, а тесты. Что именно тестируется и как. Давно ищу какую-нибудь толковую статью по тестированию ZF2. Интересуют тесты не самого ZF2, а бизнес логики, конечно.
          • 0
            Тут не особо зависит zf2 или что-то другое. Общий принцип такой:

            public function test_something_should_do_something()
            {
                $dependency = Mockery::mock('SomeBusinessServiceClass')
                                ->shouldReceive('someMethod')
                                ->with('someParams')
                                ->once()
                                ->getMock();
            
                $object_under_test = new ObjectUnderTest($dependency);
                
                $object_under_test->someSuperMethod();
            }
            


            Т.е. тестируется взаимодействие с зависимостью (при вызове someSuperMethod должен вызваться someMethod с параметрами). Так же можно делать фейковые возвраты при помощи andReturn. В примере библиотека моков — Mockery.
            • 0
              Это я понимаю и видел много примеров уровня «hello word». Интересно бы было посмотреть тесты из реального приложения, желательно большого и со сложной бизнес логикой. Буду рад, если сможете показать куски кода из каких-нибудь ваших проектов или дадите ссылку на статью, где это приводится.
              • 0
                Из реального проекта привести не могу, но могу порекомендовать книгу «Growing object-oriented software, guided by test».
      • 0
        Тем что это SL, а не DIС. Это ничем не лучше синглтонов.
        А может и даже хуже, с синглтоном я хоть быстро найду что за «UserForm».
  • +3
    Зендовский роутинг пугает :)
    • 0
      А что конкретно пугает? Роутинг довольно гибкий
      • 0
        то что на 2 маршрута ушло 40 строк
  • +2
    А можно я тут немного попиарюсь :)? Если вам интересен Twig под ZF2 попробуйте вот этот модуль: он очень классно покрыт тестами, и вообще просто замечательно и без всяких проблем работает (установка возможна как вручную, так и через Composer).
    • 0
      Я до сих пор не понимаю шаблонизаторов под php. Short tags и альтернативный синтаксис — вот тебе и шаблонизатор. Тем более Zend\View\Renderer\PhpRenderer довольно мощный, по сравнению с Zf1.

      Можете пояснить, в чем преимущества использования шаблонизатора в разрезе php?

      • +1
        Вот плюсы, которые выделяю для себя я:
        1. я точно уверен, что верстальщик не поломает PHP-код внутри шаблона,
        2. я точно уверен что ленивый программист не перенесет часть логики в шаблон (встречал я и запросы к БД в шаблонах),
        3. по мне, код Twig-шаблонов получается более компактным и читабельным.

        Да, против первых двух пунктов есть тестирование и код ревью, но мне удобнее недопустить появления этих проблем.
        • 0
          Твиг не использовал. Возникли вопросы:

          я точно уверен, что верстальщик не поломает PHP-код внутри шаблона

          что произойдет если верстальщик поломает разметку Твига?

          я точно уверен что ленивый программист не перенесет часть логики в шаблон (встречал я и запросы к БД в шаблонах)

          имхо, от такого программиста можно ожидать вещей пострашнее, чем запросы в шаблонах
      • +1
        самый большой плюс для меня — автоматическая защита от XSS. Так же в twig мне очень нравится идея наследования и переопределения блоков.
        • 0
          автоматическая защита от XSS

          будет ли XSS в таком случае:

          <script>{{variable}}</script>
          

          ?
          • 0
            в таком может быть. Но это, возможно, тот случай когда это желаемое поведение
            зато точно не будет в таком

             <td>{{variable}}</td>
            
            • 0
              В общем, расслабляться полностью нельзя.
  • 0
    используется встроенный в фреймворк шаблонизатор

    Что за встроенный в ZF2 шаблонизатор?
    • 0
      видимо имеется в виду short tags + альтернативный синтаксис
  • 0
    skeleton-application есть на packagist.org, поэтому можно обойтись без --repository-url=«packages.zendframework.com» при создании проекта

    composer search zendframework/skeleton-application

  • 0
    Зря в офф. туториале не упомянули про кеширование конфигов.

    В module_listener_options обязательно включите кеширование конфигов или все это чудо будет мержиться на лету :-)

    'cache_dir' => 'data/cache',

    'config_cache_key' => 'your_key',

    'config_cache_enabled' => true
    • 0
      а еще генерация классмапов и темплейт мапов
  • 0
    Заранее прошу прощения у фанов ZF2.

    Очень правильно сделали, что
    первыми решил изучить Zend Framework 2
    Я сейчас после Symfony 2 пытаюсь изучить Zend Framework 2 — это ужасно. Если изучая первый я восхищался как же все хорошо сделано, то с ZF2 я прикладываю всю свою силу воли. чтобы не застрелиться.

    В итоге, более-менее удовлетворительное по функционалу приложение я смог создать после прочтения книги.
    Я бы эту фразу выделил жирными красными буквами. Довольно сложное приложение с кучей сервисов на Symfony 2 я написал после прочтения официльного туториала. Ну и по ходу подглядывал в их же справочник и Поваренную книгу.

    Кто-то мне объяснит почему Zend Framework 2 популярнее чем Symfony 2?

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