Разработка приложения на Webasyst

Уроки » №1. Разработка приложения на основе PHP-фреймворка Webasyst на примере приложения «Гостевая книга»

TLDR: Исходный код готового приложения «Гостевая книга» на GitHub.

0. Режим разработки

Перед началом разработки убедитесь, что в вашей установке фреймворка Webasyst включен режим разработки debug_mode (включение этого режима отключает все механизмы кеширования во фреймворке). Режим разработки можно включить либо в настройках приложения «Инсталлер», либо в файле wa-config/config.php:

'debug' => true

1. Создание файловой структуры приложения

Для начала работы вам потребуется установленный на вашем компьютере или сервере фреймворк Вебасист. Скачать и установить его можно из инструкции по установке.

В качестве идентификатора приложения (APP_ID) возьмем guestbook и создадим в каталоге wa-apps/ подкаталог с таким же названием — wa-apps/guestbook/. Это будет основным местом хранения файлов приложения: PHP-файлов, шаблонов, стилей, скриптов JavaScript и т. п. Важно, чтобы название этого подкаталога совпадало с идентификатором приложения.

Беке́нд (административная часть, backend) приложения будет отображать список всех записей в гостевой книге в хронологическом порядке с возможностью удаления, а фронте́нд (общедоступная часть, frontend) — список записей с возможностью опубликовать новую запись.

Создадим в каталоге wa-apps/guestbook/ несколько подкаталогов, следуя рекомендациям по формированию файловой структуры приложений Вебасиста:

wa-apps/guestbook/img/ — файлы изображений
wa-apps/guestbook/lib/ — PHP-скрипты (конфигурация, модули)
wa-apps/guestbook/lib/config/ — конфигурационные файлы
wa-apps/guestbook/templates/ — HTML-шаблоны (в Вебасисте по умолчанию используется шаблонизатор Smarty)
wa-apps/guestbook/locale/ — файлы локализации приложения (в Вебасисте по умолчанию используется механизм локализации gettext)

2. Описание приложения

Создаем основной файл с описанием приложения: wa-apps/guestbook/lib/config/app.php (имя файла должно быть именно таким). Добавим в него следующий код:

<?php
return array(
  'name' => 'Guestbook', // название приложения
  'img'  => 'img/guestbook.png' // относительный путь к файлу иконки приложения
);

Название приложения и путь к файлу иконки — это обязательные параметры файла описания приложения наряду с его APP_ID. Кроме них, файл описания может содержать и другие параметры (см. подробнее о конфигурации приложений).

Иконка приложения должна иметь размер 48x48 пикселей. Так как приложения Вебасиста могут полностью изменять внешний вид бекенда, включая цвет фона, то в качестве иконки рекомендуется использовать PNG-файл с полупрозрачным фоном, чтобы изображение хорошо смотрелась на фоне разных цветов: от белого до черного. Файл иконки приложения размещаем по адресу, указанному в конфигурационном файле: wa-apps/guestbook/img/guestbook.png.

Теперь все готово для того, чтобы приложение появилось в главном меню бекенда. Для этого необходимо «включить» его в системном конфигурационном файле фреймворка wa-config/apps.php, добавив туда следующую строку:

'guestbook' => true

После добавления строки сохраняем файл и открываем бекенд фреймворка в браузере. Интерфейс приложения теперь доступен по адресу http://{адрес_установки_вебасиста}/webasyst/guestbook/. Если у вас есть полные административные права, то вы сразу увидите иконку приложения в верхнем меню Вебасиста. Приложение уже можно запускать, но оно ещё ничего «не умеет» делать.

3. Hello world!

Для начала создадим простейшую страницу, чтобы наше приложения что-нибудь отображало в браузере, например, классическое сообщение «Hello world!».

Для этого необходимо написать отдельный экшен (action). Экшен — это структурный элемент некоторого действия, выполняемого приложением. Примеры таких действий: показать отдельную страницу бекенда, показать экран фронтенда, сохранить данные из формы регистрации, удалить запись, получить новую порцию данных с помощью AJAX и т. п. Экшен представляет собой PHP-код, оформленный в виде отдельного метода класса или целого класса. Фреймворк запускает тот ли иной экшен в зависимости от входных данных (содержимого GET- или POST-запроса). Выбор конкретного экшена выполняется контроллером.

