Skip to content

meniam/model

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Модели данных

  1. Теория моделей
  2. Нотация проектирования базы данных
  3. Менеджер моделей Model
  4. Общее описание
  5. Выборки (get methods)
  6. Добавление (add methods)
  7. Изменение (update methods)
  8. Удаление (delete methods)
  9. Импорт (delete methods)
  10. Связи (link methods) 1. Теория и типы связей 2. Генерация связей по базе данных 3. Добавление своих связей
  11. Объекты сущности Entity
    1. Типы данных
    2. Getters
  12. Объект коллекции Collection
  13. Фильтры
    1. Теория фильтрации
    2. Значения по-умолчанию
    3. Каскад данных при фильтрации
    4. Как добавить свои фильтры в модель
    5. Как отфильтровать входные данные
    6. Как отфильтровать отдельное поле (filterValue)
    7. Как запретить фильтрацию
    8. Как работает фильтрация (вид изнутри)
    9. Фильтры, которые прописываются автоматически
    10. Список фильтров
  14. Валидаторы
    1. Теория валидации
    2. Как добавить свои валидаторы в модель?
    3. Как проверить отдельное поле (validateValue)
    4. Схема инициализации работы валидатора
  15. Объект результата выполнения операции
  16. Объекты условий Cond
    1. Общее описание
  • ЭТО НЕ 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'),
        )
    )
)
?>

Нужно запомнить три следующих утверждения:

  1. Поля начинающиеся не с _ - это свойства сущности;
  2. Поля, которые начинаются с _ - это связанная сущность или набор сущностей
  3. Если поле начинается с _ и заканчивается на _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 = '&#133;' // что добавить в конце обрезанной строки
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