PHP фреймворк Zippy

Архитектура

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

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

Пример:

  <body>
   <span zippy="msg"></span>
     <a  zippy="onmsg">Клик</a>
   </body>
      
  
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 предварительно созданного объекта формы.


  <body>
   <form zippy="form1">
     <input type="text" zippy="message" />
     </form>
   </body>
      
        public function __construct($params = null) {
                $this->add(new Form('form1'));
                $this->form1->add(new TextInput('message');
        }

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

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

class Example1 extends \Zippy\Html\WebPage
{
    public $counter=0;  
        // Обработчик  клика  по  ссылке
        public function OnClick($sender) {
                //Присваиваем  текст для вывода  в  тэге  span
                 
                $this->counter++;
                $this->msg->setText("Счетчик ". $this->counter);
        }
}
Таким образом может быть сохранена между обновлениями страницы любая сложная структура. Естественно, если нужно очистить данные в компоненте нужно делать это явно аналогично десктопным приложениям.
$this->msg->setText("");

  Связь между классом страницы и соответствующим HTML файлом шаблона задается разработчиком сайта что позволяет избежать жесткой структуры каталогов а также легко реализовать сменяемый дизайн и/или локализацию.Жизненный цикл страницы и навигация между страницами обеспечивается классом WebApplication в зависимости от анализа запросов браузера к серверу.

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

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

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

diagram

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

WebApplication

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

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

  
  
 class Application extends \Zippy\WebApplication
{
  
    public function getTemplate($name)
    {
        //загрузка  шаблонов  для  страниц
     
        $name = str_replace("Pages\\", "", ltrim($name, '\\'));

        $path = __DIR__ . '/' ."templates/" .  strtolower($name) . ".html";
 

        $template = file_get_contents($path);
        if ($template == false) {
            new \Exception('Неверный путь к шаблону страницы: ' . $path);
        }

        return $template;
    }

    // если  требуется  роутинг
    public function Route($uri){
         if($uri == '')  $uri = 'page1';
         
         $uria= explode("/",$uri);
         
         if($pages[$uria[0]]=='page1'){
            $this->LoadPage("\\Pages\\Page1");
         }
         else if ($pages[$uria[0]]=='page2'){    
            $this->LoadPage("\\Pages\\Page2",$uria[1]); //страница с параметром
         }
         else {
            $this->getResponse()->to404Page() ;   
         }
    }


}
   
    // создаем  экземпляр приложения с  параметром класса  главной страницы сайта
    $app = new Application('Pages\Main');

    $app->Run();
    
   
     

WebPage

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

HtmlComponent

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

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

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

Panel

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

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

  • ClickLink - служит для вызова обработчика события в классе cтраницы без возможности сделать закладку в браузере (поскольку это не имеет смысла - ссылка не ведет на какой либо ресурс а просто вызывает обработчик.)
      
      
            public function __construct($params = null) {
                    
                $this->add(new ClickLink('onmsg'))->onClick( $this, 'OnClick');
            }
            // Обработчик   
            public function OnClick($sender) {
                     
            }
    }
                
  • BookmarkableLink - используется для внешнего перехода или ЧПУ с возможностью создать закладку. Для указания адреса страницы указывается имя класса страницы с прямыми слешами
    /?p=Pages/Page1
    Могут быть указаны один или несколько (через прямой слэш) параметров
    /?p=Pages/Page1&arg=1
  • RedirectLink - используется для редиректа на другую страницу. Има класса страницы задается как параметр в конструкторе.
  • SubmitLink - отправляет форму на сервер, c возможностью вызвать обработчик события по отправке формы.

Компоненты формы ввода

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

  
        public function __construct($params = null) {
                
            $this->add(new Form('form1'))->onSubmit( $this, 'OnForm1');
        }
        // Обработчик   
        public function OnForm1($sender) {
                 
        }
Вместо формы обработчик может быть назначен одному или нескольким компонентам типа SubmitLink или SubmitButton которые могут отправлять форму. Компонент должен быть внутри формы.
  
             public function __construct($params = null) {
                
            $this->add(new Form('form1'))->onSubmit( $this, 'OnForm1');
            $this->form1->add(new SubmitLink('sl1'))->onClick( $this, 'OnForm1');
        }
        // Обработчик   
        public function OnForm1($sender) {
                 
        }

Интерфейсы

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

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

События

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

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

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

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

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

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

Например, при биндинге компонента 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->onSubmit($this, 'OnSubmit');
        $form->add(new TextInput('inputtext', new Bind($this, 'text')));
    }

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

Вывод списочных данных

DataView

Основное назначение компонента DataView вывод табличных данных.
Пример на основе демо приложения


