- Теория моделей
- Нотация проектирования базы данных
- Менеджер моделей Model
- Общее описание
- Выборки (get methods)
- Добавление (add methods)
- Изменение (update methods)
- Удаление (delete methods)
- Импорт (delete methods)
- Связи (link methods) 1. Теория и типы связей 2. Генерация связей по базе данных 3. Добавление своих связей
- Объекты сущности Entity
- Типы данных
- Getters
- Объект коллекции Collection
- Фильтры
- Теория фильтрации
- Значения по-умолчанию
- Каскад данных при фильтрации
- Как добавить свои фильтры в модель
- Как отфильтровать входные данные
- Как отфильтровать отдельное поле (filterValue)
- Как запретить фильтрацию
- Как работает фильтрация (вид изнутри)
- Фильтры, которые прописываются автоматически
- Список фильтров
- Валидаторы
- Объект результата выполнения операции
- Объекты условий Cond
- ЭТО НЕ ORM, как его понимают программисты PHP :)
- ЭТО ORM, так как иммет связи;
- Тут нет и не будет ActiveRecord;
- Основной принцип - договоренности и условности;
- Жестка формальность наименований и полей БД;
- Упрощенные объектные связи;
- Упрощенное управление данными в БД;
- Выделение сущностей и действий над ними.
Так что же это такое?! Модели - базируются на договоренностях наименования таблиц, полей и ключей в базе данных. В моделях мы выделяем сущности (Entity) и связи между сущьностями. Модель позволяет управлять данными в базе.
При проектировании моделей преследовалась цель дать инструмент для работы с базой данных для пользователей разного уровня квалификации:
- Начинающий - знает основы, может пользоваться только интерфейсом (тем что уже есть), не может сильно накосячить;
- Обычный уровень - понимает как работает, как работают связи и основные части системы, может строить сложные выборки и обработки, не может сильно накосячить;
- Гуру - понимает архитектуру, может дописывать плагины, модифицировать сами модели, может очень сильно накосячить;
От этих трех целей строится вся идея моделей.
Есть комманда разработчиков разного уровня:
- Женя - разработчик, только пришел в компанию;
- Петя - разработчик, работает в компании долго, примерно знает как все устроено;
- Вася - проектирует все новые разработки.
Жене дали задание сделать сложный раздел. Он накидывает базу данных по всем правилам. Так же не забывает, прописать все фильтры и валидаторы. Система моделей автоматически генерит ему работающий CRUD-код. Женя его использует и незадумывается над правильностью этого кода. Но, Жене нужно сделать сложную выборку или обработку в моделе, по-правилам, ему запрещено это делать самостоятельно. Он обращается к более профессиональному коллеге.
Петя, знает все принципы работы моделей, он умеет делать сложные выборки, хорошо понимает связи сущностей. Он может помочь Жене с его проблемой, совместно они строят сложную выборку, даже не сломав и не нагнув базу. Но в какой-то момент они понимают, что стандартный функционал не может решить их проблему. Нужно модифицировать саму систему (да, такое бывает)
Вася написал уже кучку плагинов и пофиксил несколько багов. Женя и Петя приходят к нему. Вася анализирует ситуацию. Если решение единоразовое, то он не будет его вносить в систему, он предложит обойти проблему. Если проблема глобального масштаба, то нужно либо дописать плагин к генератору моделей, либо изменить модели. Он вносит правки.
Таким образом мы отгораживаем разработчиков от чистого SQL и контролируем связи и запросы к базе отвечающие за выбор данных. Только гуру может внести в процесс изменение и изменить логику выборки данных из базы. При этом непрофессиональные пользователи "бездумно" оперируют связями без боязни сильно "накосячить".
Небольшое количество атомарных элементов:
- Объект (Entity)
- Набор объектов (Collection)
- Объект условий для действий (Cond)
- Менеджер объектов (Model)
Четко типизированная (константы) структура, не позволит сделать лишнего. В то же время, структура легко изменяется. Можно с легкостью модифицировать практически любые элементы, но это больше присуще архитекторам системы.
В основе лежат простые принципы проектирования базы данных. Нотация, по-которой создается база данных, достаточно хорошо выверена и логична. Многие методы имеют жестко-определенные названия, что позволяет писать функционал и не искать название метода.
Код моделей и методов работы с ней получается легко-читаемым и логичным. Можно посмотреть на переменную и четко сказать, что за модель ее сгенерила и какого типа переменная. Что бы это было, нужно всег-лишь соблюдать нотаци и следовать рекомендациям из этого описания.
Вся основная структура связей/отношений объектов строится автоматически, на основе базы данных. Главное в этой операции - это создать базу данных с правильными ключами и связями. Генератор моделей сделает всю основную работу.
Простота структуры и управления данными - основной принцип. Все операции с данными - очень просты и легки в понимани. Отношения объектов заложены в архитектуру и неподдаются изменению незатрагивая архитектуру приложения. Это накладывает определенные ограничения, но в то же время является преимуством - простота понимания.
В основе всего лежит сущность. Точнее ее содержимое. Любая сущность имеет свои свойства и связанные сущности. В моделях используется многомерный массив, связанные сущности которого помечаются нижним подчеркиванием в начале ключа (поэтому поля в базе с нижним подчеркиванием в начале имени колонки - запрещены).
<?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');
}
}
?>
Такая схема достаточно проста, понятна и логична. Абсолютно, все методы опираются на такую структуру массива.
Система моделей строилась с учетом персональных предпочтений работы с базой данных. Достаточно большая стабильность проектировки и слаженность в работе комманды достигается за счет введения общих договоренностей. Приведенные ниже, договоренности позволяют недопускать простые ошибки при разработке и проектировании различных сервисов. Мы нарабатываем свои методики, которые используем в повседневной работе.
При наименовании полей не используем различных префиксов базы данных и прочей ереси, которая повелась в времен, когда в одну базу данных впирали и форум и чатик и цмс с магазинчиком. Итак, одна полная модель базы - это все таблицы из этой базы.
Внимание: Наименование полей моделей берется из названия поле базы данных. Настроить псевдонимы - нельзя. Возможности настроить в дальнейших версиях не будет.
Так же крайне не рекомендуется использовать любые названия полей, которые необходимо экранировать: count, as, from и т.д.
Так уж повелось, но таблицы начинающиеся на "_" или заканчивающиеся на этот символ пропускаются и модели по ним не строятся. Так же не строится модель для таблиц с суффиксом "_index" в дальнейшем суффиксы и префиксы, которые не нужно индексировать можно будет настроить в конфигурационном файле.
При проектировании мы всегда опираемся на базовые сущности. Эти сущности лежат в основе каждого проекта. Например, мы проектируем магазин и хотим сделать теги для магазинов, в этом случае таблицы желательно назвать так:
- product
- tag
- product_tag_link
- product_tag_info (информация по тегу связанному с продуктом)
Но, если вы строите социальную сеть, где теги основа системы, к которым вяжется куча всего, то все должно выглядеть наоборот:
- product
- tag
- tag_product_link
- tag_product_info (информация по продуктом связанному с тегом)
то есть в начало мы ставим базовую сущность на второе место второстепенную, пример актуальной базы:
- product
- tag
- tag_alias
- tag_stat
- tag_info
- comment
- product_info
- product_rubric
- product_rubric_tag_info (таблица для описательных параметров тега для рубрики продукта)
- product_rubric_info
- product_rubric_stat
- product_rubric_index (неиндексируемая моделями таблица: суффикс _index)
- product_product_rubric_link
- product_image
- product_product_image_link
- product_comment_link
- product_tag_link
- и т.д.
У каждой модели есть метод добавления. В процессе выполнения происходят следующие действия:
- Проверка данных на пустоту, если пусто то вернуть пустой результат
- Применяем каскад данных
- Присваиваем значения по-умолчанию
- Фильтруем данные
- Проверяем данные
- Вставляем данные в базу
Результат метода это объект \Model\Result\Result
!!!ВСЕ ДАННЫЕ ДОЛЖНЫ ФИЛЬТРОВАТЬСЯ!!!
Данные которые мы получаем в моделе, по-умолчанию считаются грязными. Их нужно отмыть, накормить, проверить и поробовать запихать в базу. Дальше расскажу об этапах добавления данных и как можно настроить модель что бы все было по фен-шую.
Представьте, что на входе мы имеем ассоциативный массив с данными, где ключ - это значение поля, а значение собственно само значение этого поля. Прежде чем попасть в базу каждое поле должно быть обработано по следующей схеме:
- Значения по-умолчанию - наложение занчений по-умолчанию, кастомим через 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
При добавлении мы учитываем данные по-умолчанию. Это значит что в модельках есть сгенеренный массив в котором мы прописываем, что при создании новой записи, если неопределено поле, то ему нужно присвоить определенное значени. Настраиваются свои значения так:
<?php
public function setupDefaultsRules()
{
$this->defaultRules['test_field'] => 'Значение по-умолчанию';
}
?>
Представьте, что у вас есть сущность в которой есть поля:
- 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');
}
?>
В объекте модели нужно добавить метод:
<?php
public function setupFilterRules()
{
$this->filterRules['field'][] = Filter::getFilterInstance('\App\Filter\MyFilter');
}
?>
<?php
$inputData = array( /** data here **/ );
$filteredInputData = SomeModel::getInstance()->filterOnAdd($inputData);
?>
<?php
// Входное значение
$input = 'some date here';
$filteredInput = SomeModel::getInstance()->filterValue($input, 'field_name');
?>
Методы 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; // Разрешить фильтрацию полей при изменении
?>
Каждая модель имеет список правил фильтрации для полей:
<?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 их пропускает).
Схема работы этих методов:
- Применяем каскад данных;
- Получаем правила фильтрации;
- Фильтруем поля;
- Поля для которых нет фильтров, остаются неизменными;
Поле в базе | Что проверяем | Какие фильтры |
---|---|---|
@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 |
Класс фильтра | Опции | Что делает |
---|---|---|
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 |
Все данные которые попадают в базу данных должны быть обязательно проверены. Если данные не проверять перед вставкой, можно попасть с XSS или просто упадет приложение, так как база не ожидает таких данных.
В объекте модели нужно добавить метод:
<?php
public function setupValidatorRules()
{
// В эту переменную складываем валидаторы, которые будут работать при добавлении
// $this->validatorRules['required'];
// В эту переменную складываем валидаторы, которые будут работать при изменении
// $this->validatorRules['not_required'];;
$this->validatorRules['not_required']['field'][] = Validator::getValidatorInstance('\App\Validator\MyValidator');
}
?>
<?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();
}
?>
<?php
/**
* Самый-самый родитель
*/
class AbstractModel
{
public function getValidatorRules($reqiured)
{
$this->initValidatorRules();
}
}
/**
* Самый-самый родитель
*/
class AbstractGeneratedModel
{
public function initValidatorRules($reqiured)
{
/** тут правила сгенеренные генератором **/
$this->setupValidatorRules();
}
}
/**
* Модель куда разработчик вносит изменения
*/
class Model
{
public function setupValidatorRules($reqiured)
{
/** тут правила прописанные пользователем **/
}
}
?>
Некоторые операции, требуют в ответе вернуть не только результат, но и валидатор и другие параметры. Специально для этого придуман объект \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();
?>
Объект условий - это объект простого типа, который хранит в себе какие связи нужно подключить к выборке и как отсортировать и отфильтровать данные.
Так же объект условий содержит внутри себя следующие важные данные:
- Имя сущности для которой создан
- Имя Entity - нужна что бы сообщить модели, какого типа данные мы ожидаем
- Имя Collection - нужна что бы сообщить модели, какого типа данные мы ожидаем
Говорит из какой таблицы будет производиться выборка.
Синтаксис:
Cond::init()->from('table_name'); // SELECT FROM table_name
Cond::init()->from(array('table_alias' => 'table_name')); // SELECT FROM table_name as table_alias