Общие сведения

Что такое Zippy?

Zippy – компонентный событийно- ориентированный фреймворк на PHP, предназначенный для быстрой разработки сайтов. Zippy легок для изучения, обеспечивает полное отделение дизайна от логики, а также отделение бизнес-логики от функций обеспечения жизненного цикла и состояния элементов страницы.

Главные особенности

  • Небольшой размер.
  • Полное отделение дизайна от бизнес-логики.
  • Использование пространства имен для разделения классов и модулей.
  • Компонентная, легко расширяемая структура.
  • Интуитивно понятная архитектура фреймворка и компонентов (особенно для разработчиков имеющих опыт с RAD средами типа Delphi, ASP.NET).
  • Автоматическое сохранение состояния страницы при перезагрузке (включая деревья).
  • Прозрачная, не требующая дополнительного кодирования, поддержка Ajax.
  • Русская документация.
  • Библиотека компонентов ZCL.

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

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

Основные архитектурные решения по разделению логики и представления заимствованы с фреймворка Wicket (в некотором смысле можно рассматривать как портинг на PHP), а также (прямо или косвенно) ряд идей с таких решений как: JSF, ASP.NET, Delphi for PHP и подобных компонентно-ориентированных систем.

Для демонстрации возможностей фреймворка разработана Zippy CMS - система управления контентом с модульной архитектурой.

Сайты, сделанные на основе фреймворка:

Установка и настройка

Установка фреймоврка производится с помощью Composer.

composer require leon-mbs/zippy

В заголовке страницы указать
<link rel="stylesheet" href="vendor/leon-mbs/zippy/assets/css/allzippy.min.css">
<script src="vendor/leon-mbs/zippy/assets/js/allzippy.min.js" type="text/javascript">
  Эти файлы уже включают необходимые фронт-энд библиотеки - JQuery, Twitter Bootstrap, Awesome font и необходимые плагины.

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

Архитектура

Общие сведения

Основным элементом сайта, построенного с использованием Zippy, является страница. Страница состоит из PHP класса - наследника от класса WebPage (бэкенд) и HTML файла (фронтэнд). Для каждого элемента (HTML тэга) страницы, связанного с бизнес-логикой, создается экземпляр соответствующего класса (Zippy-компонент) в PHP коде.

Пример:

Код HTML:

  <body>
   <span zippy="msg"></span>
     <a  zippy="onmsg">Клик</a>
   </body>
         

Код PHP:

<?php

use \Zippy\Html\Label;
use \Zippy\Html\Link\ClickLink;

class Example1 extends \Zippy\Html\WebPage
{
        //В конструкторе  создаем  экземпляры компонентов
        public function __construct($params = null) {
                $this->add(new Label('msg'));
                $this->add(new ClickLink('onmsg', $this, 'OnClick'));
        }
        // Обработчик  клика  по  ссылке
        public function OnClick($sender) {
                //Присваиваем  текст для вывода  в  тэге  span
                $this->msg->setText("OK");
        }
}
?>         

Компонент обеспечивает рендеринг в HTML, сохранение состояния (в том числе для элементов ввода) а также вызов обработчиков событий возникающих при навигации пользователя по странице. Связь между компонентом и HTML представлением обеспечивается с помощью атрибута "zippy" в соответствующих HTML тэгах. При создании экземпляра компонента в его конструктор передается значение атрибута "zippy" которое присваивается полю id определенному в классе HtmlComponent, от которого наследуются все компоненты.
  Таким образом HTML код не включает в себя никаких скриплетов и прочих чужеродных вставок. При рендеринге компонент манипулируя тегом изменяет его значение и/или значение его атрибутов, отображая таким образом свои данные в выходном HTML потоке. Важное условие - иерархия компонентов страницы строго соответствует вложенности соответствующих (с атрибутом "zippy") HTML тэгов. Если, например ссылка или другой элемент находится в форме и для формы как и для элемента необходимо создать соответствующий серверный компонент то компонент ссылки должен быть добавлен к форме вызовом метода Add предварительно созданного объекта формы.
  Связь между классом страницы и соответствующим HTML файлом шаблона задается разработчиком сайта что позволяет избежать жесткой структуры каталогов а также легко реализовать сменяемый дизайн и/или локализацию.Жизненный цикл страницы и навигация между страницами обеспечивается классом WebApplication в зависимости от анализа запросов браузера к серверу.