Логически объединенные контроллеры и экшены называются модулями. Например, модуль работы с заказами в некотором приложении может предоставлять следующий набор экшенов: показать список всех заказов, удалить заказ, добавить заказ, обновить информацию о заказчика, изменить статус заказа и т. д. Модули являются структурными «кирпичиками» приложения, их исходный код располагается в каталоге wa-apps/{APP_ID}/lib/actions/.

Каждый модуль — это либо 1) отдельный PHP-файл с именем вида {APP_ID}{MODULE}.actions.php, содержащий код класса с именем вида {APP_ID}{MODULE}Actions, унаследованного от класса waViewActions и реализующего отдельные экшены, либо 2) целый подкаталог с набором PHP-файлов, содержащих логику выполнения экшенов.

В данном примере мы рассмотрим первый вариант — реализацию модуля в виде отдельного класса с набором методов, представляющих собой отдельные экшены. При таком подходе экшены в классе необходимо определить в качестве методов (функций), именованных по правилу {action}Action. Более подробное описание обоих подходов представлено в полной статье о правилах объявления экшенов и контроллеров.

Общая схема работы Вебасист-приложения выглядит следующим образом:

  1. Системный класс фреймворка FrontController находит подходящий модуль в соответствии со входными данным (полученными из GET-/POST-запросов). Название модуля обычно передается в виде отдельного параметра, например: /?module=orders&action=add (в бекенде) или по правилам маршрутизации (во фронтенде). Если модуль не указан явно, то фреймворк автоматически ищет модуль backend для бекенда либо frontend — для фронтенда.
  2. Контроллер модуля определяет (также на основании входных параметров), какой экшен нужно запустить, и передает ему управление. Если в параметрах запроса экшен не указан, то запускается экшен по умолчанию — default.

Сложные приложения обычно состоят из нескольких модулей, но для создания простой гостевой книги достаточно написать всего один модуль — модуль по умолчанию backend. Определим в этом модуле реализацию двух экшенов: default («показать все записи» — это будет экшен по умолчанию) и delete («удаление записи»). Следуя описанным выше правилам, создаем файл wa-apps/guestbook/lib/actions/guestbookBackend.actions.php и определяем в нем класс guestbookBackendActions:

<?php
class guestbookBackendActions extends waViewActions
{
  public function defaultAction()
  {
    // реализация экшена, отображающего список записей на странице (в данном примере это
    // действие по умолчанию)
  }
  public function deleteAction()
  {
    //реализация экшена удаления записи
  }
}

Оставим пока реализацию экшенов пустой и вернемся к ней в п. 6, когда будет создана структура базы данных для хранения записей гостевой книги.

Шаблон

Создадим простейший HTML-шаблон для бекенда приложения, а именно для экшена default. Шаблон будет отображать в браузере строку «Hello world!».

Файлы шаблонов для экшенов должны находиться в каталоге wa-apps/{APP_ID}/templates/actions/{MODULE}/ и иметь имя, сформированное по правилу {MODULE}{ACTION}.html. Файл, названный согласно этому правилу, фреймворк автоматически подключит после выполнения кода экшена. В нашем случае необходимо создать файл wa-apps/guestbook/templates/actions/backend/BackendDefault.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>{$wa->appName()} - {$wa->accountName()}</title>
  {$wa->css()}
  <script src="{$wa->url()}wa-content/js/jquery/jquery-1.4.2.min.js"
  type="text/javascript"></script>
</head>
<body>
  <div id="wa">
    {$wa->header()}
    <div id="wa-app">
      <div class="block">Hello world!</div>
    </div>
  </div>
</body>
</html>

Этот пример содержит каркас HTML-кода бекенда, который можно использовать в качестве основы интерфейса большинства приложений Вебасист. В шаблоне доступен хелпер $wa, предоставляющий следующие методы:

$wa->appName() возвращает название приложения;
$wa->accountName() возвращает название аккаунта, обычно отображаемое в левом верхнем углу бекенда;
$wa->css() добавляет HTML-код для подключения общих стилей Вебасиста (подробнее об интерфейсе Вебасиста);
$wa->js() добавляет HTML-код для подключения JavaScript-файлов, необходимых для работы Вебасиста;
$wa->url() возвращает URL, по которому установлен Вебасист;
$wa->header() добавляет HTML-код главного меню со списком установленных приложений.

Обновите страницу браузера с приложением guestbook. Теперь на экране будет показана надпись «Hello world!»:

4. База данных

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

Для хранения записей гостевой книги создадим таблицу в базе данных MySQL. Для этого создадим файл с описанием структуры таблиц wa-apps/guestbook/lib/config/db.php, в который добавим описание таблицы guestbook:

<?php
return array(
    'guestbook' => array(
        'id' => array('int', 11, 'null' => 0, 'autoincrement' => 1),
        'name' => array('varchar', 255, 'null' => 0),
        'text' => array('text', 'null' => 0),
        'datetime' => array('datetime', 'null' => 0),
        ':keys' => array(
            'PRIMARY' => 'id',
            'datetime' => 'datetime',
        ),
    ),
);

Это описание необходимо для установки приложения (например, на другом сервере или доменном имени), а так же при удалении приложения, чтобы удалить ненужные таблицы. Названия всех таблиц приложения должны начинаться с его APP_ID (в нашем случае это идентификатор guestbook), чтобы не возникали конфликты имен с таблицами других приложений.

Определяем модель для работы с данными. Для этого создаём файл wa-apps/guestbook/lib/models/guestbook.model.php со следующим содержимым:

<?php
class guestbookModel extends waModel
{
  protected $table = 'guestbook'; // название таблицы
}

Этот код позволит приложению работать с данными из таблицы guestbook, используя модель фреймворка Вебасист для обращения к базе данных (подробнее о модели работы с базой данных ).

5. Фронтенд

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

Контроллер фронтенда

Фронтенд создается аналогично тому, как был добавлен экшен, отображающий строку «Hello world!» в бекенде: создаем файл контроллера для фронтенда wa-apps/guestbook/lib/actions/guestbookFrontend.action.php.

Во фронтенде гостевой книги будет только одна страница, которая будет показывать список всех записей в гостевой книге и обрабатывать POST-запросы добавления новых записей. Поэтому вместо определения defaultAction мы используем упрощенный подход: объявим класс контроллера, унаследованный от системного класса waViewAction (вместо waViewActions), и опишем в нем метод execute, реализующий всю логику фронтенда. Такой подход применяется специально для модулей с единственным экшеном. Содержимое файла контроллера фронтенда guestbookFrontend.action.php:

<?php
class guestbookFrontendAction extends waViewAction
{
  public function execute()
  {
    // создаем экземпляр модели для получения данных из БД
    $model = new guestbookModel();
    // если получен POST-запрос, то нужно нужно добавить новую запись в БД
    if (waRequest::method() == 'post') {
      // получаем данные из POST-запроса
      $name = waRequest::post('name');
      $text = waRequest::post('text');
      if ($name && $text) {
        // добавляем новую запись в таблицу
        $model->insert(array(
          'name' => $name,
          'text' => $text,
          'datetime' => date('Y-m-d H:i:s')
        ));
      }
    }
    // получаем записи гостевой книги из БД
    $records = $model->order('datetime DESC')->fetchAll();
    // передаем данные в шаблон
    $this->view->assign('records', $records);
  }
}

Шаблон для фронтенда

Создадим HTML-шаблон (с использованием синтаксиса Smarty) для единственного экшена фронтенда в файле wa-apps/guestbook/templates/actions/frontend/Frontend.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>[`Guestbook`]</title>
  <style>
    .post h3 { margin-bottom: 0; }
    .post span { color: gray; font-size: 0.9em; }
    .post p { margin-top: 0; }
  </style>
