1.5. Подключаем классы для работы с базой данных и шаблонами
Мы создали структуру для нашего фреймворка, теперь пора подумать о хранение данных: новостей, товаров. Объект для работы с БД должен уметь:
- Управлять соединение с БД
- Предоставлять небольшую абстракцию от БД
- Кешировать запросы
- Сделать общие операции с БД проще
Для этого мы создадим объект 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(); ?>
Если все прошло хорошо и в базе данных есть пользователи, то у вас должно быть выведено нечно подобное:
Если что-то пошло не так и возникли ошибки, то возможно я еще не поправил в предыдущих статьях код, вы можете посмотреть работающих код на гитхабе.
Вот ошибки, которые возникли у меня по ходу написания статьи.
Меняем название класса работа с БД 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