Блог веб-программиста

13Июн/180

Обновление курса валют в Битрикс

Для Битрикса есть популярное решение для обновление курса валют - 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";
    }
}
Связано с категорией: Code Нет комментариев
8Июн/180

Достигаем цели статистики просто в 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 нужен для того, чтобы если вдруг у нас не прогрузилась метрика(привет, РКН) - у нас ничего не сломалось.

Связано с категорией: Code Нет комментариев
30Ноя/120

Обход ограничения 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;
	}
Связано с категорией: Code Нет комментариев
21Окт/120

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 файлам, относительно хоста.

Связано с категорией: Code Нет комментариев
9Авг/120

Необычное проявление ошибки 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.

В общем, странно, непредсказуемо, но логично. Ещё раз доказывает, что короткие теги не нужны.

Связано с категорией: Code Нет комментариев
28Апр/120

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. Будем надеяться, что разработчики предусмотрят достойную замену.

Связано с категорией: Code Нет комментариев
27Авг/110

Бесконечная очередь и отказ от сообщений в 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;

Связано с категорией: Code Нет комментариев
5Июн/100

Запаковываем проект в один JAR для запуска на Hadoop

Сегодня потребовалось запаковать проект на Java в один JAR файл. Нужно мне это для того, чтобы не мучиться с подключением сторонних jar и их дистрибьюции на кластер Hadoop Map Reduce. Да и к тому же, параметр -libjars из документации у меня почему то не работал.

Связано с категорией: Code Читать полностью
16Сен/093

Создаём расширяемую структуру на 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 можно автоматически генерировать в соответствии с установленными расширениями. Всё легко и просто 🙂

Связано с категорией: Code 3 Комментарии
4Сен/096

Декодируем 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 после заголовков).

Связано с категорией: Code 6 Комментарии