</head>
<body>
  <h1>[`Guestbook`]</h1>
  <form method="post" action="">
    [`Name`]:<br />
    <input name="name" type="text"><br />
    [`Message`]:<br />
    <textarea rows="4" cols="70" name="text"></textarea><br />
    <input type="submit" value="[`Post`]">
  </form>
  {foreach from=$records item=r}
    <div class="post">
      <h3>{$r.name|escape}</h3>
      <span>{$r.datetime|wa_datetime}</span>
      <p>{$r.text|escape|nl2br}</p>
    </div>
  {/foreach}
</body>
</html>

Обратите внимание на то, что все строки «обернуты» в конструкцию [` `]. Эта конструкция используется для автоматического перевода интерфейса приложения на язык, выбранный пользователем, который используется по умолчанию во всех шаблонах бекенда и фронтенда (см. подробнее об этом в п. 7 ниже). Если интерфейс вашего приложения планируется отображать только на одном языке, то конструкцию [` `] можно не использовать.

Настройка маршрутизации (routing)

Чтобы фреймворк «знал» о наличии фронтенда у вашего приложения, в конфигурационном файле приложения wa-apps/guestbook/lib/config/app.php нужно добавить строку

'frontend' => true

Кроме того, необходимо «привязать» фронтенд к определенному адресу (URL). Для этого добавляем следующее правило в системный конфигурационный файл маршрутизации wa-config/routing.php:

'guestbook' => array(
  'url' => '*', // URL, по которому будет открываться гостевая книга; можно также указать значение guestbook/*
  'app' => 'guestbook',
  'module' => 'frontend'
)

Если в вашей установке фреймворка нет такого файла, создайте его со следующим содержимым:

<?php
return array(
  // роутинг для всех хостов
  'default' => array(
    'guestbook' => array(
    'url' => '*',
    'app' => 'guestbook',
    'module' => 'frontend'
    )
  )
);

Указав правило 'url' => '*' в конфигурационном файле маршрутизации, вы «поселите» гостевую книгу в корень установки Вебасиста: открыв в браузере адрес http://{адрес_установки_вебасиста}/, вы увидите фронтенд гостевой книги. Если вместо '*' в правиле указать, например, 'guestbook/*', то гостевая книга будет открываться по адресу вида http://{адрес_установки_вебасиста}/guestbook/.

6. Бекенд

Возвращаемся к экшенам default и delete, определенным ранее в п. 3.

Настройка прав доступа

Для демонстрации того, как легко можно реализовать тонкую настройку прав доступа для пользователей бекенда, покажем это на примере функции удаления записей гостевой книги. Сообщим системе, что наше приложение поддерживает дополнительную настройку прав доступа (помимо стандартных значений «есть доступ» и «нет доступа»). Для этого в конфигурационном файле приложения wa-apps/guestbook/lib/config/app.php добавляем строку

rights => true

Затем описываем структуру прав в отдельном файле wa-apps/guestbook/lib/config/guestbookRightConfig.class.php (имя файла должен быть именно таким).

<?php
class guestbookRightConfig extends waRightConfig
{
  public function init()
  {
    // объявляем элементы настройки прав доступа
    $this->addItem('delete', 'Can delete posts', 'checkbox');
  }
}

Таким образом мы устанавливаем следующие значения:

'delete' — ключ права (название экшена)
'Can delete posts' — название действия, отображаемое на странице настройки прав пользователя (в приложении «Контакты»)
'checkbox' — тип элемента HTML-формы, который используется для настройки прав доступа

После добавления этого кода права на удаление записей для текущего пользователя можно будет проверять в экшенах приложения с помощью конструкции $this->getRights('delete'), как показано ниже в контроллере бекенда.

Контроллер бекенда

Открываем снова файл контроллера wa-apps/guestbook/lib/actions/guestbookBackend.actions.php и добавляя реализацию ранее созданных экшенов default и delete:

