Эта статья будет иметь более прикладной смысл, чем предыдущие. Мы создадим класс модели, который вы в принципе сможете использовать в своих проектах
Для начала переименуем класс Model в ideal/classes/Model.php
в Registry и файл назовем также ideal/classes/Registry.php
При разработке можете скидывать на этот класс все, что связано с настройками, с запросами, и т.п.
В местах его использования также все переименуем
<?php class Registry{ private $data = array(); function __construct($data = array()) { $this->data = $data; } function __get($name){ return isset($this->data[$name])?$this->data[$name]:null; } function __set($name,$value){ $this->data[$name] = $value; } }
в App.php
$this->config = new Registry(array_merge($default_config, $custom_config)); //... $this->uri = new Registry(Router::gi()->parse($_SERVER['REQUEST_URI']));
и в application/models/user.php
class User extends Registry{
Теперь ничего нам не мешает создать новый класс ideal/classes/Model.php
<?php class Model{ private $_data = null; function __construct() { $this->_data = new stdClass(); } function __set($name, $value) { $this->_data->$name = $value; } function __get($name) { return property_exists($this->_data, $name) ? $this->_data->$name : null; } }
отличие от Registry пока лишь в том, что мы используем объект место ассоциативного массива.
Теперь при обращении к полю экземпляра такой модели
$model = new Model(); $model->id = 5;
вызывается магический метод __set
и внутреннему объекту $data
в поле $id
записывается нужное значение.
Зачем это делать? - спросите вы
Ведь можно сделать так
$data = new stdObject(); $data->id = 5;
и все.
Далее вы поймете - зачем это было нужно
Дополним __set
и __get
нашего класса, еще парой условий
<?php class Model{ private $_data = null; function __construct() { $this->_data = new stdClass(); } function __set($name, $value) { if (method_exists($this, 'set'.$name)) { return call_user_func(array($this,'set'.$name), $value); } return $this->_data->$name = $value; } function __get($name) { if (method_exists($this, 'get'.$name)) { return call_user_func(array($this,'get'.$name)); } return property_exists($this->_data, $name) ? $this->_data->$name : null; } }
теперь в наследниках этого класса можно делать следующее
class User extends Model{ function getFullName() { return $this->name.' '.$this->surname; } function setFullName($value) { list($this->name, $this->surname) = explode(' ', $value); } // валидация function setEmail($value) { if (preg_match('#.+@.+#u', $value)) { $this->data->email = $value; } } } $user = new User(); $user->name = 'Иван'; $user->surname = 'Петров'; echo $user->fullname; //Иван Петров $user->fullname = 'Ася Демидова'; echo $user->name; //Ася $user->email = 'email'; echo $user->email; // пусто $user->email = 'email@yandex.ru'; echo $user->email; // email@yandex.ru
Уже интереснее?!
Магия, да и только. Так вы можете не только обрабатывать существующие поля, чтобы в модель заносились только валидные данные, но добавлять сколько угодно не существующих полей, которые одним своим вызовом будут запускать целую плеяду событий. В одном из моих проектов были карты, у них были объекты. Просто добавляем метод getObjects, в нем изымаем все объекты карты и в любом месте программы, к объектам карты можно обратится по полю, print_r($map->objects);
Это еще не все волшебство.
Когда нам нужно добавить данные присланные из формы, в нашу модель, мы делали бы как-то так
$user = new User(); $user->login = $_POST['login']; $user->password = $_POST['password']; $user->status = $_POST['status'] ? 1 : 0; $user->email = $_POST['email'];
Это просто, когда у нас всего 4 поля. А если их 10, 20, 50? Следить за всем этим зоопарком не будет ни времени ни возможности. Автоматизируем этот процесс.
<?php class Model{ private $_data = null; function __construct() { $this->_data = new stdClass(); } function __set($name, $value) { if ($name==='__attributes') { foreach ($value as $key=>$value) { $this->__set($key, $value); } return; } if (method_exists($this, 'set'.$name)) { return call_user_func(array($this,'set'.$name), $value); } return $this->_data->$name = $value; } function __get($name) { if ($name==='__attributes') { return &$this->_data;; } if (method_exists($this, 'get'.$name)) { return call_user_func(array($this,'get'.$name)); } return property_exists($this->_data, $name) ? $this->_data->$name : null; } }
теперь в коде, можно использовать свойство __attributes
<?php $user = new User(); $user->__attributes = $_POST; echo $user->login; // Иван
все что есть в $_POST
заполнит нашу модель данными.
Вот тут настало время подумать о безопасности модели. Что если злоумышленник засунет в $_POST
лишнее поле. Да что там злоумышленник, мы сами можем случайно добавить в форму не нужное поле. Конечно, в данном примере ничего страшного не произойдет, но что если нам нужно сохранить эти данные в таблицу?! Лишнее поле вызовет ошибку и добавит нам головной боли.
Поэтому при любом присваивании надо учитывать, необходимо ли нам такое поле или нет. Для этого подойдет массив названий поддерживаемых полей.
Добавим в класс Model поле
public $safe = array();
и в __set
в конец добавим условия
if (in_array($value, $this->safe)) { $this->_data->$name = $value; }
класс будет выглядеть так
?php class Model{ private $_data = null; public $safe = array(); function __construct() { $this->_data = new stdClass(); } function __set($name, $value) { if ($name==='__attributes') { foreach ($value as $key=>$value) { $this->__set($key, $value); } return; } if (method_exists($this, 'set'.$name)) { return call_user_func(array($this,'set'.$name), $value); } if (in_array($value, $this->safe)) { $this->_data->$name = $value; } } function __get($name) { if ($name==='__attributes') { return &$this->_data;; } if (method_exists($this, 'get'.$name)) { return call_user_func(array($this,'get'.$name)); } return property_exists($this->_data, $name) ? $this->_data->$name : null; } }
теперь в наследниках надо прописывать этот массив валидных значений
class User extends Model{ public $safe = array('id', 'login', 'password', 'email'); } $user = new User(); $user->__attributes = array( 'id'=>33, 'login'=>'Иван', 'sql'=>'trancate users;', 'email'=>'sko@ya.ru' ); echo $user->id;//33 echo $user->email;//sko@ya.ru
но
echo $user->sql;//null
Удобный класс? Теперь немного отвлечемся и подумаем, что же такое модель. Википедия дает нам такой ответ
Модель (англ. Model). Модель предоставляет знания: данные и методы работы с этими данными, реагирует на запросы, изменяя своё состояние. Не содержит информации, как эти знания можно визуализировать.
Определение верное. Модель не обязательно должна взаимодейтсвовать с базой данных или еще каким-то хранилищем. Модель вообще не означает хранение данных. Но она должна уметь обрабатывать данные и реагировать на изменение своих полей. Примером может стать простейший класс авторизации пользователя. Если у вас нет времени создавать полноценную аутентификатцию с солью, хешем и таблицей users в базе, то в single page приложениях можно ограничится таким классом авторизации.
?php class User extends Model{ function getAuth() { if ($_SESSION['auth']) { return true; } $_SESSION['auth'] = ($this->login=='ivan' and $this->password == 'parol') ? true : false; return $_SESSION['auth']; } }
и далее в контроллере User добавляем метод login
<?php class UserController extends Controller{ function actionIndex(){ $model = new User(); $this->render('index',array('model'=>$model)); } function actionLogin() { $user = new User(); if (isset($_POST['login'])) { $user->>login = $_POST['login']; $user->password = $_POST['password']; if ($user->auth) { header('Location:/'); //в случае успеха переходим на главную exit(); } else { $this->error = 'Не верный пользователь или пароль'; } } $this->render('login',array('model'=>$user)); } }
это простейшая реализация авторизации на сайте. Безусловно она не подходит тогда, когда нам требуется сделать авторизацию для нескольких пользователей. Но вполне подойдет, когда вам быстро нужно сделать закрытое приложение на чистом php. После того как пользователь пройдет авторизацию, в любом месте фреймворка можно проверить зарегистрирован ли он, или это случайный пассажир.
$user = new user(); if (!$user->auth) { header('Location:/user/login'); exit(); }
Но все же, когда речь заходит о моделях, в первую очередь вспоминают базы данных. Для того, чтобы модель взаимодейтсвовала с БД, ей нужно в этом помочь. Возьмем некий класс - оболочку, для нативных функций php с префиксом mysql_
.
Я воспользуюсь собственной разработкой db.php, но вы вольны выбирать любую другую. Обычно используют какую-либо DBO реализацию. Оболочка должна уметь вытаскивать данные, и добавлять и обновлять их. В удобной форме.
Теперь научим нашу модель сохранять данные. Добавим в класс Model метод save
и дадим ей возможность подключатся к БД. Для этого надо где-то инициировать подключение нашей оболочки и передать ее экземпляр в класс модели. Делать это для каждой модели будет накладно, потому как моделей может быть тысячи. Поэтому добавим экземпляр db
в класс App
и будем в модели использовать его.
Изменим конструктор App
. Настройки подключения к БД мы заботливо положили в application/config.db.php
и они доступны в App
как $this->config->db
или из любого места в приложении как App::gi()->config->db
public function __construct(){ $this->initSystemHandlers(); $default_config = include IDEAL.'config.php'; $custom_config = include APP.'config.php'; $this->config = new Registry(array_merge($default_config, $custom_config)); include IDEAL . 'classes/adapter/db.php'; $this->db = new db; $this->db->connect($this->config->db); }
теперь в любом месте фреймворка, можно использовать App::gi()->db
, к примеру так
$users = App::gi()->db->query('select * from user')->rows();
Но настоятельно рекомендую вам не пользоваться SQL где-либо кроме моделей. Предыдущая строка вернет все записи из таблицы users, но это будут не модели User а всего-лишь объекты с полями. Чтобы эти объекты стали моделями, поиск должна производить сама модель User, и возвращать свои экземпляры. Забегая вперед скажу, что делать мы это будем так
$users = User::models()// выведет всех пользователей
Не применяйте SQL где-либо, кроме ваших моделей.
Мы отвлеклись. Вернемся в класс Model. Теперь надо научить его сохранять данные а базу. Для этого добавим ему свойство table
и методы save
и beforeSave
Второй метод необходим для проверки всех полей, и если он вернет true то сохраняем данные.
Плохая идея - класть все яйца в одну корзину; поэтому для работы с БД, создадим отдельный класс ModelTable
сделаем его абстрактным. Чтобы никому из разработчиков, не пришло в голову, создавать его экземпляр напрямую. ideal/classes/ModelTable.php
?php abstract class ModelTable extends Model{ public $errors = array(); public $table = '{table}'; public $primary = 'id'; function beforeSave () { return !count($this->errors); } function save () { $modelname = get_called_class(); if ($this->beforeSave()) { if (!$this->__get(self::$primary)) { $res = App::gi()->db->insert($modelname::$table, $this->_data); $this->__set($modelname::$primary, App::gi()->db->id()); return $res; } else { return App::gi()->db->update( $modelname::$table, $this->_data, $modelname::$primary.'='.$this->__get(self::$primary) ); } } } }
теперь, чтобы создать запись в базе - нужно выполнить всего несколько шагов
class Post extends ModelTable{ public $table = 'posts'; public $safe = array('id','name','content'); public function beforeSave() { if (strlen($this->name)<3) { $this->errors['name'] = 'Слишком короткий заголовок'; } return parent::beforeSafe(); } } $post = new Post(); $post->__attributes = $_POST; if ($post->save()) { echo 'Данные сохранены'; } else { print_r($post->errors); }
модель умеет себя добавлять и обновлять. Последнее, чему мы ее научим в рамках данного урока - это выбирать экземпляры себя из базы.
Добавим в ModelTable метод models и getQuery
static function getQuery() { $modelname = get_called_class(); return 'select * from '.$modelname::$table; } static function models() { $items = App::gi()->db->query(self::getQuery())->rows(); $results = array(); $modelname = get_called_class(); foreach ($items as $item) { $model = new $modelname(); $model->__attributes = $item; $results[] = $model; } return $results; }
метод getQuery вы будете переопределять в своих моделях и добавлять разбиение на страницы, фильтры, join-ы и т.д. все, что нужно для нормальной работы сайта с тысячами записей. Теперь, чтобы вытянуть из базы все посты, делаем так
$posts = Post::models();
модель умеет извлекать себе подобных, но не умеет выбирать саму себя. Добавим статичный метод model
static function model($id) { $modelname = get_called_class(); $item = App::gi()->db->query('select * from '.$modelname::$table. ' where '.$modelname::$primary.'='.App::gi()->db->_($id))->row(); $model = new $modelname(); $model->__attributes = $item; return $model; }
теперь, чтобы получить модель по ее id
$post = Post::model(55);
CRUD
CRUD — (англ. create, read, update, delete — «создание, чтение, обновление, удаление») сокращённое именование 4-х базовых функций, используемых при работе с персистентными хранилищами данных. Термин стал популярным благодаря книге Джеймса Мартина (англ. James Martin) «Managing the data-base environment», выпущенной в 1983 году
.
CRUD - это не какая-то технология, это просто набор действий, которые мы должны обеспечить, чтобы пользователь мог полноценно работать с какими-либо элементами сайта: пользователи, посты, фотографии и т.д.
В большинстве фреймворков, такими элементами заведуют модели, а значит в рамках данной статьи, мы просто обязаны рассмотреть, как создать полноценную CRUD систему, используя наши модели.
Тем более, что сделать эту уже настолько просто, насколько вообще возможно.
Первое, что мы сделаем - это добавим в базу данных таблицу Posts
CREATE TABLE `posts` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , `name` VARCHAR( 256 ) NOT NULL , `content` TEXT NOT NULL );
Теперь опишем модель этой таблицы ideal/application/models/Post.php
<?php class Post extends ModelTable{ static public $table = 'posts'; public $safe = array('id','name','content'); public function beforeSave() { if (strlen($this->name)<3) { $this->errors['name'] = 'Слишком короткое название'; } return parent::beforeSave(); } }
Тут, перед сохранением в БД, мы проверяем введено ли название поста, и достаточной ли оно длины. Это единственное условие. Вы можете добавить туда сколько угодно условий.
Теперь перейдем непосредственно к CRUD, который реализуется внутри контроллера
Создадим новый контроллер ideal/application/controllers/PostController.php
<?php class PostController extends Controller{ function actionIndex() { $this->render('index',array('items'=>Post::models())); } function actionCreate() { $post = new Post(); if (isset($_POST['form'])) { $post->__attributes = $_POST['form']; if ($post->save()) { header('location:/post/index'); exit(); } } $this->render('form',array('item'=>$post)); } function actionRead($id) { $id = (int)$id; $post = Post::model($id); $this->render('read',array('item'=>$post)); } function actionUpdate($id) { $id = (int)$id ? (int)$id : (int)$_POST['form']['id']; $post = Post::model($id); if ($post->id) { if (isset($_POST['form'])) { $post->__attributes = $_POST['form']; if ($post->save()) { header('location:/post/index'); exit(); } } $this->render('form',array('item'=>$post)); } else { throw new Except('Запись не найдена'); } } }
Три действия, которые реализуют CRU и одно - для просмотра всего списка постов. Все, что осталось сделать - это создать три файла представления: index, read и form
Форма для создания и обновления ideal/application/views/post/form.php
<form action="/post/<?=App::gi()->uri->action?>" method="post"> <div class="form-group"> <label for="name">Название</label> <input type="text" class="form-control" id="name" name="form[name]" placeholder="Введите название поста" value="<?=htmlspecialchars($item->name)?>"> </div> <div class="form-group"> <label for="content">Текст</label> <textarea type="password" class="form-control" name="form[content]" id="content"><?=htmlspecialchars($item->content)?></textarea> </div> <input type="hidden" name="form[id]" value="<?=intval($item->id)?>"> <button type="submit" class="btn btn-default"><?=($item->id ? 'Сохранить' : 'Создать')?></button> </form>
Файл для вывода списка всех объектов в виде таблицы ideal/application/views/post/index.php
<a href="/post/create">Добавить запись</a> <table class="table table-bordered"> <tr> <th>ID</th> <th>Название</th> <th>Текст</th> <th>Редактировать</th> </tr> <? foreach($items as $item) {?> <tr> <td class="col-md-1"><?=$item->id?></td> <td class="col-md-4"><?=$item->name?></td> <td class="col-md-7"><?=$item->content?></td> <td class="col-md-7"><a href="/post/update/<?=$item->id?>" class="glyphicon glyphicon-pencil"></a></td> </tr> <?};?> </table>
Посмотреть, как работает CRUD можно здесь
На первый взгляд это семечки, и такое вы можете написать без всяких моделей, за 1 час. Но это всего лишь демонстрация. В реальных моделях могут быть десятки полей и правил. В вашем написанном за час скрипте бы пришлось править сотню мест, а здесь мы будем исправлять только форму и модель. Все остальное будет неизменным.
Файл read.php напишите сами, он ничем не примечателен и похож по сути на form, только без полей
Исходные коды урока посмотреть и скачать
Исходные коды конечного фреймворка посмотреть и скачать
Данный цикл статей продолжиться. В следующей статье разовьем тему авторизации сделаем полноценную ролевую систему
Комментарии
Если сайт находится не в корне, а в папке (на пример http://xdan.ru/ideal/), можно ли обойтись настройками?
Ещё не хватает постраничного вывода.
Постраничный вывод сделать не так сложно. рассмотрим в следующем уроке
Правда есть ошибки ,но они мобилизуют от тупого копипаста.
Спасибо!
Есть предложения:
Дополнить main.php
session_start();
и
define( "SECFW", true );
для последующей вставки в каждый php файл проверки:
defined('SECFW') or die('Прямой доступ к файлу закрыт.');
З.Ы. Зайдя на http://ideal.xdan.ru/post попадаем на порносайт
php запускается с любого места
данные сильно не валидятся и т.д.
Можно прикрутить например mysqli_real_escape_string
для валидации или те же preg_* и т.д. насколько хватает фантазии.
А вообще для начала сделать хотя бы один вход (я постил выше).
Автор только показал направление, остальное флаг вам в руки.
Про аутентификацию... - может лучше сделать модулем и пагинатор и ещё много всего.
Лучше сделать механизм для модулей.
http://phpclub.ru/detail/article/mysqli
коротко о главном - увеличение скорости работы иногда в 40 раз - надеюсь убедил
Вот ещё вопрос возник - про рендер и main.php (который во views) - как то не правильно это....
Может сделать какой нибудь базовый контроллер с базовым видом и от него наследоваться другим контролерам. А то как то не наглядно. Получается main.php к ядру привязан.
Сделать класс например абстрактный BaseController с методом before(внутри вызов main.php) . Ну и абстрактный(!) класс контролера с вызовом этого метода первым в рендере. Как то так...
Меняем в ..\ideal\classes\Controller.php
public $tplFileName = ''; //Добавляем переменную будет имя нашего главного шаблона
public function renderPage($content)
{
//Если существует шаблон - действуем как ранее иначе просто вывод вида контролера
$html = (!empty($this->tplFileName))?$this->_renderPartial ($this->tplPath.$this->tplFileName,array('content' =>$content), false):$content;
Ну и в приложении создаем ..\application\controllers\BaseController.php :
public $tplFileName = 'main.php';
}
И все наши контроллеры наследуем от него.
И всё прозрачно + можно ковырять своё базовый шаблон или ещё там что.
Получается js отправляет запрос на сервер к вашему контроллеру - получить список чего то (например), а он - не хороший, помимо списка, ещё и базовый шаблон вам обратно выдаёт
И вам приходиться вставлять сиё с html тэгами - внутрь html тэгов вашей страницы - что есть бред.
Вот что бы такого не было - одни контролеры наследуются от базового контроллера с базовым шаблоном, другие наследуются от чистого контроллера и выдают только то что нужно для ajax.
$.post("/controller/method", { name: "John", time: "2pm" },
function(data){
alert("Data Loaded: " + data);
});
$.ajax({
type: "POST",
url: path + "?ajaxSend=1",
...})
А в акшене:
if (isset($_GET["ajaxSend"]) && $_GET["ajaxSend"] == "1") {
die(print_r($_POST));
}
Такой велосипед поедет?)
$.ajax({
type: "POST",
url: path,
data: "ajaxSend=1",
...});
А в акшене - условие isset - избыточное, ну собствено POST:
if ($_POST["ajaxSend"] == "1") {
die(print_r($_POST));
}
Что то в этом роде....
Я например сделал себе такую возможность маршрутов:
http://my.ru/conroller/action/id1/id2/id3/id4
И заруливаю всё в такой url:
http://my.ru/conroller_id1_id2_id3_id4.html
Поисковики просто рады
Собственно ради отказа от GET наверно и придумали маршруты...
Костыль если лень создавать класс для обработки аякс запросов.
А вообще - нужен отдельный контролер для аякса (например) и у него методы для различных аяксов с различными видами(форматами возвращаемых данных) - ведь данные не просто кучей отсылаются, а с тэгами.
Для этого надо
static function models() {
$items = App::gi()->db->query(self::getQuery())->rows();
заменить на:
static function models() {
$items = App::gi()->db->query(static::getQuery())->rows();
Можно в App::gi()->db->query() сразу запрос писать.
То есть работа модели ограничивается созданием нужного запроса и получения-передачи данных в контроллер?
Можно примерчик?
Перелопатил весь код, ошибки не вижу.
Вот такая ошибка:
Class 1Controller no exist! (C:\Server\OpenServer\domains\localhost\1\ideal\classes\Singleton.php:14)
#0 C:\Server\OpenServer\domains\localhost\1\ideal\cla sses\Singleton.php(17): Singleton::getInstance('1Controller')
#1 C:\Server\OpenServer\domains\localhost\1\ideal\cla sses\App.php(17): Singleton::gI('1Controller')
#2 C:\Server\OpenServer\domains\localhost\1\main.php( 6): App->start()
#3 {main}
Одно из предположений, что где-то слеша нету или путь не так прописан.
Подскажете в чем дело?
Спасибо автору за проделанную работу
Есть от чего отталкиваться.
Теперь можно опробовать.
У меня nginx, ошибка была в контроллере.
Довольно шустрый, вполне понятный и более менее грамотно написан.
Не знаю как он проявит себя на реальном проекте, в любом случаи чем я могу помочь?
От Model норм наследуется, а от ModelTable не хочет. Что вы изменили в коде и не написали в блоге?
Стоить пост. Может это особенности моего хостинга?
А зачем класс Регистри, если есть Модел?