Генерация URL для rest API

Для генерации адреса c целью доступа к API есть специальный метод

SUGAR.App.api.buildURL(
	module, // Строка, имя подуля (Calls,Accounts,Opportunities)
	action, // действие, параметр необязательный,
			// прилепляется в конец итогового URL
	attributes, // необязательный объект,
				// можно задать id записи, link и relatedId
	params // необязательный объект для дополнения адреса get переменными
)

К примеру для привязки существующего звонка к сделке — url должен быть следующего вида
/<module>/:record/link/:link_name/:remote_id
для получения такого результата параметры должны быть следующими

SUGAR.App.api.buildURL(
	'Accounts', // основной модуль
	'', // не нужен в данном случае
	{
		link : 'calls', // связанный модуль
		id : '123-456', // ид сделки (записи основного модуля)
		relatedId : '789-012' // ид звонка (записи связанного модуля)
	}
)

на выходе получаем следующий url:
rest/v10/Accounts/123-456/link/calls/789-012
его уже можно использовать в вызове методов rest API

Для случаев типа /<module>/filter/count — достаточно указать модуль и в качестве action — оставшуюся часть ссылки «filter/count»

SUGAR.App.api.buildURL('Accounts', 'filter/count')

Параметр params этого метода введен, видимо, на всякий случай, совершенно, правда, непонятно на какой.
Этот метод всегда работает в паре с SUGAR.App.api.call, который принимает объект data со всеми данными, которые необходимо передать на сервер, и сам уже решает куда помещать эти данные: в post или в get параметры.

Белый экран в bitrix

Внезапно bitrix начал показывать белый экран на некоторых страницах и при входе в админку.
Много чего перепробовал, включение error_reporting и display_errors не помогает, пробовал вставлять в файлы инициализации, конфига, прологи, ничего не помогает.
Выход еле-еле нашелся, оказывается, в bitrix/.settings.php надо включить логирование ошибок, добавить элемент массива конфигурации:

return array (
	...
	'exception_handling' => 
		array (
			'value' => 
			array (
			'debug' => true,
			'handled_errors_types' => E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE & ~E_DEPRECATED,
			'exception_errors_types' => E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_USER_WARNING & ~E_USER_NOTICE & ~E_COMPILE_WARNING,
			'ignore_silence' => true,
			'assertion_throws_exception' => false,
			'assertion_error_type' => 256,
			'log' => array (
				'settings' => array (
					'file' => 'bitrix/modules/error.log',
					'log_size' => 1000000,
				),
			),
		),
		'readonly' => true,
	),
	...
);

После чего причину падения битрикса можно будет увидеть в файле лога bitrix/modules/error.log.
Стоит убедиться, что достаточно прав у веб пользователя для создания и записи в файл лога.
С большой степенью вероятности — причиной белого экрана будет неработоспособность какого-либо модуля. Пробуйте удалить папку модуля из bitrix/modules. И, после, установить этот модуль заново из админки.

Как запустить службу redis?

После установки redis создается служба Redis Server, которая упорно отказывается стартовать, пишет: «Служба была запущена и затем остановлена. Некоторые службы автоматически останавливаются, если они не используются другими службами или программами».
Похоже, это связано с тем, что она устанавливается в Programm Files, и запускается от имени RedisService.
Достаточно поменять учетную запись, от которой пытается запуститься служба на LocalSystem и она начинает нормально запускатьcя.
Пуск->В поиск: «cmd»->На «cmd.exe» правой кнопкой мыши->запуск от имени администратора->

sc config redis obj= "LocalSystem"

->Enter

MySql разница между двумя датами в месяцах

Приведу сразу mysql запрос выдающий количество месяцев от даты до настоящего момента (для примера)
обе даты могут быть числами или строками ГГГГММ, столбцы таблицы в формате datetime необходимо предварительно привести к такому формату:

SELECT PERIOD_DIFF(DATE_FORMAT(NOW(), "%Y%m"),DATE_FORMAT(date,"%Y%m"));

Равномерная выборка в MySql

