Extra Block Types (EBT) - New Layout Builder experience❗

Extra Block Types (EBT) - styled, customizable block types: Slideshows, Tabs, Cards, Accordions and many others. Built-in settings for background, DOM Box, javascript plugins. Experience the future of layout building today.

Demo EBT modules Download EBT modules

❗Extra Paragraph Types (EPT) - New Paragraphs experience

Extra Paragraph Types (EPT) - analogical paragraph based set of modules.

Demo EPT modules Download EPT modules

Scroll

1.5. Подключаем классы для работы с базой данных и шаблонами

08/12/2019, by Ivan

Мы создали структуру для нашего фреймворка, теперь пора подумать о хранение данных: новостей, товаров. Объект для работы с БД должен уметь:

  • Управлять соединение с БД
  • Предоставлять небольшую абстракцию от БД
  • Кешировать запросы
  • Сделать общие операции с БД проще

Для этого мы создадим объект Registry/objects/db.class.php:

<?php

/**
 * Управление БД
 * Предоставляет небольшую абстракцию от БД
 */
class database {

  /**
   * Позволяет множественное подключение к БД
   * редко используется, но иногда бывает полезно
   */
  private $connections = array();

  /**
   * Сообщает об активном соединение
   * setActiveConnection($id) позволяет изменить активное соединение
   */
  private $activeConnection = 0;

  /**
   * Запросы которые выполнились и сохранены на будущее
   */
  private $queryCache = array();

  /**
   * Данные которые были извлечены и сохранены на будущее
   */
  private $dataCache = array();

  /**
   * Запись последненго запроса
   */
  private $last;


  /**
   * Конструктор
   */
  public function __construct()
  {

  }

  /**
   * Создание нового соединения
   * @param String database hostname
   * @param String database username
   * @param String database password
   * @param String database we are using
   * @return int the id of the new connection
   */
  public function newConnection( $host, $user, $password, $database )
  {
    $this->connections[] = new mysqli( $host, $user, $password, $database );
    $connection_id = count( $this->connections )-1;
    if( mysqli_connect_errno() )
    {
      trigger_error('Error connecting to host. '.$this->connections[$connection_id]->error, E_USER_ERROR);
    }

    return $connection_id;
  }

  /**
   * Закрываем активное соединение
   * @return void
   */
  public function closeConnection()
  {
    $this->connections[$this->activeConnection]->close();
  }

  /**
   * Изменяем активное соединение
   * @param int the new connection id
   * @return void
   */
  public function setActiveConnection( int $new )
  {
    $this->activeConnection = $new;
  }

  /**
   * Сохранияем запрос в кэш
   * @param String the query string
   * @return the pointed to the query in the cache
   */
  public function cacheQuery( $queryStr )
  {
    if( !$result = $this->connections[$this->activeConnection]->query( $queryStr ) )
    {
      trigger_error('Error executing and caching query: '.$this->connections[$this->activeConnection]->error, E_USER_ERROR);
      return -1;
    }
    else
    {
      $this->queryCache[] = $result;
      return count($this->queryCache)-1;
    }
  }

  /**
   * Получение количества строк в кэше
   * @param int the query cache pointer
   * @return int the number of rows
   */
  public function numRowsFromCache( $cache_id )
  {
    return $this->queryCache[$cache_id]->num_rows;
  }

  /**
   * Получение строк из кэша
   * @param int the query cache pointer
   * @return array the row
   */
  public function resultsFromCache( $cache_id )
  {
    return $this->queryCache[$cache_id]->fetch_array(MYSQLI_ASSOC);
  }

  /**
   * Сохраняем кэш
   * @param array the data
   * @return int the pointed to the array in the data cache
   */
  public function cacheData( $data )
  {
    $this->dataCache[] = $data;
    return count( $this->dataCache )-1;
  }

  /**
   * Получаем данные из кэша
   * @param int data cache pointed
   * @return array the data
   */
  public function dataFromCache( $cache_id )
  {
    return $this->dataCache[$cache_id];
  }

