среда, 2 ноября 2011 г.

PHP определение языка пользователя

Если вы делаете сайт на нескольких языках, то вы обязательно столкнётесь с задачей:
Как определить язык пользователя на стороне сервера (в PHP)?
Один из часто используемых вариантов - использовать Geo IP и дальше уже по стране определять язык.
Но на мой взгляд более правильный способ - это использовать заголовки, которые для этого предназначены, а именно Accept-Language, в котором перечислены с указанием приоритета предпочитаемые языки.
Ниже представлена функция на PHP, которая парсит заголовок Accept-Language и находит наиболее подходящий язык для пользователя из тех, что переданы массивом (параметр $languages).
При этом, если ни одного языка не нашлось в заголовке, то будет использоваться первый из массива $languages.
function getAcceptLanguage($languages)
{
    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) || 
        !$_SERVER['HTTP_ACCEPT_LANGUAGE']) {
        return $languages[0];
    }  
   
    /*
     * Разбираем заголовок Accept-Language
     *
     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
     * 14.4 Accept-Language
     * Accept-Language = "Accept-Language" ":"
     *                   1#( language-range [ ";" "q" "=" qvalue ] )
     * language-range  = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
     *
     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
     * 3.9 Quality Values
     * qvalue         = ( "0" [ "." 0*3DIGIT ] )
     *                | ( "1" [ "." 0*3("0") ] )
     */
    preg_match_all("/([a-z]{1,8})(?:-([a-z]{1,8}))?(?:\s*;\s*q\s*=\s*(1|1\.0{0,3}|0|0\.[0-9]{0,3}))?\s*(?:,|$)/i",
                   $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches);

    // Результат по умолчанию - первый из доступных языков
    $result = $languages[0];
    $max_q = 0;
   
    for ($i = 0; $i < count($matches); $i++) {
        // Выделяем очередной язык
        $lang = $matches[1][$i];
        if (!empty($matches[2][$i])) {
            // Переводим ru-RU в ru_RU (т.к. локаль ru_RU)
            $lang .= '_'.$matches[2][$i]; 
        }
        // Определяем приоритет
        if (!empty($matches[3][$i])) {
            $q = (float)$matches[3][$i];   
        } else {
            $q = 1.0;
        }  
        // Проверяем есть ли проверяемый язык в массиве доступных
        if (in_array($lang, $languages) && ($q > $max_q)) {
            $result = $lang;
            $max_q = $q;
        }
        // Если язык только из первой части (например, просто ru, а не ru-RU) и более приоритетный язык еще не найден, 
        // то пробуем найти в массиве доступых языков тот, который начинается так же   
        elseif (empty($matches[2][$i]) && ($q * 0.8 > $max_q)) {
            $n = strlen($lang);
            foreach ($languages as $l) {
                if (!strncmp($l, $lang, $n)) {
                    $result = $l;
                    // Поскольку не точное совпадение, то уменьшаем q на 20%
                    $max_q = $q * 0.8;
                    break;
                }
            }
        }
    }
    return $result;
}
Пример использования:
$locale = getAcceptLanguage(array('en_US', 'ru_RU', 'de_DE'));

понедельник, 26 сентября 2011 г.

Git удаление файла или папки из истории

Если вы случайно закоммитили ненужный файл или папку в git-репозиторий и уже сделали push, то чтобы удалить все следы этого файла или папки в том числе и из истории, достаточно выполнить команду:
git filter-branch --tree-filter "rm -rf PATH" HEAD
где PATH - это относительный путь до файла или папки.
После этого выполните (чтобы перезаписать историю изменений):
git push origin master --force

четверг, 22 сентября 2011 г.

Подсказки в input и textarea (placeholder)

Еще одна довольно частая задача в веб-проектах - это показывать подсказки в input и textarea, чтобы сэкономить место, да и просто для красоты.
Например, такие:

До HTML5 это приходилось решать примерно так:
<input onblur="if (!this.value) {this.style.color= '#ccc';this.value = this.defaultValue}" onfocus="if (this.value == this.defaultValue) {this.value = ''; this.style.color= '#ccc'}" style="color: #ccc;" type="text" value="Поиск" />
Для тех, кто не знает про замечательный атрибут defaultValue, поясню - в нём сохраняет то значение, которое было у поля при загрузке страницы.
На самом деле уже заметно, что делается это нетривиально, но на этом сложности не заканчиваются.
При сабмите формы нужно тоже смотреть, если у поля this.value == this.defaultValue, то нужно сбрасывать это значение, чтобы оно не было отправлено на сервер.
В итоге получаем, что для решения задачи нам нужно написать несколько строк javascript, которые ну никак не красят код страницы + без JS это работать не будет.

К счастью, в HTML5 позаботились об этом и ввели атрибут placeholder http://dev.w3.org/html5/spec/common-input-element-attributes.html#the-placeholder-attribute
И теперь достаточно указать этот атрибут, чтобы получить подсказки для полей:
<input placeholder="Подсказка" type="text" />
Рабочие примеры:

пятница, 9 сентября 2011 г.

Множественная загрузка файлов с помощью HTML5

В некоторых формах, хочется позволять загружать сразу несколько файлов.
Раньше были только следующие варианты сделать это:
  1. Использовать флэш + js http://swfupload.org/
    Тут возникает много всяких трудностей и появляются всякие подводные камни, начиная с того, что флэш установлен не у всех, а заканчивая тем, что если вы хотите позволять загружать файлы только авторизованным пользователям, то вам нужно еще как-то передавать PHPSESSID (ну или другой идентификатор сессии), однако нужная сессия может не стартануть из-за настроек безопасности сервера (в частности из-за Suhosin).  Поэтому этот вариант всегда требует альтернативного способа.
  2. Есть еще один вариант: сначала показывать один <input type="file" name="files[]" />, а рядом добавить кнопочку еще один файл, по которой с помощью javascript создавать новый элемент input и добавлять его на страницу. Тут всё прозрачно, но не очень юзабельно для пользователя, ведь намного удобнее сразу выбрать несколько файлов и один раз нажать кнопку ОК.
Возможно есть и другие достаточно извращённые варианты реализации массовой загрузки файлов, но все они либо доставляют проблемы разработчикам/админам, либо неудобства пользователю.

С появлением HTML5 эта задача сильно упростилась, потому что у <input type="file" /> появился новый атрибут multiple (http://dev.w3.org/html5/markup/input.file.html).
Чтобы реализовать множественную загрузку файлов, достаточно прописать этот атрибут у нужного элемента.

Простейший пример:
<form action="" method="post" enctype="multipart/form-data">
    <input type="file" name="files[]" multiple="" />
    <br />
    <input type="submit" />
</form>
Это будет работать во всех современных браузерах: Google Chrome, Firefox, Opera.

четверг, 8 сентября 2011 г.

Лучшая минификация javascript с помощью Closure Compiler

Практически в любом веб-проекте возникает необходимость минификации javascript.
На мой взгляд, на сегодняшний день лучше всех остальных с этой задачей справляется Closure Compiler http://closure-compiler.appspot.com
Помимо онлайн-инструмента, есть также REST API, которое позволяет автоматизировать процесс.
Но проще всего использовать скрипт, написанный на Java, который можно скачать здесь:
http://code.google.com/intl/ru-RU/closure/compiler/docs/gettingstarted_app.html
В простейшем случае (SIMPLE_OPTIMIZATIONS) достаточно выполнить команду:
java -jar compiler.jar --js file1.js file2.js --js_output_file file.min.js
Более подробно об уровнях сжатия и оптимизации можете прочитать здесь: http://code.google.com/intl/ru-RU/closure/compiler/docs/compilation_levels.html

суббота, 9 июля 2011 г.

Функции json_encode() и json_decode() на PHP

Я занимаюсь разработкой разработкой веб-приложений на PHP, которые пользователи могут скачать и поставить у себя на хостинге. А поскольку на виртуальных хостингах в России PHP обновляется очень медленно, то нет возможности пользоваться многими полезными расширениями PHP, т.к. их может не быть.
Один из таких примеров - это функции для работы с JSON: json_encode() и json_decode(). Подробнее об этих функциях можно прочитать в документации http://www.php.net/manual/ru/ref.json.php
Проблема в том, что JSON extension входит в состав PHP по умолчанию только, начиная с версии 5.2.0
В реальности на сегодняшний день до сих пор на многих хостингах в рунете установлен PHP 5.1.6

Довольно сложно представить современное веб-приложение/сайт без использования ajax, и функция json_encode() становится просто необходимой для формирования JSON.
И если у вас нет json extension вы получите одну из ошибок:
Fatal error: Call to undefined function json_encode() in ...
Fatal error: Call to undefined function json_decode() in ...
Что же делать?
Нужно реализовать эти функции на PHP!
Можно, конечно, использовать уже готовое, например: http://pear.php.net/package/Services_JSON/
Но! Я встретил уже два сервера, где Service_JSON работал некорректно с русским языком. Тогда не было времени разбираться с причинами и было решено написать свою функцию json_encode() собрав воедино все полезное из комментариев на php.net.

Чтобы не приходилось думать о наличии json в PHP лучше всего делать так:
if (!function_exists('json_encode')) {
      function json_encode($value)
      {
           // Тут реализация функции json_encode на PHP
      }
}

if (!function_exists('json_decode')) {
      function json_encode($json, $assoc = false)
      {
           // Тут реализация функции json_encode на PHP
      }
}
После этого вы можете спокойно везде использовать функции json_encode() и json_decode() вне зависимости от версии PHP и наличия json extension.

Все исходники выложены на гитхабе: https://github.com/alexmuz/php-json
Функции реализованы как по отдельности в процедурном стиле (файлы json_encode.php и json_decode.php - можно подключать только то, что нужно), так и отдельным статическим классом phpJson.

Простейший вариант использования:
require_once("phpJson.class.php");
После этого функции json_encode и json_decode станут доступны.