Модели данных
- Теория моделей
- Менеджер моделей Model
- Общее описание
- Выборки (get methods)
- Добавление (add methods)
- Изменение (update methods)
- Удаление (delete methods)
- Импорт (delete methods)
- Связи (link methods) 1. Теория и типы связей 2. Генерация связей по базе данных 3. Добавление своих связей
- Объекты сущности Entity
- Типы данных
- Getters
- Объект коллекции Collection
- Фильтры
- Теория фильтрации
- Значения по-умолчанию
- Каскад данных при фильтрации
- Как добавить свои фильтры в модель
- Как отфильтровать входные данные
- Как отфильтровать отдельное поле (filterValue)
- Как запретить фильтрацию
- Как работает фильтрация (вид изнутри)
- Фильтры, которые прописываются автоматически
- Список фильтров
- Валидаторы
- Объект результата выполнения операции
- Объекты условий Cond
Chapter 1. Теория моделей
Chapter 1.1. Что это такое?
- ЭТО НЕ ORM, как его понимают программисты PHP :)
- ЭТО ORM, так как иммет связи;
- Тут нет и не будет ActiveRecord;
- Основной принцип - договоренности и условности;
- Жестка формальность наименований и полей БД;
- Упрощенные объектные связи;
- Упрощенное управление данными в БД;
- Выделение сущностей и действий над ними.
Так что же это такое?! Модели - базируются на договоренностях наименования таблиц, полей и ключей в базе данных. В моделях мы выделяем сущности (Entity) и связи между сущьностями. Модель позволяет управлять данными в базе.
Chapter 1.2. Цели моделей?
При проектировании моделей преследовалась цель дать инструмент для работы с базой данных для пользователей разного уровня квалификации:
- Начинающий - знает основы, может пользоваться только интерфейсом (тем что уже есть), не может сильно накосячить;
- Обычный уровень - понимает как работает, как работают связи и основные части системы, может строить сложные выборки и обработки, не может сильно накосячить;
- Гуру - понимает архитектуру, может дописывать плагины, модифицировать сами модели, может очень сильно накосячить;
От этих трех целей строится вся идея моделей.
Пример
Есть комманда разработчиков разного уровня:
- Женя - разработчик, только пришел в компанию;
- Петя - разработчик, работает в компании долго, примерно знает как все устроено;
- Вася - проектирует все новые разработки.
Жене дали задание сделать сложный раздел. Он накидывает базу данных по всем правилам. Так же не забывает, прописать все фильтры и валидаторы. Система моделей автоматически генерит ему работающий CRUD-код. Женя его использует и незадумывается над правильностью этого кода. Но, Жене нужно сделать сложную выборку или обработку в моделе, по-правилам, ему запрещено это делать самостоятельно. Он обращается к более профессиональному коллеге.
Петя, знает все принципы работы моделей, он умеет делать сложные выборки, хорошо понимает связи сущностей. Он может помочь Жене с его проблемой, совместно они строят сложную выборку, даже не сломав и не нагнув базу. Но в какой-то момент они понимают, что стандартный функционал не может решить их проблему. Нужно модифицировать саму систему (да, такое бывает)
Вася написал уже кучку плагинов и пофиксил несколько багов. Женя и Петя приходят к нему. Вася анализирует ситуацию. Если решение единоразовое, то он не будет его вносить в систему, он предложит обойти проблему. Если проблема глобального масштаба, то нужно либо дописать плагин к генератору моделей, либо изменить модели. Он вносит правки.
Таким образом мы отгораживаем разработчиков от чистого SQL и контролируем связи и запросы к базе отвечающие за выбор данных. Только гуру может внести в процесс изменение и изменить логику выборки данных из базы. При этом непрофессиональные пользователи "бездумно" оперируют связями без боязни сильно "накосячить".
Chapter 1.3. Как достигаются цели?
Небольшое количество атомарных элементов:
- Объект (Entity)
- Набор объектов (Collection)
- Объект условий для действий (Cond)
- Менеджер объектов (Model)
Четко типизированная (константы) структура, не позволит сделать лишнего. В то же время, структура легко изменяется. Можно с легкостью модифицировать практически любые элементы, но это больше присуще архитекторам системы.
В основе лежат простые принципы проектирования базы данных. Нотация, по-которой создается база данных, достаточно хорошо выверена и логична. Многие методы имеют жестко-определенные названия, что позволяет писать функционал и не искать название метода.
Код моделей и методов работы с ней получается легко-читаемым и логичным. Можно посмотреть на переменную и четко сказать, что за модель ее сгенерила и какого типа переменная. Что бы это было, нужно всег-лишь соблюдать нотаци и следовать рекомендациям из этого описания.
Вся основная структура связей/отношений объектов строится автоматически, на основе базы данных. Главное в этой операции - это создать базу данных с правильными ключами и связями. Генератор моделей сделает всю основную работу.
Простота структуры и управления данными - основной принцип. Все операции с данными - очень просты и легки в понимани. Отношения объектов заложены в архитектуру и неподдаются изменению незатрагивая архитектуру приложения. Это накладывает определенные ограничения, но в то же время является преимуством - простота понимания.
Chapter 1.4. Основа моделей
В основе всего лежит сущность. Точнее ее содержимое. Любая сущность имеет свои свойства и связанные сущности. В моделях используется многомерный массив, связанные сущности которого помечаются нижним подчеркиванием в начале ключа (поэтому поля в базе с нижним подчеркиванием в начале имени колонки - запрещены).
<?php
$product = array(
'id' => 12,
'name' => 'Товар',
'price' => 12.98,
'brand_id' => 12,
'_brand' => array(
'id' => 12,
'name' => 'levis'
'_brand_alias_collection' => array(
array('id' => 1
'name' => 'levi\'s'),
array('id' => 2
'name' => 'levi\'strauses co'),
)
)
)
?>
Нужно запомнить три следующих утверждения:
- Поля начинающиеся не с _ - это свойства сущности;
- Поля, которые начинаются с _ - это связанная сущность или набор сущностей
- Если поле начинается с _ и заканчивается на _collection - это набор сущностей
Эти утверждения основа, но так же можно встретить связанные данные, которые не являются ни сущностью, ни набором. Это так называемые кешируемые данные или агрегатные.
<?php
$product = array(
'id' => 12,
'name' => 'Товар',
'price' => 12.98,
'brand_id' => 12,
'_brand_name' => 'levis',
'_brand' => array(
'id' => 12,
'name' => 'levis'
'_brand_alias_collection' => array(
array('id' => 1
'name' => 'levi\'s'),
array('id' => 2
'name' => 'levi\'strauses co'),
)
)
)
?>
Обратите внимание на _brand_name - это либо агрегатные данные либо кешируемые, то есть этот параметр может появиться в выборке:
<?php
// Опции для выборки
$cond = ProductModel::getIntance()->getCond()
->join(ProductModel::JOINT_BRAND) // Присоединяем бренд
->column(array('*', '_brand_name' => 'brand.name')) // _brand_name - агрегатное свойство
$product = ProductModel::getIntance()->get($cond);
?>
Но так же это может появиться в Entity.
<?php
class MyEntity {
public function getBrandName()
{
if ($this->has('_brand_name')) {
return $this->get('_brand_name');
} elseif ($this->getBrand()) {
$this['_brand_name'] = $this->getBrand()->getName();
}
return $this->get('_brand_name');
}
}
?>
Такая схема достаточно проста, понятна и логична. Абсолютно, все методы опираются на такую структуру массива.
Chapter 2. Менеджер моделей Model
Chapter 2.1. Добавление данных add
У каждой модели есть метод добавления. В процессе выполнения происходят следующие действия:
- Проверка данных на пустоту, если пусто то вернуть пустой результат
- Применяем каскад данных
- Присваиваем значения по-умолчанию
- Фильтруем данные
- Проверяем данные
- Вставляем данные в базу
Результат метода это объект \Model\Result\Result
Chapter 5. Фильтры
Chapter 5.1. Теория фильтрации
!!!ВСЕ ДАННЫЕ ДОЛЖНЫ ФИЛЬТРОВАТЬСЯ!!!
Данные которые мы получаем в моделе, по-умолчанию считаются грязными. Их нужно отмыть, накормить, проверить и поробовать запихать в базу. Дальше расскажу об этапах добавления данных и как можно настроить модель что бы все было по фен-шую.
Представьте, что на входе мы имеем ассоциативный массив с данными, где ключ - это значение поля, а значение собственно само значение этого поля. Прежде чем попасть в базу каждое поле должно быть обработано по следующей схеме:
- Значения по-умолчанию - наложение занчений по-умолчанию, кастомим через setupDefaultsRules
- Каскад полей - если поле пустое, возможно значение можно взять с другого поля, кастомим через setupFilterCascadeRules
- Фильтрация данных - на каждое поле натравливаются фильтры, которые можно кастомить через методы setupFilterRules
- Проверка данных - теперь мы все это проверям, кастомить через метод setupValidatorRules
Как проектируем
При создании базы данных мы прописываем поля. При прописывании поле в базе данных, нужно думать, какие данные должны попасть в базу. Другими словами, мы должны отвечать на несколько важных вопросов для каждого поля:
- Что должно быть в поле;
- Что может прийти (прийти может что угодно и еще чуть-чуть);
- Как я буду это фильтровать;
Пример
В базу данных нужно добавить поле price, помня ошибки прошлого мы делаем его decimal(9, 2), что бы не было проблем с точностью вычислений.
- Что должно быть в поле:
- Число с плавающей точкой
- Что может прийти:
- 1 200 (пробел)
- 1,200 (число с разделителем)
- 1.200 (число с разделителем)
- 1,200.00 (число с разделителем и копейками)
- 1.200,00 (число с разделителем и копейками)
- 1,200.000.00 (неправильное число с разделителем и копейками)
- 1.200,00.00 (неправильное число с разделителем и копейками)
- $1.200,00.00 dollars (мусор)
- Как будем фильтровать:
- Подчистим от левых данных;
- Float наложить точно не получится;
- Нужно делать разбор числа
Вывод: нужно написать свой фильтр, например, \App\Filter\Price
Chapter 5.2. Значения по-умолчанию
При добавлении мы учитываем данные по-умолчанию. Это значит что в модельках есть сгенеренный массив в котором мы прописываем, что при создании новой записи, если неопределено поле, то ему нужно присвоить определенное значени. Настраиваются свои значения так:
<?php
public function setupDefaultsRules()
{
$this->defaultRules['test_field'] => 'Значение по-умолчанию';
}
?>
Chapter 5.3. Каскад данных при фильтрации
Представьте, что у вас есть сущность в которой есть поля:
- name - название чего-то
- h1 - заголовок
- title - название где-то еще
- meta_title - название для заголовка браузера
- slug - часть от URL
но на входе есть только name, так почему же его нельзя использовать для формирования других полей?!
<?php
public function setupFilterCascadeRules()
{
// Эти фильтры работают только при добавлении, для фильтров на update используйте updateFilterCascade
// Если имя прийдет пустое попробовать взять по-очереди из h1, title, meta_title
$this->addFilterCascadeRules['name'] => array('h1', 'title', 'meta_title');
$this->addFilterCascadeRules['h1'] => array('name', 'title', 'meta_title');
$this->addFilterCascadeRules['title'] => array('name', 'h1', 'title');
$this->addFilterCascadeRules['meta_title'] => array('name', 'h1', 'meta_title');
$this->addFilterCascadeRules['slug'] => array('name', 'h1', 'title', 'meta_title');
}
?>
Chapter 5.4. Как добавить свои фильтры в модель
В объекте модели нужно добавить метод:
<?php
public function setupFilterRules()
{
$this->filterRules['field'][] = Filter::getFilterInstance('\App\Filter\MyFilter');
}
?>
Chapter 5.5. Как отфильтровать входные данные
<?php
$inputData = array( /** data here **/ );
$filteredInputData = SomeModel::getInstance()->filterOnAdd($inputData);
?>
Chapter 5.6. Как отфильтровать отдельное поле (filterValue)
<?php
// Входное значение
$input = 'some date here';
$filteredInput = SomeModel::getInstance()->filterValue($input, 'field_name');
?>
Chapter 5.7. Как запретить фильтрацию
Методы filterOnAdd и filterOnUpdate вторым параметром принимают объект условий, за флаг фильтрации отвечают следующие константы:
<?php
Cond::FILTER_CASCADE_ON_ADD = true | false; // Разрешить каскад полей в момент фильтрации при добавлении
Cond::FILTER_CASCADE_ON_UPDATE = true | false; // Разрешить каскад полей в момент фильтрации при изменении
Cond::FILTER_ON_ADD = true | false; // Разрешить фильтрацию полей при добавлении
Cond::FILTER_ON_UPDATE = true | false; // Разрешить фильтрацию полей при изменении
?>
Chapter 5.8. Как работает фильтрация (вид изнутри)
Каждая модель имеет список правил фильтрации для полей:
<?php
$this->filterRules = array(
'id' => array(
Filter::getFilterInstance('\App\Filter\Id')
),
'slug' => array(
Filter::getFilterInstance('\App\Filter\Slug')
),
'create_date' => array(
Filter::getFilterInstance('\App\Filter\Date'),
Filter::getFilterInstance('\Zend\Filter\Null')
),
'modify_date' => array(
Filter::getFilterInstance('\App\Filter\Date')
),
'status' => array(
Filter::getFilterInstance('\App\Filter\EnumField')
)
);
?>
Эти правила инициализируются методом:
<?php
$this->getFilterRules();
?>
После инициализации основных правил фильтрации запускается метод в котором разработчик может управлять текущими правилами фильтрациями:
<?php
$this->setupFilterRules();
?>
На эти данные опираются все методы фильтрации. Дальше при добавлении/измении запускается механизм фильтрации:
- filterOnAdd
- filterOnUpdate
Работают они по одному принципу, различие только в отношении к неопределенным полям (update их пропускает).
Схема работы этих методов:
- Применяем каскад данных;
- Получаем правила фильтрации;
- Фильтруем поля;
- Поля для которых нет фильтров, остаются неизменными;
Chapter 5.9. Фильтры, которые прописываются автоматически
Поле в базе | Что проверяем | Какие фильтры |
---|---|---|
@type - tinyint @type - smallint @type - mediumint @type - int @type - bigint |
- Число без плавающей точки |
App\Filter\Int App\Filter\Null (if nullable) |
@type - enum |
- Латинские буквы, цифры и символ подчеркивания |
App\Filter\EnumField App\Filter\Null (if nullable) |
@type - char @type - varchar @type - enum @type - tinyblob @type - tinytext @type - blob @type - text @type - mediumblob @type - mediumtext @type - longblob @type - longtext @type - timestamp |
- Отсутствие пробельных символов по краям |
App\Filter\StringTrim App\Filter\Null (if nullable) |
id _id$ |
- Число без плавающей точки - Положительное - Не ноль |
App\Filter\Id |
name[_translate или _alias]$ title[_translate или _alias]$ h1[_translate или _alias]$ meta_title[_translate или _alias]$ |
- Строка без пробельных симоволов по краям - Без тегов - Без двойных пробельных символов - Начинается с большой буквы - Html Entity заменены на UTF-8 символы |
App\Filter\Name |
slug |
- Только латиница и цифры - Остутствуют пробельные символы - Начинается и заканчивается либо на букву, либо на цифру - Теги и недопустимые символы вырезаются - Из русского текста делаем транслит - Ограничен длинной в 255 символов |
App\Filter\Slug |
description _description text _text |
- Накладывается вырезатель опасных тего - Разрешены все теги кроме опасных - Накладывается типограф |
App\Filter\Text |
url _url$ |
- Валидные данные url - Запрещены теги - Запрещены символы не подходящие для url - Не хранится schema URL |
App\Filter\Url |
level pos |
- Только число - Только целое - Только положительное |
App\Filter\Abs |
email _email$ |
- Символы разрешенные в E-Mail |
App\Filter\Email |
price _price$ |
- На выходе float число | App\Filter\Price |
^is_ | - Может содержать только y или n | App\Filter\IsFlag |
_hash$ |
- Может содержать только [A-H0-9] только в верхнем регистре - Максимальная длинна 40 символов |
App\Filter\Hash |
_stem$ |
- Стем слов |
App\Filter\Stem |
_count$ | - На выходе int число (положительное) |
App\Filter\Abs App\Filter\Abs |
_date$ | - На выходе дата в формате Y-m-d H:i:s | App\Filter\Date |
Chapter 5.10. Список существующих фильтров
Класс фильтра | Опции | Что делает |
---|---|---|
App\Filter\Abs | нет | Делает значение абсолютным |
App\Filter\Date | нет | Пытается создать дату в формате Y-m-d H:i:s из разных данных |
App\Filter\Email | нет | Удаляет символы запрещенные в E-Mail |
App\Filter\EntityDecode | нет | Заменяет HTML-entities на обычные символы |
App\Filter\EnunField | нет | Оставляет символы разрешенные для значений enum-полей |
App\Filter\Float | нет | Приводит значение к Float |
App\Filter\Hash | нет | Удаляет символы не относящиеся к hash, оставляет первые 40 символов |
App\Filter\Id | нет | Приводит к абсолютному числу |
App\Filter\IsFlag | нет | Очищает все что не Y и не N |
App\Filter\Md5 | нет | Делает из строки md5 |
App\Filter\Name | нет | Фильтрует имя, вырезает теги, лишнее пробелы и тд |
App\Filter\PlainText | нет | Удаляет теги и немного подчищает. Декодит HTML-Entity |
App\Filter\Price | нет | Из мусора умеет делать цену товара, например |
App\Filter\Slug | нет | Все что не английский - в транслит, заменяет пробелы на -, оставляет символы разрещенные для использования в URL |
App\Filter\Stem | нет | Берет основания от всех слов |
App\Filter\StemHash | нет | Делает hash текста с использованием stem |
App\Filter\Stopword | нет | Фильтрует нежелательные слова |
App\Filter\StringTrim | нет | Тупо, trim |
App\Filter\Truncate |
length = 80 // длина строки etc = '…' // что добавить в конце обрезанной строки break_words = true | false; // разрешить "рвать" слова middle = true | false; // сделать обрезку текста по-центру |
Обрезка текста |
App\Filter\Truncate40 | нет | Взять первые 40 символов |
App\Filter\Truncate128 | нет | Взять первые 128 символов |
App\Filter\Truncate255 | нет | Взять первые 255 символов |
App\Filter\Ucfirst | нет | Делает первый символ в верхнем регистре, остальные в нижнем |
App\Filter\Url | все сложно, смотри код | Фильтрует URL |
App\Filter\Xml | нет | Чистит код от нечистой силы, что ломает XML |
Chapter 6. Валидаторы
Chapter 6.1. Теория валидации?
Все данные которые попадают в базу данных должны быть обязательно проверены. Если данные не проверять перед вставкой, можно попасть с XSS или просто упадет приложение, так как база не ожидает таких данных.
Chapter 6.2. Как добавить свои валидаторы в модель?
В объекте модели нужно добавить метод:
<?php
public function setupValidatorRules()
{
// В эту переменную складываем валидаторы, которые будут работать при добавлении
// $this->validatorRules['required'];
// В эту переменную складываем валидаторы, которые будут работать при изменении
// $this->validatorRules['not_required'];;
$this->validatorRules['not_required']['field'][] = Validator::getValidatorInstance('\App\Validator\MyValidator');
}
?>
Chapter 6.3. Как проверить отдельное поле (validateValue)
<?php
// Входное значение
$input = 'some date here';
// ВНИМАНИЕ!!! Валидация поля работает по методу когда не проверяется наличие поля, то есть как у update
// $this->validatorRules
/** @var $validator boolean|Validator **/
$validator = SomeModel::getInstance()->validateValue($input, 'field_name');
// Работать так
if ($validator !== true) {
// Ошибка
$messages = $validtor->getMessages();
}
?>
Chapter 6.4. Схема инициализации работы валидатора
<?php
/**
* Самый-самый родитель
*/
class AbstractModel
{
public function getValidatorRules($reqiured)
{
$this->initValidatorRules();
}
}
/**
* Самый-самый родитель
*/
class AbstractGeneratedModel
{
public function initValidatorRules($reqiured)
{
/** тут правила сгенеренные генератором **/
$this->setupValidatorRules();
}
}
/**
* Модель куда разработчик вносит изменения
*/
class Model
{
public function setupValidatorRules($reqiured)
{
/** тут правила прописанные пользователем **/
}
}
?>
Chapter 7. Объект результата выполнения операции
Некоторые операции, требуют в ответе вернуть не только результат, но и валидатор и другие параметры. Специально для этого придуман объект \Model\Result\Result Схема работы очень проста, он фиксирует текущий результат, а так же вложенные (например, при импорте данных) Объект имеет вложенную структуру. То есть внутри одного Result может содержать несколько других Result
Ниже перечень основных методов по работе с Result
<?php
$result = Model::getInstance()->add(/** data **/);
// При вставке не было ошибок
if ($result->isValid()) {
// do something
}
// Идентификатор добавленной записи/записей (может быть массивом)
$insertId = $result->getResult();
// Объект валидатора (ошибки брать тут)
$validator = $result->getValidator();
// Получить перечень ошибок с учетом вложенных результатов
$error = $result->getErrors();
?>
Chapter 8. Объект условий Cond
Chapter 8.1. Общее описание
Объект условий - это объект простого типа, который хранит в себе какие связи нужно подключить к выборке и как отсортировать и отфильтровать данные.
Так же объект условий содержит внутри себя следующие важные данные:
- Имя сущности для которой создан
- Имя Entity - нужна что бы сообщить модели, какого типа данные мы ожидаем
- Имя Collection - нужна что бы сообщить модели, какого типа данные мы ожидаем
Chapter 8.2. Method from
Говорит из какой таблицы будет производиться выборка.
Синтаксис:
Cond::init()->from('table_name'); // SELECT FROM table_name
Cond::init()->from(array('table_alias' => 'table_name')); // SELECT FROM table_name as table_alias