Встала интересная задача — за произвольный срок показать динамику процесса.
Произвольный срок может быть и годом и тремя годами — показатели собираются каждый день, пока процесс установлен на мониторинг показателей. То есть даты могут идти не по-порядку, процесс может быть снят с мониторинга несколько месяцев, потом снова возвращен. Загружать на график все значения за период не просто бессмысленно, но и вредно, и загрузку страницы это замедлит и браузер сделает менее отзывчивым.
Поэтому надо выбрать порядка, скажем, 30 значений: этого будет достаточно, чтобы судить об общей динамике за выбранный период и выглядеть на графике будет это замечательно и нагрузка на браузер нулевая.
Самая главная сложность — это сделать равномерную выборку среди значений которые идут не по-порядку по дате, по ИД и подавно не по-порядку.
Для начала: как понять, что среди значений отсортированных предварительно по возрастанию даты понять, что это именно каждое N/30 значение?
Первое, что приходит в голову — это остаток от деления порядкового номера на N/30. Допустим, значений у нас 100.
Если отбирать записи, у которых порядковый номер делится на 3, получится набор из 33 записей, это как-бы нормально.
Хуже если записей 45, тогда, если округлить делитель до 2 получим 22 результата в наборе, если в меньшую сторону — до одного — то аж 45. Нехилый такой разброс.
Значит, чтобы всегда получать 30 записей в результирующем наборе, надо учитывать значение N/30 хотя бы до одного знака после запятой.
Берем, умножаем на 10 результат деления N/30, умножаем на 10 порядковый номер и остаток от целочисленного деления номера на коэффициент должен быть не точно нулем, а меньше 10.
Пример получившегося кода для MySql:

# Получаем количество записей 
SELECT COUNT(*) AS `N` 
FROM `table` 
WHERE `date` BETWEEN :dateFrom AND :dateTo ;
# koef = N / 30 * 10
# Задаем начальное значение счетчика записей
SET @numRow = 0;
# Делаем выборку
SELECT *, (@numRow := @numRow + 10) as `numRow`
FROM `table`
WHERE @numRow % :koef < 10 
    AND `date` BETWEEN :dateFrom AND :dateTo
ORDER BY `date`

Реальные запросы в phalconphp

C самого первого дня использования phalconphp меня начал волновать вопрос: какие запросы реально приходят на выполнение в базу данных mysql от phalconphp.
Официальную документацию я прочел от корки до корки. В ней в разделе Logging Low-Level SQL Statements (название раздела звучит очень обнадеживающе) предлагается повесить обработчик на событие beforeQuery базы данных и из этого события записывать low-lewel запросы в лог. Дальше — больше: прямо сразу в следующем же разделе «Profiling SQL Statements» нам предлагают использовать профайлер, который запишет наш запрос в стек и засечет время начала выполнения запроса, в обработчике события afterQuery останавливаем профайл. И после формирования страницы пробегаемся по всем профайлам и записываем все в лог.
Как же велико было мое разочарование когда я увидел лог:

SELECT `users`.`id`, `users`.`email`, `users`.`name` FROM `users` WHERE `users`.`email` LIKE :email: LIMIT :2;
INSERT INTO `users` (`email`,`name`) VALUES (?, ?);

Какой же это б**ть low-lewel sql?? Прям lower больше уже некуда %)
Такое положение вещей меня не устраивает, поэтому пришлось придумывать решение. Спешу поделиться тем, что у меня получилось.
Все сервисы, кроме роутера для удобства я инициализирую в одном файле app/config/services.php

< ?php 
/**
 * @return \Phalcon\DI\FactoryDefault
 */
