PHP Generation
5 subscribers
17 photos
18 links
PHP G - Ваш проводник в увлекательный мир PHP
Download Telegram
Введение в метрики для PHP разработчика

Всем привет. Я php разработчик и в свободное время пишу телеграм ботов. Зачастую они требуют дополнительного мониторинга работоспособности, который я реализую через связку Prometheus + Grafana. Когда я решил писать статью про метрики, я сначала планировал досконально описать пошагово как настроить окружение, как разворачивать, как строить графики. Но прикинув объем материала я решил пойти по пути наименьшего сопротивления. Сделать простое приложение, засунуть его в докер, и попутно обвешать его всем необходимым. Что бы любой желающий мог самостоятельно поднять его у себя на домашней машине и посмотреть, как это работает. В итоге за вечер написал подобие магазина в виде телеграм бота. Цель статьи познакомить читателя с принципами работы с метриками, а не написать какое-то достойное приложение.

https://habr.com/ru/post/544582/
Enum в PHP 8.1 — для чего нужен enum, и как реализован в PHP

Через несколько дней заканчивается голосование по первой итерации реализации enum в PHP 8.1 . Уже видно, что голосов “за” гораздо больше, так что давайте кратко пройдемся и посмотрим, что же нам приготовили авторы языка.

https://telegra.ph/Enum-v-PHP-81--dlya-chego-nuzhen-enum-i-kak-realizovan-v-PHP-03-14
​12 факторов: не хранить пароли в коде

п. 3 в «12 факторах» говорит — храните конфигурацию в переменных окружения. По-русски это значит «не храните пароли в коде». Сейчас объясню.

Вот представьте, что вы выкладываете на прод приложение, которое хранит своё состояние в постгресе, и единственное место, куда оно умеет за ним ходить, — это localhost с логином root и паролем secret. Проблема усугубляется тем, что все популярные веб-фреймворки по дефолту предлагают хранить пароли именно в коде — settings.py в джанго или secrets.yml в рельсе. Джуны действительно так делают. А это плохо:

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

Во-вторых, все люди, которые имеют доступ к вашей кодовой базе, знают, какие у вас доступы к продакшн-данным. А значит, прощайте, быстрые задания в реальной кодовой базе для стажёров, а скорее всего, и демонстрация реальной кодовой базы на собеседованиях.

Так что храните все пароли в переменных окружения. К примеру, если используете джанго, можете взять django-environ, который строит нормальный мостик между переменными окружения и settings.py.
Допустим, мы проектируем пакетный обработчик команд. Чтобы узнавать о каждой успешной операции, добавим простой аргумент-слушатель $onEach.

/**
* @template T of object
* @psalm-param iterable<T> $commands
* @psalm-param callable(T): void $onEach
*/
function handleBatch(iterable $commands, callable $onEach): void
{
foreach ($commands as $command) {
// ...

$onEach($command);
}
}

Теперь можно, например, инкрементировать прогресс-бар при вызове из консольной команды.

handleBatch($commands, static function () use ($progressBar): void {
$progressBar->advance();
});

Однако не всегда слушатель будет нужен, поэтому для простоты контракта сделаем его необязательным аргументом. Решение "в лоб": ?callable $onEach = null и потом if (null !== $onEach) { $onEach($command) }.

А теперь применим паттерн NullObject. Для этого добавим в проектный functions.php элементарную function void(): void {} и попробуем её в качестве значения по умолчанию в сигнатуре обработчика: callable $onEach = 'void'.

👹 Fatal error: Default value for parameters with callable type can only be NULL.

Эхх, видимо, без null здесь никак не обойтись. Но мы не сдаемся и красиво комбинируем.

function handleBatch(iterable $commands, ?callable $onEach = null): void
{
$onEach ??= 'void';

// ...
}

🎉 Ура, так работает.

Плюсы этого подхода по сравнению с if:
• лаконичность: 1 строка вместо 3;
• простота восприятия: одно выражение в начале функции имеет меньшую цикломатическую сложность, чем условие в цикле;
• универсальность: легко переиспользовать во всех подобных ситуациях.
12 факторов: не писать логи в файлы

В «12 факторах» написано: «логи — это поток событий». Сейчас объясню чуть проще.

Если джуниору поставить задачу записывать какие-то события в лог, скорее всего, его код будет выглядеть как-то так:

with open('logs/events.log') as fp:
fp.writeln('Event has happened')

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

