Пых
8.27K subscribers
262 photos
14 videos
6 files
569 links
Блог Валентина Удальцова о разработке на PHP.

Хобот @phpyhobot
https://youtube.com/@phpyh
https://vkvideo.ru/@phpyh
https://t.me/isPHPdying

Статистика: https://t.me/INOTAROBOT?start=st1219340804

Для связи используйте личные сообщения канала.
Download Telegram
💙 Typhoon 0.4.0

https://github.com/typhoon-php/typhoon/releases/tag/0.4.0

Вчера ночью, ровно через 5 месяцев после 0.3.0, поставил следующий минорный тег. По сути, он, конечно, мажорный, так как 0.y релизы могут ломать обратную совместимость, чем мы не преминули многократно воспользоваться. 😈 С этой версии будем вести Changelog, так как капитальных переписываний в ближайшее время не ожидается.

Это был долгий, но полный важных осознаний путь. В цикле постов расскажу про каждое.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥29🎉10👍2
Пых
💙 Typhoon 0.4.0 https://github.com/typhoon-php/typhoon/releases/tag/0.4.0 Вчера ночью, ровно через 5 месяцев после 0.3.0, поставил следующий минорный тег. По сути, он, конечно, мажорный, так как 0.y релизы могут ломать обратную совместимость, чем мы не преминули…
💙 Typhoon 0.4. Тип not

Как вы наверняка знаете, в Psalm и PHPStan есть типы non-empty-string, non-empty-list<TValue> и non-empty-array<TKey, TValue>. В далёкой 0.2 версии тайфуна они обрабатывались индивидуально — у каждого был свой метод в TypeVisitor. Затем в 0.3 мы попытались сократить избыточность, внедрив конструктор non-empty<T>, но не покидало ощущение, что это костыль. И, наконец, месяц назад пришло осознание, что нужен конструктор не для непустоты, а для отрицания.

В самом деле, выведем статически типы внутри вот такой функции:


/**
* @template T
* @param T $bar
*/
function foo(mixed $bar): void
{
if (is_string($bar)) {
// тут у $bar тип T & string

return;
}

// а тут у $bar тип T & !string

trim($bar); // ошибка: ожидается string
}


Очевидно, что для статического анализатора отрицание может быть очень полезно. В TypeScript был такой PR, но почему-то заглох. Psalm и PHPStan частично поддерживают ! в аннотациях @assert:


/**
* @psalm-assert !null $value
* @phpstan-assert !null $value
*/
function assertNotNull($value): void
{
if ($value === null) {
throw new InvalidArgumentException();
}
}


Ну а в typhoon/type 0.4 not теперь first-class тип. Семейство non-empty-*, а заодно и non-falsy-string выражаются так:


non-empty-string = string & !''
non-falsy-string = truthy-string = non-empty-string & !'0'
non-empty-list<TValue> = list<TValue> & !array{}
non-empty-array<TKey, TValue> = array<TKey, TValue> & !array{}

Здесь array{} — это запечатанный array-shape без элементов, то есть []. Его ещё часто пишут как array<never, never>.


Чтобы обойти types::nonEmptyString через TypeVisitor, достаточно смаршрутизировать его как пересечение с соответствующими аргументами:


enum types implements Type
{
// ...
case nonEmptyString;

public function accept(TypeVisitor $visitor): mixed
{
return match ($this) {
// ...
self::nonEmptyString => $visitor->intersection($this, [
self::string,
self::not(self::string('')),
]),
}
}
}
Please open Telegram to view this post
VIEW IN TELEGRAM
👍22🔥161👎1🤯1
Пых
💙 Typhoon 0.4.0 https://github.com/typhoon-php/typhoon/releases/tag/0.4.0 Вчера ночью, ровно через 5 месяцев после 0.3.0, поставил следующий минорный тег. По сути, он, конечно, мажорный, так как 0.y релизы могут ломать обратную совместимость, чем мы не преминули…
💙 Typhoon 0.4. self, parent, static

В документации PHP self, parent и static называются относительными типами классов (relative class types). С этой троицей мы боремся с самого начала. Каждый релиз — новый раунд.

Раунд 1

Возьмём пару классов:


class A extends stdClass
{
public function self(): self { return $this; }
public function parent(): parent { return $this; }
public function static(): static { return $this; }
}

final class B extends A {}


С self и parent здесь всё просто и однозначно: в классе A self = A, parent = stdClass, в класс B копируем эти 2 метода с уже разрешёнными типами A и stdClass. Значит, self и parent можно не оформлять как самостоятельные типы, а сразу заменять на конкретные классы из скоупа.

Можем ли мы так же разрешить static? Нет, так как это имя не текущего, а вызываемого класса (см. позднее статическое связывание): в A static резолвится как A, а в B — как B. При построении рефлексии мы должны в каждом унаследованном методе обновить static с учётом текущего скоупа. Для этого замоделируем static как first-class тип, который знает, где он сейчас находится, и получим typhoon/type 0.2:


A,B::self() возвращают types::object(A::class)
A,B::parent() возвращают types::object(stdClass::class)
A::static() возвращает types::static(A::class)
B::static() возвращает types::static(B::class)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🔥2😐1
Пых
💙 Typhoon 0.4. self, parent, static В документации PHP self, parent и static называются относительными типами классов (relative class types). С этой троицей мы боремся с самого начала. Каждый релиз — новый раунд. Раунд 1 Возьмём пару классов: class A…
💙 Typhoon 0.4. self, parent, static.

В Typhoon 0.3 я начал добавлять поддержку трейтов. И всё, естественно, поломалось. 🙈

Раунд 2


trait T
{
public function self(): self { return $this; }
public function parent(): parent { return $this; }
public function static(): static { return $this; }
}


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

Нативная рефлексия, кстати, вообще не парится: всегда возвращает ReflectionNamedType с 'self', 'parent' или 'static'. Мы так делать не хотим, потому что это просто откладывание проблемы на потом.

Постепенно приходит понимание, что в трейтах относительные типы работают как плейсхолдеры. В Psalm и PHPStan их даже можно ограничивать через аннотации @require-extends и @require-implements. Всё это напоминает... дженерики!

Действительно,


/**
* @psalm-require-extends stdClass
* @phpstan-require-extends stdClass
*/
trait T
{
public function self(): self { return $this; }
public function parent(): parent { return $this; }
public function static(): static { return $this; }
}

final class A extends stdClass
{
use T;
}


можно с натяжкой переписать как


/**
* @template self of object
* @template parent of stdClass
* @template static of self
*/
trait T
{
public function self(): self { return $this; }
public function parent(): parent { return $this; }
public function static(): static { return $this; }
}

final class A extends stdClass
{
/**
* @use T<self, parent, self>
*/
use T;
}


Выглядит логично, но кривовато, потому что в реальности это не объявленные пользователем дженерики, а какие-то встроенные, да ещё и доступные в статике. Тем не менее, на тот момент идея мне очень нравилась, и, подставив пару костылей, я её реализовал в typhoon 0.3 и даже гордо рассказал про это на Стачке:


