Разработка, HowTo

PHP: дело о загадачном пробеле

%d0%b2%d1%8b%d0%b4%d0%b5%d0%bb%d0%b5%d0%bd%d0%b8%d0%b5_103Или история о том, как побились картинки.

Однажды от пользователей пришла жалоба, что новые картинки, которые они на проект грузят не отображаются. Только если полностью обновить страницу.

Проверил данные в кеше — картинки были. Все как положено — пережатые и с водяными знаками. После ряда танце с бубном удалось выяснить, что скрипт, который картинку генерирует пока ее в кеше нет отдает пробел в каждой картинке — на скриншоте это хорошо выдно. Идет пробел, а после стандартный заголовок jpeg.

Чтож. Очень вероятно, что какой-то вредный скрипт оказался с пробелом. Мы же помним вро стандартные открывающиеся и закрывающиеся теги php, которые принесли много головной боли.

Но как среди тысяч строк кода выяснить, где начинается вывод злобного пробела?

  1. отключить вывод самой картинки и убедится, что пробел все еще выводится.
  2. отключить буферизацию вывода на стороне сервера — убедится, что пробел есть
  3. в самом конце скрипта поставить setcookie или header — в логах пояится сообщение вида «Warning: Cannot modify header information — headers already sent by (output started at file.php:XXX)». Где file.php — это соответсвенно файл, а XXX — номер строки, где начался вывод.

Чтобы отключить буферизацию нужно прописать в конфигах php.ini

output_buffering=0

Или в конфигах апача или htaccess

php_flag "output_buffering" Off
Обработка изображений, Разработка

JavaScript: копируем img в canvas

Существует несколько способов скопировать имеющееся на странице изображение (img) в canvas. Конечно все они сводятся к простейшему drawImage, но каждый из этих методов применим в своем контексте.

Первый и самый очевидный (а еще универсальный) — загрузить изображение при помощи объекта Image. Он работает только в случае, когда у img есть атрибут src.

var img = new Image();
var srcImg = document.getElementById('src-img');
img.onload = function() {
    // создаем (или получаем) канву
    var backCanvas = document.getElementById('dest-canvas');
    backCanvas.width = this.width;
    backCanvas.height = this.height;
    var backCtx = backCanvas.getContext('2d');

    // рисуем
    backCtx.drawImage(this, 0,0);
};
img.setAttribute('src', srcImg.src);

Недостаток — еще один запрос в сеть.

Второй способ — менее универсален. Картинка у нас уже есть — почему ее нельзя нарисовать на канве?

var backCanvas = document.getElementById('dest-canvas');
var srcImg = document.getElementById('src-img');
backCanvas.width = srcImg.naturalWidth;
backCanvas.height = srcImg.naturalHeight;

var backCtx = backCanvas.getContext('2d');

// рисуем картинку
backCtx.drawImage(srcImg, 0,0);

И вот тут мы используем naturalWidth/naturalHeight. Зачем?

Все очень просто. Если в первом случае width и height означали размеры картинки, то во втором — это будут физические размеры элемента img на странице. И если картинка внутри него будет отмасштабирована, то мы мы скопируем только часть картинки размером width на height.

Как на иллюстрации ниже (картинка там имеет разрешение в десять раз больше нежели физические размеры img и масштабируется).

Выделение_001

А свойства naturalWidth/naturalHeight не поддерживаются в ряде браузеров. поэтому способ универсальным назвать нельзя.

Разработка, HowTo

PHP: array_map и ключи массива

Интересно, почему такой вопрос часто всплывает на форумах или где-то еще?

Сначала постараюсь ответить на вопрос «нафига?», а потом на вопрос «как?».

Мы привыкли, что в в питоне, js и множестве других языков и фреймворков есть функция map, которая применяет некоторую функцию-обработчик к каждому элементу в списке данных и возвращающую результат в том же порядке. Пруф.

Но никто не ждал, что хеш-таблица и массив в контексте одного из языков будет означать одно и то же. Опять же пруф.