1. Никто не будет знать, пока не прочитает код, что logs/events.log — это артефакт, который надо хранить. Если запустить ваше приложение внутри докер-контейнера, то все ваши логи будут жить не больше, чем время жизни контейнера с приложением.
2. Приложение нельзя будет нормально запустить в нескольких экземплярах. Если у вас работают 5 копий (5 контейнеров или 5 серверов), то никто и никогда не скажет, на каком именно сервере лежит logs/events.log, придётся отсматривать все 5.
3. Файлы с логами нужно ротировать, чтобы не заканчивалось место. А если вы пишете по 100 событий в секунду — ротировать быстро.

Все эти проблемы решают централизованные системы логгинга — graphana loki, papertrail или даже datadog. Когда вы их подключаете, ваше приложение пишет логи не в файл, а через сокет на центральный сервер. Архивация и ротация у вас теперь на одной машине. А самое главное — поиск: любое средство хранения логов даёт вам поиск, который работает гораздо удобнее, чем grep.
Как сделать тестируемым класс, использующий нечистую встроенную функцию PHP?

Очень просто — объявить функцию необязательным параметром конструктора.

final class Service
{
/**
* @psalm-var callable(): int
*/
private $time;

/**
* @psalm-param ?callable(): int $time
*/
public function __construct(?callable $time = null)
{
$this->time = $time ?? 'time';
}

public function do(): void
{
$time = ($this->time)();
}
}

$service = new Service();

$serviceForTesting = new Service(static fn (): int => 1234567890);

Не советую использовать либы-костыли типа php-mock/php-mock.
12 факторов: неизменяемые релизы

Версия приложения, которую можно запустить в проде (или стейджинге), — это релиз. Релиз — иммутабельная штука: нельзя взять и что-нибудь поменять в релизе. Если нужно что-то поменять в коде приложения, делают новый релиз.

Релизы нужны, чтобы однозначно идентифицировать версию приложения в трейсах ошибок (к примеру, см. как клёво работает Sentry с релизами), а также для того, чтобы чётко было понятно, куда откатываться в случае проблем с деплоем: если не зашёл новый релиз — просто запускаем старый.

Релизы называют либо как хеш последнего коммита (типа релиз 19e267f04), либо просто по номерам билдов, типа релиз #19374. Если приложение пакуют в докер, номер релиза обычно записывают в лейбл контейнера.
Как пропустить первый элемент итератора в цикле? А пройти пять начиная с третьего?

💩 if (!isset($firstSkipped))
💩 if ($i++ < 2)

Все гораздо проще и лаконичнее с LimitIterator.
foreach (new LimitIterator($iterator, $offset = 2, $limit = 5) as $item) {
// ...
}

Обратите внимание, что первый аргумент LimitIterator имеет тип Iterator. То есть можно передать, например, Generator или ArrayIterator. Однако часто простые обходимые объекты реализуют IteratorAggregate, который не является подтипом Iterator. Как тут быть? Сначала на ум приходит забрать из него итератор вызовом $object->getIterator(). Она неверная, потому что IteratorAggregate::getIterator возвращает супертип Traversable, то есть это опять-таки может быть IteratorAggregate. Правильное решение — обернуть наш объект в IteratorIterator, который превращает любой Traversable в Iterator.

Итак, проход по первым трём элементам любого обходимого объекта будет выглядеть так:
foreach (new LimitIterator(new IteratorIterator($traversable), 0, 3) as $item) {
// ...
}
12 факторов: сервисы — это подключаемые ресурсы

Если попросить джуна настроить отсылку почты, он вполне может впасть в крайность и вызвать /usr/sbin/sendmail. Если попросить сделать файловое хранилище, то он сделает папочку /home/app/files.

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

Более продвинутый джун уже знает про SES и S3: он затянет в приложение их официальные либы, решив существенную часть проблем. Но не все: к примеру, на openshift уже не смигрируешь, да и на локальной машине отладка и тестирование сильно усложнится — нужно будет либо мoкать хождение в амазон, либо на каждый тест, который, скажем, генерит юзера, класть его аватарку в облако.

Продвинутые ребята рассматривают такие зависимости как внешние ресурсы, поддерживая для них полноценные абстракции. К примеру, в джанге складывают файлы через storage api и django-storages, а почту шлют через django-anymail. Поменял переменную окружения — и вот уже файлы складываются на рамдиск вместо s3, а почта шлётся через sendgrid вместо postmark.
В PhpStorm наконец-то добавят поддержку Psalm и PHPStan 🎊