  /**
   * Удаляем запись из таблицы
   * @param String the table to remove rows from
   * @param String the condition for which rows are to be removed
   * @param int the number of rows to be removed
   * @return void
   */
  public function deleteRecords( $table, $condition, $limit )
  {
    $limit = ( $limit == '' ) ? '' : ' LIMIT ' . $limit;
    $delete = "DELETE FROM {$table} WHERE {$condition} {$limit}";
    $this->executeQuery( $delete );
  }

  /**
   * Обновляем запись в таблице
   * @param String the table
   * @param array of changes field => value
   * @param String the condition
   * @return bool
   */
  public function updateRecords( $table, $changes, $condition )
  {
    $update = "UPDATE " . $table . " SET ";
    foreach( $changes as $field => $value )
    {
      $update .= "`" . $field . "`='{$value}',";
    }

    // remove our trailing ,
    $update = substr($update, 0, -1);
    if( $condition != '' )
    {
      $update .= "WHERE " . $condition;
    }

    $this->executeQuery( $update );

    return true;

  }

  /**
   * Вставляем запись в таблицу
   * @param String the database table
   * @param array data to insert field => value
   * @return bool
   */
  public function insertRecords( $table, $data )
  {
    // setup some variables for fields and values
    $fields  = "";
    $values = "";

    // populate them
    foreach ($data as $f => $v)
    {

      $fields  .= "`$f`,";
      $values .= ( is_numeric( $v ) && ( intval( $v ) == $v ) ) ? $v."," : "'$v',";

    }

    // remove our trailing ,
    $fields = substr($fields, 0, -1);
    // remove our trailing ,
    $values = substr($values, 0, -1);

    $insert = "INSERT INTO $table ({$fields}) VALUES({$values})";
    $this->executeQuery( $insert );
    return true;
  }

  /**
   * Выполнение запроса к бд
   * @param String the query
   * @return void
   */
  public function executeQuery( $queryStr )
  {
    if( !$result = $this->connections[$this->activeConnection]->query( $queryStr ) )
    {
      trigger_error('Error executing query: '.$this->connections[$this->activeConnection]->error, E_USER_ERROR);
    }
    else
    {
      $this->last = $result;
    }

  }

  /**
   * Получить строки последнего запроса, исключая запросы из кэша
   * @return array
   */
  public function getRows()
  {
    return $this->last->fetch_array(MYSQLI_ASSOC);
  }

  /**
   * Получить количество строк последнего запроса
   * @return int the number of affected rows
   */
  public function affectedRows()
  {
    return $this->$this->connections[$this->activeConnection]->affected_rows;
  }

  /**
   * Проверка безопасности данных
   * @param String the data to be sanitized
   * @return String the sanitized data
   */
  public function sanitizeData( $data )
  {
    return $this->connections[$this->activeConnection]->real_escape_string( $data );
  }

  /**
   * Декструктор, закрывает соединение
   * close all of the database connections
   */
  public function __deconstruct()
  {
    foreach( $this->connections as $connection )
    {
      $connection->close();
    }
  }
}
?>

Прежде, чем перейти к подключению к БД, давайте посмотрим что делает наш класс. Мы сможем делать простые операции добавления, обновления, удаления через методы класса:

// Вставка
$registry->getObject('db')->insertRecords( 'products', array('name'=>'Кружка' ) );
// Обновление
$registry->getObject('db')->updateRecords( 'products', array('name'=>'Кружка красная' ), 'ID=2' );
// Удаление
$registry->getObject('db')->deleteRecords( 'products', "name='Кружка красная'", 5 );

Также наш класс поддерживает кеширование.

Теперь давайте добавим еще один объект управления шаблонами Registry/objects/template.class.php

<?php

// Константа определенная в index.php, чтобы избежать вызова класса из другого места
if ( ! defined( 'FW' ) )
{
  echo 'Этот файл может быть вызван только из index.php и не напрямую';
  exit();
}

/**
 * Класс работы с шаблонами
 */
class template {

  private $page;

  /**
   * Конструктор
   */
  public function __construct()
  {
    // Далее мы добавим этот класс страницы
    include( APP_PATH . '/Registry/objects/page.class.php');
    $this->page = new Page();

  }

