Цели которые были поставлены, для реализации в новой версии:
1. Избавиться от директории API, которая содержит "кашу" классов.
2. Привести все сущности (товары, бренды) к единому интерфейсу.
3. Избавиться от класса "автозагрузчика" Okay.
4. Избавиться от RewriteRules в .htaccess, это файл настройки сервера, и там не должно быть логики работы приложения.
5. Более четко выделить в системе паттерн Front controller (в частности избавиться от директории ajax).
6. Изменить формат формирования SQL запросов.
7. Спроектировать систему таким образом, чтобы в будущем её можно было покрывать автотестами.
И конечно же постараться сохранить простоту системы.
Более детально по целям.
1.
Папка API содержала классы товаров, брендов... и классы Database, Design, Validate...
И даже здесь "каша" не заканчивалась. Класс Products содержит методы add_image(), update_image(), delete_image(), get_spec_images(), update_spec_images() и т.д.,
я думаю масштаб проблемы виден.
Поэтому мы папку API поделили на Entities и Core.
В папку Entities попали классы сущностей, которые наследуются от абстрактного Entity.
В папку Core попали классы, отвечающие за работу "ядра" системы.
Все классы ядра регистрируются в DI (Dependency Injection) контейнере, с обязательным указанием зависимостей, если таковые имеются.
Чтобы зарегистрировать сервис ядра, его нужно прописать в файле Core/config/services.php.
Прописываются они в виде массива, где ключ массива, это имя сервиса, значение еще массив, с ключами 'class' содержащим название класса сервиса и 'arguments', содержащим массив зависимостей.
Зависимости также должны быть зарегистрированными сервисами. Сервисы не могут содержать циклическую зависимость. В сервисе все зависимости нужно ловить в методе __construct() в таком порядке, как их указали в Core/config/services.php в arguments.
Сущностью считается что либо, что имеет свою таблицу в БД (исключением есть некоторые, хранимые данные в файлах) и имеющая CRUD операции.
Еще мы не выносили в отдельные сущности таблицы связей (ok_products_categories, ok_related_products...).
Чтобы создать сущность, нужно создать её класс в директории Entities и унаследовать его от абстрактного Entity (Okay\Core\Entity\Entity), который имплементирует интерфейс Okay\Core\Entity\EntityInterface.
В абстрактном Entity уже описаны стандартные CRUD методы.
Детальнее об CRUD методах:
get_product($id), get_brand($id) сейчас приведены к методу get($id)
get_products($filter), get_brands($filter) сейчас приведены к методу find($filter)
count_products($filter), count_brands($filter) сейчас приведены к методу count($filter)
delete_product($id), delete_brand($id) сейчас приведены к методу delete($ids) - принимает массив ID сущностей
Класс сущности должен содержать некоторые настройки (статические свойства класса).
protected static $table = '__products'; содержащее название таблицы сущности.
protected static $tableAlias = 'p'; содержащее алиас таблицы.
protected static $fields = ['id','url'...]; содержащее массив колонок сущности.
protected static $defaultOrderFields = ['name DESC','id DESC'...]; содержащее массив колонок с направлением сортировки по умолчанию.
protected static $searchFields = ['id','url'...]; содержащее массив колонок сущности, по которым должен происходить текстовый поиск (поддерживаются только колонки сущности).
Если нужен расширеный поиск (по другим таблицам) нужно переопределить метод filter__keyword($keywords).
protected static $alternativeIdField = 'url'; альтернативное поле, по которому будет происходить выборка в методе get($id) если $id передали как строку, иначе метод get($id) ищет по колонке id.
Если сущность мультиязычна, нужно добавить мультиязычные настройки
protected static $langFields = ['name','meta_title'...]; содержащее массив мультиязычных колонок сущности.
protected static $langObject = 'products'; название языковой таблицы сущности, без "ok_lang_"
protected static $langObject = 'product'; название сущности в языковой таблице без суфикса "_id" (в таблице "ok_lang_products" колонка "product_id")
Также если к сущности нужно добавить в выборке колонки, которых нет у самой сущности (это результаты подзапросов, колонки с других таблиц),
нужно заполнить массив protected static $additionalFields;
На этом конфигурирование сущности закончено.
Фильтрация результатов выборки.
Ранее нужно было в методе get_products($filter) описывать фильтры вида:
Код: Выделить всё
if (!empty($filter['id'])) {
$where .= $this->db->placehold(' AND p.id in(?@)', (array)$filter['id']);
}
if (!empty($filter['brand_id'])) {
$where .= $this->db->placehold(' AND p.brand_id in(?@)', (array)$filter['brand_id']);
}
if (isset($filter['featured'])) {
$where .= $this->db->placehold(' AND p.featured=?', intval($filter['featured']));
}
...
и если присмотреться, все они фильтруют по определенным полям сущности.
Теперь, если нам нужно фильтровать по полю, которое равно полю или входит в массив значений, такой фильтр можно вообще не описывать,
здесь отработает "магический фильтр". Он сам определит тип данных, это строка (или int) или массив, и добавит соответствующее условие в WHERE
Пример.
Код: Выделить всё
$productsEntity->find([
'brand_id' => [1,2,3],
'featured' => 1,
]);
Построится запрос SELECT ... FROM ok_products WHERE p.brand_id IN (1,2,3) AND p.featured=1
Если же нужно добавить возможность фильтровать по индивидуальным фильтрам, или по полю сущности, с измененным алгоритмом (иначе от магического фильтра)
нужно описать protected метод filter__filter_name($value) {...} в классе сущности (Обратите внимание на два символа подчеркивания).
Пример.
Код: Выделить всё
$productsEntity->find([
'category_id' => [1,2,3],
]);
нужно объявить метод, который на вход примет массив переданных id категорий
protected function filter__category_id($categoriesIds)
Код: Выделить всё
{
$this->select->join('INNER', '__products_categories AS pc', 'p.id = pc.product_id AND pc.category_id IN(:category_ids)');
$this->select->bindValue('category_ids', $categoriesIds);
$this->select->groupBy(['p.id']);
}
Точно так же у сущности есть "магические" сортировки. Если передать в сортировку значение name, name_asc или name_desc
и у сущности есть поле name (не важно мультиязычное оно или нет), добавится автоматическая сортировка по этому полю в указанном направлении.
Если же нужно определить какую-то кастомную сортировку, нужно в классе объявить метод customOrder($order = null) {...},
который вернет массив колонок, по которым нужно сортировать
Пример.
Код: Выделить всё
protected function customOrder($order = null)
{
$orderFields = [];
// Пример, как реализовать кастомную сортировку.
Код: Выделить всё
switch ($order) {
case 'some_custom_order' :
$orderFields = [
'visible',
'name'
];
break;
}
return $orderFields;
}
чтобы выбирать сущности в отсортированном виде, нужно перед методом find() вызвать метод order(...) который возвращает $this
Пример.
Код: Выделить всё
$productsEntity->order('name_desc')->find([
'category_id' => [1,2,3],
]);
2. Выше уже описано, что все сущности приведены к единому интерфейсу Okay\Core\Entity\EntityInterface
3. Все классы окая теперь соответствуют стандарту PSR-4 и загружаются через composer autoload.
4. Файл .htaccess теперь используется только для настроек сервера (закрыть доступ, настроить кеширование...) и для реврайта запросов на index.php (Front controller).
Для роутинга теперь используется роутер https://github.com/bramus/router, с указанием роутов в файле Core/config/routes.php.
Роутом считается элемент массива, где ключ, это имя роута, а значение - ассоциативный массив параметров.
Обязательные параметры роута:
"slug" - содержит полное представление структуры урла в данном роуте (вид шаблона).
"params" - содержит массив с ключами "controller" содержащим имя контроллера для данного роута и "method" содержащего имя метода контроллера, который необходимо запустить.
В поле "slug" если какая-то часть роута является переменной (урл товара etc) нужно указывать эту часть как переменную "{$url}".
Имя переменной произвольно, но очень рекомендуется (дабы избежать путаницы) для частей роута, которые отвечают именно за URL сущности, переменной давать название "{$url}"
Впоследствии для slug "/products/{$url}" будет сгенерирован паттерн "/products/([^/]+)", если нужно для переменной {$url} задать другой паттерн, его можно указать в роуте
в параметре patterns, который содержит ассоциативный массив, где ключ это название переменной из поля slug, а значение regexp.
Также для переменных, которым назначена регулярка, как не обязательное (кол-во повторений от 0) можно задать значение по умолчанию, которое будет передано в метод контроллера.
Чтобы задать значение по умолчанию, нужно роуту добавить параметр defaults, который содержит ассоциативный массив, где ключ это название переменной из поля slug, а значение произвольный текст.
Пример:
Код: Выделить всё
'category' => [
'slug' => '/catalog/{$url}{$filtersUrl}',
'patterns' => [
'{$filtersUrl}' => '/?(.*)',
],
'params' => [
'controller' => 'CategoryController',
'method' => 'render',
],
'defaults' => [
'{$filtersUrl}' => 'some string',
],
],
Далее, в методе контроллера можно "поймать" все переменные из роута по их названию, порядок значения не имеет.
т.е. в методе render() контроллера CategoryController URL "/catalog/detskie-igrushki" мы можем принимать переменную $url как аргумент
Пример:
public function render($url, $filtersUrl = '') {...}
Также ранее у нас был один класс Okay, который знал все обо всем. Теперь в методе контроллера, получить экземпляр класса сервиса ядра
или экземпляр класса нужного Entity можно через внедрение зависимости DI.
Для этого в методе контроллера, нужно указать дополнительный аргумент с указанием typehint нужного класса и DI контейнер автоматически передаст эти зависимости.
Пример:
В методе render() контроллера CategoryController
нам нужно работать с экземпляром класса Okay\Entities\CategoriesEntity и (реально не нужно, но например) Okay\Core\Comparison, и ловить переменные $url и $filtersUrl.
Код: Выделить всё
use Okay\Entities\CategoriesEntity;
use Okay\Core\Comparison;
class CategoryController extends AbstractController
{
public function render(CategoriesEntity $categoriesEntity, Comparison $comparison, $url, $filtersUrl = '') {...}
}
для генерации урлов, на странице, нужно использовать использовать smarty плагин {url_generator},
который принимает один, обязательный параметр route, содержащий имя роута.
Если роут имеет переменные в поле slug, нужно передать значения этих переменных, по их именам.
Например для роута категории, нужно вызывать так: {url_generator route="category" url=$cat->url}.
Еще можно добавлять параметр absolute=1 чтобы URL строился не отнисительный, а абсолютный.
5. Все обработки ajax-а теперь происходят в классах контроллеров. Т.е. создается отдельный роут для ajax-ов, например
Код: Выделить всё
'ajax_search' => [
'slug' => '/ajax/search_products',
'params' => [
'controller' => 'ProductsController',
'method' => 'ajaxSearch',
],
],
6. Сущности используют для построения SQL запросов queryBuilder Aura.SqlQuery
ссылка на документацию https://github.com/auraphp/Aura.SqlQuery/blob/3.x/docs/index.md
В каждом экземпляре класса сущности, есть свойство $select содержащее экземпляр класса Aura\SqlQuery\Mysql\Select
Посмотреть то что получилось уже сейчас можно скачав бета версию по ссылке
На данный момент разработка ещё ведется, но мы хотим услышать мнение наших пользователей о промежуточном варианте развития нашей системы и сделать её лучше вместе.