Предположительно, плагины войдут в релиз 2020.3, так что ждём сентябрьский EAP.

Впервые плагины оформлены в виде опенсорс-проектов на GitHub, так что мы можем наблюдать как на языке со статическим анализом реализуется поддержка внешнего статического анализа для языка без статического анализа 🤣 Если серьезно, то проекты можно использовать как эталоны для собственных плагинов.

Подробнее в блоге JetBrains: https://blog.jetbrains.com/phpstorm/2020/07/phpstan-and-psalm-support-coming-to-phpstorm/.

Как отчаянный псалмовец 😜 очень рад быть к этому причастным.
Во время карантина по просьбе @pronskiy записал для команды обзорный скринкаст по фичам Psalm.
12 факторов: экспортировать сервисы через порты

Когда-то, когда сайты модно было делать на ПХП, конфигурация среды размазывалась буквально везде. Какие-то редиректы можно было настроить в .htaccess, логирование нужно было настроить в конфигурации Apache, версию самого Apache нужно было выбрать через контрольную панель хостинга. Сайт, написанный под один хостинг, мог не заработать на другом — где-то ПХП был подключён как модуль Apache, а где-то — через FastCGI, и это требовало разной механики работы с заголовками.

Это породило страшные вещи вроде рекомендаций по разворачиванию на 50 строк или гигантских виртуальных машин на Vagrant. Такой подход делает почти невозможной смену среды выполнения и съедает кучу ресурсов — как разработчика, который читает мануалы или обслуживает виртуалки, так и его машины, которая вынуждена всё это крутить.

Сейчас принято делать приложения, которые хостят себя сами. На node.js так было с самого начала — попробуйте найти хоть один проект, который имеет веб-интерфейс и не содержит внутри простого HTTP-сервера на express.js. У Java такая практика тоже принята давно — там испокон веков существуют Jetty и Tomcat (а сейчас наверняка что-то более модное).

Ну а если пишете на чём-нибудь скриптовом вроде питона, обязательно включите в докер-образ uwsgi\gunicorn — пусть ваше приложение просто выставляет наружу порт, обращаясь к которому можно получить все нужные сервисы. Так вы не только упростите деплой и масштабирование, но и сделаете возможным всякие интересные интеграционные штуки вроде возможности прогнать браузерные тесты относительно свежесобранного приложения прямо в процессе CI.
12 факторов: хранить состояние вне процесса

Запущенное приложение не должно рассчитывать на внутреннее состояние процесса. Когда-то было принято хранить на диске буквально всё. Кроме сессий в /var/tmp, нормальной практикой было сжимать загруженные в админку изображения в момент их запроса. Типа запросил пользователь фоточку товара 400x400, а у нас в кеше есть только 200x200 — идём в оригинал, сжимаем его в 400x400 и сохраняем на диск. Другим пользователям уже отдаём кешированную версию. Думаю, не нужно рассказывать, чем это плохо, если вы читали мои предыдущие посты про 12 факторов.

Такой подход до сих пор проскакивает в маргинальных инструментах вроде django-webpack-loader: эта штука собирает JS в момент запроса и сохраняет где-то на диске. Конечно же, если вы деплоите JS, который почему-то не собирается, вы об этом узнаете не в CI, а от пользователей, которым прилетит неработающий бандл.

Не делайте так. Все преобразования выполняйте в процессе CI, а промежуточные данные храните в отдельных сервисах — данные в базе, сессии в кеше, а картинки где-нибудь в S3.
Журнал Glamour включил трансгендера, PHP разработчика, активного контрибьютора Symfony, Манро Бергдорф в свою премию «Женщина Года».

Этот трап-активист ещё в 2017 году стал первой трансгендерной моделью L'Oreal, но был уволен за расистские высказывания:

«Честно, у меня больше нет сил говорить о расистском насилии со стороны белых людей. Да, ВСЕХ белых людей. Поскольку большинство из вас даже не осознает, что все ваше существование, привилегии и успех как расы построен на спинах, крови и гибели цветных людей. Все ваше существование пропитано расизмом».

Однако уже в 2020 году, после смерти Джорджа Флойда, L'Oreal решили извиниться перед трапом и возобновили с ним сотрудничество.