function getDi() {
  static $di = null;
  if (!$di) {
    $di = new \Phalcon\DI\FactoryDefault();
  }
  return $di;
}
getDi()->setShared('config', include __DIR__ . '/config.php');
getDi()->setShared('logger', function() {
  $logger = new \Phalcon\Logger\Adapter\File(__DIR__ . '/../logs/' . date('Y-m-d') . '.log');
  return $logger;
});
getDi()->setShared('profiler', '\Phalcon\Db\Profiler');
getDi()->setShared('db', function() {
  $db = new \Phalcon\Db\Adapter\Pdo\Mysql(array(
    "host"   => getDi()->getConfig()->database->host,
    "username" => getDi()->getConfig()->database->username,
    "password" => getDi()->getConfig()->database->password,
    "dbname"   => getDi()->getConfig()->database->dbname,
    "charset"  => getDi()->getConfig()->database->charset,
    "options"  => array(
      PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES' . getDi()->getConfig()->database->charset,
    )
  ));
  getDi()->getEventsManager()->attach('db', function($event, $db) {
    $sql = $db->getSQLStatement();
    // service questions for clarification of structure of a database disturb me
    // you can remove the following block to see all queries
    if (strpos($sql, 'DESCRIBE') === 0) {
      return true;
    }
    static $profileStarted = false;
    if ($event->getType() == 'beforeQuery') {
      // Just in case we will deny queries on change and cleaning of a database.
      if (preg_match('/(drop|alter|truncate) /i', $sql, $operation)) {
        throw new \Exception('Operation '.$operation[1].' was denied', 405);
      }
      if ($profileStarted) {
        getDi()->getProfiler()->stopProfile();
      }
      $vars = $db->getSQLVariables();
      $keys       = array();
      $values     = array();
      $myKeys     = array();
      $pregMyKeys = array();
      if ($vars) {
        foreach ($vars as $placeHolder=>$var) {
          // fill array of placeholders
          if (is_string($placeHolder)) {
            $keys[] = '/:'.ltrim($placeHolder, ':').'/';
          } else {
            $keys[] = '/[\?]/';
          }
          $myKeys[] = '::myKey'.(++$num).'::';
          $pregMyKeys[] = '/::myKey'.$num.'::/';
          // fill array of values
          // It makes sense to use RawValue only in INSERT and UPDATE queries and only as values
          // in all other cases it will be inserted as a quoted string
          if ((strpos($sql, 'INSERT') === 0 || strpos($sql, 'UPDATE') === 0) && $var instanceof \Phalcon\Db\RawValue) {
            $var = $var->getValue();
          } elseif (is_null($var)) {
            $var = 'NULL';
          } elseif (is_numeric($var)) {
            $var = $var;
          } else {
            $var = getDi()->getDb()->escapeString(mb_substr($var, 0, 500));
          }
          $values[] = $var;
        }
        $sql = preg_replace($keys, $myKeys, $sql, 1);
        $sql = preg_replace($pregMyKeys, $values, $sql, 1);
      }
      $sql = preg_replace('/ (WHERE|FROM|LEFT JOIN|SET|GROUP BY|HAVING|ORDER BY|LIMIT)/', PHP_EOL . str_repeat(' ', 8) . '$1', $sql);
      $sql = PHP_EOL . str_repeat(' ', 8) . $sql;
      $memoryUsage = round(memory_get_peak_usage(true)/1024/1024, 2).'MB';
      getDi()->getProfiler()->startProfile($memoryUsage.' '.$sql);
      $profileStarted = true;
    }
    if ($event->getType() == 'afterQuery') {
      getDi()->getProfiler()->stopProfile();
      $profileStarted = false;
    }
  });
  $db->setEventsManager(getDi()->getEventsManager());
  return $db;
});

Подключаем все необходимые сервисы, подключаем ивент-менеджер к базе данных и непосредственно перед запросом заменяем все плейсхолдеры на соответствующие им значения с учетом типов значений, после чего у нас есть тот запрос, что уходит на выполнение в базу данных.
Сначала попробовал заменять ключи сразу на значения, но, тогда, если значение — строка и в ней есть по-какой-то причине знаки вопросов, то замена происходит коряво, вместо замены последующих плейсхолдеров в искомом SQLStatement значения подставляются внутрь уже вставленной строки. Ничего лучше, чем сначала заменить исходные плейсхолдеры на свои, вида «::myKey{Num}::», а потом уже заменить эти плейсхолдеры на реальные зачения — на скорую руку не придумалось. Решение работает, поэтому дальше заморачиваться не стал. Если знаете решение изящнее — предложите, буду благодарен.

Ну и осталось теперь сохранить все профайлы в лог в bootstrap файле public/index.php