  /**
   * Добавляет тег в страницу
   * @param String $tag тег где мы вставляем шаблон, например {hello}
   * @param String $bit путь к шаблону
   * @return void
   */
  public function addTemplateBit( $tag, $bit )
  {
    if( strpos( $bit, 'Views/' ) === false )
    {
      $bit = 'Views/Templates/' . $bit;
    }
    $this->page->addTemplateBit( $tag, $bit );
  }

  /**
   * Включаем шаблоны в страницу
   * Обновляем контент страницы
   * @return void
   */
  private function replaceBits()
  {
    $bits = $this->page->getBits();
    foreach( $bits as $tag => $template )
    {
      $templateContent = file_get_contents( $template );
      $newContent = str_replace( '{' . $tag . '}', $templateContent, $this->page->getContent() );
      $this->page->setContent( $newContent );
    }
  }

  /**
   * Заменяем теги на новый контент
   * @return void
   */
  private function replaceTags()
  {
    // получаем теги
    $tags = $this->page->getTags();
    // перебераем теги
    foreach( $tags as $tag => $data )
    {
      if( is_array( $data ) )
      {

        if( $data[0] == 'SQL' )
        {
          // Заменяем теги из кешированного запроса
          $this->replaceDBTags( $tag, $data[1] );
        }
        elseif( $data[0] == 'DATA' )
        {
          // Заменяем теги из кешированных данных
          $this->replaceDataTags( $tag, $data[1] );
        }
      }
      else
      {
        // заменяем теги на контент
        $newContent = str_replace( '{' . $tag . '}', $data, $this->page->getContent() );
        // обновляем содержимое страницы
        $this->page->setContent( $newContent );
      }
    }
  }

  /**
   * Заменяем теги данными из БД
   * @param String $tag тег (токен)
   * @param int $cacheId ID запросов
   * @return void
   */
  private function replaceDBTags( $tag, $cacheId )
  {
    $block = '';
    $blockOld = $this->page->getBlock( $tag );

    // Проверяем кэш для каждого из запросов...
    while ($tags = Registry::getObject('db')->resultsFromCache( $cacheId ) )
    {
      $blockNew = $blockOld;
      // создаем новый блок и вставляем его вместо тега
      foreach ($tags as $ntag => $data)
      {
        $blockNew = str_replace("{" . $ntag . "}", $data, $blockNew);
      }
      $block .= $blockNew;
    }
    $pageContent = $this->page->getContent();
    // удаляем разделители из шаблона, чистим HTML
    $newContent = str_replace( '<!-- START ' . $tag . ' -->' . $blockOld . '<!-- END ' . $tag . ' -->', $block, $pageContent );
    // обновляем контент страницы
    $this->page->setContent( $newContent );
  }

  /**
   * Заменяем контент страницы вместо тегов
   * @param String $tag тег
   * @param int $cacheId ID данных из кэша
   * @return void
   */
  private function replaceDataTags( $tag, $cacheId )
  {
    $block = $this->page->getBlock( $tag );
    $blockOld = $block;
    while ($tags = Registry::getObject('db')->dataFromCache( $cacheId ) )
    {
      foreach ($tags as $tag => $data)
      {
        $blockNew = $blockOld;
        $blockNew = str_replace("{" . $tag . "}", $data, $blockNew);
      }
      $block .= $blockNew;
    }
    $pageContent = $this->page->getContent();
    $newContent = str_replace( $blockOld, $block, $pageContent );
    $this->page->setContent( $newContent );
  }

  /**
   * Получаем страницу
   * @return Object
   */
  public function getPage()
  {
    return $this->page;
  }

  /**
   * Устанавливаем контент страницы в зависимости от количества шаблонов
   * передаем пути к шаблонам
   * @return void
   */
  public function buildFromTemplates()
  {
    $bits = func_get_args();
    $content = "";
    foreach( $bits as $bit )
    {

      if( strpos( $bit, 'skins/' ) === false )
      {
        $bit = 'Views/Templates/' . $bit;
      }
      if( file_exists( $bit ) == true )
      {
        $content .= file_get_contents( $bit );
      }

    }
    $this->page->setContent( $content );
  }

  /**
   * Convert an array of data (i.e. a db row?) to some tags
   * @param array the data
   * @param string a prefix which is added to field name to create the tag name
   * @return void
   */
  public function dataToTags( $data, $prefix )
  {
    foreach( $data as $key => $content )
    {
      $this->page->addTag( $key.$prefix, $content);
    }
  }

