Разработка, 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
Разработка, 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).

Разработка

PHP+Apache: глюк?

Сегодня столкнулся с совершенно с чудовищным по своей странности багом.

Есть код. Простейший.

$a = array('' => 'value');

$key = '';
$falseKey = false;
$falseKey = (string)$falseKey; // $falseKey === '' будет true

var_dump(isset($a[$key]));
var_dump(isset($a[$falseKey]));

Вы думаете, что в обоих случаях код выведет true?
А вот и нет.

Существуют какие-то глюки в связке модуля пхп и апача, которые приводят к тому, что во втором случае код выдаст false.

Это не вылечилось перезагрузкой апача. Вылечилось лишь его полной остановкой и запуском.

Любопытно, что данный баг воспроизвелся лишь на одном сервере. На других абсолютно идентичных он не воспроизводился.

UPD (13.12.2015):
Таки «автором» этого глюка выступило расширение xdebug. К сожалению детального разбора проблемы я не осуществлял. Просто если вы встретились с неверным пониманием языком типов переменных, то смотрите в сторону xdebug.

Разработка

Symfony2: страница 404 и авторизация

Все знают, что в symfony2 404я страница не попадает под действие фаерволов. А это значит, что даже пытаясь кастомизировать 404ую страницу мы не сможем получить имя пользователя и его роль в системе. Так как механизм авторизации попросту не загружается.

Однако, существует решение, которое позволяет кастомизировать страницу 404 с учетом пользовательских данных.

Для этого нам надо завести роут подпадающий под действия фаерволлов и при этом откликающийся на любой (!) введенный адрес перехода.

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

namespace ProjectBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class DefaultController extends Controller
{
    //...

    /**
     * Данный роут перехватывает все переходы в системе, которые не охвачены другими роутами.
     * @Route("/{path}", name="_inner404Redirect")
     */
    public function inner404Redirect()
    {
        throw new NotFoundHttpException();
    }
}

Источник

HowTo

Symfony2: обработка исключения 404

Допустим, что нам нужно обработать исключение 404 и вместо стандартного ответа сервера (или шаблона ошибки ответить специфической страницей.

Как кастомизировать страницы подробно описано в официальной документации.

Нас будет интересовать пункт работы с эвентом kernel.exception.

Можно посмотреть один из примеров его использования.

Наш кейс: если symfony не смог обработать роут, то управление передается своему обработчику.

namespace AppBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class ExceptionListener
{
    protected $templating;
    protected $kernel;

    public function __construct(EngineInterface $templating, $kernel)
    {
        $this->templating = $templating;
        $this->kernel = $kernel;
    }
    
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        if ($event->getException() instanceof NotFoundHttpException) {
            $response = new Response();
            $response->setContent("Какие-то данные");
            //$response->setStatusCode(200) работать не будет
            $response->headers->set('X-Status-Code', 200);
            $event->setResponse($response);
        }
    }
}

В config.yml прописывем что-то вроде

services:
    kernel.listener.app_exception_listener:
        class: AppBundle\Listener\ExceptionListener
        arguments: [@templating, @kernel]
        tags:
            - { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

И обратите внимание, что переопределение статуса ответа в виде

$response->setStatusCode(200)

работать не будет. Мы не сможем переопределить статус отвта на 2хх с помощью этого метода (на любой другой можно).

Для того, чтобы мы могли переопределить ответ 404 нужно использовать метод

$response->headers->set('X-Status-Code', 200);
Разработка

Symfony2, Doctrine2, Postgresql и кодировки

Суть проблемы: в doctrine2 нет возможности выбрать кодировку подключения для драйвера pdo_pgsql. Совсем никак. Нет. Даже не пытайтесь. У вас ничего не получится.

Вот незадача: в mysql есть опция драйвера pdo PDO::MYSQL_ATTR_INIT_COMMAND. Благодаря этой опции можно устанавливать кодировку подключения при помощи

set names 'utf8'

И даже драйвер mysql поддерживает установку кодировки при помощи опции charset в настройках подключения.

Если мы покопаемся в файле драйвера, то увидим, что кодировка исправно обрабатывается

<b>Doctrine\DBAL\Driver\PDOMySql\Driver</b>

    /**
     * Constructs the MySql PDO DSN.
     *
     * @param array $params
     *
     * @return string The DSN.
     */
    private function _constructPdoDsn(array $params)
    {
        $dsn = 'mysql:';
        if (isset($params['host']) && $params['host'] != '') {
            $dsn .= 'host=' . $params['host'] . ';';
        }
        if (isset($params['port'])) {
            $dsn .= 'port=' . $params['port'] . ';';
        }
        if (isset($params['dbname'])) {
            $dsn .= 'dbname=' . $params['dbname'] . ';';
        }
        if (isset($params['unix_socket'])) {
            $dsn .= 'unix_socket=' . $params['unix_socket'] . ';';
        }
        if (isset($params['charset'])) {
            $dsn .= 'charset=' . $params['charset'] . ';';
        }

        return $dsn;
    }

Для драйвера pdo_pgsql (Doctrine\DBAL\Driver\PDOPgSql\Driver) нет ничего подобного.

При этом сам драйвер вполне успешно с кодировками работает.

Однако, безвыходных ситуаций не бывает. Чтобы как-то изменить кодировку при работе с базой pgsql можно применять события symfony2. А конкретно событие postConnect из doctrine2.

Все, что нам потребуется — это реализовать собственный листенер этого события.

namespace DatabaseBundle\Event\Listeners;

use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;
use Doctrine\Common\EventSubscriber;

/**
 * Событие инициализации подключения pgsql.
 * Позволяет установить кодировку бд.
 */
class PgsqlConnectionInit implements EventSubscriber
{
    /**
     * Используемая кодировка
     *
     * @var string
     */
    private $_charset;

    /**
     * Конфигурирование кодировки при создании класса
     *
     * @param string         $charset   The charset.
     */
    public function __construct($charset = 'utf8')
    {
        $this->_charset = $charset;
    }

    /**
     * @param \Doctrine\DBAL\Event\ConnectionEventArgs $args
     *
     * @return void
     */
    public function postConnect(ConnectionEventArgs $args)
    {
        $args->getConnection()->executeQuery("SET NAMES ?", array($this->_charset));
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscribedEvents()
    {
        return array(Events::postConnect);
    }
}

А затем подключить этот эвент в config.yml

services:
  pgsql.connection.init:
    class: DatabaseBundle\Event\Listeners\PgsqlConnectionInit
    tags:
      - { name: doctrine.event_listener, event: postConnect }

Теперь все ок 🙂

Разработка

Symfony2: cannot redeclarate class

Fatal error: include() [<a href="http://contoso.com/app/function.include">function.include</a>]: Cannot redeclare class symfony\bundle\frameworkbundle\frameworkbundle in /srv/www/contoso.com/vendor/composer/ClassLoader.php on line <i>412</i>

Да-да. Есть такая противная ошибка.

Она лечится либо отключением apc, либо установкой для него следующего набора опций

apc.include_once_override = 0
apc.canonicalize = 0
apc.stat = 0