ru
Feedback
Пых

Пых

Открыть в Telegram

Блог Валентина Удальцова о разработке на PHP. Хобот @phpyhobot https://youtube.com/@phpyh https://vkvideo.ru/@phpyh https://t.me/isPHPdying Статистика: https://t.me/INOTAROBOT?start=st1219340804 Для связи используйте личные сообщения канала.

Больше
8 107
Подписчики
+324 часа
-47 дней
+430 день
Архив постов
Пых
8 109
Видеосообщение00:44

Пых
8 109
Whole Value a.k.a. Value Object Я сейчас работаю над кодом, которому для работы требуется список параметров функции:

/**
 * @param list<ReflectionParameter> $parameters
 */
function make(array $parameters): Something {}
На первый взгляд, всё здорово. list<ReflectionParameter> выражает минимально необходимое знание для решения задачи да и вообще это список из объектов-значений, а не каких-то там примитивов. Но есть нюанс. В такой make можно передать невозможный список параметров, например:

$trimReflection = new ReflectionFunction(trim(...));

make(array_reverse($trimReflection->getParameters()));
С точки зрения типов всё верно, но инвариант "опциональные параметры идут после обязательных" в переданном списке нарушен, что может привести к неправильной работе функции. Можно передать параметры от разных функций (тогда $parameters[$i]->getDeclaringFunction() будет давать неконсистентный результат), можно поставить вариадик в начало — способов задать неверный список много, потому что list<ReflectionParameter> ничего толком не регламентирует. Если функция работает с концепцией "список параметров валидной сигнатуры", она должна принимать ReflectionFunctionAbstract и сама вызывать getParameters():

function make(ReflectionFunctionAbstract $function): Something
{
    $parameters = $function->getParameters();

    // ...
}
Теперь в теле make можно быть уверенным, что список параметров удовлетворяет всем инвариантам, ведь отрефлексировать функцию с неверной сигнатурой не получится... Хотя подождите... Можно же так поломать:

final class MessedReflectionFunction extends ReflectionFunction
{
    public function getParameters(): array
    {
        return array_reverse(parent::getParameters());
    }
}

make(new MessedReflectionFunction(trim(...)));
Но это уже разговор про LSP и что наследники не должны нарушать инварианты родителей — тема для другого поста. 😊 Мораль такая: принимайте не просто минимум знаний, а минимум, гарантирующий необходимые инварианты. Это и есть концепция Whole Value. ⸻ Этот пост я разместил неделю назад в 🐘 PHPeople.

Пых
8 109

Пых
8 109
Горю с PHPStan Все привыкли ограничивать дженерики типов сверху (upper bound): @template T of U. Но бывают ситуации, когда дженерик логично ограничить снизу (lower bound), в частности другим дженериком. Пример. Представьте, что существует ссылка на какое-то значение и нужно заапкастить (преобразовать в супертип) тип значения в этой ссылке:

use Psr\Log\LoggerInterface as Logger;
use Monolog\Logger as Monolog;
use Typhoon\Type;

/**
 * @template T
 */
interface Ref
{
    /**
     * @template S, где S — супертип для T
     * @param Type<S> $super
     * @return self<S>
     */
    public function as(Type $super): self;
}

/**
 * @param Ref<Monolog> $ref
 * @return Ref<Logger>
 */
function upcastLogger(Ref $ref): Ref
{
    return $ref->as(Type\objectT(Logger::class));
}
В Java wildcards это выражается так: ? super T. В Scala оператором: [S >: T]. В PHPStan, о чудо, тоже начали об этом думать аж в 2021 году: phpstan/phpstan/issues/5179. Я был в курсе, и, не глядя в issue, решил попробовать. Пишу:

/**
 * @template S super T
 * @param Type<S> $super
 * @return self<S>
 */
public function as(Type $super): self;
Анализатор говорит No issues!, я радостно продолжаю работать. А потом намеренно пробую вызвать $ref->as(Type\stringT) и всё равно вижу No issues! Разбираюсь и понимаю, что PHPStan поддержал пару лет назад синтаксис super, но только в PHPDoc-парсере! Исходный тикет в анализаторе всё ещё не закрыт! Представьте, что в PHP добавили property hooks только на уровне синтаксиса — по факту они вообще не работают. Как вам такая фича? Между PHP-анализатором и PHPDoc-парсером есть фундаментальный каплинг (то есть, кохижен). Нельзя поправить одно и не учесть это в другом. Нормально разделять компоненты и делать один более универсальным, но тогда должны быть фича-флаги, при помощи которых второй компонент выключает (opt-out) или не включает (opt-in) неподдерживаемое поведение в первом. ⸻ Этот пост я разместил неделю назад в 🐘 PHPeople.

Пых
8 109
base64url Хотел предложить base64url_encode(), base64url_decode() в Core — давно стандартизованная кодировка, используется в JWT, OAuth 2.0 и WebAuthn. Но потом нашёл в почте прошлогодний тред с куда более фундаментальным RFC:

namespace Encoding;

enum Base64
{
    case Standard;
    case UrlSafe;
    case Imap;
}

enum PaddingMode
{
    case VariantControlled;
    case StripPadding;
    case PreservePadding;
}

enum DecodingMode
{
    case Forgiving;
    case Strict;
}

enum TimingMode
{
    case Variable;
    case Constant;
}

/**
 * @throws UnableToEncodeException
 */
function base64_encode(
    string $data,
    Base64 $variant = Base64::Standard,
    PaddingMode $paddingMode = PaddingMode::VariantControlled,
    TimingMode $timingMode = TimingMode::Variable,
): string;

/**
 * @throws UnableToDecodeException
 */
function base64_decode(
    string $data,
    Base64 $variant = Base64::Standard,
    DecodingMode $decodingMode = DecodingMode::Strict,
    TimingMode $timingMode = TimingMode::Variable,
): string;
Ну и аналогичные функции для base16, base32, base58 (Bitcoin) и base85 (PDF). https://wiki.php.net/rfc/data_encoding_api Спросил автора, не планирует ли он возобновить работу над RFC. Вот его ответ:
Hi Valentin, We are still working on it with Tim Düsterhus doing the full implementation. Best regards
Так что ждём! ⸻ Этот пост я разместил неделю назад в 🐘 PHPeople.

Пых
8 109
Используешь Dev Containers для локальной разработки?
Anonymous voting

Пых
8 109
Видеосообщение00:57

Пых
8 109
Хроники пакета exceptionally Вы наверняка знаете проект thecodingmachine/safe.
A set of core PHP functions rewritten to throw exceptions instead of returning false when an error is encountered.

// вместо 
$contents = @file_get_contents($file);

if ($contents === false) {
    throw new RuntimeException("File {$file} is not readable");
}

// пишем
$contents = Safe\file_get_contents($file);
Такой подход мне всегда казался максимально бредовым: скодогенерировать обёртки над всеми стандартными PHP-функциями и написать плагины под каждый статанализатор вместо того, чтобы воспользоваться композицией функций. Поэтому ночью 3 августа 2019 года я написал библиотечку vudaltsov/exceptionally, которая решала проблему куда более элегантно:

$contents = exceptionallyCall('file_get_contents', $file);
Тогда ещё не было стрелочных функций и first class callable синтаксиса, поэтому краткая запись могла выглядеть только так. Забавно, что утром в тот же день в паре кварталов от моей квартиры загорелся склад, подробнее в личном канале. Через 2 дня я предложил RFC в Symfony, а ещё через неделю Yonel Ceruto добавил статический метод ErrorHandler::call(), он там есть и по сей день. Но настали времена, когда я больше не пишу на Symfony, а функция всё ещё актуальна. Поэтому я решил перенести vudaltsov/exceptionally в thesis/exceptionally, упростить и приправить дженериками:

/**
 * @template T
 * @param callable(): (T|false) $function
 * @return T
 * @throws \ErrorException
 */
function exceptionally(
    callable $function,
    int $errorLevels = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED,
): mixed {}

// PHP < 8.6
$contents = exceptionally(static fn () => file_get_contents($file));

// PHP >= 8.6 с partial function application
$contents = exceptionally(file_get_contents($file, ...));
Исключение ErrorException, кстати, существует в ядре ровно для этой задачи, поэтому помимо стандартных параметров оно принимает файл и номер строки. Пользуйтесь Thesis Exceptionally на здоровье!

composer require thesis/exceptionally
⸻ Этот пост я разместил неделю назад в 🐘 PHPeople. Кстати, общая группа там теперь бесплатная — подписывайся!

Пых
8 109
Repost from Пых.конф
Прошло ровно полгода с Пых.конф’25 — что это значит? Это значит, что записи докладов теперь доступны каждому! Смотреть 28 кру
Прошло ровно полгода с Пых.конф’25 — что это значит? Это значит, что записи докладов теперь доступны каждому! Смотреть 28 крутых докладов на 📹 YouTube, 📹 VK Видео.

Пых
8 109
Пересоздал, залетайте! 📹 YouTube 📹 VK Видео

Пых
8 109
Сейчас пересоздам. Минуты 2 подождите.

Пых
8 109
Пишем "полифил" для clone() из PHP 8.5 Open Source Цех #6 Залетайте! На стриме: • внимательно изучим RFC: Clone with v2, • склонируем kenny1911/php-clone-with, • подтянем агентом Codex phpt тесты из php-src, • доведём либу до состояния полифила (в кавычках, потому что назвать функцию clone не получится). 📹 YouTube 📹 VK Видео

Пых
8 109
Опускаем возвращаемые типы В некоторых функциональных языках (Haskell, OCaml) можно явно не декларировать типы. Они реализуют вывод типов на базе алгоритма Хиндли — Милнера. Простой пример:

fun a b -> a + b
Проанализировав эту функцию, мы можем заключить, что в неё можно передавать только пары типов, для которых определён оператор +, например, числа. Возвращаемый тип для конкретной пары уже тривиально выводится. В таких языках типы обычно указывают только в верхнеуровневых функциях, чтобы избежать неоднозначности и зафиксировать API. Однако, Хиндли — Милнер очень плохо дружит с подтипизацией и юнионами, поэтому в условных Scala и TypeScript нельзя опускать типы параметров (интересный тикет для TS, кстати). А вот возвращаемые типы почти всегда можно:

def twss(message: String) = message + " That's what she said!"

twss("The box is too big to fit in the hole") // String

function twss(message: string)
{
  return message + " That's what she said!"
}

twss("The box is too big to fit in the hole") // string
Это интуитивно понятно: когда мы знаем типы аргументов и умеем выводить типы выражений и инструкций, мы элементарно можем вывести возвращаемый тип, проанализировав все ветви выполнения. Теперь к PHP. Наши статанализаторы, грубо говоря, работают в двух режимах: • рефлектор — читает сигнатуру, но не смотрит внутрь функции — полезен для стабов и вендорного кода, • чекер — читает сигнатуру и проверяет "кишки" — используется для кода проекта. Тут важно, что в режиме чекера анализатор не запоминает выведенный из тела функции возвращаемый тип. В общем случае это ок, потому что публичные контракты должны быть задекларированы явно, но для приватных методов было бы здорово запоминать выведенный тип:

final readonly class App
{
    // Валентин: PHPStan, выведи и запомни тут тип по-братски!
    private function twss(string $message)
    {
        return $message . " That's what she said!";
    }

    public function greet(string $message): string
    {
        // PHPStan: Иди на х*й, Валентин!
        // Method App::greet() should return string but returns mixed.
        return $this->twss($message);
    }
}
Тут мы получили ошибку статанализа, хотя этот код ни при каких условиях не сломается. Очень жаль! Можно было бы в приватных методах писать меньше кода... Но! Выведенный возвращаемый тип учитывается в анонимках:

PHPStan\dumpType(
    array_map(
        static fn (int $int) => new BcMath\Number($int),
        [1, 2],
    ),
);
// array{BcMath\Number, BcMath\Number}
И никаких других ошибок! Оно и понятно: анонимные функции — часть процедурного кода, а не сигнатур, поэтому они обязательно анализируются целиком. Ну и кроме того, PHPStan не поддерживает PHPDoc для анонимок (эта фича была оценена Ондрюхой в 1500-2000 EUR, которые ему, видимо, так никто и не дал), поэтому выведенный тип имеет выше приоритет, чем нативный. Это я всё к чему: не указывайте возвращаемые типы в анонимках — это простой и типобезопасный способ сокращать визуальный долг и хоть немного чувствовать себя функциональным программистом.

Пых
8 109
Оказалось, что на сегодняшний вечер уже был запланирован стрим Кирилла Несмеянова в PHPeople — по слухам, он будет линчевать компоненты Thesis! Тогда встречаемся в Цеху завтра, в пятницу, в 19:00. А пока ловите мой пост про типы, тоже, кстати, из 🐘 PHPeople:

Пых
8 109
Open Source Цех #6 К сожалению, сегодня не получится постримить. 😭 Но я жду всех завтра в 19:00 в «Цехе»! Доработаем библиотеку подписчика kenny1911/php-clone-with, чтобы она работала как clone() в PHP 8.5. 📹 YouTube 📹 VK Видео #open_source_цех

Пых
8 109
Тем временем в php-src подъехал PR с реифицированными дженериками от аккаунта, зарегистрированного сегодня... https://github.com/php/php-src/pull/21317 https://www.reddit.com/r/PHP/comments/1rharkk/someone_just_created_pr_with_fully_working/

Пых
8 109
Типабезопасный код — когда разработчик подавляет замечания статанализатора. #пыхослов
Типабезопасный код — когда разработчик подавляет замечания статанализатора. #пыхослов

Пых
8 109
PHP-линч #32 Через 10 минут начинаем, пишите, что хотите сегодня посмотреть! 📹 YouTube 📹 VK Видео #php_линч

Пых
8 109
Wikipedia, ВКонтакте, WordPress и даже PornHub — всё это когда-то выросло на PHP. Язык программирования, который все давно «похоронили», продолжает спокойно держать половину интернета. В новом выпуске подкаста «Программный комитет» вместе с разработчиком и автором телеграм-канала «Пых» Валентином Удальцовым разбираемся, почему PHP до сих пор живее всех живых, кто пишет на нём современные продукты и зачем компании продолжают вкладываться в его развитие. ▶️ Смотреть | ▶️ Слушать
😮 Мы снимали этот подкаст на международной IT-конференции «Стачка»! В этом году она пройдёт 10-11 апреля в Ульяновске и 3-4 октября в Петербурге.