Содержание цикла статей:
- 1. Вводная статья
- 2. Маршрутизация, контролеры, экшены, шаблоны
- 3. Модели. Элементарные действия с базой данных
- 4. Проектируем блог
- 5. Исправление неточностей структуры
Введение
В предыдущей статье мы с вами написали каркас сайта, который использует в своей основе MVC подход. На данный момент у нас работает маршрутизация, сделано разделение на модели, контроллеры и отображения(view). Но поскольку предыдущая статья получилась большая, я затронул работу с моделями вскользь, в этой статье мы это наверстаем.
Модели в большинстве случаев используются для работы с базой данных, то есть все запросы для получения, обновления, удаления или записи в БД будут осуществляться в моделях. В контролерах не должно быть работы с базой, вместо запросов должно быть обращение к методам моделей.
Структура моделей
Для удобства работы с базой данных, принято делать для каждой таблицы отдельную модель – класс, а в методах каждой модели уже описывать взаимодействие с таблицами. Давайте это реализуем. Как известно, для обращения к бд, необходимо делать подключение, но так как мы уже создаем соединение в файле index.php будем его использовать и в моделях. Так же для удобства можно создать абстрактный класс, от которого наши модели будут наследовать типовые методы. К таким методам можно отнести элементарные действия с базой данных – получение одной записи по id, получение всех записей таблицы и тд.
Давайте создадим абстрактный класс models_base.php и расположим его в папке classes. Код класса будет выглядеть так:
Abstract Class Model_Base { protected $db; protected $table; private $dataResult; public function __construct($select = false) { // объект бд коннекта global $dbObject; $this->db = $dbObject; // имя таблицы $modelName = get_class($this); $arrExp = explode('_', $modelName); $tableName = strtolower($arrExp[1]); $this->table = $tableName; // обработка запроса, если нужно $sql = $this->_getSelect($select); if($sql) $this->_getResult("SELECT * FROM $this->table" . $sql); } // получить имя таблицы public function getTableName() { return $this->table; } // получить все записи function getAllRows(){ if(!isset($this->dataResult) OR empty($this->dataResult)) return false; return $this->dataResult; } // получить одну запись function getOneRow(){ if(!isset($this->dataResult) OR empty($this->dataResult)) return false; return $this->dataResult[0]; } // извлечь из базы данных одну запись function fetchOne(){ if(!isset($this->dataResult) OR empty($this->dataResult)) return false; foreach($this->dataResult[0] as $key => $val){ $this->$key = $val; } return true; } // получить запись по id function getRowById($id){ try{ $db = $this->db; $stmt = $db->query("SELECT * from $this->table WHERE id = $id"); $row = $stmt->fetch(); }catch(PDOException $e) { echo $e->getMessage(); exit; } return $row; } // запись в базу данных public function save() { $arrayAllFields = array_keys($this->fieldsTable()); $arraySetFields = array(); $arrayData = array(); foreach($arrayAllFields as $field){ if(!empty($this->$field)){ $arraySetFields[] = $field; $arrayData[] = $this->$field; } } $forQueryFields = implode(', ', $arraySetFields); $rangePlace = array_fill(0, count($arraySetFields), '?'); $forQueryPlace = implode(', ', $rangePlace); try { $db = $this->db; $stmt = $db->prepare("INSERT INTO $this->table ($forQueryFields) values ($forQueryPlace)"); $result = $stmt->execute($arrayData); }catch(PDOException $e){ echo 'Error : '.$e->getMessage(); echo '<br/>Error sql : ' . "'INSERT INTO $this->table ($forQueryFields) values ($forQueryPlace)'"; exit(); } return $result; } // составление запроса к базе данных private function _getSelect($select) { if(is_array($select)){ $allQuery = array_keys($select); array_walk($allQuery, function(&$val){ $val = strtoupper($val); }); $querySql = ""; if(in_array("WHERE", $allQuery)){ foreach($select as $key => $val){ if(strtoupper($key) == "WHERE"){ $querySql .= " WHERE " . $val; } } } if(in_array("GROUP", $allQuery)){ foreach($select as $key => $val){ if(strtoupper($key) == "GROUP"){ $querySql .= " GROUP BY " . $val; } } } if(in_array("ORDER", $allQuery)){ foreach($select as $key => $val){ if(strtoupper($key) == "ORDER"){ $querySql .= " ORDER BY " . $val; } } } if(in_array("LIMIT", $allQuery)){ foreach($select as $key => $val){ if(strtoupper($key) == "LIMIT"){ $querySql .= " LIMIT " . $val; } } } return $querySql; } return false; } // выполнение запроса к базе данных private function _getResult($sql){ try{ $db = $this->db; $stmt = $db->query($sql); $rows = $stmt->fetchAll(); $this->dataResult = $rows; }catch(PDOException $e) { echo $e->getMessage(); exit; } return $rows; } // уделение записей из базы данных по условию public function deleteBySelect($select){ $sql = $this->_getSelect($select); try { $db = $this->db; $result = $db->exec("DELETE FROM $this->table " . $sql); }catch(PDOException $e){ echo 'Error : '.$e->getMessage(); echo '<br/>Error sql : ' . "'DELETE FROM $this->table " . $sql . "'"; exit(); } return $result; } // уделение строки из базы данных public function deleteRow(){ $arrayAllFields = array_keys($this->fieldsTable()); array_walk($arrayAllFields, function(&$val){ $val = strtoupper($val); }); if(in_array('ID', $arrayAllFields)){ try { $db = $this->db; $result = $db->exec("DELETE FROM $this->table WHERE `id` = $this->id"); foreach($arrayAllFields as $one){ unset($this->$one); } }catch(PDOException $e){ echo 'Error : '.$e->getMessage(); echo '<br/>Error sql : ' . "'DELETE FROM $this->table WHERE `id` = $this->id'"; exit(); } }else{ echo "ID table `$this->table` not found!"; exit; } return $result; } // обновление записи. Происходит по ID public function update(){ $arrayAllFields = array_keys($this->fieldsTable()); $arrayForSet = array(); foreach($arrayAllFields as $field){ if(!empty($this->$field)){ if(strtoupper($field) != 'ID'){ $arrayForSet[] = $field . ' = "' . $this->$field . '"'; }else{ $whereID = $this->$field; } } } if(!isset($arrayForSet) OR empty($arrayForSet)){ echo "Array data table `$this->table` empty!"; exit; } if(!isset($whereID) OR empty($whereID)){ echo "ID table `$this->table` not found!"; exit; } $strForSet = implode(', ', $arrayForSet); try { $db = $this->db; $stmt = $db->prepare("UPDATE $this->table SET $strForSet WHERE `id` = $whereID"); $result = $stmt->execute(); }catch(PDOException $e){ echo 'Error : '.$e->getMessage(); echo '<br/>Error sql : ' . "'UPDATE $this->table SET $strForSet WHERE `id` = $whereID'"; exit(); } return $result; } }
Теперь приведу код модели для таблицы с пользователями (в таблице три столбца – id, first_name и last_name).
):
Class Model_Users Extends Model_Base { public $id; public $name; public $last_name; public function fieldsTable(){ return array( 'id' => 'Id', 'first_name' => 'First name', 'last_name' => 'Last name' ); } }
Теперь подробно рассмотрим код модели и абстрактного класса. С моделью все просто – мы наследуем все методы у Model_Base, создаем свойства с такими же именами, как и имена столбцов в таблице с пользователями. И создаем метод, при обращении к которому возвращается массив с именами всех полей таблицы. Имена полей таблицы нам необходимы при выполнении методом в классе Model_Base – при обращении к базе данных.
Абстрактная модель у нас получилась большая, но и работу она выполняет не маленькую, возможно, что всех ее методов нам будет достаточно для создания блога, но если чего-то будет не хватать, мы всегда можем расширить класс. Этим и хорош ООП – всегда можно без проблем и без вреда для сайта расширять функционал.
На данный момент наши модели умеют получать, обновлять, удалять и создавать записи в таблицах базы данных. Давайте рассмотрим несколько примеров того, что можно делать с бд, используя наши модели.
Получение записей из бд
Получение нескольких строк из таблицы:
// создаем запрос $select = array( 'where' => 'id >= 1 AND id <= 5', // условие 'group' => 'first_name', // группируем 'order' => 'id DESC', // сортируем 'limit' => 10 // задаем лимит ); $model = new Model_Users($select); // создаем объект модели $usersInfo = $model->getAllRows(); // получаем все строки var_dump($usersInfo); // выводим данные
Получение одной строки из таблицы:
$select = array( 'where' => 'id = 2' ); $model = new Model_Users($select); $usersInfo = $model->getOneRow(); var_dump($usersInfo);
Помимо получения строк, мы можем получать значения конкретных столбцов:
// запрос $select = array( 'where' => 'id = 2' ); $model = new Model_Users($select); $model->fetchOne(); // извлекаем данные // получаем значения столбцов $firstName = $model->first_name; $lastName = $model->last_name; // выводим var_dump($firstName); var_dump($lastName);
Создание записи в бд
Также просто, как и получать, мы можем и записывать данные:
// создаем объект $model = new Model_Users(); // задаем значения для полей таблицы $model->id = 10; // id можно и пропустить, если для этого поля настроен авто инкремент $model->first_name = 'Иван'; $model->last_name = 'Иванов'; $result = $model->save(); // создаем запись var_dump($result); // проверяем результат: true или false
Обновление записи в бд
Обновление записей в таблице тоже не составит проблем, выглядеть это будет так:
// запрос $select = array( 'where' => 'id = 10' ); // модель $model = new Model_Users($select); // извлекаем данные $model->fetchOne(); // задаем новые значения $model->first_name = 'Петр'; $model->last_name = 'Петров'; // обновляем запись $result = $model->update(); var_dump($result); // проверяем результат: true или false
Удаление записи в бд
И последнее элементарное действие с базой данных – удаление записей.
// модель $model = new Model_Users(); // условие удаления $select = array( 'where' => 'id > 10' ); // удаляем $result = $model->deleteBySelect($select); var_dump($result); // проверяем результат. Вернется количество удаленных строк
И еще один доступный вариант удаления, для одной записи:
// запрос $select = array( 'where' => 'id = 10' ); // модель $model = new Model_Users($select); // извлекаем данные $model->fetchOne(); // удаляем строку $result = $model->deleteRow(); var_dump($result);
Я думаю, что на этом стоит закончить сегодняшнюю статью, информации для размышления и осмысления пока достаточно. В следующей статье уже начнем проектировать блог.
Cкачать исходники и дамп тестовой таблицы можно архивом тут.
Отличная статья! А Вы не могли бы выложить исходники по этой статье?
Сейчас ломаю голову, как построить свою CMS. Просмотрел кучу примеров реализации mvc, везде что то да не так, а знания пока не позволяют прям полностью самому построить структуру)
Ваш метод нравится, Вы не описываете совсем уж простые вещи. Буду рад продолжению цикла статей.
Добрый день, я рад, что статьи кому-то оказались действительно полезными!
Исходники и дамп тестовой таблицы я выложил — в конце статьи.
Новые статьи будут выходить примерно раз в неделю.
Здравствуйте! Я вот сейчас делаю по MVC что-то типа фреймворка, чтобы можно было не подправляя код основных классов, сделать из сайта-визитки интернет-магазин или корпоративный портал.
Вот моя текущая структура папок:
application // находится само приложение
backend // администраторская панель
orders // Модуль «Заказы»
shop // Модуль «Товары»
categories // Модуль «Категории» и т.д.
frontend // внешняя часть сайта
controllers
models
bootstrap.php
system // системные классы и все необходимые элементы
classes // системные классы
View.php
Controller.php
Model.php
Route.php и т.д.
themes // Темы сайта
default
css
js
images
views
functions.php // Класс с методами текущей темы
.htaccess
config.php
index.php
Что скажете?
Вот у меня просто вопрос. Для каждой страницы по сути, свой контроллер, ну или для группы страниц. А как в таком случае сделать, чтобы какая нибудь функция выполнялась на всех страничках сайта. Например, я хочу, чтобы меню, которое создается в админке, отображалось на всех страничках. Где мне запросом его вытягивать лучше?
Заранее благодарю Вас за ответ))
Добрый день!
Для удобства, стоит вынести все служебные файлы, которые относятся к фреймворку, в отдельную папку — так будет проще переносить их с проекта на проект.
Чтобы использовать функции во всех контроллерах, можно создать вспомогательный класс и в нем писать общие для всех страниц методы.
Для решения проблемы с меню, можно писать логику формирования и вывода в шаблоне. Если смотреть по моей структуре, то в файле first_layouts.php, который используется на всех страницах. Но хочу заметить, что писать запросы в сомом шаблоне не совсем красиво и правильно, поэтому в шаблоне старайтесь минимизировать логику — сделайте там просто вызов вспомогательного класса и просто выведите, то что он вернет. А уже в самом классе опишите обращение к моделям(к бд) и формирование меню.
Написание вспомогательных классов и вынос фреймворка в отдельную папку я планирую описать чуть позже — в следующих статьях, но поскольку Вы спросили могу даль ссылку на репозиторий с исходниками, в которых я уже реализовал вынос папки с фреймворком, сделал разделение на модули — админка и сайт находятся в разных папках, а также написал вспомогалтельный для использования в любом мест сайла — класс называется service_isf.php, находится в папке с фреймворком. Вызывать его очень просто, например вызвать редирект можно вот так: ISF::redirect(‘index/index’);.
Вот ссылка на репозиторий, некоторые моменты еще «сырые», но в целом все рабочее, я постоянно обновляю репозиторий — можете переодически смотреть обновления:
https://github.com/Ipatov/example-blog Исходники лежат в папке www
Отлично! Спасибо Вам!
Блин, структуру не получилось передать. Вот так правильнее будет уж:
application // находится само приложение
backend // администраторская панель
orders // Модуль "Заказы"
shop // Модуль "Товары"
categories // Модуль "Категории" и т.д.
frontend // внешняя часть сайта
controllers
models
bootstrap.php
system // системные классы и все необходимые элементы
classes // системные классы
View.php
Controller.php
Model.php
Route.php и т.д.
themes // Темы сайта
default
css
js
images
views
functions.php // Класс с методами текущей темы
.htaccess
config.php
index.php
Ваши статьи оказались более чем полезны. Спасибо вам великое за колоссальный труд. Ваш подкаст вдохновил и подтолкнул меня наконец то слезть с проце_дурки. Конечно еще учить и учить, читать и читать. Во второй главе присутствовали небольшие ошибки в коде, разобрав и устранив которые я проникся в структуру до мозга костей и вник в суть MVC. И если бы там все было гладко, красиво и в белом пальто — я бы так и не понял смысла работы. Спасибо еще раз, бро.
PS Может и бойан но хочу тоже поделиться чем то полезным для ООП-шника. phpclasses.org — шикарный репозиторий классов php. И это не жалкий скриптовый варезник. Джа, бро!
Спасибо за добрые слова! После таких комментариев хочется писать и писать :) Жаль только сейчас со временем туговато
Спасибо за статью, внятно написана. Залил исходники на сервер, не работает. Если есть возможность, объсните, где косяк:
Warning: include(/config.php): failed to open stream: No such file or directory in /srv/http/temp2/index.php on line 5
Warning: include(): Failed opening '/config.php' for inclusion (include_path='.:/usr/share/pear') in /srv/http/temp2/index.php on line 5
Notice: Use of undefined constant DB_HOST - assumed 'DB_HOST' in /srv/http/temp2/index.php on line 8
Notice: Use of undefined constant DB_NAME - assumed 'DB_NAME' in /srv/http/temp2/index.php on line 8
Notice: Use of undefined constant DB_USER - assumed 'DB_USER' in /srv/http/temp2/index.php on line 8
Notice: Use of undefined constant DB_PASS - assumed 'DB_PASS' in /srv/http/temp2/index.php on line 8
Warning: PDO::__construct(): php_network_getaddresses: getaddrinfo failed: Name or service not known in /srv/http/temp2/index.php on line 8
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known' in /srv/http/temp2/index.php:8 Stack trace: #0 /srv/http/temp2/index.php(8): PDO->__construct('mysql:host=DB_H...', 'DB_USER', 'DB_PASS') #1 {main} thrown in /srv/http/temp2/index.php on line 8
У Вас какая версия php? Должно работать на 5.4
5.5.15
Извиняюсь за столь долгое ожидание.
Проблема в подключении файла конфигураций, достаточно подправить путь в индексом файле:
// подключаем конфиг
include (‘config.php’);
Я выложил пятую статью, в которой поправил все неточности, которые присутствовали ранее.