Обновление курса валют в Битрикс
Для Битрикса есть популярное решение для обновление курса валют - https://marketplace.1c-bitrix.ru/solutions/asd.currencyrate/
Однако, решение не работает, если базовая валюта не рубли.
В моём случае, когда это фунты(GBP), решаю это вот таким куском гавнокода:
<?php include __DIR__ . '/bitrix.php'; $contents = file_get_contents('http://www.cbr.ru/scripts/XML_daily.asp'); $neededCurrency = 'GBP'; // Глупая проверка на то, что нам вернули что-то нормальное if (stripos($contents,$neededCurrency) > 0) { $xml = simplexml_load_string($contents); $date = $xml->attributes()->Date; CModule::IncludeModule('currency'); $format = "DD.MM.YYYY"; // получим формат текущего сайта $new_format = CSite::GetDateFormat("SHORT"); // переведем дату из одного формата в другой $new_date = $DB->FormatDate($date, $format, $new_format); // в результате получим дату в новом формате echo "Date $date - $new_date\n"; $arFilter = [ "CURRENCY" => "RUB", 'DATE_RATE' => $new_date, ]; $by = "date"; $order = "desc"; $row = CCurrencyRates::GetList($by, $order, $arFilter)->Fetch(); if ($row === false) { foreach ($xml->Valute as $valute) { if (((string)$valute->CharCode) === $neededCurrency) { $value = (float)str_replace(',', '.', ((string)$valute->Value) ?? 0); if ($value > 0) { $value = number_format(1 / $value * 1.05, 4, '.', ''); $arFields = [ "RATE" => $value, "RATE_CNT" => 1, "CURRENCY" => "RUB", "DATE_RATE" => $new_date, ]; if (!CCurrencyRates::Add($arFields)) { echo "Ошибка добавления курса $date - $value\n"; echo $APPLICATION->GetException() . "\n\n"; } else { echo "Обновили курс $date - $value\n"; } return; } } } } else { echo "Курс на $date уже есть\n"; } }
Достигаем цели статистики просто в AjaxForm MODX
Чтобы засчитывались цели Яндекс.Метрики или Google Analytics в MODX при использовании FormIt и AjaxForm достаточно трёх простых шагов:
Шаг 1. Заводим цели как Javascript событие:
Шаг 2. В вёрстке каждой форме добавляем data-атрибут data-goal="название вашей цели", например
<form data-goal="zakaz">
Шаг 3. Добавляем простой обработчик:
$(document).on('af_complete', function(event, response) { var form = response.form; var goal = form.data('goal') || null; if (goal) { try { window.yaCounter48925430.reachGoal(goal); } catch (e) {} } });
try-catch нужен для того, чтобы если вдруг у нас не прогрузилась метрика(привет, РКН) - у нас ничего не сломалось.
Обход ограничения max_input_vars в PHP без php.ini
Сегодня столкнулся с ограничением на количество входящих из формы в скрипт переменных в PHP.
Отвечает за это параметр max_input_vars, который по-умолчанию равен 1000.
Поскольку я разрабатываю CMS, то было бы не правильным в .htaccess менять это значение. Поэтому я решил обойти это.
С данной проблемой у меня сталкивается админка. А поскольку она не работает и не должна работать без JS, то я использовал такой JavaScript:
$("#editSaveButton").on('click', function(){ form = $("<form method=\"POST\"></form>") .attr('action', $('#editForm').attr('action')) .append( $("<input type=\"hidden\" name=\"serializedData\">").val( $("#editForm").serialize() ) ) .append( $("#editForm input[name=token]").clone() //@todo token name can be different ) .appendTo($("body")) .submit(); return false; }); |
И вот такой код в PHP, чтобы заменить $_POST нужными данными:
private function prepareSerializedData() { if (isset($_POST['serializedData'])) { $_POST = CMap::mergeArray($_POST, $this->parse_str($_POST['serializedData'])); unset($_POST['serializedData']); } } private function parse_str($string) { $parts = explode("&", $string); $result = array(); foreach ($parts as $part) { $parsed = array(); parse_str($part, $parsed); $result = array_merge_recursive($result, $parsed); } return $result; } |
Less.js и кеширование
Есть такой замечательный скрипт less.js, который позволяет компилировать LessCSS прямо в браузере. Скрипт поддерживает кеширование скомпонованного less средствами HTML5 Local storage.
Но есть одна неприятная особенность - подключенные через @include файлы не проверяются на изменения, что в свою очередь ведет к некоторым неудобствам при разработке.
Для очистки кеша я нашел на просторах интернета такой скрипт:
function destroyLessCache(pathToCss) { // e.g. '/css/' or '/stylesheets/' var host = window.location.host; var protocol = window.location.protocol; var keyPrefix = protocol + '//' + host + pathToCss; for (var key in window.localStorage) { if (key.indexOf(keyPrefix) === 0) { delete window.localStorage[key]; } } } |
Собственно используя destroyLessCache("/bootstrap/less");
вместе с загрузкой страницы, мы сбрасываем весь кеш less.js.
В качестве параметра функция принимает путь к less файлам, относительно хоста.
Необычное проявление ошибки failed to open stream: No such file or directory
В Yii Framework, как и во многих других фреймворках, принято подгружать классы стандартной для PHP автозагрузкой.
Сегодня наткнулся на такой вот странный баг, проявившийся при разворачивании ранее рабочего сайта на другом сервере:
PHP Error [2]
include(EasmSelectEx.php): failed to open stream: No such file or directory (/Users/bethrezen/Documents/DotPlant-private/yii-framework/framework/YiiBase.php:423)
#0 /Users/bethrezen/Documents/DotPlant-private/yii-framework/framework/YiiBase.php(423): DotPlantWebApplication->handleError()
#1 /Users/bethrezen/Documents/DotPlant-private/yii-framework/framework/YiiBase.php(423): autoload()
#2 unknown(0): autoload()
#3 /Users/bethrezen/Documents/DotPlant-private/yii-framework/framework/web/CWidgetFactory.php(148): spl_autoload_call()
#4 /Users/bethrezen/Documents/DotPlant-private/yii-framework/framework/web/CBaseController.php(147): CWidgetFactory->createWidget()
#5 /Users/bethrezen/Documents/DotPlant-private/yii-framework/framework/web/CBaseController.php(173): AsmSelect->createWidget()
#6 /Users/bethrezen/Documents/DotPlant-private/protected/modules/User/widgets/AsmSelect.php(75): AsmSelect->widget()
Казалось бы проблема в отсутствующем файле, но это не так. Файл присутствует и права на месте.
Проблема же была в том, что в этом файле использовались short tags - <?
вместо <?php
.
В общем, странно, непредсказуемо, но логично. Ещё раз доказывает, что короткие теги не нужны.
Yii: CStatRelation и defaultScope у реляций
Ночью столкнулся с внезапной проблемой в Yii Framework 1.
Оказывается, статистические запросы через CStatRelation не поддерживает scopes в параметрах. Так что, если мы имеем defaultScope в нашем связанно элементе, то отменить его действие мы не сможем.
Как повторить эту проблему. Допустим имеем класс RevisionsSets с таким вот определением реляций:
/** * @return array relational rules. */ public function relations() { return array( 'pricesCount'=>array( self::STAT, 'Revisions', 'revisionSetId', ), ); } |
Ну и в самом классе Revisions имеем следующую группу условий по-умолчанию:
public function defaultScope() { $rev = Config::model()->getConfigValue("Shop", "CurrentRevision")->value; $t = $this->getTableAlias(false, false); return array( 'condition' => "$t.revisionSetId=:rev", 'params' => array('rev'=>$rev), ); } |
Что же происходит при вызове $revisionsSetModel->pricesCount
?
STAT-реляция забирает количество записей, но при этом применяет defaultScope от модели Revisions и в итоге для всех экземпляров RevisionsSets считается некорректное количество записей.
Исправить такое поведение можно было бы допливанием CStatRelation и реализацией там поддержки условий, как это например сделано в HAS_MANY, MANY_MANY, BELONGS_TO.
Но мне в голову пришел более быстрый вариант. Я создал класс RevisionsResetScope, который наследует Revisions и переопределяет defaultScope:
<?php class RevisionsResetScope extends Revisions { public function defaultScope() { return $this->resetScope(); } } |
Этот класс и используем в реляции:
/** * @return array relational rules. */ public function relations() { return array( 'pricesCount'=>array( self::STAT, 'RevisionsResetScope', 'revisionSetId', ), ); } |
Вот такой костыль. Кстати, на форуме Qiang Xue решил отказаться от статистических запросов и CStatRelation в Yii Framework 2. Будем надеяться, что разработчики предусмотрят достойную замену.
Бесконечная очередь и отказ от сообщений в RabbitMQ + Thumper + PHP AMPQlib
По работе столкнулся с одной задачей и решил использовать сервер очереди сообщений RabbitMQ в связке с PHP 5.3 через PHP-ampqlib и библиотеку Thumper.
Чтобы сделать Consumer(worker), который будет обрабатывать бесконечную очередь надо задать $consumer->consume(-1);. Тогда в цикле Thumper, где проверяется нужно ли нам выходить всё будет хорошо и наш обработчик будет работать вечно.
Но появился ещё один интересный вопрос - что делать, если внутри функции обработки сообщения произошел Exception? Я решил, что тогда мы отказываемся от этого сообщение и оно идет к другому обработчику. Реализовывать это лучше расширением класса Consumer:
class MyConsumer extends Consumer { public function processMessage($msg) { try { parent::processMessage($msg); } catch (Exception $e) { echo "Message rejected due to exception.\n".$e->getMessage()."\n"; $msg->delivery_info['channel']->basic_reject($msg->delivery_info['delivery_tag'],true); //throw $e; } } }
Соответственно ваш Consumer должен быть объектом класса MyConsumer.
Если же всё таки нужно кинуть этот Exception выше - убираем комментарий со строки throw $e;
Запаковываем проект в один JAR для запуска на Hadoop
Сегодня потребовалось запаковать проект на Java в один JAR файл. Нужно мне это для того, чтобы не мучиться с подключением сторонних jar и их дистрибьюции на кластер Hadoop Map Reduce. Да и к тому же, параметр -libjars из документации у меня почему то не работал.
Создаём расширяемую структуру на Yii Framework
Хочу предложить Вашему вниманию концепт расширяемой структуры приложения на Yii Framework. Я уже писал как сделать свои Actions на Yii.
Сегодня же речь пойдёт о написании стандартных экшенов для контроллеров и их расширения.
Предположим, в приложении у нас все контроллеры наследуются от класса dotPlantFrontMainController.
Нам необходимо, чтобы у всех контроллеров было действие Breadcrumb.
Для этого, в базовом контроллере dotPlantFrontMainController переопределяем функцию actions() следующим образом:
public function actions() { return array( 'breadcrumb' => 'application.extensions.actionBreadcrumb', ); } |
Если в унаследованном контроллере(скажем News) мы используем экшены из других файлов, то нам необходимо также переопределить функцию примерно вот так:
public function actions() { // return external action classes, e.g.: return array_merge( parent::actions(), array( 'admin'=>'application.controllers.News.ActionAdmin', ) ); } |
Таким образом, в News будут экшены определенные в базовом классе dotPlantFrontMainController плюс ActionAdmin.
Переписать же actionBreadcrumb можно уже в теле самого News.
В итоге имеем легко расширяемую структуру. В самом же базовом классе массив actions можно автоматически генерировать в соответствии с установленными расширениями. Всё легко и просто 🙂
Декодируем gzip страницу на PHP
Недавно при разработке одного сайта, столкнулся с проблемой декодирования на PHP полученной через curl страницы, сжатой в gzip.
Допустим у нас есть код, который делает запрос через php-CURL с возвращением заголовков:
$ch = curl_init(); curl_setopt($ch, CURLOPT_URL,$url); //curl_setopt ($ch, CURLOPT_VERBOSE, 2); // Отображать детальную информацию о соединении curl_setopt ($ch, CURLOPT_USERAGENT, 'Mozilla/5.0'); //Прописываем User Agent, чтобы приняли за своего curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); // Возвращать результат curl_setopt ($ch, CURLOPT_HEADER, 1); // Наши заголовочки curl_setopt ($ch, CURLINFO_HEADER_OUT, 1); // Где то наткнулся на этот параметр, решил оставить curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, 30); $result = curl_exec($ch); |
В итоге получаем в $result нашу страницу с заголовками сервера. Проверим, закодированна ли она и дешифруем её, если нужно:
if (strstr($result,"Content-Encoding: gzip")) { $result = preg_replace("/(.*)Content\-Encoding: gzip\s+/isU","",$result); $result = gzinflate(substr($result, 13)); } |
Вот и всё. Если Вам нужно просто gzip контент расшифровать, то можно в substr заменить 13 на 10. (+3 из-за \n после заголовков).