Жизненный цикл страницы

При первом открытии страницы приложение создает экземпляр класса страницы. В конструкторе страницы создаются экземпляры всех компонентов страницы, выполняется биндинг и назначение обработчиков событий. Затем экземпляр класса страницы сериализуется со всем содержимым и записывается в сессионное хранилище. После этого приложение загружает HTML шаблон с места на диске, указанного разработчиком в функции getTemplate, парсит его и вызывает метод рендеринга класса страницы. Класс страницы рендерит все дочерние компоненты. Компонент изменяет соответствующий ему (связанный через атрибут zippy) HTML тэг корректируя его содержание и/или атрибуты. После рендеринга страницы приложение отправляет измененный HTML код браузеру.

При запросе со страницы (например клик по ссылке) приложение десериализует экземпляр класса страницы из сессии и находит компонент инициатор запроса (ссылка, кнопка и т.д.). Компонент активизирует связанный с ним пользовательскую функцию – обработчик событияб которым обычно является метод класса страницы, поскольку это позволяет иметь доступ ко всем компонентам страницы и таким образом реализовать всю бизнес логику связанную с поведением страницы, обменом данных и т.д. Затем экземпляр страницы, (возможно с измененным состоянием) сохраняется в сессионном хранилище, выполняется рендеринг и отправка ответа в браузер. Сессионное хранилище сохраняет историю изменения страниц что позволяет отдавать страницу с соответствующим "историческим" состоянием при навигации браузера кнопками "вперед"/"назад".

diagram

Основные компоненты

WebApplication

Класс приложения. Выполняет разбор HTTP запроса, управляет жизненным циклом страницы и формирует ответ для клиента. Для использования создать экземпляр приложения и задать минимум одну функцию загрузки шаблона по имени класса страницы. Функция обработки устанавливается методом setTemplate и может задаватся строковым именем или лямбдой. Входным параметром является имя класса страницы, выходным содержание шаблона. Управляя загрузкой шаблона можно легко реализовать сменяемые темы сайта.

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

WebPage

Класс страницы. Для создания класса необходимо расширить абстрактный класс WebPage. В конструкторе создаются все компоненты страницы. Иерархия компонентов должна строго соответствовать вложенности тэгов с атрибутом zippy в шаблоне. Как правило, в классе страницы создаются функции обработчиков событий а также реализуется логика работы страницы. Экземпляры компонентов страницы создаются один раз при первом обращении к странице. Отслеживать жизненный цикл страницы можно переопределив методы beforeRequestHandle(), afterRequestHandle() и пр.

HtmlComponent

Базовый класс для всех компонентов. Каждый компонент имеет уникальный в пределах страницы номер элемента (поле id) , соответствующий аттрибуту zippy из соответствующего тэга HTML шаблона. Каждый компонент расширяющий базовый класс должен реализовать метод RenderImpl() который отвечает за рендеринг (прорисовку) компонента на странице путем изменения HTML тэга связанного с данным компонентом. Свойство attributes позволяет управлять атрибутами HTML тэга. Содержимое тэга задается классами потомками в зависимости от его типа.

HtmlContainer

Базовый класс контейнера для компонентов. Контейнерами являются: класс страницы, класса HTML формы, панели (обычно тэг DIV) и прочие компоненты которые могут содержать в себе другие объекты типа HtmlComponent (то есть HTML тэги которые могут содержать вложенные тэги). Сам HtmlContainer также является наследником от HtmlComponent. В классе перегружены методы __set() и __get() поэтому к вложенным компонентам можно обращаться используя динамическое свойство совпадающее с ID компонента. Например:


<form zippy="form1">
<input  type="text" zippy="username">
</form>

$form = new Form();
$form = add(new TextInput('username'));
...
$form->username->getText();

Диаграмма иерархии основных компонентов

diagram

Label

Служит для вывода текстовых данных на странице. Как правило отображается с помощью тэга SPAN но можно использовать TD, DIV и прочие которые могут иметь текстовое содержимое внутри тега.