  public function parseTitle()
  {
    $newContent = str_replace('<title>', '<title>'. $this->$page->getTitle(), $this->page->getContent() );
    $this->page->setContent( $newContent );
  }

  /**
   * Подставляем теги и токены, заголовки
   * @return void
   */
  public function parseOutput()
  {
    $this->replaceBits();
    $this->replaceTags();
    $this->parseTitle();
  }

}
?>

Также мы определили вызывает объект Page в шаблонизаторе, поэтому нам нужно его определить Registry/objects/page.class.php:

<?php

/**
 * Наш класс для страницы
 * Этот класс позволяет добавить несколько нужных нам вещей
 * Например: подпароленные страницы, добавление js/css файлов, и т.д.
 */
class page {

  private $css = array();
  private $js = array();
  private $bodyTag = '';
  private $bodyTagInsert = '';

  // будущий функционал
  private $authorised = true;
  private $password = '';

  // элементы страницы
  private $title = '';
  private $tags = array();
  private $postParseTags = array();
  private $bits = array();
  private $content = "";

  /**
   * Constructor...
   */
  function __construct() { }

  public function getTitle()
  {
    return $this->title;
  }

  public function setPassword( $password )
  {
    $this->password = $password;
  }

  public function setTitle( $title )
  {
    $this->title = $title;
  }

  public function setContent( $content )
  {
    $this->content = $content;
  }

  public function addTag( $key, $data )
  {
    $this->tags[$key] = $data;
  }

  public function getTags()
  {
    return $this->tags;
  }

  public function addPPTag( $key, $data )
  {
    $this->postParseTags[$key] = $data;
  }

  /**
   * Парсим теги
   * @return array
   */
  public function getPPTags()
  {
    return $this->postParseTags;
  }

  /**
   * Добавляем тег
   * @param String the tag where the template is added
   * @param String the template file name
   * @return void
   */
  public function addTemplateBit( $tag, $bit )
  {
    $this->bits[ $tag ] = $bit;
  }

  /**
   * Получаем все теги
   * @return array the array of template tags and template file names
   */
  public function getBits()
  {
    return $this->bits;
  }

  /**
   * Ищем все блоки на странице
   * @param String the tag wrapping the block ( <!-- START tag --> block <!-- END tag --> )
   * @return String the block of content
   */
  public function getBlock( $tag )
  {
    preg_match ('#<!-- START '. $tag . ' -->(.+?)<!-- END '. $tag . ' -->#si', $this->content, $tor);

    $tor = str_replace ('<!-- START '. $tag . ' -->', "", $tor[0]);
    $tor = str_replace ('<!-- END '  . $tag . ' -->', "", $tor);

    return $tor;
  }

  public function getContent()
  {
    return $this->content;
  }

}
?>

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

Создадим метод storeCoreObjects() в Registry/registry.class.php:

    public function storeCoreObjects()
    {
      $this->storeObject('database', 'db' );
      $this->storeObject('template', 'template' );
    }

В нем мы будем писать подключения каких классов происходить.

Давайте еще заполним немного данных, а именно создадим таблицу users. В этой таблице будет три поля id, name, email. Я приложу к гиту sql-файл с базой данных для примера.

Давайте теперь выведем главную страницу, для этого нужно создать шаблон Views/Templates/main.tpl.php:

<html>
<head>
    <title> Powered by PCA Framework</title>
</head>
<body>
<h1>Our Members</h1>
<p>Below is a list of our members:</p>
<ul>
<!-- START members -->
<li>{name} {email}</li>
<!-- END members -->
</ul>
</body>
</html>

Как вы видите мы задали вывод тэга members и токенов {name}, {email}. Я думаю в одной из статей мы подробно разберем работу шаблонизатора. Теперь вернемся в index.php и подключим шаблон и базу данных.

Теперь наш index.php выглядит вот так:

<?php
/**
 * Framework
 * Framework loader - точка входа в наш фреймворк
 *
 */
  
// стартуем сессию
session_start();