types::template('self', types::atClass(T::class))
types::template('parent', types::atClass(T::class))
types::template('static', types::atClass(T::class))
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥6😐1
Пых
💙 Typhoon 0.4. self, parent, static. В Typhoon 0.3 я начал добавлять поддержку трейтов. И всё, естественно, поломалось. 🙈 Раунд 2 trait T { public function self(): self { return $this; } public function parent(): parent { return $this; } public…
💙 Typhoon 0.4. self, parent, static

Есть у меня одна черта: я периодически возвращаюсь ко всем своим костылям, пока не сделаю нормально или не уволюсь. О том, что для относительных типов не стоит использовать дженерики, кричали разные куски кода, но всегда казалось, что будет слишком жирно делать их first-class. А потом я задумался над тем, как работают с этими типами анонимные функции.

Раунд 3


final class A extends ArrayObject
{
public function self() { return fn (): self => new self(); }
public function parent() { return fn (): parent => new parent(); }
public function static() { return fn (): static => new static(); }
}

final class B extends stdClass {}

$self = (new A())->self();
echo $self()::class; // A
echo $self->call(new B)::class; // B

$parent = (new A())->parent();
echo $parent()::class; // ArrayObject
echo $parent->call(new B)::class; // stdClass

$static = (new A())->static();
echo $static()::class; // A
echo $static->call(new B)::class; // B


Получается, что self, parent и static отлично себя чувствуют в анонимных функциях и, как и в трейтах, резолвятся в зависимости от скоупа, в том числе поддерживают его изменение в рантайме. Посмотрев на этот сниппет, мы наконец-то согласились, что относительные типы класса везде в PHP ведут себя логично и единообразно и должны быть замоделированы как first-class типы.

Теперь в typhoon/type 0.4 можно создавать self, parent и static с классом скоупа и без него. Последнее как раз требуется в трейтах и непривязанных анонимных функциях. В TypeVisitor каждый относительный тип обрабатывается индивидуально. Ну и, конечно, поддерживаются аргументы типов (а-ля self<X, Y>). Комбо-пример:


trait T
{
/**
* @return array{self, parent, static}
*/
public function types(): array
{
return [$this, $this, $this];
}
}

abstract class A extends stdClass
{
use T;
}

final class B extends A {}

$type = TyphoonReflector::build()
->reflectClass(B::class)
->methods()['types']
->returnType();

echo stringify($type);

// array{self@A, parent@stdClass, static@B}
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥3🤔2
👨‍🏫 Хардкорный курс PHP, 4-й поток

Традиционно после 0.X релиза Typhoon я запускаю X-й поток курса! Сегодня X = 4.

Набор на четвёртый поток будет осуществляться как обычно. Завтра (в среду, 7 августа) в 15:00 по Москве на канале Пых появится ссылка на срез знаний. Необходимо его пройти, чтобы попасть на курс. При проверке я сначала отберу анкеты с правильными ответами, а затем отранжирую их по времени. Удачи!

Обновлённая страничка курса: hardcorephp.ru
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥36👍165🥱3😢1
Пых
👨‍🏫 Срез знаний для 4-го потока Хардкорного курса PHP! https://forms.gle/PFvHT7vujHEPuSs29 Удачи! 😉️️️️️️
🤨 Задачка со звёздочкой

Теперь, когда первая сотня абитуриентов прислала анкеты, предлагаю всем подумать над внеконкурсным заданием. Решите 5-ю задачу с теми же вводными в обратную сторону. То есть у вас должен получиться класс, который, наоборот, позволит использовать Psr\Middleware как Symfony\Subscriber. Идеи можно обсуждать в комментариях к этому посту. Позже всё разберём на стриме.

Gist с полной формулировкой этого варианта задачи: https://gist.github.com/vudaltsov/dc4f372692d2eabbc8c3d29cd4de0ccd

Навеяно, кстати, весьма драматическими событиями. В 2018-м году сообщество Symfony активно обсуждало невыполнение PHP-FIG своей "framework interoperability" миссии, так как компоненты HttpFoundation и HttpKernel нельзя малой кровью адаптировать под PSR-7. Апогеем стал pull-request Фабьена "Remove Symfony" в PHP-FIG. Тогда же у Symfony появился никому не нужный альтернативный набор контрактов.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍82🔥2
Пых
🤨 Задачка со звёздочкой Теперь, когда первая сотня абитуриентов прислала анкеты, предлагаю всем подумать над внеконкурсным заданием. Решите 5-ю задачу с теми же вводными в обратную сторону. То есть у вас должен получиться класс, который, наоборот, позволит…
🎉 Под занавес дня задача поддалась!

Сначала Вадим верно подметил, что суть решения сводится к приостановке потока управления внутри Middleware и предложил использовать для этого генераторы. Затем Александр скинул прототип на базе файберов.

Ну а вот полное решение с использованием Fiber и WeakMap, которое я зафиксировал вскоре после публикации задачи: код, 3v4l. Обсудим подробнее на разборе среза знаний!

P.S.: Всем спокойной ночи!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥22👍103🤮1
Benchmarking Laravel with Swoole, FrankenPHP, RoadRunner, php-fpm, and ngx-php

https://youtu.be/ZB129Tjkas8
https://github.com/pronskiy/ngx-php_laravel

Позавчера Рома Пронский выложил новый ролик, в котором он забенчмаркал запуск Laravel через основные популярные рантаймы (php-fpm, RoadRunner, Swoole, FrankenPHP) + малоизвестный проект ngx-php, который по результатам Web Framework Benchmarks оставляет позади даже Swoole и workerman.
👍21🔥167
Пых
👨‍🏫 Срез знаний для 4-го потока Хардкорного курса PHP! https://forms.gle/PFvHT7vujHEPuSs29 Удачи! 😉️️️️️️
😂 Срез знаний Кирилла Несмеянова!

Назови несколько принятых в PHP 8.4 изменений.
Изменено значение у констант PHP_VERSION и PHP_MINOR_VERSION

Почему некоторые расширения, например, ext-pcntl, в composer.json принято прописывать с констрейнтом "*"?
Звёздочка обычно означает примечание, сноску на полях. Возможно, для таких расширений автор прописывает потом примечания автора. Ну, типа, "добавлю-ка я pcntl, сорян виндузятники, но вы идёте лесом".

По какому принципу выбраны значения констант ReflectionProperty::IS_*?
Ребята решили ради прикола написать 1 << 1, 1 << 2, 1 << 3, и т.д., чтобы никто не понял что это значит.

Можно ли расширить возвращаемый тип метода в дочернем классе и почему?
Конечно можно! Ни одной статьи в Конституции РФ или Кодексах за нарушение подобного не предусмотрено. Да и это не такое порицаемое обществом занятие, чтобы ещё за него кто-то осуждал. Всё в рамках приличия.

Как соотносятся понятия "полиморфизм" и "наследование"?
Они оба на написаны кириллицей и в кавычках (только "наследование" хуже, т.к. в нём 12 букв, а "полиморфизм" в этом плане меньше весит, т.к. всего 11 букв).

Интересный факт: Если написать эти слова наоборот, то получится "мзифромилоп" и "еинаводелсан", но их размер не изменится. А ещё из букв в слове "полиморфизм" можно составить слово "зоофил", а из слова "наследование" можно составить "лениновед".

Теперь ты тоже будешь знать это!

Реши задачу.

final readonly class SymfonyIntegrator
{
public function integrate(): void
{
$previous = \getcwd();

try {
\chdir(__DIR__);

\copy('https://getcomposer.org/installer', __DIR__ . '/composer-setup.php');

require __DIR__ . '/composer-setup.php';

new \Symfony\Component\Process\Process([
\PHP_BINARY,
__DIR__ . '/composer.phar',
'create-project',
'symfony/skeleton',
])->run();

// Больше нам PSR фреймворк не нужен
} finally {
$previous && \chdir($previous);
}
}
}


Почему попросить ИИ-помощника написать тесты может быть не очень хорошей идеей?
Нельзя показывать работодателю, что всю твою работу может заменить скрипт на питоне.

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

Что ждёшь от курса?
Ну хотя бы доллар по 25, как в началах 2000х. Был бы норм курс.
Please open Telegram to view this post
VIEW IN TELEGRAM
😁151👍22🤡12👏42🥴2🦄1
👨‍🏫 Набор на 4-й поток Хардкорного курса завершён!

Все студенты получили письма с приглашениями на почту.

Немного статистики и наблюдений:
108 ответов на момент написания этого поста.
▸ 20 человек набралось на 64-й анкете (в прошлый раз на 41-й), она была отправлена через 54 минуты после старта.
▸ Как будто бы в этот раз вы чаще прибегали к помощи чат-ботов. Некоторые ответы были слишком вышколенными и высокопарными. Программист, который на скорость проходит опрос, так никогда не напишет. В решении 5-й задачи некоторые даже оставили типовые комментарии. В общем, когда я видел явные признаки использования ИИ, я переходил к следующему ответу. И наоборот, к тем, кто по-человечески ошибался в тексте или коде, я был более внимателен.

Сегодня (9 августа) в 19:00 разберём срез и задачу со звёздочкой, приглашаю всех на стрим!

https://youtu.be/wGegvTFidaA

P.S.: Форму не буду закрывать в демонстрационных целях. Заполняйте сколько хотите или используйте на собесах.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18🔥81👎1🤡1
☠️ eval

Думаю, никому не нужно представлять языковую конструкцию eval. В 2024 все пыхари знают её как древнее могущественное зло, которое ни при каких условиях не должно оказаться на проде. Если в проекте есть хоть один eval, он автоматически считается 💩.

Такое отношение к eval непоследовательно. Если вы используете Symfony Dependency Injection, Doctrine ORM, Ocramius Proxy Manager или любой другой пакет с кодогенерацией, вы фактически используете eval, только завуалированный. С точки зрения безопасности нет никакой разницы между тем, чтобы выполнить код из строки, и тем, чтобы сначала записать строку в файл, а затем его выполнить.

Что на самом деле важно

1. При использовании eval и кодогенерации нужно 1000 раз убедиться, что входные данные либо вообще не могут попасть в код, либо строго санитизируются и правильно интерполируются с использованием var_export() или Typhoon Exporter.

2. Код, исполняемый через eval, require и include получает доступ к текущей области видимости и переменным. Для изоляции можно обернуть вызов в статическое замыкание, см. пример.

На десерт. В коде Symfony 7 есть eval. И не один... А как минимум 3... Уверен, что почти в каждом фреймворке есть. Поищите и скиньте в комментарии.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍54🔥10🤔72🌚2🙏1👀1
▶️ PHP-линч #22

Пора возобновить наши встречи! Жду всех в эту среду в 18:30 по Москве.

Одна заявка на линч у меня уже есть от Сергея Предводителева: vjik/telegram-bot-api. Пишите в комментариях, что ещё хотите посмотреть.

https://youtu.be/zuGtL5u2lsY
Please open Telegram to view this post
VIEW IN TELEGRAM
5🔥45👍7
👍7
Podlodka PHP Crew «Практическая архитектура»

На следующей неделе стартует пятая Podlodka PHP. Программа:

• Упорядочиваем хаос: архитектурная документация по модели C4 / Кирилл Сурогатов
• Как стать x2 разработчиком, используя явное состояние / Кирилл Мокевнин
• Нам уже нужен CQRS? А теперь? / 💙 Валентин Удальцов
• PHP + Prometheus: Работа с кастомными метриками / Алексей Солодкий
• RabbitMQ vs Kafka: обрабатываем большие объёмы / Михаил Каморин
• Построение качественных банковских интеграций на PHP / Дмитрий Кириллов
• PHP будет долго жить: переезжаем на Roadrunner / Владимир Плахотников
• Нет ничего более постоянного, чем Temporal / 🔥 Павел Бучнев, Алексей Гагарин
• Перевозим приложение на PHP в Kubernetes / Вадим Дворовенко

Сегодня последний шанс купить билет по сниженной цене. По промокоду phpyh5 можно получить дополнительную скидку 500 руб.

https://podlodka.io/phpcrew
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍21🔥111🥴1
Контекст в PSR-3 Logger

Периодически сталкиваюсь с неправильным использованием $context в PSR логгере. Давайте вместе внимательно перечитаем стандарт.

1.3.1 Every method accepts an array as context data. This is meant to hold any extraneous information that does not fit well in a string. The array can contain anything. Implementors MUST ensure they treat context data with as much lenience as possible. A given value in the context MUST NOT throw an exception nor raise any php error, warning or notice.


Жирным выделил то, что многие упускают: вы можете передавать в контекст абсолютно любые значения, в том числе массивы, объекты, замыкания и ресурсы. Не нужно самостоятельно проводить нормализацию. По стандарту это задача логгера, а не ваша. Вот как, например, Monolog обработает контекст со сложными элементами:


$logger = new Monolog\Logger(
name: 'app',
handlers: [new Monolog\Handler\StreamHandler(STDOUT)],
processors: [new Monolog\Processor\PsrLogMessageProcessor()]
);

final readonly class Foo
{
public function __construct(
public string $public = 'bar',
private string $private = 'baz',
) {}
}

$logger->debug('Message', [
'birthday' => new DateTimeImmutable('06.04.2019'),
'object' => new Foo(),
'closure' => static fn (): int => 1,
'resource' => STDIN,
]);


[2024-09-27T13:02:16.303651+00:00] app.DEBUG: Message {"birthday":"2019-04-06T00:00:00+00:00","object":{"Foo":{"public":"bar"}},"closure":{"Closure":[]},"resource":"[resource(stream)]"} []


1.3.2 If an Exception object is passed in the context data, it MUST be in the 'exception' key. Logging exceptions is a common pattern and this allows implementors to extract a stack trace from the exception when the log backend supports it. Implementors MUST still verify that the 'exception' key is actually an Exception before using it as such, as it MAY contain anything.


Исключения нужно добавлять в контекст с ключом 'exception'. Никогда не раскладывайте объект исключения на массив вида ['message' => $e->getMessage(), 'line' => $e->getLine(), ...] — так вы не даёте процессорам и форматтерам самим принять решение относительно состава лога исключения и глубины его нормализации.


$logger->critical('Oops!', [
'exception' => new LogicException(
message: 'Level 2',
previous: new RuntimeException('Level 1'),
),
]);


[2024-09-27T13:14:04.880133+00:00] app.CRITICAL: Oops! {"exception":"[object] (LogicException(code: 0): Level 1 at test.php:27)\n[previous exception] [object] (RuntimeException(code: 0): Level 2 at test.php:29)"} []


1.2.2 The message MAY contain placeholders which implementors MAY replace with values from the context array.
Placeholder names MUST correspond to keys in the context array.
Placeholder names MUST be delimited with a single opening brace { and a single closing brace }.
Placeholder names SHOULD be composed only of the characters A-Z, a-z, 0-9, underscore _, and period .. The use of other characters is reserved for future modifications of the placeholders specification.
Users SHOULD NOT pre-escape placeholder values since they can not know in which context the data will be displayed.


Ну и конечно же не забывайте про плейсхолдеры в сообщении. Вместо интерполяции и sprintf куда удобнее писать так:


$logger->notice('Process {pid} started at {time}', [
'pid' => getmypid(),
'time' => new DateTimeImmutable(),
]);


[2024-09-27T13:23:29.306682+00:00] app.NOTICE: Process 45104 started at 2024-09-27T13:23:29.306681+00:00 {"pid":45104,"time":"2024-09-27T13:23:29+00:00"} []
6👍136🔥399
Live PHP meetup 3 октября

В этот четверг в Санкт-Петербурге пройдёт очередной Live PHP. Вот мои ожидания от докладов.

GIGA IDE Cloud - сверхновая IDE во вселенной веб-разработки. Хочу послушать из первых уст про успехи Сбера на поприще IDE. Пора искать замену PhpStorm.
Transaction Script и Transaction Outbox в PHP: Эффективные паттерны для управления транзакциями. В моей картине мира Transaction Script и Transaction Outbox — это как гранатовый сок и гранатовый браслет. Интересно узнать, как Станислав уместил две ортогональные концепции в один доклад.
Страх и ненависть PSR. Недавно с Кириллом Мокевниным (интервью выйдет в октябре) обсуждали, что, несмотря на все проблемы, PHP всё-таки смог в стандарты в отличие от многих других языков. Качественный наброс от Кирилла Несмеянова будет очень кстати. С нетерпением жду!
Как растут проекты. Дима Елисеев, с удовольствием послушаю любые твои мысли, только, пожалуйста, не затягивай. 🙏

Короче, я взял билеты на Сапсан 🚝, и в четверг в 18:30 буду как штык в IT-баре Failover.

https://t.me/live_php_news/27
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥19👍74🐳2
Через два часа выступаю на Podlodka PHP Crew!

Сегодняшний доклад — результат недавнего переосмысления многочисленных статей и выступлений Udi Dahan про CQRS, наложенный на мой опыт примения и преподавания этого архитектурного стиля.

Мне бы хотелось показать вам CQRS с нестандартного ракурса. Посмотрим, что у меня получится. 🤫

Кстати, я всегда был убеждён, что отец CQRS — только Greg Young, но оказалось, что Udi тоже к этому причастен:

... both myself [Udi Dahan] and Greg Young (arguably the first two to talk about it and the two who ultimately collaborated on naming it – and now Google knows we didn’t mean “cars”) ...
Please open Telegram to view this post
VIEW IN TELEGRAM
2🔥33👍11🐳41