Фреймворк содержит несколько разновидностей компонентов для HTML ссылок. Все они привязываются к тэгу <a> шаблона. Наиболее используемые:

  • ClickLink - служит для вызова обработчика события в классе cтраницы без возможности сделать закладку в браузере (поскольку это не имеет смысла - ссылка не ведет на какой либо ресурс а просто вызывает обработчик.)
  • BookmarkableLink - используется для внешнего перехода и возможности создать закладку.
  • RedirectLink - используется для редиректа на другую страницу. Может быть bookmarkable или нет.
  • SubmitLink - отправляет форму на сервер, c возможностью вызвать обработчик события по отправке формы

Компоненты формы

Компоненты соответствуют тегам элементов ввода HTML формы. Для принятия данных по отправке формы компоненты реализуют метод getRequestData() в котором считывают «свои» данные с $_POST или $_GET переменных. При рендеринге как и другие визуальные компоненты форматируют тэги в соответствии со своими значениями. Если значения не были изменены, отображаются данные что были при вводе формы. Таким образом автоматически сохраняется состояние всех элементов формы - полей ввода, переключателей и т.д при перезагрузке страницы.

Интерфейсы

Для взаимодействия между собой компоненты существует набор интерфейсов которые должен реализовать тот или иной компонент. Наиболее используемые:

  • Requestable – компонент способен обрабатывать HTTP запрос.
  • ClickListener – компонент может вызывать серверный обработчик события при клике мышкой.
  • EventReceiver – может иметь методы обработчики события
  • SubmitDataRequest – компонент принимает данные с формы.

События

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

Для назначения обработчика компоненту инициатору указывается объект-получатель (обычно $this - объект класса страницы) и имя метода-обработчика в текстовом виде. Метод-обработчик содержит параметр $sender – ссылку на объект источник (одно и тоже событие может быть назначено нескольким компонентам) и, при необходимости, дополнительные параметры. Обработчик назначается с помощью соответствующего setHandler или (для линков и кнопок) прямо в конструкторе.

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

Механизм событий избавляет разработчика от забот по функционированию страницы, разборе запроса и формированию ответа клиенту. Разработчик работает с методами событий аналогично приложениям типа Delphi.
Пример:

  • OnClick - обработчик клика по ссылке
  • OnSubmit - вызывается при отправке формы

Биндинг (привязка данных)

Позволяет связать переменную или свойство класса со данными компонента для того чтобы работать не с компонентами напрямую а с переменными или полями бизнес-объектов.

Например, при биндинге компонента Label с полем страницы $msg в функциях бизнес логики достаточно присвоить значение полю и текст будет выведен на страницу компонентом Label. Если, например, какая либо переменная привязана к компоненту TextInput при вводе данных с поля формы значение поля автоматически присвоится переменной. Биндинг задается ссылкой и именем привязываемого свойства.

        
<php

use  \Zippy\PropertyBinding as  Bind;

class Example2 extends \Zippy\Html\WebPage {

    public $msg, $text;

    public function __construct($params = null) {

        $this->add(new Label('outputtext', new Bind($this, 'msg')));
        $form = $this->add(new Form('form1'));
        $form->setSubmitHandler($this, 'OnSubmit');
        $form->add(new TextInput('inputtext', new Bind($this, 'text')));
    }

    public function OnSubmit($sender) {
        $this->msg = $this->text;
    }
}

// ...

class Example3 extends \Zippy\Html\WebPage {

    public $msg, $user;

    public function __construct($params = null) {

        $this->user = new User();

        $this->add(new Label('msg', new Bind($this,'msg')));
        $form = $this->add(new Form('form1'));
        $form->add(new SubmitLink('save'))->setClickHandler($this, 'OnSubmit');
        //  Привязываем поля  формы  ввода  членам  класса  User
        $form->add(new TextInput('name1', new Bind($this->user, 'name1')));
        $form->add(new TextInput('name2', new Bind($this->user, 'name2')));
        $form->add(new TextInput('name3', new Bind($this->user, 'name3')));
    }

    public function OnSubmit($sender) {
        $this->msg = $this->user->name1 . ' ' .
                     $this->user->name2 . ' ' .
                     $this->user->name3;
    }
}