<table >
         <tr><th>ФИО</th><th>Возраст</th><th> </th></tr>
         <tr zippy="list"><td><span zippy="fio"></span></td>
         <td><span zippy="age"></span></td>
         <td><a  zippy="edit">Редактировать></a> </td>
         </tr>
         </table> 
         
     

     
    public function __construct()
    {
       $this->add(new DataView('list',new DataSource(),$this,'listOnRow'))->Reload();
    
    } 
  public function listOnRow($row){
    //получаем  объект данных связанный с  строкой 
    $item = $row->getDataItem();

    //создаем  компоненты в  строке
    $row->add(new Label('fio',$item->fio));
    $row->add(new Label('age',$item->age));
    $row->add(new ClickLink('edit'))->onClick($this,'editOnClick');
  }    

  //обработчик редактирования
  public function editOnClick($sender){
   //получаем  объект данных связанный с  строкой 
   $item = $sender->getOwner()->getDataItem();
   
   //заполняем форму редактирования  и т.д.
  }
     
Для инициализации вывода необходим источник данных, возвращающим массив обьектов классов реализующих интерфейс DataItem или расширающий класс Entity и обработчик в котором будут создаваться компоненты вывода.
Строка данных - обычный контейнер, базирующийся на теге <tr>. Добавлять можно любые компоненты (кроме вложенных DataView). DataView сам размножит компоненты данных для каждой строки
При необходимости можно добавить пагинатор Paginator или Pager .
<div zippy="pag></div> 

     $this->add(new DataView('list',new DataSource(),$this,'listOnRow'));
     //добавляем  пагинатор для вывода
     $this->add(new Paginator("pag",$this->list));
     //устанавливаем количество  строк  на  странице
     $this->list->setPageSize(25));
     
     $this->list->Reload();
     
DataView может использовать для вывода и другие блочные элементы, например если надо вывести список товаров или галерею рисунков. Для примера выше (бекенд тот же)

<div class="row">
<div class="col-md-4" zippy="list">
         <span zippy="fio"></span>
         <span zippy="age"></span>
 </div> 
 </div> 
     

DataTable

Компонент DataTable предназначен для работы с тегом <table>. Компонент сам заголовки и пагинацию (если указано) но выводит данные только в виде строк. Компонент чаще всего используется для вывода различного рода репортов и прочих данных, не требующих бэкенд компонентов. При создании компонента нужно создать экземпляры столбцов с указанием необходимых параметров. Для кастомизации вывода предусмотрено н обработчик setCellDrawEvent рендеринга ячейки, которому передается имя столбца и идентификатор записи. Также можно установить обработчик setCellClickEvent клика по ячейке

<table zippy="report></div> 

      //выводим  таблицу  с  заголовком  и пагинатором
      $this->add(new \Zippy\Html\DataList\DataTable("report", new DataSource(), true, true));
       
      // создаем  столбцы с  указанием  сортировки
      $this->report->addColumn(new Column('fio', 'ФИО', true ));
      $this->report->addColumn(new Column('age', 'Возраст', true ));
     

Источники данных

Источник данных - промежуточный компонент, унифицирующий передачу данных от разных источников компонентам DataView и DataTable. Источник данных должен реализовать интерфейс DataSource. Методы интервейса предписывают реализацию источником условий выборки, сортировки и пагинации.
Существует несколько видов источников.
ArrayDataSource
ArrayDataSource применяется в случае использования массивов как набора данных. Если массив может изменятся в процессе работы, следует передавать массив не напрямую а использовать биндинг (см. демо приложение).
EntityDataSource
EntityDataSource применяется для работы с Entity. Источнику передается как параметр имя класса сущности унаследованной от Entity.
Пользовательский источник
Для сложных выборок разработчик может создать свой класс источника данных и если нужно связать его с компонентами страницы (например фильтрацией).


        class DocDataSource implements \Zippy\Interfaces\DataSource
        {
            private function getWhere()
            {
                //возвращает  условие для  SQL запроса
                return $where;
            }        
            public function getItemCount()
            {
                //количество  для пагинатора
                return Document::findCnt($this->getWhere());
            } 

            public function getItems($start, $count, $sortfield = null, $asc = null)
            {
                //выбирает данные 
                $docs = Document::find($this->getWhere(), "document_date desc,document_id desc", $count, $start);
                return $docs;
            }
            
            public function getItem($id)
            {
                
            }            
        }
        
        используется  как  обычный источник
       $this->add(new DataView('list',new DocDataSource(),$this,'listOnRow'))->Reload();
  
        

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

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

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

Если в шаблоне дочерней страницы в <head> есть метатеги <title>,<description> и <keyword>, при рендеринге дочерней страницы эти теги будут перенесены с заменой в базовый шаблон.

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

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

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

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

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

Ajax

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

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

Пример:
              
 
$this->add(new ClickLink('time'))->onClick($this,'OnClick',true);
$this->add(new Label('msg'));

// ...

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

 
 

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

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

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

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

Кеширование

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

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

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

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

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

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

Диаграмма Ганта

Компонент представляет собой реализацию серверной части для jquery.ganttview.

Капча

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

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

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

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

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

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

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

Twitter Bootstrap компоненты

Некоторые компоненты, базирующиеся на Twitter Bootstrap.