< ?php
try {
  if (!isset($_GET['_url']) || $_GET['_url'] == '/index.html') {
    $_GET['_url'] = '/';
    $_SERVER['REQUEST_URI'] = '/index.php?_url=/';
  }
  include __DIR__ . '/../app/config/services.php';
  $application = new \Phalcon\Mvc\Application();
  $application->setDI(getDi());
  getDi()->getProfiler()->startProfile($_SERVER['REQUEST_METHOD'].' '.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
  getDi()->getProfiler()->stopProfile();
  echo $application->handle()->getContent();
} catch (Exception $e) {
  include 'error.php';
}
foreach (getDi()->getProfiler()->getProfiles() as $profile) {
  $time = sprintf('%01.4f', $profile->getTotalElapsedSeconds());
  getDi()->getLogger()->info($time.' sec: '.$profile->getSQLStatement());
}
$time = sprintf('%01.4f', getDi()->getProfiler()->getTotalElapsedSeconds());
getDi()->getLogger()->info($time.' sec: total' . PHP_EOL . PHP_EOL);

Сначала создаем профайл, котором запоминаем метод текущего запроса и запрошенный адрес и уже после формирования всей страницы пробегаем по всем итемам в профайлере и записываем в лог: время обработки каждого запроса, сам (настоящий low-lewel) запрос и потребляемую сайтом память на момент выполнения запроса.
Наконец-то теперь в логах именно те запросы, что приходят в базу данных. Их уже можно скопировать в phpmyadmin, поиграться с параметрами, проанализировать и при необзодимости внести правки в код. Надеюсь, что кому-то еще это решение будет полезным.

git ––force push

Если  вам, как и мне, вдруг, приспичило сделать pull текущей ветки с перезаписью всех измененных файлов, то у меня есть рецепт. Лично для меня такая надобность актуальна: база данных распухла до размеров больше гигабайта и локально тестировать сайт уже не представляется возможным. WAMP натуральным образом дохнет  в корчах при обращении к сайту, поэтому я настроил netbeans на выгрузку файлов при сохранении. Делаю изменения и тестирую прямо на сервере на тестовом поддомене. Коммичу и пушу изменения в реп и кое-что дорабатываю с часик дома, после чего по приходе на работу вполне успешно делаю пулл на рабочем компьютере. Проблема возникала в плане выгрузки всех актуальных изменений на сервер. Делать синхронизацию средствами netbeans по мне слишком муторно. При попытке сделать git pull — он вполне обоснованно начинал ругаться фразами типа:

error: Your local changes to '*****' would be overwritten by merge.  Aborting.
Please, commit your changes or stash them before you can merge.

Просит типа сделать коммит сделанных изменений. Но делать коммит на сервере в такой ситуации, как выяснилось, откровенно хреновая идея. Можно поиметь кучу геморроя с разрешением конфликтов, что собственно у меня и произошло. Делать это на сервере без нормального редактора для разрешения конфликтов, сказать, что проблематично — это будет слишком мягко сказано, да и в принципе — все конфликты совершенно идиотские: одни и те же изменения оказываются в разных коммитах.
Гугление «git —force push» показало, (подумалось, что должно же быть что-то подобное), что такой опции в гите нет, но навело на хреновую идею, высказанную на паре сайтов — сделать git hard reset head, перед git pull. Не вздумайте повторить.
Это было даже худшей идеей, чем сделать коммит, этот долбаный ресет снес к фигам мне все файлы из заигноренных директорий: все загруженные пользователями файлы и сгенерированные отчеты. Как же хорошо, что до этого я догадался заархивировать все файлы проекта и положить в директорию выше проекта.

Хочу сказать что гуглил и мучился я достаточно долго прежде чем набрел на нужный рецепт. Нужная мне команда выглядит так:

git checkout -- .

Она возвращает все измененные файлы проекта в индексе git к состоянию последнего успешного pull. После чего совершенно спокойно можно сделать git pull. Все директории и файлы на сервере не внесенные в индекс git останутся нетронутыми. Именно то, что мне так давно было нужно.
git status выдает, что:

# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.

этим он говорит, что последний коммит был сделан не на сервере, что мы получили его на сервер, запустив pull. То есть состояние проекта на один коммит впереди, чем локальный репозиторий проекта на сервере. Это нормально.
Ну и покажет, если есть файлы не в индексе:

# Untracked files:
#   (use "git add ..." to include in what will be committed)

Это повод задуматься о внесении дополнений в .gitignore

Fatal error: Call to a member function getMessage() on a non-object in modmanagercontroller.class.php on line 557

MODX такое выдал в логи при переносе на другой хостинг. А в браузере белый экран.

Дело в том, что  файле настроек ./core/config/config.inc.php все пути абсолютные. Поэтому надо их все переписать на правильные. Абсолютный путь к сайту хранится в переменной $modx_base_path.

В моем случае — $modx_base_path= ‘/home/username/site.ru/public_html/’;

Берем эту строку вместе с первой кавычкой до последнего слеша (пледний слеш остается) — ‘/home/username/site.ru/public_html и меняем ее по всему документу на dirname(__FILE__).’/../..

modx 557

Расстановка переносов в html тексте на русском языке

Существует серия правил css для разных браузеров hyphens, -moz-hyphens, -webkit-hyphens, -ms-hyphens

К сожалению эти правила не работают в chrome, поэтому пришлось искать выход.
Я не сторонник нагрузок на сервер, которых можно избежать, поэтому начал искать нет ли уже готового решения на javascript — пусть работу, которую можно переложить на браузер — делает браузер. Плюс к тому, на стороне сервера пришлось бы изменять текст, который попадет в браузер, что может плохо сказаться на индексации поисковиками. и как быть?

Работа с куками

chocochipПерепробовал Н-ное количество всяких возможных расширений для работы с cookie браузера. Остановился на ChocoChip.
chocochip popup

Самый удобный для постоянного использования: снести ссылки текущего домена, добавить или изменить конкретную куку — все в пару кликов, не виснет, не глючит, для редактирования expire даты снабжен календариком.

web developer cookieWeb developer тоже, конечно, все это умеет — но для каждого действия на клик больше, при добавлении куки придется править Path и Expires, календарик разработчики зажопили, поэтому делать это совершенно неудобно.

Другие расширения тоже были все с подобными прибабахами.

Chocochip мне понравился сразу, как установил пару лет назад, когда пересел с firefox на chrome и искал замену привычным инструментам. До сих пор он меня устраивает, рекомендую.