?>
        
      

Дополнительные возможности

Наследование страниц

Наследование страниц предназначено для решения проблемы "единообразия" страниц. Если необходимо иметь на сайте меню, логотип и прочие неизменяемые от страницы к странице части тогда нужно разместить их в базовой странице а изменяемый контент вынести в дочерние. На уровне классов дочерняя страница просто наследуется от базовой наследуя все ее компоненты и обработчики, на уровне разметки - содержимое тэга BODY дочерней страницы вкладывается внутрь базовой вместо тэга <childpage/>.

Пример:

Базовая страница


<html>
  <body>
    <span zippy="baselabel"></span>
    <childpage/>
  </body>
</html>
              
            
              
<?php

namespace Pages;

use \Zippy\Html\Label;

class BasePage extends \Zippy\Html\WebPage {

    public function __construct($params = null) {

        parent::__construct($params);
        $this->add(new Label('baselabel'))->setText('Родительская страница');
    }
}
?>
          
            

Дочерняя страница:

              
<html>
  <body>

    <span zippy="сhildlabel"> </span>

  </body>
</html>
              
            
              
<?php

namespace Pages;

use \Zippy\Html\Label;

 class ChildPage extends BasePage {

    public function __construct($params = null) {

        parent::__construct($params);
        $this->add(new Label('сhildlabel'))->setText('Дочерняя страница');
    }
}
?>
      
            

Дочерние страницы имеют доступ ко всем компонентам объявленным в родительской и может ими управлять. Например подсвечивать текущий пункт меню.

Фрагменты страниц (виджеты)

Фрагмент страницы - это самостоятельный блок, который добавляется в страницу как обычный компонент, но при этом имеет свой шаблон (загружается аналогично шаблону страницы). Используется, если на некоторых страницах необходимо иметь один и тот же блок данных. Например: форма поиска, блок вывода рекламы и т.д. Фрагмент может содержать любые другие компоненты и обработчики событий как обычная страница. В разметку страницы компонент обычно добавляется с помощью тэга DIV. Создается класс фрагмента наследованием от класса PageFragment который в свою очередь наследован от HtmlContainer.

Пользовательские компоненты

Пользовательский компонент позволяет програмно сформировать произвольное содержимое для тэга (как правило DIV). Для создания пользовательского компонента (по сути пользовательского тэга) нужно создать класс наследник от CustomComponent и перегрузить абстрактный метод getContent(). Этот метод должен вернуть HTML код, который будет записан в тэг в шаблоне страницы, предназначенный для вывода компонента.

Ajax

Поскольку фреймворк автоматически обеспечивает сохранность состояния страницы, использование AJAX в Zippy менее востребовано по сравнению с другими решениями. Тем не менее ряд компонентов могут использовать асинхронную обработку событий. Как правило использование AJAX не требует какого то особого кодирования и настройки. Например, для AJAX обработчика onClick() по клику на ссылке для компонента ClickLink нужно только указать третий параметр как true. Для отправки формы через AJAX используется компонент AjaxSubmitLink. Обработчики события и бизнес логика страницы выполняются аналогично обычному синхронному.

Также многие компоненты имеют возможность обновлятся на странице после AJAX вызова. Поскольку обычный рендеринг всей страницы в этом случае не выполняется, необходимо указать обновляемые элементы в обработчике события (метод страницы updateAjax()). Для этих компонентов (реализующих интерфейс AjaxRender) формируется клиентский скрипт, который обновляет их после AJAX ответа.

Пример:
              
<php

// ...

$this->add(new ClickLink('time'))->setAjaxClickHandler($this,'OnClick');
$this->add(new Label('msg'));

// ...

public function OnClick($sender) {
        $this->msg->setText('Я обновлен  через  AJAX');
        //указываем  обновляемые  компоненты
        $this->updateAjax('msg');
}

// ...

?>

Сменяемые темы

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

ЧПУ(SEF) и роутинг

Роутинг осуществляется вызовом метода приложения setRoute, предается кастомная функция ( в виде строковго имени или лямбды). Функция принимает параметром URI и загружает страницу соответствующую вызову. Если присутствуют параметры, они идут через прямой слеш и передаются конструктору страницы. Например, для /user/2 конструктор страницы будет public function __construct($id). Функций обработки может быть добавлено лююое количество.