error_reporting(E_ALL);
// задаем некоторые константы
// Задаем корень фреймворка, чтобы легко получать его в любом скрипте
define( "APP_PATH", dirname( __FILE__ ) ."/" );
// Мы будем использовать это, чтобы избежать вызов скриптов не из нашего фреймворка
define( "FW", true );
 
/**
 * Магическая функция автозагрузки
 * позволяет вызвать необходимый -controller- когда он нужен
 * @param String the name of the class
 */
function __autoload( $class_name )
{
    require_once('Controllers/' . $class_name . '/' . $class_name . '.php' );
}
 
// подключаем наш реестр
require_once('Registry/registry.class.php');
$registry = Registry::singleton();


// мы храним список всех объектов в классе регистра
$registry->storeCoreObjects();

// здесь должны быть ваши доступы к бд
$registry->getObject('db')->newConnection('localhost', 'root', '', 'framework');

// Подключаем шаблон главной страницы
$registry->getObject('template')->buildFromTemplates('main.tpl.php');

// Делаем запрос к таблице пользователей
$cache = $registry->getObject('db')->cacheQuery('SELECT * FROM users');

// Добавяем тег users, чтобы вызвать его в шаблоне,
// в этом теге будут доступны поля таблицы через токены {name}, {email}
$registry->getObject('template')->getPage()->addTag('users', array('SQL', $cache) );

// Устанавливаем заголовок страницы
$registry->getObject('template')->getPage()->setTitle('Our users');

// Парсим страницу в поисках тегов и токенов и выводим страницу
$registry->getObject('template')->parseOutput();
print $registry->getObject('template')->getPage()->getContent();

// выводим имя фреймворка, чтобы проверить, что все работает
print $registry->getFrameworkName();
 
exit();
 
?>

Если все прошло хорошо и в базе данных есть пользователи, то у вас должно быть выведено нечно подобное:

Our users

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

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

Меняем название класса работа с БД Registry/objects/db.class.php:

Index: Registry/objects/db.class.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- Registry/objects/db.class.php	(revision b1ffa3bbfce4e95ace7ed735e9412e9332e17d50)
+++ Registry/objects/db.class.php	(revision )
@@ -4,7 +4,7 @@
  * Управление БД
  * Предоставляет небольшую абстракцию от БД
  */
-class database {
+class db {
 
   /**
    * Позволяет множественное подключение к БД
\ No newline at end of file

Определил статические классы, где это это было нужно, переименовал класс работы с БД Registry/registry.class.php:

Index: Registry/registry.class.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- Registry/registry.class.php	(revision b1ffa3bbfce4e95ace7ed735e9412e9332e17d50)
+++ Registry/registry.class.php	(revision )
@@ -69,15 +69,15 @@
      * @param String $key the key for the array
      * @return void
      */
-    public function storeObject( $object, $key )
+    public static function storeObject( $object, $key )
     {
-        require_once('objects/' . $object . '.class.php');
+        require_once('Registry/objects/' . $object . '.class.php');
         self::$objects[ $key ] = new $object( self::$instance );
     }
 
     public function storeCoreObjects()
     {
-      $this->storeObject('database', 'db' );
+      $this->storeObject('db', 'db' );
       $this->storeObject('template', 'template' );
     }
 
@@ -86,7 +86,7 @@
      * @param String $key the array key
      * @return object
      */
-    public function getObject( $key )
+    public static function getObject( $key )
     {
         if( is_object ( self::$objects[ $key ] ) )
         {
\ No newline at end of file

Потребовалось создать контроллер db с db.php

Controllers/db/
Controllers/db/db.php

 Поправил ошибку в шаблонизаторе Registry/objects/template.class.php:

Index: Registry/objects/template.class.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- Registry/objects/template.class.php	(revision b1ffa3bbfce4e95ace7ed735e9412e9332e17d50)
+++ Registry/objects/template.class.php	(revision )
@@ -194,7 +194,7 @@
 
   public function parseTitle()
   {
-    $newContent = str_replace('<title>', '<title>'. $this->$page->getTitle(), $this->page->getContent() );
+    $newContent = str_replace('<title>', '<title>'. $this->page->getTitle(), $this->page->getContent() );
     $this->page->setContent( $newContent );
   }
 
\ No newline at end of file