В чем преимущество redis’а в качестве хранилища кэша?
быстрое хранилище в памяти;
несколько серверов redis могут быть объединены в кластер;
несколько web-серверов могут подключаться к одному серверу redis;
можно сбросить кэш на диск, чтобы не потерять его при перезагрузке;
различные модели вытеснения лишнего кэша;
В общем redis послужит отличной заменой memcached. Собственно, исходники bitrix/modules/main/classes/general/cache_memcache.php и послужили болванкой для данных экспериментов. Посмотрел в исходники битрикса, обнаружил, что ничего не мешает и все довольно просто.
Redis
Итак, для начала устанавливаем redis. (если у вас windows, то можно скачать инсталлятор отсюда https://github.com/MSOpenTech/redis/releases) Поскольку сервер будет использоваться для хранения кэша данных, то настроим в конфиге максимальный объем используемой памяти:
maxmemory 100mb
и политику вытеснения из памяти:
maxmemory-policy volatile-ttl
данная политика означает, что при нехватке выделенной памяти для новой записи, будут выброшены записи, время жизни которых наиболее приближено к окончанию.
Если нет возможности установить дополнительные модули, то можно через composer установить https://github.com/nrk/predis. Использование Predis не требует никаких дополнительных модулей, но надо будет изменить несколько строк в примере. Мне показалось, что работа с скомпилированными библиотеками будет быстрее, чем полностью интерпретаторная реализация общения с редисом.
Bitrix
Создаем php файл (отдельный или в составе собственного модуля) где будет размещаться наш класс ‘CPHPCacheRedis’. В файле .settings, в разделе ‘cache’ вместо строки ‘type’ указываем массив:
При кешировании в битриксе используются три уровня:
basedir
initdir
filename
когда мы в первый раз делаем запись в кэш, то создаются 3 записи:
запись с ключом basedir, которая содержит рандомный хэш basedir_version
запись с ключом basedir_version . "|" . init_dir , которая содержит рандомный хэш initdir_version
и собственно наша запись с ключом basedir_version . "|" . initdir_version . "|" filename , содержащая кэшируемые данные
При массовой очистке кэша, например функцией cleanDir или кнопкой очистки кэша из админки, битрикс поступает следующим образом:
очищает запись с ключом basedir_version . "|" . init_dir (в случае с cleanDir)
очищает запись с ключом basedir (в случае с «очистить кэш» из админки)
Таким образом записи типа filename остаются «бесхозными» и продолжают висеть в памяти пока не истечет их время жизни или они не будут вытеснены политикой ограничения размера памяти. Однако redis позволяет получить список ключей по маске, поэтому в моем примере я удаляю все ключи, соответствующие маске удаляемого уровня, а потом уже удаляю саму запись
К сожалению у меня нет задач с огромным количеством кэшей, чтобы замерить сильно ли упадет производительность при очистке. Если есть необходимость использовать классический подход битрикса, то можно оставить только
подключил redis по вашей статье. Для подключения модуля php_igbinary хостинг перевел php в режим cgi. Php-cgi технология устаревшая. Если не подключать php_igbinary, будет ли все работать?
Main\Type\Date::isCorrect($mydate,'d.m.Y') не совсем коррект.
В ней используется попытка создать экземпляр класса DateTime и если нет эксепшена, то возвращает true Используемая при этом функция DateTime::createFromFormat() простопереводит все в секунды, поэтому Main\Type\Date::isCorrect будет возвращать true для дат типа "35.19.2015" и т.д.
function validateDate($date, $format = 'Y-m-d')
{
$d = DateTime::createFromFormat($format, $date);
// The Y ( 4 digits year ) returns TRUE for any integer with any number of digits so changing the comparison from == to === fixes the issue.
return $d && $d->format($format) === $date;
}
Но нужно следить за форматом входных данных. Например, дата '2012-2-25' не будет валидной при такой проверке.
Если во время операции произошла одна или несколько ошибок, их текст можно получить из результата:
$result = BookTable::update(...);
if (!$result->isSuccess()) {
$errors = $result->getErrorMessages();
}
Создается обманчивое впечатление, что проверкой isSuccess можно отлавливать ошибки работы с БД. Однако это не так. Ошибки уровня БД обрабатываться не будут. Т.е. ошибки валидации, несоответствия полей в классах DataManager обработаются, а вот попытки записать NULL в поле NOT NULL или ошибки дубликата ключей вызовут просто Exception. В DataManager такой код:
try {
операции с БД
}
catch (\Exception $e)
{
// check result to avoid warning
$result->isSuccess();
throw $e;
}
return $result;
не логичнее ли было все же вернуть $result и дать программисту возможность самому решить, разрулить ситуацию или вызвать throw? Ну а так все равно придется оборачивать каждую операцию в свой дополнительный try catch
Ошибки уровня БД - not null, несоответствие типов данных, неверные имена полей - не обрабатываются принципиально. В новом ядре (в отличие от старого) программист должен сам валидировать данные при использовании низкоуровневых методов таблетов (add, update, getList).
Пользователи магазина имеют 3 категории скидок в зависимости от оборота. Скидки привязаны к пользователям и не зависят от суммы или категории товара.
В битриксе нет скидок, привязываемых к пользователю. Можно включить пользователя в определенную группу, у которой есть право на скидку, но автоматизировать этот процесс не получится. Хорошо бы было иметь инфоблок со скидками и выгружать на сайт содержимое инфоблока в виде XML.
Я в конце концов реализовал следующим образом:
Так как скидки фиксированные, 3-х типов, то:
1. Создал 3 группы пользователей (пустых, пользователей туда не включаем). 2. Создал 3 скидки, в свойствах которых прописал право на скидку для соответствующей группы 3. Создал инфоблок скидок с свойствами привязки к пользователю и символьным кодом скидки 4. в init.php прописал обработчик события OnAfterUserLogin и OnAfterUserLoginByHash
AddEventHandler("main", "OnAfterUserLogin", "MyOnAfterUserLogin");
AddEventHandler("main", "OnAfterUserLoginByHash", "MyOnAfterUserLogin");
// создаем обработчик события "OnAfterUserLogin"
function MyOnAfterUserLogin(&$fields)
{
global $USER;
// если логин OK
if ($USER->IsAuthorized())
{
$UserID = $USER->GetID();
if($UserID > 0)
{
$GetUser = CUser::GetByID($UserID);
$arUser = $GetUser->Fetch();
$MemberOfList = CUser::GetUserGroup($UserID); // смотрим в каких группах состоит
$MemberMask = array(2,3); // Коды групп "Все, в том числе и неавторизованные" и "зарегистрированные пользователи"
$ArDiff = array_diff($MemberOfList,$MemberMask); // Проверяем, что это обычный юзер и больше ни в каких группах не состоит
if (empty($ArDiff))
{
CModule::IncludeModule("iblock");
$SelectGroups = CIBlockElement::GetList(
array(),
array(
"IBLOCK_CODE" => "USER_DISCOUNT",
"PROPERTY_DISCOUNT_USER" => $UserID
),
false,
false,
array("PROPERTY_DISCOUNT_USER","PROPERTY_DISCOUNT_GROUP") // поля ИД юзера и символьный код группы скидки
);
if ($SelectGroup = $SelectGroups->Fetch())
{
$arGroups = CGroup::GetList(
($by="c_sort"),
($order="desc"),
array(
"STRING_ID" => $SelectGroup["PROPERTY_DISCOUNT_GROUP_VALUE"] // находим группу по символьному ИД
)
);
if ($arGroup = $arGroups->Fetch())
{
$CurGroups = $USER->GetUserGroupArray();
$CurGroups[] = $arGroup["ID"];
$USER->SetUserGroupArray($CurGroups); // добавляем юзера в группу скидки на время его сессии
}
}
}
}
}
}
В итоге в момент логина юзер временно добавляется в группу, соответствующую скидке. А в инфоблоке можно менять уровень скидки либо руками, либо выгружая автоматически.
На сколько я понял принадлежность пользователя к конкретной скидки вы получаете из внешнего источника, загрузкой в инфоблок. А так сразу будете проводить привязку пользователя к конкретной группе.
Что касается паранойи то на мой взгляд достаточно жестко ограничить работу с определенными группами.
Дык загрузка в инфоблок будет стандартными средствами. Т.е. через отправку catalog.xml компоненту интеграции с 1с. С той только разницей, что передаваться будут не товары, а информация по юзер=>группа сидки. Соответственно придется писать обработчик на загрузку товаров из 1с либо кастомизировать компонент интеграции с 1с. У меня (IMHO) наименее "кровавое" решение
Чебан Валерий, "даже" здесь в том смысле, что уже можно использовать на текущем проекте. А также (в случае миграции на орм) можно построить всю логику на уже используемых инфоблоках, а потом заменить entity на использование своих таблиц
Ребят, есть инфоблок, данных много, спокойно работаю с ним через стандартное API, фильтрую по свойству типа "Число" и "Список" с 5 вариациями, как тоже самое провернуть через ElementTable::getList()? Как там указать фильтр к уже имеющимся данным по свойствам?
Плотно работаю с ORM битрикса, все замечательно, но вот понадобилось работать с иерархическими структурами. Единственным методом работы с деревьями являлся старый добрый класс CIBlockSection, но он меня не устраивает по многим причинам:
Все деревья находятся в одной таблице
Деревья привязаны к инфоблокам
Собственные поля можно добавить только через UF_
Ну и т.д.
Решение вроде бы лежит на поверхности. Решено было создать свой класс - потомок от Bitrix\Main\Entity\DataManager и реализовать в нем логику работы с деревьями из CIBlockSection. Способы реализации виделись следующие:
Полностью переписать методы add, update, delete. Сей способ показался мне довольно геморройным. Неизвестно, что изменится в родителе при очередном обновлении и не будет ли из-за этого глючить потомок
Реализовать логику в обработчиках событий onBefore… onAfter… Но тогда пришлось бы жестко контролировать, чтобы в обработчиках событий потомков нашего класса вызывался parent::
Вызывать собственные обработчики событий, а после из них вызывать стандартные методы onBefore… onAfter… Не вышло, т.к. константы с именами обработчиков в DataManager вызываются как self::, а не static::, поэтому переписывать их в наследнике бесполезно
Регистрировать дополнительные обработчики событий, отрабатывающие независимо от того, используется обработка событий в потомках или нет. Вот на этом способе я и остановился
От выбора типа зависит порядок выполнения обработчиков. Собственные методы onBefore… onAfter… реагируют на modern, а classic посылается раньше. Я выбрал classic, поэтому сначала выполнится мой обработчик, а затем собственный, унаследованный от DataManager
Чтобы наш обработчик не регистрировался повторно, добавим свойство класса
protected static $eventHandlers = array();
и выставим флаг
static::$eventHandlers[$eventType]=true;
Теперь переписываем методы add, update, delete следующим образом:
public static function add(array $data)
{
static::handleEvent(self::EVENT_ON_BEFORE_ADD, 'treeOnBeforeAdd');
static::handleEvent(self::EVENT_ON_AFTER_ADD, 'treeOnAfterAdd');
return parent::add($data);
}
public static function upd ate($primary, array $data)
{
static::handleEvent(self::EVENT_ON_BEFORE_UPDATE, 'treeOnBeforeUpdate');
static::handleEvent(self::EVENT_ON_AFTER_UPDATE, 'treeOnAfterUpdate');
return parent::update($primary, $data);
}
public static function delete($primary)
{
static::handleEvent(self::EVENT_ON_DELETE, 'treeOnDelete');
return parent::delete($primary);
}
Далее пишем наши обработчики, в которых уже и реализуем логику работы с деревом
public static function treeOnBeforeAdd(Entity\Event $event){}
public static function treeOnAfterAdd(Entity\Event $event){}
public static function treeOnBeforeUpdate(Entity\Event $event){}
public static function treeOnAfterUpdate(Entity\Event $event){}
public static function treeOnDelete(Entity\Event $event){}
Примеры работы
Пример ORM-класса находится в файле lib/nstest.php все поля, кроме NAME являются обязательными
Во избежание разрушения структуры дерева при совместном доступе желательно блокировать таблицу на запись и откатывать изменения при возникновении ошибок. Для этого создаем методы lockTable() и unlockTable(). В mySQL нельзя использовать одновременно LOCK TABLE и START TRANSACTION (https://dev.mysql.com/doc/refman/5.7/e...tions.html) поэтому метод lockTable() выполняет:
SET AUTOCOMMIT = 0;
LOCK TABLES…
А unlockTable()
UNLOCK TABLES
SE T AUTOCOMMIT = 1
Алгоритм работы с NSDataManager при использовании транзакций примерно такой:
Короче через сутки 58000 добавлено. глубина вложенности 27. Дальше обрываю, поскольку надо работать с мускулем. Параметры php 7, mysql 5.7, Windows 10, включенный Каспер, за весь эксперимент php не превысил 9.7 Мб Вся нагрузка легла на mysql
kopoBko, для скорости вставки и изменения надо смотреть в сторону closure tables. Но в них свой геморрой: квадратичный рост количества записей, проблема с выводом отсортированного дерева. В моих задачах от силы пару тысяч записей будет и глубина не более 3. Зато скорость получения нужной ветки или проверка на вхождение в ветку замечательная.
Есть еще технология nested intervals, но там усложнена реализация переноса ветки, зато повышается скорость. Но опять же, поскольку скорость достигается за счет выделения заранее смежных интервалов, то все равно может возникнуть ситуация, когда необходимого интервала не будет и придется апдейтить всю таблицу
Не претендую на новизну. Для кого-то написанное ниже - прописные истины, а для кого-то "а что так можно было?". Просьба не бить за элементарные примеры
В корне нашего шаблона создаем файл gulpfile.js - "руководство по сборке" со следующим содержимым:
var elixir = require('laravel-elixir');
/*
true - создавать source maps
false - не создавать source maps
*/
elixir.config.sourcemaps = false;
elixir(function(mix) {
mix.sass('app.scss', 'css')
.browserify('app.js', 'js');
});
Поскольку Elixir по умолчанию работает со структурой папок Laravel, то необходимо вторым параметром указывать каталоги для итоговых файлов. Иначе создаcтся лишний каталог "public". В данном скрипте мы указываем, что необходимо обработать исходники:
Updated: main (16.0.10) Улучшена совместимость с PHP 7.
Падает на модуле веб аналитики. Call to undefined static CModule::$GLOBALS в include.php include.php зашифрован, особо не глянешь чего там ))) Отключение аналитики решает проблему
т.е. он выполняется на дефолтной БД. В уже подключенной базе битрикса заново выполняются установки локали, а в БД mybase приходится устанавливать локаль вручную:
ну, это не беда, установил. На радостях решил работать со сторонней базой средствами ORM. Создал необходимые классы таблиц. Для коннекта со сторонней базой переопределил метод Entity\DataManager::getConnectionName() :
public static function getConnectionName()
{
return 'mybase';
}
пытаюсь выполнить getList() и получаю "Table 'bx_basename.mytable' doesn't exist (400)" т.е. таблица не найдена в дефолтной базе.
Запустил трассировку и что же я вижу? В bitrix/modules/main/lib/entity/query.php в функции buildQuery() опять же стоит
$connection = Main\Application::getConnection();
т.е. несмотря на переопределенный метод ORM в любом случае коннектится только к дефолтной базе!
Вопрос. Нафига тогда создавать методы для работы с другим соединением если все в итоге сведется к дефолтной БД?
Микулич Евгений, немного поправлю - анонсировали новое ядро (вместе с новой идеологией разработки, новым жизненным циклом страницы, "рубильником", который позволяет переключаться между старым и новым ядром) два с половиной года назад (со словами все уже готово, вот чуть-чуть и будет в 12 версии). Потом ровно год его "выпускали", а когда выпистили, оказалось, что они передумали и никакого нового ядра (в том смысле, в котором его анонсировали в 2012) не будет.
Коваленко Алексей, я предпочитаю не гадать, а сразу сообщать, благо, это занимает только пару минут. ТП бывает странновата, но тем не менее, имею положительный опыт, пару багов с моей наводки закрыли через 1-2 минорных релиза.
Присоединяюсь. ТП очень адекватно и достаточно быстро принимает в работу корректные замечания. Если скинуть что на что нужно поменять, с исходниками, и корректным подтверждением своей правоты - они вполне профессионально себя ведут.
Группы на сайте создаются не только сотрудниками «1С-Битрикс», но и партнерами компании. Поэтому мнения участников групп могут не совпадать с позицией компании «1С-Битрикс».