Кеширование

Поскольку содержимое страниц уже сериализуется и хранится с объекте сессии а объект сессии в нагруженых приложениях должен быть закеширован (перегрузкой saveSession() и restoreSession()), то кеширование страниц не требует специального решения.

Вспомагательный шаблонизатор

Применение вспомагательного шаблонизатора (используется Mustache) не совсем согласуется с компонентной архитектурой фреймворка. Но , как показала практика, иногда использование компонентов для вывода текстов и управление видимостью элементов страницы с помощью комплнентов (как правило Label и Panel) получается несколько громоздким, поскольку данные операции фактически не требуют бэкенд обьектов. В этих случаях можно использовать более традиционый шаблонизатор. Данные для шаблонизатора присваюваются массиву _tvars члену класса WebPage

Библиотека компонентов

Библиотека представляет собой набор компонентов построенных на расширении стандартных компонентов фреймворка. Библиотека не входит в ядро фреймворка и располагается в отдельном пространстве имен ZCL.

Библиотека состоит из следующих компонентов:

jqGrid бэк-енд

Компонент представляет собой реализацию серверной части для JavaScript библиотеки jqGrid. Пример подключения можно посмотреть на странице примера, описание методов на странице API.

Клиентская часть практические не отличается от стандартной установки согласно описания библиотеки jqGrid, кроме дополнительного атрибута zippy в теге таблицы.

На сервере создаем компонент \ZCL\JqGrid\JQGrid и как любой другой компонент добавляем его в класс страницы. Назначаем функции-обработчики для для отправки данных на клиента (возвращает массив со структурой полей таблицы) и для редактирования данных (принимает параметры от клиентской части) в соответствии с которыми изменяет содержимое БД.

Капча

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

Для реализации собственного алгоритма достаточно отнаследоваться от компонента и переопределить метод OnCode для генерации кода и/или метод OnImage для генерации изображения. Описание методов на странице API.

для вывода компонента на странице используется обычный тег <img>.

Компоненты для работы с БД

Нeсколько компонентов для работы с базами данных. Компоненты базируются на библиотеке ADODB, что позволяет писать код, защищенный от sql-иньекций и переносимый между разными типами БД.
(На данный момент компоненты работы с БД выделены в отдельный проект ZDB)

DB - Класс-синглетон, использующийся как обертка для соединения с БД.

Entity - Базовый класс для персистентных сущностей , реализованный в виде паттерна ActiveRecord и имплементирующий интерфейс DataItem фреймворка. В специальном перегружаемом методе задаются метаданные расположения сущности в БД -таблица, представление (если есть), имя поля первичного ключа. Это позволяет автоматизировать наиболее частые рутинные операции типа вставки и обновления записи в БД, отображение данных в таблицах и других списочных элементах страницы. Классы сущностей самостоятельно генерят SQL код для обновления и вставки в таблицу БД (используется автогенерация из ADODB)

TreeEntity - класс-наследник Entity, предназначенный для работы с иерархически организованными сущностями. В дополнение к функционалу родительского класса имеет возможномть манипулировать дочерними элементами (поиск по дереву, удаление, перемещение веток), строить дерево для компонента Tree на основе набора данных из БД. Данные должны быть организованы в соответствии с алгоритмом материализованного пути.

EntityDataSource - универсальный источник данных реализующий интерфейс DataSource, позволяющий связать страничные компоненты табличного и списочного вывода с БД. В параметрах класса задаются имя класса-сущности (Entity) и при необходимости условие для выборки. Это позволяет разработчику избежать создания специализированного DataSource для простых линейных выборок.

Примеры использования компонентов можно посмотреть в Демонстрационной CMS

Компонент отображения древовиднх данных

Компонент позволяет отображать данные, организованные в иерархические структуры (к примеру категории товаров) Дерево генерится на странице автоматически и сохраняет состояние при перезагрузке страницы, так же есть возможность добавить чекбоксы и динамическую подгрузку узлов через Ajax. Примеры использования на странице примеров а также в демонстрационной CMS - модули Файловое хранилище и Магазин).