Этот постыдный момент из жизни структуры привел к тому, что одним из самых частых применений массивов (в пхп конечно же) стало создание отображений категория=>какое-то значение.

А после того как у вас появилось отображение вы, вероятно, захотите это где-нибудь на сгенерированной странице отобразить.

Т.е. показать пользователю сам параметр, а скобках его категорию.

Как мы делаем это в языках (фреймворках), которые разделяют понятие массива и хеш-таблицы? Перебираем объект, который содержит категории и формируем по нему массив выводимых данных. Наверное так.

И тут мы открываем руководство по php и видим, что она применяет некий колбек ко всем элементам массива и после этого возвращает новый массив.

Удобно же! Применили колбек к массиву и получили обработанный.

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

var_dump(array_map(function(....

И что-то не заладилось. 🙂 Ключи в колбек не попадают. Можно сделать все через foreach, но тогда нам потребуется еще одна переменная. А тут все было просто и наглядно.

И мы идем в гугель: «php array_map with keys».

А зачем гуглить-то?

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

var_dump(array_map(function($key, $value) {
  return "{$key} => {$value}";
}, array_keys($array), $array));

И все отлично работает. И притом правильно. Относительно конечно же. И никаких вам лишних переменных.

А тем временем в коде вновь и вновь появляются конструкции вида

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

$description = [];

foreach ($array as $key => $value) {
  $description[] = "{$key} => {$value}";
}

Или еще хуже.

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

$description = [];

array_walk($array, function($value, $key) {
  $description[] = "{$key} => {$value}";
});

Проблема кейса из статьи может быть и раздута, но при вчитывании в сотни строк кода более-менее понятными сходу являются только первые два варианта.

Вариант же с array_walk на понятность никак не претендует (как и решение подобной задачи при помощи .each в jquery).

Разработка, HowTo

Git: откат мержа, который не был запушен

Иногда мы делаем мерж некоторой ветки в master и случается такое, что нам нужно по какой либо причине откатить ветку (мы не делали push еще в удаленный репозитарий), либо просто пытаемся сделать git pull забыв, что есть непрокоммиченые изменения.

При git pull мы увидим следующую картину

Git Pull Failed
You have not concluded your merge (MERGE_HEAD exists). Please, commit your changes before you can merge.

И чтобы откатить наш неудачный мерж не нужно делать никаких reset —hard HEAD=1.

Достаточно сделать

git reset --hard ORIG_HEAD

Тем самым мы приводим локальную ветку мастера к виду, в котором она был до неудачного мержа.

Разработка, HowTo, linux

Обновление нескольких git-репозитариев в папке

2016-03-08-22:48:31_580x134 Порой у нас в каталоге накапливается много-много git-репозитариев, которые хочется обновить в один заход. Для этого есть маленький скрипт, который обновляет все репозитарии, которые сможет найти в папке, переданной в качестве аргумента.

#!/bin/bash

if test "$#" -ne 1; then
    echo "usage: $0 <dirname>"
		echo "Find and update all git repos in specified folder"
    exit 1
fi

if [ -d $1 ]; then
	find $1 -type d -name .git | xargs -n 1 dirname | sort | while read line; do echo "Update repo $line" && pushd `pwd` > /dev/null && cd $line && git pull && popd > /dev/null; done
else
	echo "\"$1\" does not exists"
fi

Использование очень простое

$ gitup ~/projects

Репозитарий на github.

Разработка, HowTo

Redis: как скопировать базу без простоя проекта

redisАга. У нас проблемы: нам нужно переехать сервер редиса без даунтайма проекта. Но нам надо не только переехать, но еще и данные сохранить.

Чаще всего предлагается решение в виде остановки проекта, копирования базы в новый инстанс, его запуск, переключение проекта и другие танцы.

Существует даже команда migrate, которая гарантирует атомарное копирование ключей и данных.

Но она работает в рамках одной бд (а по дефолту у редиса их целых 16), а так же обладает рядом негативных последствий ввиду своей атомарности. Поэтому подходит она совершенно для других целей.

Сразу на ум приходят репликации 🙂 А данное решение их умеет.

Допустим у нас два сервера (у меня они на портах 6379 и 6479 соответственно). И нам нужно переключить проект с первого на второй сервер. Второй нулевой только что поднятый. А с первым ведется работа.

2016-02-24-20:28:27_805x48

Посмотрим, что в каждой из баз покажет команда

keys *

Основной редис

2016-02-24-20:29:30_482x426

Новый редис

2016-02-24-20:29:47_190x32

Как видим пока базы не синхронизированы.

Теперь нужно сделать новую базу репликой старой

> slaveof 127.0.0.1 6379
OK
> info
...
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0

Дополнительно может потребоваться авторизоваться на мастере чтобы началась репликация

config set masterauth <password>

После этого можем опять посмотреть, что в базу скопировалось

keys *

2016-02-24-21:00:18_441x414

Как видим: все данные успешно засинхронизировались (вполне возможно это займет продолжительное время ввиду объема данных).

После того, как репликация завершена нам нужно сделать две вещи:

  • превратить слейва в мастер
  • настроить приложение на работу с новым мастером
slaveof no one

Эта команда выключает копирование данных с мастера (но не удаляет их) и теперь уже дело за настройкой приложения.

Да. Небольшой даунтайм все же был (секунд 10-15), но это гораздо лучше, чем предлагаемые решения.

Однако, обратите внимание, что если вы активно пишете в редис, то при такой миграции баз часть данных можно и потерять.

Почитать про репликации.

Разработка

AngularJS: сервисы, фабрики, провайдеры

Чтоже это за странные service, factory и provider. Казалось бы — выполняют почти одну и туже функцию, подключаются через DI. Зачем их так много?

На абстрактный пример можно глянуть тут (к слову, автор привел очень хороший пример).

Но никто не рассказывает, зачем оно в реальной практике нужно.

Сервис

Он чаще всего используется для создания разделяемого ресурса.

var constructor = function constructor() {
    this.foo = 'foo'
    this.bar = function bar() {
        
    }
}

.service('someService', constructor)

Когда сервис создается первый раз, то выполняется

var someServiceImpl = new constructor()

И в дальнейшем при подключении зависимостей через DI в объекты будет всегда передаваться someServiceImpl.

Фабрика
это более продвинутая конструкция. Она не является разделяемым ресурсом сама по себе. При каждом использовании фабрики через DI ее значение — это результат вызова фабричной функции.

var factoryFn = function () {
    var someClass = function() {}
    
    return someClass;
}

.factory('someFactory', factoryFn)

При первом инстанцировании фабрики будет вызвана функция factoryFn и ее результат — это уже и есть разделяемое между всеми участниками системы значение.

Свое применения фабрика находит в первую очередь для реализации моделей. Как в примере выше. Создается и описывается класс модели someClass и фабрика при инстанцировании возвращает ссылку на него. В последующем можно использовать этот класс как конструктор объектов.

.controller('someController', ['someFactory', function(someFactory) {
    // Тут мы успешно инстанцируем новый объект someClass
    var someClassImpl = new someFactory();
}])

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

Но вся мощь провайдеров в том, что их можно конфигурировать (в отличие от фабрик или сервисов). Т.е. на этапе инициализации вашего приложения можно передать провайдеру некоторые опции, которые нельзя указать в дальнейшем.

Как пример — библиотека для oauth-авторизации. на этапе конфигурирования в нее передаются токены.

var providerFn = function() {
    var someModel = function() {
        // это модель
    }
    
    this.configure = function() {
        // делаем какую-то конфигурацию
    }
    
    this.$get = function() {
        return new someModel()
    }
}

.provider('someProvider', providerFn)

И теперь самое интересное: при подключении провайдера через DI в качестве разделяемого объекта будет выступать то, что вернула функция $get.

В то же время на этапе конфигурирования нам доступен сам инстанс providerFn и мы можем использовать его функционал по конфигурированияю

.config(['someProvider', function(someProvider) {
    //...
    someProvider.configure(options)
}])