<?php
class guestbookBackendActions extends waViewActions
{
  /**
  * Действие по умолчанию, вывод всех записей из гостевой книги
  * URL: guestbook/
  */
  public function defaultAction()
  {
    // создаем экземпляр модели для получения данных из БД
    $model = new guestbookModel();
    // получаем записи гостевой книги из БД
    $records = $model->order('datetime DESC')->fetchAll();
    // передаем записи в шаблон
    $this->view->assign('records', $records);
    // передаём URL фронтенда в шаблон
    $this->view->assign('url', wa()->getRouting()->getUrl('guestbook'));
    /*
    * передаём в шаблон права пользователя на удаление записей из гостевой книги
    * права описаны в файле lib/config/guestbookRightConfig.class.php
    */
    $this->view->assign('rights_delete', $this->getRights('delete'));
  }
  /**
  * удаление записи из гостевой книги
  * URL: guestbook/?action=delete&id=$id
  */
  public function deleteAction()
  {
    // если у пользователя есть права на удаление записей из гостевой книги
    if ($this->getRights('delete')) {
      // получаем id удаляемой записи
      $id = waRequest::get('id', 'int');
      if ($id) {
        // удаляем запись из таблицы
        $model = new guestbookModel();
        $model->deleteById($id);
      }
    }
    // перенаправление пользователя на главную страницу приложения
    $this->redirect(wa()->getAppUrl());
  }
}

Дополняем HTML-шаблон бекенда wa-apps/guestbook/templates/actions/backend/BackendDefault.html, заменяя отображение строки «Hello world!» на вывод данных, переданных в шаблон в коде экшена:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>{$wa->appName()} - {$wa->accountName()}</title>
  {$wa->css()}
  <script src="{$wa_url}wa-content/js/jquery/jquery-1.4.2.min.js"
  type="text/javascript"></script>
</head>
<body>
  <div id="wa">
    {$wa->header()}
    <div id="wa-app">
      <div class="block">
        <ul class="zebra">
        {foreach from=$records item=r}
          <li>
          {if $rights_delete}<a class="count" href="?action=delete&id={$r.id}">
            <i class="icon16 remove-red"></i></a>{/if}
            <span class="hint">
              <strong>{$r.name|escape}</strong>
              {$r.datetime|wa_datetime}
            </span>
            {$r.text|escape|nl2br}
          </li>
        {/foreach}
        </ul>
        <a href="{$url}" target="_blank">[`Guestbook on site`]</a>
        <i class="icon10 new-window"></i>
      </div>
    </div>
  </div>
</body>
</html>

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

7. Локализация

В Вебасисте используется механизм локализации gettext или (если gettext на сервере не установлен) его PHP-реализация, встроенная во фреймворк (подробнее о локализации).

В коде HTML-шаблонов, приведенных в этом руководстве, используются строки на английском языке. Строки, указанные внутри конструкции [` `], является ключами локализации. Чтобы перевести интерфейс гостевой книги на русский язык, необходимо создать файл wa-apps/guestbook/locale/ru_RU/LC_MESSAGES/guestbook.po и скомпилировать его с помощью POedit — кроссплатформенной программы для перевода файлов локализации gettext.

С помощью POedit в файле locale/ru_RU/LC_MESSAGES/guestbook.po необходимо добавить пары «ключ — значение», где ключ представляет собой строку из HTML-шаблона, а значение — перевод на русский язык. Вебасист автоматически подключит локализацию, если у приложения есть файл с переводом.

Ниже приведен пример файла локализации гостевой книги на русский язык wa-apps/guestbook/locale/ru_RU/LC_MESSAGES/guestbook.po:

msgid ""
msgstr ""
"Project-Id-Version: Guestbook\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2011-04-27 10:30+0300\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=((((n%10)==1)&&((n%100)!=11))?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));\n"
"X-Poedit-Language: Russian\n"
"X-Poedit-Country: Russian Federation\n"
"X-Poedit-SourceCharset: utf-8\n"
"X-Poedit-Basepath: .\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPath-1: .\n"
msgid "Guestbook"
msgstr "Гостевая книга"
msgid "Guestbook on site"
msgstr "Гостевая книга на сайте"
msgid "Name"
msgstr "Имя"
msgid "Message"
msgstr "Сообщение"
msgid "Post"
msgstr "Написать"
msgid "Can delete posts"
msgstr "Может удалять записи"

Если вы используете фреймворк с веб-сервером Apache на сервере под управлением Windows, то после компиляции файла локализации может потребоваться перезапуск Apache.

8. Готово!

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