PHP
Статьи по этой теме:
Миниатюры на PHP
Офлайн-версия сайта, или PDF и PHP
PHP и UTF-8
If-Modified-Since и кеширование
Управление зависимостями на примере composer
Очередь на основе PHP-FPM
Применил на практике прием, когда асинхронная очередь обработки сообщений реализовывается через PHP-FPM по протоколу fastcgi. На удивление, всё заработало сразу, никакой наладки не потребовалось.
Обычно PHP-FPM обрабатывает запросы от веб-сервера, например, nginx. Но никто не запрещает обращаться к PHP-FPM напрямую. Если все доступные рабочие процессы заняты, сообщения в нем как раз и ждут своей очереди на обработку.
Положительные стороны:
- Не нужны дополнительные компоненты в системе.
- PHP-FPM сам заботится о запуске рабочих процессов, достаточно отредактировать конфиг.
Отрицательные стороны:
- Нет надежного хранения сообщений. Если процесс PHP-FPM упадет, сообщения потеряются.
- Нет мониторинга. Если нужен — придется делать самостоятельно.
Чтобы сделать такую очередь, возьмите готовые библиотеки для общения по протоколу fastcgi, например, hollodotme/fast-cgi-client.
Как определить домен из PHP
Илья Бирман написал про баг в Эгее, когда сайт доступен по разным доменам, и RSS кешируется то с одним доменом, то с другим.
Эгея, чтобы узнать, на каком сервере она работает, смотрит, по какому адресу её открыли — больше ей это узнать неоткуда.
Проблема этого подхода в том, что в PHP (и в любом языке вообще) не существует универсального надежного способа узнать, на каком домене открыли страницу сайта.
HTTP_HOST и SERVER_NAME
Для этих целей обычно проверяют серверную переменную HTTP_HOST. Но в ней всего лишь содержимое заголовка Host из http-запроса. Этот заголовок — часть стандарта HTTP/1.1, и в HTTP/1.0 он не обязателен. Правда, без этого заголовка не заработают виртуальные хосты — разные сайты на общем сервере. Но даже в таком случае среди сайтов есть сайт по умолчанию, открывающийся при заходе напрямую по IP. Так вот, когда устаревшие клиенты (в том числе нормальные браузеры за старыми или специально настроенными прокси) открывают сайт по умолчанию, переменная HTTP_HOST будет пустой.
Есть еще одна серверная переменная — SERVER_NAME. Обычно она содержит хост, определенный в конфигурации веб-сервера. Но на него тоже нельзя стопроцентно положиться. Например, в nginx хост по умолчанию задается конструкцией
server_name _;
Сайт будет прекрасно открываться, но при этом в SERVER_NAME окажется знак подчеркивания.
Подробности для дальнейшего чтения на стековерфлоу: HTTP_HOST vs. SERVER_NAME.
Параметр конфигурации
Если вы делаете распространяемый движок для работы на разных серверах, у вас нет гарантированного способа определить хост, по которому открыт сайт. В моем движке S2 я скопировал способ из PunBB. В нем установочный скрипт «угадывает» адрес сайта (протокол + домен + порт + подпапка) в том числе на основе HTTP_HOST, дает возможность этот адрес отредактировать и сохраняет результат в конфигурационный файл. Затем именно этот адрес используется для генерации ссылок.
Как альтернативу Илья советует настроить редиректы. Это правильно, но, опять же, не всегда выполнимо. Например, вы настроили на сервере https, но не хотите делать редирект с http на https (вы хотите поддерживать старые браузеры, но у вас нет отдельного IP-адреса на каждый домен).
Когда одна и та же страница открывается по разным адресам, Гугл рекомендует в явном виде указывать canonical-адреса:
<link rel="canonical" href="https://example.com/some/url" />
Именно они попадут в поисковую выдачу. Ясно, что движок не сможет сгенерировать такой тег, если не будет знать, на каком из доменов он на самом деле работает.
Кстати, давно хотел написать о том, что https — это новый www. Он вынуждает совершать дополнительные бессмысленные действия при настройке сайта вроде редиректов с www. Ради https мне пришлось сделать в S2 поддержку тега link rel="canonical"
.
Пишем объектно-ориентированный код в PhpStorm — В кресле препода №1
В прошлом посте я разрушал мифы о среде разработки PhpStorm. В продолжение я записал скринкаст о том, как в ней писать объектно-ориентированный код.
Скринкаст рассчитан на людей, не владеющих уверенно ООП. На записи я перевожу фрагмент кода из процедурного стиля в
Содержание:
00:19 Процедурный стиль vs. объектно-ориентированный стиль
01:11 PHP не для процедурного программирования
02:22 ООП в PHP: много рутины
03:05 Задача: показать не только приемы работы в PhpStorm, но и пользу от ООП
04:08 Выбираем код для рефакторинга
05:10 Создаем класс: пространство имен; методы; константы
10:36 Автозагрузка классов через composer
13:28 Разбираем проблемы кода
15:34 Возвращаем вместо массива объект (DTO)
24:29 Избавляемся от глобальных переменных по принципу инверсии зависимостей (dependency inversion)
29:29 Наполняем DTO логикой: __toString
33:16 Рефакторинг
35:14 Наполняем DTO логикой: валидация в конструкторе
39:40 Получился код по принципам SOLID
40:24 Проблема создания сервисов
41:04 Решение с помощью контейнеров зависимостей; подключение Pimple через composer
46:01 Обзор изменений, привнесенных объектно-ориентированным подходом
48:09 Дополнение: подключаем библиотеку поиска Rose, описывая сервисы в контейнере
01:01:38 Подведение итогов
Оптимизация памяти в PHP и функция serialize
Хорошая статья на Хабре про особенности выделения памяти в PHP. Обычно на расход памяти в
Не так давно я писал требовательный к памяти скрипт. Это скрипт поиска, ранняя версия которого используется на сайте правил русского языка, а адаптированная версия перекочевала в мой движок сайтов.
Я немного поколдовал с кодом и в итоге сократил потребление памяти более чем в два раза. Раньше для индексации этого сайта нужно было 32 мегабайта памяти, а теперь достаточно и 16. Кроме методов из статьи, я применил запись чисел в системе счисления по основанию 36 (перевод осуществляется функцией
Дело в том, что функция
file_put_contents($filename, 'a:'.count($array).':{');
$buffer = '';
$length = 0;
foreach ($array as $word => $data)
{
$chunk = serialize($word).serialize($data);
$length += strlen($chunk);
$buffer .= $chunk;
if ($length > 100000)
{
file_put_contents($filename, $buffer, FILE_APPEND);
$buffer = '';
$length = 0;
}
}
file_put_contents($filename, $buffer.'}', FILE_APPEND);
Запись происходит порциями размером около 100 килобайт. Этот код подходит для сохранения в файл массива с большим количеством элементов среднего размера и решает проблему перерасхода памяти функцией
Загадка специалистам по PHP
Как вы думаете, что выведут следующие операторы?
<?php
echo preg_match('#тес#iu', 'Такой Вот Тест');
echo preg_match('#Тес#Siu', 'Такой Вот Тест');
echo preg_match('#тес#Siu', 'Такой Вот Тест');
echo preg_match('#во#Siu', 'Такой Вот Тест');
Логика подсказывает, что 1111, а на опыте оказалось 1101. Причем и в Windows, и в Linux (Debian, PHP 5.2.6). Я подумал, что комбинация модификаторов Siu несовместима (и даже убрал в отлаживаемом коде модификатор S). Но почему тогда последнее регулярное выражение срабатывает правильно?
Кто подскажет, в чем тут дело?
UTF-8 bad chars
Вопрос о «плохих» данных в UTF-8. Иногда такое знание оказывается полезным. Например, в корректной UTF-8 строке не могут встретиться байты 0xC0, 0xC1. Это может пригодиться при обработке строк для экранировки неизменяемых последовательностей символов (таких, как html-теги). Экранируемые подстроки вырезаются из строки, на их место ставятся символы с кодом 0xC0, строка обрабатывается, после чего подстроки возвращаются назад, вместо 0xC0.
#.*#/u
У регулярных выражений PHP есть специальный модификатор u для работы со строками в кодировке UTF-8. Оказывается, вставлять этот модификатор во все подряд регулярные выражения не только бессмысленно, но и вредно. Если шаблон может работать после удаления модификатора u, то он будет работать без него быстрее, зачастую существенно быстрее.
Вообще-то, знакомства с устройством кодировки UTF-8 достаточно, чтобы понять, почему строки в этой кодировке обрабатываются медленнее. Однако я не сопоставил этот факт с тем, что неоправданное употребление модификатора u может сильно замедлить регулярное выражение, и обнаружил такое замедление случайно.
Когда же модификатор u необходим? Только тогда, когда в регулярном выражении указывается количество символов или в квадратных скобках присутствуют символы, не входящие в нижнюю половину таблицы ASCII.
В процессе оптимизации можно попытаться изменить регулярное выражение и убрать из него модификатор u.
Как всегда, лучше проверять на практике необходимость модификатора u в каждом конкретном регулярном выражении и его влияние на время выполнения скрипта.
PHP: навигация
Некоторое время назад Илья Бирман написал про подсветку ключевых слов. В комментариях после моего замечания о возможности использовать функцию preg_replace развязалась небольшая дискуссия о том, как правильно нужно генерировать подобные вещи. Вот что писал Илья:
*_replace — это вообще не наш метод, надо сразу всё правильно генерировать, а не резать по живому потом.
...
А генерировать неправильный контент, чтобы потом его героически исправить — это левак, нужно сразу генерировать правильный.
Рассмотрим достоинства и недостатки различных подходов к генерации контента на простом примере навигационных ссылок.
Использование preg_replace позволяет сделать код коротким и понятным.
$cur_url = 'item2.htm';
$menu = '<a href="item1.htm">item1</a><br />
<a href="item2.htm">item2</a><br />
<a href="item3.htm">item3</a><br />
<a href="item4.htm">item4</a><br />
<a href="item5.htm">item5</a>';
$menu = preg_replace(
'#<a href="'.$cur_url.'">([^<]*)</a>#',
'<span>\\1</span>',
$menu);
Однако на мой взгляд этот код может быть расценен в соответствии с цитатой как «левак». Я не знаю, какой способ является правильным в этой ситуации с точки зрения Ильи, но могу предположить, что он должен быть примерно таким:
$cur_url = 'item2.htm';
$menu_array = array(
'item1.htm' => 'item1',
'item2.htm' => 'item2',
'item3.htm' => 'item3',
'item4.htm' => 'item4',
'item5.htm' => 'item5'
);
$menu = '';
foreach ($menu_array as $url => $link) {
if ($url != $cur_url)
$menu .= '<a href="'.$url.'">'.$link.'</a><br />';
else
$menu .= '<span>'.$link.'</span><br />';
}
Этот код является чуть более громоздким. К тому же, у метода не всё в порядке с производительностью. Проведенные тесты показали, что он примерно в три раза медленнее, чем предыдущий.
Можно применить и третий способ:
$cur_url = 'item2.htm';
if ($url != 'item1.htm')
$menu = '<a href="item1.htm">item1</a><br />';
else
$menu = '<span>item1</span><br />';
if ($url != 'item2.htm')
$menu .= '<a href="item2.htm">item2</a><br />';
else
$menu .= '<span>item2</span><br />';
if ($url != 'item3.htm')
$menu .= '<a href="item3.htm">item3</a><br />';
else
$menu .= '<span>item3</span><br />';
if ($url != 'item4.htm')
$menu .= '<a href="item4.htm">item4</a><br />';
else
$menu .= '<span>item4</span><br />';
if ($url != 'item5.htm')
$menu .= '<a href="item5.htm">item5</a>';
else
$menu .= '<span>item5</span>';
Он еще более громоздкий, да еще и избыточный. Хотя данный способ в полтора раза быстрее первого, в подобной ситуации я отдаю предпочтение использованию preg_replace.
PHP и timestamp
На мой взгляд, функции time(), mktime(), date(), gmmktime(), gmdate() недостаточно хорошо описаны в документации. Легко запутаться при попытках понять, что же происходит в разных часовых поясах. Вот доходчивое объяснение (правда, на английском). Вкратце его суть в следующем. Метка времени (timestamp) фиксированного момента одна и та же для всех часовых поясов. Функции date() и mktime() преобразуют timestamp ко времени в часовом поясе, установленном на сервере, и обратно. Функции gmdate() и gmmktime() делают то же самое, но только для гринвичского времени.
PHP: mkdir
Сегодня потратил немало времени в попытках понять, почему права у директории dir после выполнения функции mkdir('dir', 0777); не выставляются в 777. А ведь в документации написано:
На аргумент mode также влияет текущее значение umask, которое можно изменить при помощи umask().
Тема umask осталась нераскрытой. В общем, про второй параметр у функции mkdir() можно забыть, а правильный код выглядит так:
mkdir('dir');
chmod('dir', 0777);