<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="https://viktorprogger.name/ru/atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ru-RU">
  <id>https://viktorprogger.name/ru/</id>
  <title>Виктор Бабанов</title>
  <subtitle>Блог Виктора Бабанова о технологиях, техниках и процессах, связанных с веб-разработкой</subtitle>
  <updated>2026-08-29T21:02:56.899Z</updated>
  <generator>@vuepress/plugin-feed</generator>
  <link rel="self" href="https://viktorprogger.name/ru/atom.xml"/>
  <link rel="alternate" href="https://viktorprogger.name/ru/"/>
  <category term="Разработка: техники"/>
  <category term="Dont do this"/>
  <category term="Personal"/>
  <category term="Разработка: процессы"/>
  <entry>
    <title type="text">Мое dev окружение для PHP проектов</title>
    <id>https://viktorprogger.name/ru/posts/dev-environment-docker-for-php-projects-with-roadrunner.html</id>
    <link href="https://viktorprogger.name/ru/posts/dev-environment-docker-for-php-projects-with-roadrunner.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Разработка проектов требует не только навыков программирования, но и умения настроить эффективное рабочее окружение. В этой статье я поделюсь своим опытом создания dev-окружения для PHP-проектов с использованием <strong>Docker</strong>. Я расскажу, как организовать файловую структуру, настроить <code>Dockerfile</code> и <code>docker-compose.yml</code>, а также поделюсь полезными советами по оптимизации и безопасности. Независимо от того, работаете ли вы над pet-проектом или готовитесь к командной разработке, эти практики помогут вам создать надежную и гибкую среду разработки.</p>
<p><img src="/dev-environment.webp" alt="Dev окружение с Docker (generated by ChatGPT)" title="Dev окружение с Docker (generated by ChatGPT)"></p>
<p>В основу этого окружения легли мои любимые и часто используемые инструменты:</p>
<ul>
<li><a href="https://www.docker.com/" target="_blank" rel="noopener noreferrer">Docker</a> для контейнеризации</li>
<li><a href="https://docs.docker.com/compose/" target="_blank" rel="noopener noreferrer">Docker compose</a> для управления контейнерами</li>
<li>PHP последней версии</li>
<li><a href="https://getcomposer.org/" target="_blank" rel="noopener noreferrer">Composer</a> для управления зависимостями PHP</li>
<li><a href="https://xdebug.org/" target="_blank" rel="noopener noreferrer">xDebug</a> для отладки приложения</li>
<li><a href="https://roadrunner.dev/" target="_blank" rel="noopener noreferrer">RoadRunner</a> для запуска приложения в режиме Long-Running</li>
<li>PHP-фреймворк Yii3</li>
<li>БД <a href="https://www.postgresql.org/" target="_blank" rel="noopener noreferrer">PostgreSQL</a>, т.к. она умеет в keep-alive коннекты, <a href="https://stackoverflow.com/questions/23399111/safely-keeping-mysql-connections-alive" target="_blank" rel="noopener noreferrer">в отличие от некоторых</a>.</li>
</ul>
]]></summary>
    <content type="html"><![CDATA[<p>Разработка проектов требует не только навыков программирования, но и умения настроить эффективное рабочее окружение. В этой статье я поделюсь своим опытом создания dev-окружения для PHP-проектов с использованием <strong>Docker</strong>. Я расскажу, как организовать файловую структуру, настроить <code>Dockerfile</code> и <code>docker-compose.yml</code>, а также поделюсь полезными советами по оптимизации и безопасности. Независимо от того, работаете ли вы над pet-проектом или готовитесь к командной разработке, эти практики помогут вам создать надежную и гибкую среду разработки.</p>
<p><img src="/dev-environment.webp" alt="Dev окружение с Docker (generated by ChatGPT)" title="Dev окружение с Docker (generated by ChatGPT)"></p>
<p>В основу этого окружения легли мои любимые и часто используемые инструменты:</p>
<ul>
<li><a href="https://www.docker.com/" target="_blank" rel="noopener noreferrer">Docker</a> для контейнеризации</li>
<li><a href="https://docs.docker.com/compose/" target="_blank" rel="noopener noreferrer">Docker compose</a> для управления контейнерами</li>
<li>PHP последней версии</li>
<li><a href="https://getcomposer.org/" target="_blank" rel="noopener noreferrer">Composer</a> для управления зависимостями PHP</li>
<li><a href="https://xdebug.org/" target="_blank" rel="noopener noreferrer">xDebug</a> для отладки приложения</li>
<li><a href="https://roadrunner.dev/" target="_blank" rel="noopener noreferrer">RoadRunner</a> для запуска приложения в режиме Long-Running</li>
<li>PHP-фреймворк Yii3</li>
<li>БД <a href="https://www.postgresql.org/" target="_blank" rel="noopener noreferrer">PostgreSQL</a>, т.к. она умеет в keep-alive коннекты, <a href="https://stackoverflow.com/questions/23399111/safely-keeping-mysql-connections-alive" target="_blank" rel="noopener noreferrer">в отличие от некоторых</a>.</li>
</ul>
<!-- more -->
<p>К сожалению, я так и не оформил привычное мне окружение для разработки в шаблон, который можно было бы использовать в команде <code>composer create project</code>. Но это и не очень удобно, т.к. от проекта к проекту шаблон изменяется, эволюционирует. В каждый следующий проект я беру версию из предыдущего. Однако, основные принципы уже давно не меняются, и сегодня я покажу вам в первую очередь их.</p>
<h2>Dockerfile и сохранение стейта</h2>
<p>Во-первых, в корне каждого проекта у меня есть папка <code>.docker</code>, куда я скидываю все специфичные для него вещи:</p>
<ul>
<li><code>Dockerfile</code> для образов контейнеров PHP и других</li>
<li>Конфиги, которые зашиваются внутрь образов контейнеров или подключаются томами. Например, основной <code>php.ini</code> добавляется внутрь образа, а включение xDebug подключается томом только в среде локальной разработки</li>
<li>Стейт контейнеров. Я всегда сохраняю на диск стейт БД (в проде иначе и не получится), а в dev-окружении - ещё и кеш композера, чтобы его команды выполнялись быстрее.
Вот пример структуры из последнего моего проекта, которую я коммичу в репозиторий:</li>
</ul>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-"><span class="line"><span>.docker</span></span>
<span class="line"><span>├──data</span></span>
<span class="line"><span>│  ├──composer</span></span>
<span class="line"><span>│     └──.gitignore</span></span>
<span class="line"><span>│  └──postgres</span></span>
<span class="line"><span>│     └──.gitignore</span></span>
<span class="line"><span>└──php</span></span>
<span class="line"><span>   ├──Dockerfile</span></span>
<span class="line"><span>   ├──php.ini</span></span>
<span class="line"><span>   └──xdebug.ini</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>Папка <code>data</code> - для сохранения стейта из контейнеров между их перезапусками. Внутри каждой подпапки лежит файл <code>.gitignore</code> с двумя строками: <code>*</code> для игнора всего в этой папке и <code>!.gitignore</code> для того, чтобы сам этот файл закоммитить.<div class="language-.gitignore line-numbers-mode" data-highlighter="shiki" data-ext=".gitignore" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-.gitignore"><span class="line"><span>*</span></span>
<span class="line"><span>!.gitignore</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div></li>
<li>Папка <code>php</code> содержит <code>Dockerfile</code> и конфиги для контейнера с PHP</li>
<li>Если мне понадобится билдить еще какие-то контейнеры - я создам еще одну папку рядом, и в нее тоже положу <code>Dockerfile</code> и все необходимые конфиги.</li>
</ul>
<p>А вот - стартовый вариант моего Dockerfile:</p>
<div class="language-dockerfile line-numbers-mode" data-highlighter="shiki" data-ext="dockerfile" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-dockerfile"><span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">FROM</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> php:8.3-cli-bullseye</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">RUN</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> apt update \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; apt install -y  \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        libicu-dev \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        linux-headers-generic \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        zip \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        libpq-dev \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; apt install -y $PHPIZE_DEPS \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; docker-php-ext-configure intl \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; docker-php-ext-install -j$(nproc) \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        pdo \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        pdo_pgsql \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        pgsql \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        pcntl \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        sockets \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        intl \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        opcache \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; pecl install xdebug \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; docker-php-ext-enable intl \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; pecl clear-cache \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; apt purge -y $PHPIZE_DEPS \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; apt clean</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">COPY</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> --from=composer:latest /usr/bin/composer /usr/bin/composer</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">ADD</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> .docker/php/php.ini /usr/local/etc/php/conf.d/40-custom.ini</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">WORKDIR</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> /var/www</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">ARG</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> USER_ID=1000</span></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">ARG</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> GROUP_ID=1000</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">RUN</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> groupadd -g $GROUP_ID appuser &#x26;&#x26; \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    useradd -m -N -g $GROUP_ID -u $USER_ID appuser &#x26;&#x26; \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    usermod -L appuser &#x26;&#x26; \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    chown appuser:appuser .</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">USER</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> appuser</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">COPY</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> --chown=appuser:appuser ./composer.* configuration.php ./</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">RUN</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> composer install \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        --prefer-dist \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        --no-ansi \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        --no-dev \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        --no-interaction \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        --no-plugins \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        --no-progress \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        --no-scripts \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; vendor/bin/rr get --no-config -q -n</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">COPY</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> --chown=appuser:appuser ./ ./</span></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">RUN</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> composer du --classmap-authoritative</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">CMD</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> [</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"./rr"</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"serve"</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">]</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>В нем происходит следующее:</p>
<ol>
<li>Ставим все необходимые системные пакеты с помощью <code>apt</code>, <code>docker-php-ext</code> и <code>pecl</code>, включаем расширения PHP.
<ol>
<li>Заметьте: xDebug я устанавливаю, но <strong>не включаю</strong>. Включу и настрою я его отдельным файлом настроек чуть ниже. Поступаю я так для того, чтобы на проде он не работал ни при каких условиях, включая невнимательность.</li>
</ol>
</li>
<li>Устанавливаем <code>composer</code></li>
<li>Добавляем конфиг PHP для прода</li>
<li>Создаём пользователя, под которым будет работать приложение. Это важно, т.к. внутри контейнера будут создаваться и изменяться файлы (пакеты композера, логи, результаты изменения от phpcsfixer и пр.), и нам нужно, чтобы пользователь с хост-системы (мы сами) мог читать и редактировать эти файлы. Значения id пользователя и группы мы принимаем в виде аргументов, чтобы была возможность установить их разными для разных систем.</li>
<li>Отдельно добавляем в контейнер файлы <code>composer.json</code>, <code>composer.lock</code> и <code>configuration.php</code> (конфиг для <code>yiisoft/config</code>), да ставим зависимости с помощью <code>composer install</code>. И сразу скачиваем бинарник RoadRunner. Делаем этот шаг до копирования файлов проекта, т.к. докер закеширует результат и не будет повторять эти действия, пока два исходных файла не поменяются, т.е. пока список зависимостей и их версий остается неизменным.</li>
<li>Добавляем все файлы проекта</li>
<li>Делаем <code>composer du --classmap-authoritative</code>. Этот шаг преследует сразу две цели:
<ol>
<li>Очевидная: после добавления файлов проекта мы <a href="https://getcomposer.org/doc/articles/autoloader-optimization.md#optimization-level-2-a-authoritative-class-maps" target="_blank" rel="noopener noreferrer">оптимизируем скорость работы автолоудера</a>.</li>
<li>Неочевидная: по хуку команды <code>dump-autoload</code> генерируется карта мержей для <a href="https://github.com/yiisoft/config/blob/master/README.md#how-it-works" target="_blank" rel="noopener noreferrer">yiisoft/config</a>.
Если есть вопросы по его содержимому - всегда рад ответить на них <a href="https://t.me/viktorprogger_channel_ru" target="_blank" rel="noopener noreferrer">в своем телеграм-канале</a>.</li>
</ol>
</li>
</ol>
<h2>Конфиг docker compose</h2>
<p>Во-вторых, в репозитории с проектом всегда есть файл <code>docker-compose.yml</code>. Да, мне пора его переименовать в <code>compose.yaml</code> под новый стандарт, но я все забываю 😅 Вот его примерное must have содержимое:</p>
<div class="language-yaml line-numbers-mode" data-highlighter="shiki" data-ext="yaml" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-yaml"><span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">version</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"3.8"</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">services</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">  php</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    depends_on</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      db</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">        condition</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">service_healthy</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    build</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      dockerfile</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">.docker/php/Dockerfile</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      context</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">./</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      args</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">        USER_ID</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${USER_ID:-1000}</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">        GROUP_ID</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${GROUP_ID:-1000}</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    command</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"./rr serve -c .rr.dev.yaml"</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"${USER_ID:-1000}:${GROUP_ID:-1000}"</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    restart</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">unless-stopped</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    ports</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${WEB_PORT:-80}:80</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    volumes</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">./.docker/php/php.ini:/usr/local/etc/php/conf.d/40-custom.ini:ro</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">./.docker/php/xdebug.ini:/usr/local/etc/php/conf.d/99-xdebug.ini:ro</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">./.docker/data/composer:/home/appuser/.composer</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">./:/var/www</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    environment</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      TZ</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">Asia/Almaty</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      PHP_IDE_CONFIG</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${PHP_IDE_CONFIG:-}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      DB_NAME</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_NAME}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      DB_HOST</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_HOST}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      DB_PORT</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_PORT}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      DB_LOGIN</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_LOGIN}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      DB_PASSWORD</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_PASSWORD}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      XDEBUG_MODE</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${XDEBUG_MODE:-off}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      XDEBUG_CONFIG</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"client_host=host.docker.internal"</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      XDEBUG_TRIGGER</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"yes"</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  </span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">  db</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    image</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">postgres:15-alpine</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    restart</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">unless-stopped</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    volumes</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">.docker/data/postgres/db:/var/lib/postgresql/data</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    environment</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      POSTGRES_PASSWORD</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_PASSWORD:-dev}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      POSTGRES_USER</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_USER:-dev}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      POSTGRES_DB</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${DB_NAME:-dev}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    ports</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"${DB_PORT:-5432}:5432"</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    healthcheck</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      test</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: [</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"CMD-SHELL"</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"pg_isready -U ${DB_USER:-dev}"</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">]</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      interval</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">3s</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      timeout</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">3s</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      retries</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">10</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Из этого примера видно, что я активно пользуюсь переменными окружения. Их значения задаются в файле <code>.env</code>, который добавлен в .gitignore. В репозиторий вместо него я коммичу .env.example, который содержит все возможные переменные окружения с пустыми или фейковыми значениями. Благодаря такому подходу приложение зависит от переменных окружения, а не от способов их добавления. Ну а вот - пример моего <code>.env.example</code>:</p>
<div class="language-env line-numbers-mode" data-highlighter="shiki" data-ext="env" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-env"><span class="line"><span>##################################</span></span>
<span class="line"><span># Port forwarding</span></span>
<span class="line"><span>##################################</span></span>
<span class="line"><span>WEB_PORT=80</span></span>
<span class="line"><span></span></span>
<span class="line"><span>##################################</span></span>
<span class="line"><span># xDebug settings</span></span>
<span class="line"><span>##################################</span></span>
<span class="line"><span># Uncomment next line for xDebug usage with PhpStorm</span></span>
<span class="line"><span># PHP_IDE_CONFIG=serverName=docker</span></span>
<span class="line"><span># XDEBUG_MODE=develop,debug</span></span>
<span class="line"><span></span></span>
<span class="line"><span># LINUX ZONE. Next configs are for xDebug in linux environment only.</span></span>
<span class="line"><span># COMPOSE_FILE=docker-compose.yml:docker-compose.linux.yml</span></span>
<span class="line"><span># Uncomment HOST_IP and set host ip in the docker network when using linux, look at docker-compose.linux.yml</span></span>
<span class="line"><span># Although it usually is 172.17.0.1, your real value can be easily found with command `ip address | grep docker`,</span></span>
<span class="line"><span># you'll see it like "inet 172.17.0.1/16 ..."</span></span>
<span class="line"><span># HOST_IP=172.17.0.1</span></span>
<span class="line"><span></span></span>
<span class="line"><span>##################################</span></span>
<span class="line"><span># Database settings</span></span>
<span class="line"><span>##################################</span></span>
<span class="line"><span>DB_HOST=db</span></span>
<span class="line"><span>DB_PORT=5432</span></span>
<span class="line"><span>DB_NAME=dev</span></span>
<span class="line"><span>DB_LOGIN=dev</span></span>
<span class="line"><span>DB_PASSWORD=dev</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Если вы не знаете, зачем нужны те или иные настройки xDebug - рекомендую прочесть мой <a href="/ru/posts/xdebug-docker-config-example.html" target="_blank">пример настройки xDebug в докере</a>.</p>
<h2>Особенности разработки под Linux</h2>
<p>Если вы ведете разработку на Linux и не используете Docker Desktop (а я именно так и делаю), то внутри контейнеров вам недоступен хост <code>host.docker.internal</code>. И чтобы была возможность пользоваться xDebug (ему ведь надо самостоятельно коннектиться к IDE из контейнера) всегда добавляю в <code>docker-compose.yml</code> в сервис с PHP этот блок:</p>
<div class="language-yaml line-numbers-mode" data-highlighter="shiki" data-ext="yaml" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-yaml"><span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">extra_hosts</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">host.docker.internal:${HOST_IP:-172.17.0.1}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>А когда я планирую работать над проектом не один, то вытаскиваю этот блок в соседний файл, который обычно называю <code>docker-compose.linux.yml</code>, в основном же файле остаётся все остальное. И чтобы каждый раз не писать в каждой команде список подключаемых конфигов (например, <code>docker compose -f docker-compose.yml -f docker-compose.linux.yml run --rm php php -v</code>), в <code>.env</code> я также добавляю такую переменную окружения:</p>
<div class="language-env line-numbers-mode" data-highlighter="shiki" data-ext="env" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-env"><span class="line"><span>COMPOSE_FILE=docker-compose.yml:docker-compose.linux.yml</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h2>Запуск</h2>
<p>В первую очередь, конечно, добавляем <code>composer.json</code> и прочие файлы проекта. А непосредственно запуск такого окружения прост до банальности:</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">docker</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> compose</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> up</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66"> -d</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>При первом запуске контейнер сначала сбилдится. Это может занять больше 5 минут в зависимости от мощности машины и качества канала в интернет. А когда он окончательно запустится - все готово к работе и отладке.</p>
<div class="hint-container tip">
<p class="hint-container-title">Совет</p>
<p>Небольшой совет: чтобы быть увереным, что проект заведется на проде, добавляйте/обновляйте php-зависимости тоже через это окружение, а не локально установленным <code>composer</code>'ом:</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">docker</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> composer</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> run</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66"> --rm</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66"> --no-deps</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> php</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> composer</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> require</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> foo/bar</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>Если у какого-то пакета будет зависимость от окружения (например, необходимо расширение PHP <code>pcntl</code>, которое вы себе решили не ставить в <code>Dockerfile</code>), вы сразу увидите, в чем проблема. Также это поможет поставить именно те версии пакетов, которые совместимы с вашим окружением, если такие доступны.</p>
</div>
<h2>Заключение</h2>
<p>Создание эффективного dev-окружения - это не просто настройка инструментов, а эволюционный процесс. Мой подход, который я описал в этой статье, сформировался за годы работы над различными проектами, от пет-проектов до крупных командных разработок.</p>
<p>Ключевая идея, которую я хотел бы подчеркнуть - это баланс между стандартизацией и гибкостью. С одной стороны, мы стремимся к воспроизводимому окружению, которое легко развернуть на любой машине. С другой - нам нужна возможность быстро адаптировать конфигурацию под специфику конкретного проекта.</p>
<p>Использование Docker, тщательно продуманная структура <code>.docker</code> директории, гибкие настройки через переменные окружения - все это позволяет достичь этого баланса. Мы получаем стабильное окружение, которое при этом легко кастомизировать.</p>
<p>Особое внимание я уделил оптимизации производительности (кеширование данных, грамотное построение слоев в <code>Dockerfile</code>, оптимизация автолоудера) и удобству. Эти аспекты часто упускаются из виду, особенно на ранних этапах проекта, но они критически важны для долгосрочного успеха.</p>
<p>Надеюсь, что мой опыт и подход, описанные в этой статье, помогут вам не только быстрее настраивать окружение для ваших проектов, но и переосмыслить сам процесс организации разработки. Помните, что каждый проект уникален, и не бойтесь экспериментировать и адаптировать эти практики под свои нужды.</p>
<p>Сегодня я прошелся по верхам, не углубляясь. Здесь затронуто очень много тем, копать вглубь которых можно очень долго. Пишите мне о том, что из этого вам интересно, ставьте лайки в соцсетях, и на такие темы я опубликую больше контента. А связаться со мной можно <a href="https://t.me/viktorprogger_channel_ru" target="_blank" rel="noopener noreferrer">в телеграм-канале</a>. Давайте вместе делать процесс разработки более эффективным и приятным!</p>
]]></content>
    <category term="Разработка: техники"/>
    <published>2024-09-30T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Как делать не надо #1. Несуществующие поля трейтов</title>
    <id>https://viktorprogger.name/ru/posts/dont-do-this-non-existent-trait-fields.html</id>
    <link href="https://viktorprogger.name/ru/posts/dont-do-this-non-existent-trait-fields.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Этой статьей я открываю рубрику из практики &quot;Как делать не надо&quot;. В ней будет код из реальных проектов, из которого я уберу все отсылки к самим проектам. И, конечно, разъяснение: почему так делать нельзя, и как бы поступить стоило.</p>
<p>Итак, первый пациент. Внутри трейта используются поля его наследников. То есть, что-то такое:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">trait</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Foo</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">string</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        return</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">field</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> .</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> self</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">::</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">CONSTANT</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> .</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">method</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">.</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66"> true</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="hint-container warning">
<p class="hint-container-title">Примечание</p>
<p>Или даже хуже, <code>$this-&gt;{$attribute}</code>. Но это уже совсем ни в какие ворота не лезет, т.к. шанс схлопотать 500ю ошибку поднимается почти до 100%.</p>
</div>
]]></summary>
    <content type="html"><![CDATA[<p>Этой статьей я открываю рубрику из практики &quot;Как делать не надо&quot;. В ней будет код из реальных проектов, из которого я уберу все отсылки к самим проектам. И, конечно, разъяснение: почему так делать нельзя, и как бы поступить стоило.</p>
<p>Итак, первый пациент. Внутри трейта используются поля его наследников. То есть, что-то такое:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">trait</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Foo</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">string</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        return</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">field</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> .</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> self</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">::</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">CONSTANT</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> .</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">method</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">.</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66"> true</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="hint-container warning">
<p class="hint-container-title">Примечание</p>
<p>Или даже хуже, <code>$this-&gt;{$attribute}</code>. Но это уже совсем ни в какие ворота не лезет, т.к. шанс схлопотать 500ю ошибку поднимается почти до 100%.</p>
</div>
<!-- more -->
<h2>Что плохого?</h2>
<p>Проблема кроется в том, что технически класс, использующий этот трейт, не обязан объявлять ни поле, ни константу, ни метод. <em>Их там может не быть</em>. При этом можно даже статический анализатор обмануть, если добавить трейту такой комментарий:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">/**</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic"> * @mixin SomeClass</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic"> */</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Разумеется, только в том случае, если в <code>SomeClass</code> есть и <code>$field</code>, и <code>CONSTANT</code>, и <code>method()</code>.</p>
<p>Проблема возникнет, когда этот трейт добавят еще к одному классу, а разработчик не будет знать или забудет объявить эти три вещи.</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Как правило, в трейтах такого качества и кода присутствует немало, 500-1000 строк и больше. Подобные косяки становится совсем непросто увидеть. А с учетом того, что проект навряд ли покрыт тестами, проблема вполне может доехать до продакшена и вызвать проблемы у конечных пользователей.</p>
</div>
<h2>Как правильно?</h2>
<p>Использовать абстрактные методы:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">trait</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Foo</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">string</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        return</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">getField</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">.</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">getConstant</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">.</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">method</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    abstract</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> protected</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getField</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">string</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    abstract</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> protected</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getConstant</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">string</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    abstract</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> protected</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> method</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">string</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Таким образом у нас появляется техническое ограничение на уровне языка: любой класс, подключающий в себя этот трейт, будет <strong>обязан</strong> объявить также три метода, от которых зависит основной функционал трейта.</p>
<div class="hint-container tip">
<p class="hint-container-title">Совет</p>
<p>Только не забывайте добавлять методам подробные комментарии о том, какие значения он должен возвращать и какие исключения в каком случае выкидывать.</p>
</div>
<h2>Правильный вариант 2</h2>
<p><em>Для адептов идей &quot;Трейты - зло&quot; и &quot;Композиция - сила&quot;</em>:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">interface</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> BarInterface</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getField</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> string</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getConstant</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> string</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> method</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> string</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">readonly</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> final</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Foo</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">BarInterface</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">string</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        return</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">getField</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">.</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">getConstant</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">.</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">method</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Здесь мы заменяем трейт на две вещи:</p>
<ul>
<li>Интерфейс с теми же тремя методами, что в трейте были <code>abstract</code> и <code>protected</code></li>
<li>Класс, в который мы реализацию этого интерфейса передаем, чтобы вызвать эти методы и использовать результаты их выполнения.</li>
</ul>
<div class="hint-container tip">
<p class="hint-container-title">Совет</p>
<p>Результат, вроде бы, такой же, но есть нюанс. Теперь классы, реализующие <code>BarInterface</code>, не связаны общим кодом,
что снижает показатель связанности кода (coupling) и увеличивает связанность (cohesion).
Дальнейшее развитие и рефакторинг такого кода пройдут легче.</p>
</div>
]]></content>
    <category term="Dont do this"/>
    <published>2023-04-23T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Неуловимая 502, или как сеньоры не могли найти ошибку</title>
    <id>https://viktorprogger.name/ru/posts/elusive-502-gateway-timeout.html</id>
    <link href="https://viktorprogger.name/ru/posts/elusive-502-gateway-timeout.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Какие ошибки в программировании страшнее всего? Я бы выделил два типа:</p>
<ul>
<li>те, из-за которых бизнес теряет много денег</li>
<li>и те, которые встречаются реже всего.</li>
</ul>
<p>Почему первые - понятно сразу, а что со вторыми? Дело в том, что чем реже мы встречаем какой-то тип ошибок - тем сложнее понять, чем они вызваны.<br>
Так случилось и у меня на работе. Однажды утром тестировщики заметили, что часть запросов бэкенду возвращала ошибку 502 (Gateway Timeout). Эта ошибка тормозила релиз, и за неё взялись все старшие разработчики и devops-инженер. Поначалу считали, что эту ошибку возвращает Nginx, и бэкенд не при чём. Через некоторое время поняли, что виноват всё-таки PHP. На что только не грешили: отключали BlackFire, меняли настройки OpCache, пробовали разные патч-версии в PHP, и так далее. Однако, сама ошибка оказалась не в инфраструктуре, а непосредственно в коде приложения.</p>
<img src="/http-502-bad-gateway.jpg" style="width: 100%" alt="Ошибка 502 Bad Gateway">
]]></summary>
    <content type="html"><![CDATA[<p>Какие ошибки в программировании страшнее всего? Я бы выделил два типа:</p>
<ul>
<li>те, из-за которых бизнес теряет много денег</li>
<li>и те, которые встречаются реже всего.</li>
</ul>
<p>Почему первые - понятно сразу, а что со вторыми? Дело в том, что чем реже мы встречаем какой-то тип ошибок - тем сложнее понять, чем они вызваны.<br>
Так случилось и у меня на работе. Однажды утром тестировщики заметили, что часть запросов бэкенду возвращала ошибку 502 (Gateway Timeout). Эта ошибка тормозила релиз, и за неё взялись все старшие разработчики и devops-инженер. Поначалу считали, что эту ошибку возвращает Nginx, и бэкенд не при чём. Через некоторое время поняли, что виноват всё-таки PHP. На что только не грешили: отключали BlackFire, меняли настройки OpCache, пробовали разные патч-версии в PHP, и так далее. Однако, сама ошибка оказалась не в инфраструктуре, а непосредственно в коде приложения.</p>
<img src="/http-502-bad-gateway.jpg" style="width: 100%" alt="Ошибка 502 Bad Gateway">
<!-- more -->
<h3>Предисловие</h3>
<p>В качестве вводных данных расскажу, что приложение основано на фреймворке yii2 и изначально писалось не самыми толковыми разработчиками. Например, DI-контейнер не использовался, вместо него были компоненты, как это и было задумано изначально в фреймворке на заре его существования. Когда проект выстрелил, в него пришли специалисты выше уровнем, и к старому коду начали писать более выверенный архитектурно. Из-за одного такого изменения у нас и получилось ошибка, которая на фронтенде выглядела как 502, и по которой ни в каких логах не было записей. Первое, что пришло в голову: такая ситуация с логами возникает из-за переполнения памяти. То есть, когда php-процесс использует больше памяти, чем ему выделено системой. С этой ситуацией мы знакомы и умеем её распознать, почему и проверили её сразу же. Но это была не она.</p>
<h3>Описание проблемы</h3>
<p>Оказалась, что наша проблема была схожего характера: процесс отваливался из-за достижений другого системного ограничения, не памяти. В весьма неочевидном месте мы достигали лимита по максимальной вложенности вызовов функций. Другими словами, мы вошли в рекурсию. Причём рекурсия эта была неявной, то есть функция вызывала не сама себя, а другую функцию, которая в свою очередь вызывала первую функцию, а та снова вторую, а та снова первую…</p>
<p>Пришли мы к этому следующим образом. В конфигурации у нас был компонент (назовём его для ясности <code>foo</code>). Конфигурация этого компонента выглядела вот так:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'foo'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> => [</span></span>
<span class="line"><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">    'class'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> => </span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">FooClass</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">::</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">,</span></span>
<span class="line"><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">    'property1'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> => </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'value1'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">,</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">]</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Один из наших разработчиков добавил определение этого класса в DI-контейнер, чтобы была возможность получать этот класс уже настроенным в конструтор других классов:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">FooClass</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">::</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> => </span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">static</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> fn</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() => </span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">Yii</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">::</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$app</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">get</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'foo'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">),</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>Что мы имеем в результате? Когда код проекта обращался к компоненту, фреймворк пытался создать этот компонент. Видя в определении компонента необходимость получить объект класса, он искал этот класс сначала в контейнере, где находил определение <code>Yii::$app-&gt;get('foo')</code>, и снова пытался получить компонент. И так - по кругу. В качестве хотфикса мы удалили определение для DI-контейнера (оно нигде не использовалось). Правильным же решением было перенести настройку класса из описания компонента в определения DI-контейнера, в настройках компонентов оставив только <code>'foo' =&gt; FooClass::class</code>.</p>
<p>Вот так простейшая ошибка сумела обмануть разработчиков с опытом более 10 лет. Просто мы слишком редко ее встречаем.</p>
]]></content>
    <category term="Personal"/>
    <published>2022-09-14T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">IT-пузырь лопнул?</title>
    <id>https://viktorprogger.name/ru/posts/has-it-bubble-burst.html</id>
    <link href="https://viktorprogger.name/ru/posts/has-it-bubble-burst.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Год назад при смене работы я даже не стал публиковать резюме. Действовал я так из предыдущего опыта: каждый раз, открывая резюме в общий доступ, приходилось очень много общаться с компаниями, с которыми у нас заведомо ничего не могло получиться. Поэтому я спросил у знакомых, какие компании сейчас ищут разработчиков, постучался в парочку и нашёл отличное место. Позже, правда, выяснилось, что мы с этой компанией друг другу не совсем подходим, и я уволился. Уверен, что если бы я открыл резюме, мне бы снова, как и за пару лет до того, пришлось бы откапывать себя из откликов эйчаров. Лично для меня с тех пор ситуация сильно изменилась.</p>
<img src="/bubble.jpg" style="width: 100%" alt="IT-пузырь">
]]></summary>
    <content type="html"><![CDATA[<p>Год назад при смене работы я даже не стал публиковать резюме. Действовал я так из предыдущего опыта: каждый раз, открывая резюме в общий доступ, приходилось очень много общаться с компаниями, с которыми у нас заведомо ничего не могло получиться. Поэтому я спросил у знакомых, какие компании сейчас ищут разработчиков, постучался в парочку и нашёл отличное место. Позже, правда, выяснилось, что мы с этой компанией друг другу не совсем подходим, и я уволился. Уверен, что если бы я открыл резюме, мне бы снова, как и за пару лет до того, пришлось бы откапывать себя из откликов эйчаров. Лично для меня с тех пор ситуация сильно изменилась.</p>
<img src="/bubble.jpg" style="width: 100%" alt="IT-пузырь">
<!-- more -->
<div class="hint-container note">
<p class="hint-container-title">Disclaimer</p>
<p>Статья была написана в конце декабря, когда я еще не знал, какую глупость совершил при публикации резюме. Сделанный вывод можно найти <a href="#post-scriptum-%D0%B8-%D0%B2%D0%BD%D0%B5%D0%B7%D0%B0%D0%BF%D0%BD%D0%B0%D1%8F-%D1%80%D0%B0%D0%B7%D0%B2%D1%8F%D0%B7%D0%BA%D0%B0">в конце статьи</a>. Публикую только сейчас, потому что не находил времени для шлифовки текста перед публикацией.</p>
</div>
<h2>Неожиданно долгие поиски</h2>
<p>Мои зарплатные ожидания остались на том же уровне, но после публикации резюме на HeadHunter мне не написала ни одна компания. Ни одного отклика. Я, скажем цензурно, удивился, и стал писать отклики сам. Заодно опубликовал свое резюме на других job-бордах. Не все компании, кому я писал, отправили хоть какой-то ответ, часть из них отказала молча. С какими-то я некоторое время ещё вел диалог. Прошел несколько собеседований по всем этапам, но мне везде отказали. Я начинал потихоньку паниковать: мои запасы денег истощились еще в момент переезда в Казахстан, позже я добавил себе еще 1 кредит. А клиентов, тем более - постоянных, на фрилансе быстро не найти.</p>
<p>Лишь в одной компании сделали своеобразный подарок на новый год в виде оффера 😃 К ним я и вышел после новогодних праздников в РФ.</p>
<p>Параллельно с этим подтягиваю свой английский, чтобы спокойно общаться с потенциальными работодателями из Европы и Америки. Полагаю, что я и сейчас мог бы попробовать, но очень уж боязно: за несколько лет отсутствия практики в английском мой уровень упал где-то до В1.</p>
<p>Ну а до тех пор, пока не вышел к новому работодателю, я решил сосредоточиться на проектной работе, фрилансе и консультациях других компаний. Всё же, опыта у меня довольно много: я не только писал код, но также участвовал в формировании и изменении процессов it-отделов компаний, в которых работал. Много наблюдал за тем, насколько хорошо или плохо работают те или иные процессы в более продвинутых it-компаниях.</p>
<div class="hint-container info">
<p class="hint-container-title">Вопрос</p>
<p>И тут возникает вопрос к зрительному залу: сложилось ли у вас впечатление, что IT-пузырь лопнул и работу найти стало сложнее?</p>
</div>
<h2>Post Scriptum и внезапная развязка</h2>
<p>Когда я уже нашел работу, hh наконец показал мне уведомление о том, что <em>мое резюме видят только компании в Казахстане</em>: ведь я в нем честно указал Алматы. Зарплату при этом я указал в рублях. При курсе рубля, выросшем в 1.5 раза с весны 2022 года, такая з/п в пересчете на тенге
была неадекватно высокой, и казахстанские компании я заинтересовать не мог. А российские просто не видели мое резюме. Как на ваш взгляд, проблема у меня была только в этом, и с наймом в IT и PHP все в порядке, или таки сдулся наш пузырь? Да и пузырь ли это вообще? Давайте <a href="https://t.me/viktorprogger_channel_ru" target="_blank" rel="noopener noreferrer">обсудим</a>.</p>
]]></content>
    <category term="Personal"/>
    <published>2023-01-10T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Как мне понадобилось написать Строитель</title>
    <id>https://viktorprogger.name/ru/posts/how-did-i-use-the-builder-design-pattern.html</id>
    <link href="https://viktorprogger.name/ru/posts/how-did-i-use-the-builder-design-pattern.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>В мире разработки существует множество архитектурных паттернов. Часть из них мы используем каждый день, часть - реже. Наверняка каждый из вас много раз видел синглтон и фабрику. Многие их писали. А вот когда я впервые прочёл про паттерн <a href="https://refactoring.guru/ru/design-patterns/builder" target="_blank" rel="noopener noreferrer">Строитель</a>, то не понял поначалу, в какой ситуации его можно применить. Что совсем смешно: я регулярно использовал его реализацию (QueryBuilder из фреймворка Yii), но глаз настолько замылился, что я не смог сопоставить название и функционал этого класса с соответствующим паттерном проектирования 😂 Конечно, через некоторое время до меня дошло. А еще через некоторое - нашлась ситуация, в которую паттерн Строитель вписался идеально.</p>
<img style="margin: auto; display: flex" src="/builder-en.png" alt="Архитектурный паттерн Строитель">
]]></summary>
    <content type="html"><![CDATA[<p>В мире разработки существует множество архитектурных паттернов. Часть из них мы используем каждый день, часть - реже. Наверняка каждый из вас много раз видел синглтон и фабрику. Многие их писали. А вот когда я впервые прочёл про паттерн <a href="https://refactoring.guru/ru/design-patterns/builder" target="_blank" rel="noopener noreferrer">Строитель</a>, то не понял поначалу, в какой ситуации его можно применить. Что совсем смешно: я регулярно использовал его реализацию (QueryBuilder из фреймворка Yii), но глаз настолько замылился, что я не смог сопоставить название и функционал этого класса с соответствующим паттерном проектирования 😂 Конечно, через некоторое время до меня дошло. А еще через некоторое - нашлась ситуация, в которую паттерн Строитель вписался идеально.</p>
<img style="margin: auto; display: flex" src="/builder-en.png" alt="Архитектурный паттерн Строитель">
<!-- more -->
<p>На текущий момент это произошло лишь однажды. Обычно я работал на взрослых проектах, где либо подобные задачи уже были решены, либо это было страшное легаси без ресурсов на рефакторинг. Итак, где же мне понадобился Строитель?</p>
<h3>Задача: оптимизировать товарные фиды</h3>
<p>Проект был интернет-магазином и маркетплейсом сразу. С него выгружался добрый десяток товарных фидов. Это были файлы различных форматов со списком товаров, их категорий и характеристик. А выгружались они на другие маркетплейсы или, что чаще, на различные платформы, предоставляющие услуги рекламы и аналитики. У каждого фида были собственные настройки: какие товары/бренды/категории должны в него входить, какие данные должны быть доступны по товарам и т.п. Они хранились в БД и выставлялись в UI нашими маркетологами.</p>
<p>Генерация каждого фида происходила независимо от остальных. Изначально она занимала до 10 минут на каждый фид и сильно нагружала систему, т.к. очень активно читались данные из БД. И если к определенному времени надо было сгенерировать все фиды, то процедура эта растягивалась на полтора часа, в течение которых реплика БД для чтения работала на пределе.</p>
<h3>Первая итерация оптимизации</h3>
<p>Первым делом я сделал две вещи:
Использовал для поиска товаров и получения части данных по ним SphinxSearch. Начальная версия фидов писалась еще до внедрения Сфинкса, внедрить в нее что-то новое было сложно, поэтому руки до него дошли только теперь.
Сделал генерацию фидов единым процессом. Данные можно было получить один раз, и по ним - построить необходимые фиды. Фид поделился на две вещи: фильтр, который отбирал только нужные товары, и шаблон, который создавал файл из данных. Мы брали товар и пропускали через все фиды. Если фильтр пропускал его - писали информацию об этом товаре в файл. И так - каждый товар.</p>
<p>Такой подход вывел время создания фидов в константу: около 6 минут вне зависимости от их количества. Однако, можно было и лучше: в некоторых фидах использовалось только 20-25% товаров, но при их отдельном запуске все равно прогонялись через фильтр все товары.</p>
<h3>Вторая итерация: Строитель</h3>
<p>Второе, что пришло мне в голову - создать собственный QueryBuilder с верхнеуровневыми “запросами”: <code>withBrands()</code>, <code>withCategories()</code>, <code>withPhotoType()</code> и пр. Когда требовалось сгенерировать несколько фидов - по каждому из них брался инстанс QueryBuilder’а, и они сливались в один, чтобы выбрать из БД только те товары, которые необходимы текущему набору фидов. При этом сохранилась независимость сущностей в модуле генерации фидов от структуры БД, а код непосредственно генерации изменился только в одном месте: получение товаров из репозитория изменилось с <code>getProducts(): iterable</code> на <code>getProducts(QueryBuilder …$queries): iterable</code></p>
<p>Расскажите, как часто случалось вам в вашей практике писать Строители? Расширение QueryBuilder из ActiveRecord не в счет 😉 Жду ваших комментариев в <a href="https://t.me/viktorprogger_channel_ru" target="_blank" rel="noopener noreferrer">телеграм-чате блога</a> 👍 А в новой статье увидимся через две недели!</p>
]]></content>
    <category term="Personal"/>
    <published>2022-07-18T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Как выбрать инфраструктурный инструмент</title>
    <id>https://viktorprogger.name/ru/posts/how-to-choose-an-infrastructure-tool.html</id>
    <link href="https://viktorprogger.name/ru/posts/how-to-choose-an-infrastructure-tool.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Когда IT-продукт развивается, обязательно наступает момент, когда становится нужно принести в него какой-то новый инструмент или заменить старый на что-то более подходящее. И вроде бы статей на тему выбора инструментов написано много, сказать нового больше нечего, а вопрос все равно остается актуальным, местами даже острым.</p>
<img src="/choice.jpg" style="width: 100%" alt="Как выбрать инфраструктурный инструмент">
<p>Вообще есть несколько популярных подходов:</p>
<ul>
<li>Налево пойдешь - хайповый инструмент найдешь</li>
<li>Направо пойдешь - что-то проверенное и надежное возьмешь</li>
<li>Прямо пойдешь - любимый инструмент прикрутишь</li>
</ul>
<p>Они хороши тем, что при их использовании не надо думать. Просто берешь то, что хочется - и дело с концом 😃 Ну а если есть нужда или желание подойти к вопросу более серьезно - прошу под кат, будем разбираться.</p>
]]></summary>
    <content type="html"><![CDATA[<p>Когда IT-продукт развивается, обязательно наступает момент, когда становится нужно принести в него какой-то новый инструмент или заменить старый на что-то более подходящее. И вроде бы статей на тему выбора инструментов написано много, сказать нового больше нечего, а вопрос все равно остается актуальным, местами даже острым.</p>
<img src="/choice.jpg" style="width: 100%" alt="Как выбрать инфраструктурный инструмент">
<p>Вообще есть несколько популярных подходов:</p>
<ul>
<li>Налево пойдешь - хайповый инструмент найдешь</li>
<li>Направо пойдешь - что-то проверенное и надежное возьмешь</li>
<li>Прямо пойдешь - любимый инструмент прикрутишь</li>
</ul>
<p>Они хороши тем, что при их использовании не надо думать. Просто берешь то, что хочется - и дело с концом 😃 Ну а если есть нужда или желание подойти к вопросу более серьезно - прошу под кат, будем разбираться.</p>
<!-- more -->
<p>Итак, если не “по-быстрому”, то как же выбирать?</p>
<h3>1. Составить список требований</h3>
<p>Самый первый и самый очевидный шаг. Но не все так просто, как хотелось бы. Он должен быть актуален на прямо сейчас и обозримое будущее. То есть, нужно выдержать баланс между двумя направлениями:</p>
<ul>
<li>Не заглядывать в далекое будущее с потенциальным ростом компании “ва многа разов” и увеличением RPS с текущих 0.1 до 1000.</li>
<li>Не оставить за бортом реальных требований, если и не текущих, то из обозримого будущего (скажем, в 1-3 года)</li>
</ul>
<h3>2. Выбрать несколько кандидатов, отвечающих всем требованиям</h3>
<p>А как выберете - провести их сравнительный анализ, учитывая составленные ранее требования. И не забыть учесть еще несколько пунктов:</p>
<ol>
<li>Удобство использования.<br>
Инструмент, которым вы собираетесь регулярно пользоваться, не должен чинить препятствий своему использованию, а еще лучше - делать этот процесс приятным и удобным.</li>
<li>Частота обновлений.<br>
Некоторые старые, надежные инструменты со временем уходят в стагнацию и перестают обновляться. В этом случае даже если сейчас такой инструмент представляется идеальным решением, есть довольно большая вероятность, что его вскоре снова придется менять, так как он либо перестанет соответствовать требованиям безопасности, либо не станет реализовывать новые возможности, которые появятся у него быстро развивающихся конкурентов. С другой стороны - молодые проекты. Они могут загнуться из-за маленького комьюнити и недостатка мейнтейнеров.</li>
<li>Наличие комьюнити, интеграций, плагинов.<br>
Отсутствие у нового крутого инструмента большого комьюнити и большого количества интеграций, которые есть у более проверенных его собратьев, может стать очень серьезным препятствием в развитии вашей компании. Отсутствие возможности быстро встроить инструмент в ваши текущие процессы - это ощутимая проблема. С более старыми инструментами, медленнее работающим, менее удобными, вы нередко можете сэкономить десятки, а то и сотни часов работы просто потому, что ваши проблемы уже кем-то решены. А каким бы классным инструмент ни был, на заре своего развития у него всегда есть одно препятствие: пользователям необходимо решать свои проблемы самостоятельно.</li>
<li>Наличие экспертизы в команде.<br>
Даже если незнакомый инструмент не выглядит сложным, а гайды по установке и настройке умещаются на один экран, это отнюдь не значит, что у вас не возникнет сложностей через месяц. А поскольку экспертизы в работе с этим инструментом у вас нет - неизвестно, сколько времени уйдет на устранение ошибок или оптимизацию его работы.</li>
</ol>
<p>В общем случае при анализе стоит учитывать только основной, общий для всех систем функционал. Я говорю о том, что все без исключения инструменты инфраструктуры рано или поздно устаревают. Сразу думайте о том, как вы будете менять этот инструмент на какой-то другой. Не на какой-то конкретный, а на абстрактный, систему кэширования - на другую систему кэширования. В случае с системами, для которых нет стандартов вроде PSR, для этого придется самостоятельно провести более глубокий анализ, чтобы выявить этот самый общий функционал.</p>
<div class="hint-container tip">
<p class="hint-container-title"> </p>
<p>Если речь идет о системе кэширования, то тут можно руководствоваться актуальным PSR. Сама система кэширования при этом может давать значительно больше возможностей как, например, это делает Redis. Я не рекомендую использовать эти возможности в коде напрямую. Конечно, Redis можно использовать не только как систему кэширования, но и как базу данных. Это нормально. А вот использовать его как систему кэширования, в которой используются функции базы данных, не предусмотренные PSR - рано или поздно приведет к проблемам.</p>
</div>
<h3>3. Взвесить все за и против выбранных инструментов</h3>
<p>Даже в схожих ситуациях в схожих компаниях выбор может радикально отличаться, так как ценность того или иного пункта тоже будет меняться. Для кого-то важнее удобство использования системой, для кого-то - размер комьюнити и сопутствующих инструментов, а у кого-то этот инструмент будет встраиваться в настолько быстрорастущий бизнес, что для него основным фактором станет пропускная способность, а вовсе не удобство использования и скорость его внедрения. Вы можете составить таблицу, определив вес для каждой особенности инструментов. Так вы сделаете процесс анализа более наглядным.</p>
<h3>Пример из жизни</h3>
<p>А теперь давайте пройдемся по какому-нибудь реальному инструменту, который мы попробуем выбрать по схеме выше. В компании, где я работаю, недавно столкнулись с необходимостью сменить сервер очередей. До замены использовался Redis, но он перестал нас устраивать. Объем информации, пропускаемой через него, растет, ценность ее высокая, и мы не готовы терять ее в случае сбоя. Отсюда вытекает <em>главный критерий, по которому он нас не устраивает: надежность доставки сообщений</em>. Маршрутизацией сообщений между очередями, сложной логикой их доставки и прочими подобными вещами мы не занимаемся. Рост же трафика, пропускаемого через очереди, увеличивается максимум в два раза за год. Еще один критерий: скорость запуска нового инструмента в продакшен, нам необходимо запустить его в течение месяца. Поэтому какой-нибудь совсем новый инструмент не подходит, т.к. мы не готовы тратить лишнее время на его внедрение.</p>
<p>На этапе выбора инструмента все свелось к классическому противостоянию:</p>
<ul>
<li><strong>RabbitMQ</strong>. С ним у пары наших разработчиков уже был опыт работы. Также-это довольно популярный сервер очередей, имеющий богатый набор функционала и работающий по стандарту amqp. Он наиболее хорошо документирован и проще в установке и обслуживании, чем другие брокеры, реализующие amqp.</li>
<li><strong>Apache Kafka</strong> - весьма популярен среди компаний энтерпрайз-уровня. Работает по собственному стандарту.</li>
<li><strong>NATS</strong> долго рассматривался как один из вариантов, т.к. не сразу выяснилось, что он не персистит сообщения, а значит - теряет их после выключения. А еще против него говорило то, что он принимает сообщения только по UDP, что добавляет рисков к их потере.</li>
</ul>
<div class="hint-container info">
<p class="hint-container-title">Disclaimer</p>
<p>Подробный анализ Kafka и RabbitMQ выходит за пределы темы этой статьи, материалов подобного характера предостаточно в интернете. Я же остановлюсь исключительно на выборе согласно нашим собственным критериям.</p>
</div>
<p>Итак, оценим двух оставшихся конкурентов по их возможностям и недостаткам.</p>
<h4>RabbitMQ</h4>
<ul>
<li>С ним уже работали некоторые люди в нашей команде, так что у него есть преимущество на старте.</li>
<li>Есть возможность гибкого роутинга сообщений благодаря протоколу amqp. Фишка крутая, но не учитывается в рейтинге, т.к. это нельзя переиспользовать при переходе на другой брокер. Их использование приведет к усложнению перехода к другому инструменту, когда наступит соответствующий момент.</li>
<li>Способ кластеризации. У RabbitMQ для кластеризации и поддержки высокой доступности используется синхронизация баз данных. Это работает быстро, четко, но не прощает сетевых ошибок. Другими словами, если ноды брокера находятся на разных серверах, в разных дата-центрах, неизбежно будут возникать сетевые проблемы, которые будут приводить к рассинхронизации данных в нодах.<div class="hint-container info">
<p class="hint-container-title">Shovel</p>
<p>Также есть альтернатива в виде плагина Shovel, который под капотом использует стандарт amqp и просто пересылает сообщения из очереди одного сервера на обменник (exchange) другого, при этом корректно обрабатывая сетевые ошибки, но добавляя ощутимые накладные расходы, т.к. это не копирование сырых данных, а полноценная обработка сообщения.</p>
</div>
</li>
<li>Гарантии доставки сообщений зависят от настроек и поддерживаются только два типа:
<ul>
<li>at least once, сообщение будет доставлено не менее одного раза (иногда может быть и больше)</li>
<li>at most once, сообщение будет доставлено не более одного раза (но может и не быть доставлено вовсе)</li>
</ul>
</li>
<li>Есть возможность включения записи сообщений на диск для того, чтобы они не терялись при перезапуске брокера очередей.</li>
<li>Пропускная способность у Кролика неплохая, но серьезно падает при включении записи сообщений на диск, до нескольких тысяч сообщений в секунду. У других брокеров пропускная способность на порядки выше (от сотен тысяч до миллионов в секунду), но мы и в эти числа в обозримом будущем укладываемся.</li>
</ul>
<h4>Apache Kafka</h4>
<ul>
<li>Мы много о нем слышали, но вживую - не видели.</li>
<li>Реализует только паб/саб, не умеет в маршрутизацию сообщений. Что, опять-таки, не минус, а констатация факта.</li>
<li>Возможность кластеризации - есть из коробки, без ощутимой потери производительности и ограничений на локальную сеть, что позволяет делать readonly реплики, хранить разные шарды очереди на разных серверах и пр. Минус: требуется еще один инструмент, ZooKeeper.</li>
<li>Гарантии доставки сообщений также зависят от настроек, но поддерживаются все три возможных типа, включая “exactly once”, т.е. гарантию того, что сообщение будет доставлено получателю ровно один раз, не больше и не меньше. Однако, с этим режимом гарантий доставки возникают сложности при настройке кластеров Кафки. Конечно, соответствующие рецепты уже есть в интернете.</li>
<li>Запись сообщений на диск не выключается ввиду архитектурных особенностей: очереди в Кафке - это журналы, в которые сообщения добавляются продюсерами, и откуда они читаются консьюмерами. Другими словами, все пришедшие в Кафку сообщения всегда доступны для чтения, в т.ч.давно обработанные.</li>
<li>Пропускная способность - около 1 миллиона сообщений в секунду.</li>
</ul>
<p>При сравнении плюсов и минусов этих двух инструментов становится очевидно, что Кафка ощутимо выигрывает. <em>И все же, мы сделали выбор в пользу RabbitMQ.</em> Все дело в самом первом его плюсе: эта система нам уже знакома, и меньше вероятность того, что мы встретимся с проблемами в продакшене. При этом мы не планируем делать географически распределенную систему, т.е. особенности кластеризации Кролика в нашем случае не являются минусом.</p>
<p>У Кафки же его архитектурное отличие приносит значительные сложности. Во-первых, у Кафки нет собственно очереди: у него есть журналы, разбитые на партиции, и из каждой партиции может читать ровно один консьюмер. Мы с такой схемой никогда не работали, и высока вероятность выстрела в ногу при работе с реальными данными в продакшене. Особенно сильно мы чувствуем опаску при мыслях о масштабировании при обработке сообщений. Кролику можно просто добавить консьюмеров. А с Кафкой необходимо заранее продумывать количество партиций и распределение сообщений по ним для сохранения очередности выполнения задач в некоторых случаях.</p>
<p>Наша компания растет и развивается, и мы не зарекаемся от вероятности того, что мы же и будем через пару лет менять RabbitMQ на Kafka, если вдруг наши требования к брокеру очередей изменятся. А чтобы эта замена прошла безболезненно - не станем завязываться на особенности работы текущего инструмента.</p>
<h3>Выводы</h3>
<p>Если следовать приведенному выше плану, то можно выбрать любой инструмент из доступных вариантов. Конечно, он оставляет место для риска: если выбрать однозначно самый лучший из доступных, но незнакомый инструмент, можно во время эксплуатации наткнуться на подводные камни. Эти риски тоже стоит закладывать при выборе инструмента. Приведу план подготовки к выбору инфраструктурного инструмента еще раз:</p>
<ol>
<li>Составить список требований, не упуская реальных и не выдумывая чего-то сверх необходимого.</li>
<li>Выбрать несколько кандидатов и провести их сравнительный анализ.</li>
<li>Оценить выигрышные и проигрышные позиции у разных инструментов, руководствуясь текущими приоритетами.</li>
</ol>
<p>Расскажите, следовали ли вы подобному плану в своей практике? Какие курьезы случались из-за неучтенных требований или особенностей инструмента? Жду вас <a href="https://t.me/viktorprogger_channel_ru" target="_blank" rel="noopener noreferrer">в телеграм-чате</a>, буду рад услышать ваши истории и пообщаться!</p>
]]></content>
    <category term="Разработка: процессы"/>
    <published>2022-08-08T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Запретная тема или Что делать с прокрастинацией</title>
    <id>https://viktorprogger.name/ru/posts/how-to-deal-with-procratination.html</id>
    <link href="https://viktorprogger.name/ru/posts/how-to-deal-with-procratination.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Есть такая тема среди нас, айтишников, которую принято обходить стороной. Дело в том, что мы не так уж много часов в течение дня способны продуктивно работать. С одной стороны, это ни для кого не секрет. С другой - это не принято обсуждать, и мы почему-то думаем, что никто, кроме нас самих, не в курсе. В идеальном мире программист с хорошим опытом, лет тридцати-сорока, вполне способен с большой эффективностью работать пару часов в день, остальное время уделяя всевозможной рутине: ревью кода, общение с коллегами, разбор задач, и так далее. В реальном же мире нас за каждым углом подстерегает прокрастинация. Недаром одним из самых популярных мемов в IT сейчас являются мемы про выгорание.</p>
<img style="margin: auto; display: flex" src="/procrastination_ru.png" alt="Выгорание">
]]></summary>
    <content type="html"><![CDATA[<p>Есть такая тема среди нас, айтишников, которую принято обходить стороной. Дело в том, что мы не так уж много часов в течение дня способны продуктивно работать. С одной стороны, это ни для кого не секрет. С другой - это не принято обсуждать, и мы почему-то думаем, что никто, кроме нас самих, не в курсе. В идеальном мире программист с хорошим опытом, лет тридцати-сорока, вполне способен с большой эффективностью работать пару часов в день, остальное время уделяя всевозможной рутине: ревью кода, общение с коллегами, разбор задач, и так далее. В реальном же мире нас за каждым углом подстерегает прокрастинация. Недаром одним из самых популярных мемов в IT сейчас являются мемы про выгорание.</p>
<img style="margin: auto; display: flex" src="/procrastination_ru.png" alt="Выгорание">
<!-- more -->
<h2>Интро</h2>
<p>Проблема кроется как раз в том, что о ней не принято говорить. При 8-часовом рабочем дне большинство разработчиков искренне считают, что они обязаны быть максимально продуктивными все 8 часов. Либо они уверены, что так считает их начальство. В результате мы либо действительно пытаемся работать значительно более продуктивно, чем можем, либо старательно делаем вид, что так и есть.</p>
<div class="hint-container tip">
<p class="hint-container-title">Анекдот</p>
<p>Если программист быстро отвечает в чате, значит - он в данный момент занят работой.</p>
</div>
<p>Табуированность этой темы приводит к почти полному отсутствию материалов о ней в открытом доступе, из-за чего нам приходится самостоятельно выбираться из собственноручно вырытой ямы. И если не удается - следом идет то самое выгорание. Посмотрите, знакомая схема?</p>
<ol>
<li>Я прокрастинирую и не могу эффективно работать</li>
<li>Я злюсь на себя за то, что не могу сделать достаточно много, либо обижаюсь на неинтересные задачи/неправильное руководство/устаревание инструментов.</li>
<li>Результативность снижается, самобичевание - повышается</li>
<li>Полное выгорание</li>
</ol>
<p>Да, море статей про выгорание рассказывают, как его не допустить. Но при этом почти нигде не говорится о прокрастинации, хотя она приходит задолго до выгорания. А самое забавное - что статьи о выгорании читают те, кто в него уже попался. Как правило, за первые несколько лет работы в профессии у большинства из нас случается парочка выгораний. Конечно, в результате мы получаем богатый опыт, благодаря которому в следующий раз таких кризисов становится избежать проще. Но, тем не менее, редко кому удаётся сделать это в полной мере. И уж тем более - мало кому приходит в голову делиться своим опытом с младшими коллегами.</p>
<div class="hint-container info">
<p class="hint-container-title">Инфо</p>
<p>Ведь “если я выгорел - это мои собственные проблемы”. Это показатель того, что “я не справился”. По крайней мере, так принято считать в обществе.</p>
</div>
<p>И в то же время прокрастинация - это то, что случается абсолютно со всеми людьми. Рано или поздно, так или иначе. Обычно с прокрастинацией мы плотно знакомимся ещё в школе. И если в этом возрасте все шишки принято валить на систему образования и наших учителей, то в случае с любимой работой всё оказывается гораздо сложнее. Человек снова сталкивается с ощущением невозможности продолжать свою работу, при этом испытывая чувство ответственности перед собой и работодателем, в результате чего делает худший из возможных выборов: начинает себя винить.</p>
<blockquote>
<p>Вот, я опаздываю со сроками сдачи задачи. Ведь я же так люблю эту работу, почему я не могу сделать что-то на её благо?</p>
</blockquote>
<p>Каюсь, я и сам грешен. Я до сих пор не смог избавиться от этого рефлекса. И всё же, это не более, чем рефлекс. Прокрастинация - это состояние, в которое много раз в течение жизни попадает любой человек. А значит, у него есть определённые механики. Определённые причины появления и способы устранения.</p>
<div class="hint-container tip">
<p class="hint-container-title">Совет</p>
<p>Наверное, работу я люблю, но не эту. Эй, HR'ы, я свободен!</p>
</div>
<h2>Как бороться с прокрастинацией?</h2>
<p>Никак.</p>
<h2>Как работает прокрастинация?</h2>
<p>Важно понять: это защитный механизм. С ним не надо бороться, т.к. это всего лишь сигнал о том, что что-то не так. И хотя локальных причин ее возникновения может быть бесконечное множество, в общем и целом прокрастинация - это сигнал об усталости. Вы устали и не можете продолжать работать. Усталость может быть любой, в том числе и по не вполне очевидным причинам: физическая, моральная, от запрета себе быть собой. Конечно, в первую очередь стоит отдохнуть. Сходить в отпуск или хотя бы один-два вечера посвятить себе любимому вместо всяких “должен”.</p>
<p>Но что делать, если после отдыха усталость не уходит? Для начала - проверьте, действительно ли вы отдыхаете во время отдыха, или продолжаете строить в уме всевозможные системы (не обязательно в плане разработки) и думать о том, сколько еще не сделано. А может, переключаетесь с одной работы на другую? В этом случае - отдохните. По-настоящему. А если не помогает и это - попробуйте следующие варианты.</p>
<h2>Причины возникновения прокрастинации и ее решения</h2>
<h3>Невыполнение хотелок.</h3>
<p>Наш ум устроен так, что мы все время вынуждены выбирать между &quot;хочу&quot; и &quot;надо&quot;. И для максимальной эффективности их необходимо чередовать. Если вы все время свои хотелки откладываете на потом, на после “более важных вещей”, после &quot;надо&quot;, вас ждёт прокрастинация и выгорание. Какими бы ненужным и глупыми ни казались ваши хотелки - позвольте их себе, регулярно позволяйте их себе. Например, для меня долгое время отдушиной была работа над фреймворком yii3. Рабочие задачи я писал &quot;как надо&quot;, а фреймворк писал &quot;как хочу&quot;. Сюда же - компьютерные и спортивные игры, походы, музыка, кино и пр.</p>
<h3>Перегруженность планами.</h3>
<p>Как часто вы откладывали какую-то задачу на потом? Может быть, меняли приоритеты? А бывает ли такое, что какая-то задача откладывается на потом ещё и ещё раз? Конечно, так бывает у всех. И порой это превращается в проблему. Проблема состоит в том, что мы стараемся удержать в голове очень большое количество задач. Необязательно это большие задачи по написанию кода. Это может быть проверка почты, обязательства кому-то ответить, ожидающее ревью кода и так далее. Проблема в том, что чем больше таких задач мы пытаемся запомнить - тем больше сил на это тратим. Ее решение - список дел. Лично я подхожу к нему в два этапа:</p>
<ol>
<li>Сначала - выписываю все свои планы. Вернее, все свои планы и хотелки. Всё, что я так или иначе собираюсь сделать и в ближайшее время, и позже. После чего этот список подвергается редактуре: я убираю из него всё, что считаю ненужным в текущей ситуации. Это не просто вычеркивание пункта из списка, это решение не двигаться в этом направлении, не тратить никакие силы на достижение цели. Полный отказ от нее.</li>
<li>А затем этот список сортирую по срочности и распределяю по нескольким категориям. Например, домашние дела и рабочие дела. Плюс помечаю некоторые дела как срочные. Цель этого списка - в том, чтобы в дальнейшем делать только то, что в нем находится. Снять когнитивную нагрузку с попыток вспомнить и верно приоритизировать все задачи.<br>
Звучит это дико, однозначно. Для меня это тоже выглядело странной хренью поначалу. Но вы все же попробуйте. Этот метод освободит вам значительное количество ресурсов, позволит эффективно работать значительно дольше, чем это возможно сейчас. И отвечаю сразу на вопрос о ситуации, когда необходимо сделать что-то, чего нет в списке: вносите это дело в список. Ещё раз: не делайте ничего, чего нет в списке, а если что-то такое всё же надо сделать - внесите это дело в список.</li>
</ol>
<h3>Частое переключение.</h3>
<p>У людей есть такая особенность: нам нужно время, чтобы переключаться на разнотипные задачи. Другими словами, если вы работали, и вас что-то отвлекло, то вам потребуется до получаса времени на то, чтобы вернуться обратно в рабочее состояние и работать также эффективно, как вы работали до этого. И это - если вы в хорошем состоянии: выспались, не утомлены и так далее. В менее продуктивном состоянии этот процесс займёт ещё больше времени.<br>
Таким образом, появляется простая рекомендация: на время работы обеспечить себе максимально комфортную среду, исключив отвлекающие факторы: выключите уведомления на телефоне, попросить жену не беспокоить, отправить детей в детский сад, и так далее. Только представьте: если раз в 10 минут на ваш телефон будет приходить уведомления, это будет на 10 минут выбивать из колеи. А это значит, что в течение рабочего дня вы так и не начнёте работать настолько эффективно, насколько вы можете. И, уверяю, вам только кажется, что уведомление из чата вас не отвлекают: на самом деле на долю секунды ваш ум отвлекается на мысль: &quot;А надо ли мне прочесть это сообщение?&quot;. И это сбивает с рабочего ритма.</p>
<h3>Избыток эндорфиновых всплесков.</h3>
<p>Результат другой крайности между &quot;хочу&quot; и &quot;надо&quot;. Когда мы выполняем свою хотелку, то получаем заряд эндорфинов прямо в мозг. А если регулярно идём на поводу у своих хотелок, то со временем получаем зависимость от них и неизбежно приходим к самым простым способами получения дозы эндорфинов. Это еда и всевозможные ленты: тикток, инстаграм, сериалы, чтение содержимого чатов и т.п. И, конечно, игры. Особенно - игры с короткими раундами и кликеры.<br>
Все эти вещи объединены одним: каждая из них - простой и доступный способ получения дозы эндорфинов. А мозг их любит. И если вы дошли до этого этапа, путь к продуктивности будет связан с абстинентным синдромом. Вам все время будет хотеться ещё. Хотя бы один бутерброд, хоть одним глазком посмотреть сериал, всего один квест завершить в игре… Поверьте моему опыту 😄 В какой-то момент я понял, что любую свободную минуту я обязательно заполняю телефоном. Если времени совсем мало или нет желания что-то конкретное делать - листаю чаты. В других случаях - игры/сериалы. По сравнению с этим все остальные способы времяпрепровождения стали скучными, в том числе - работа.<br>
А технически дело в том, что рецепторы в мозгу выработали привыкание к эндорфинам. И единственный путь - уменьшить дозу, чтобы чувствительность восстановилась, и мир снова заиграл яркими красками. Заодно освободится куча времени на другие дела, а общий тонус повысится.</p>
<h3>Психотерапевт.</h3>
<p>Сегодня сходить к психотерапевту - не такой зашквар, как считалось 10 лет назад. Не пренебрегайте этой возможностью. Хороший психотерапевт обязательно сократит ваш путь от прокрастинации к продуктивности, т.к. поможет быстро и эффективно найти и устранить причины ее возникновения. Почему я говорю “психотерапевт”, а не “психолог”? Если совсем коротко, поэтому:</p>
<ul>
<li>Психолог - это выпускник с корочкой. Так же, как и любой ваш бывший однокурсник - программист. Можно утверждать, что любой психолог знает основы работы психики, а вот глубже - далеко не факт.</li>
<li>Психотерапевт - это психолог, прошедший теоретическую и практическую подготовку к ведению психотерапии, т.е. работе с людьми. В нашем понимании - это человек, освоивший как минимум php, MySQL, JS, CSS и хотя бы один php-фреймворк или CMS. Психотерапевт уже знает не только о психике абстрактно, но и том, как с ней работать.</li>
<li>Психиатр - это психотерапевт, который имеет право выписывать лекарства. При прокрастинации в этом обычно нет нужды.</li>
</ul>
<h2>Постскриптум и литература</h2>
<p>Есть и другие причины для прокрастинации, как и другие способы борьбы с ней. Главное - не надо воспринимать ее как нечто ненормальное и пытаться с ней бороться. Это лишь сигнал организма. Если вам не подошло ничего выше, или есть желание разобраться в вопросах прокрастинации и эффективности получше, рекомендую этого отличного парня, професионального прокрастинатолога: <a href="https://mnogosdelal.ru/" target="_blank" rel="noopener noreferrer">Максим Дорофеев</a>. Он дает очень дельные советы в доступной форме и в разных видах: книги, видео, форум и чат. Ссылка не оплачена, мне действительно нравится то, что и как он делает. Искренне советую познакомиться с его творчеством, а начать можно с одного из его <a href="https://www.youtube.com/watch?v=DukfcM24tgk" target="_blank" rel="noopener noreferrer">выступлений на конференции</a>.</p>
<p>Хорошего вам дня и не выгореть! 😊</p>
]]></content>
    <category term="Разработка: процессы"/>
    <published>2022-07-01T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Как начать разработку OpenSource продукта?</title>
    <id>https://viktorprogger.name/ru/posts/how-to-start-develop-opensource.html</id>
    <link href="https://viktorprogger.name/ru/posts/how-to-start-develop-opensource.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Все мы так или иначе пользуемся разными OpenSource-продуктами. Что-то нравится больше, что-то - меньше.
И тому, что нам нравится, зачастую хочется помочь стать еще лучше. Один из способов - это помочь финансово,
но мы же и сами разработчики, и порой мы представляем себя среди авторов замечательного инструмента,
которым с удовольствием пользуемся сами. По крайней мере, так раньше было у меня. А затем мечта стала реальностью.
Если для тебя это тоже мечта - прошу под кат: там и моя история, и ответ на то, как тебе тоже присоединиться
к разработке любимого OpenSource-инструмента.</p>
]]></summary>
    <content type="html"><![CDATA[<p>Все мы так или иначе пользуемся разными OpenSource-продуктами. Что-то нравится больше, что-то - меньше.
И тому, что нам нравится, зачастую хочется помочь стать еще лучше. Один из способов - это помочь финансово,
но мы же и сами разработчики, и порой мы представляем себя среди авторов замечательного инструмента,
которым с удовольствием пользуемся сами. По крайней мере, так раньше было у меня. А затем мечта стала реальностью.
Если для тебя это тоже мечта - прошу под кат: там и моя история, и ответ на то, как тебе тоже присоединиться
к разработке любимого OpenSource-инструмента.</p>
<!-- more --> 
<p><img src="/opensource.jpg" alt></p>
<p><a href="#%D0%BA%D0%B0%D0%BA-%D1%81%D0%B0%D0%BC%D0%BE%D0%BC%D1%83-%D0%BD%D0%B0%D1%87%D0%B0%D1%82%D1%8C-%D0%BF%D0%B8%D1%81%D0%B0%D1%82%D1%8C-%D0%BA%D0%BE%D0%B4-%D0%B2-opensource-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B5"><strong>TL;DR: Как начать.</strong></a></p>
<div class="hint-container note">
<p class="hint-container-title">Thank you</p>
<p>Спасибо <a href="https://leonidchernenko.ru/" target="_blank" rel="noopener noreferrer">Леониду Черненко</a> за помощь с вычиткой английской версии этой статьи.
Это моя первая работа на английском языке, и я очень благодарен за его помощь и поддержку.</p>
</div>
<h3>Моя история</h3>
<p>А сейчас я один из core-разработчиков фреймворка Yii3. Я очень люблю этот фреймворк, и люблю помогать ему становиться лучше.
История о том, как я вошел в команду разработки популярного фреймворка, проста до банальности.
Однажды я зашел в чат фреймворка в telegram и увидел, как Александр Макаров, мейнтейнер фреймворка, отвечает
на вопросы в чате. На все, не выбирая &quot;интересное&quot; или &quot;полезное&quot;. Кажется, он тогда написал что-то на тему
того, чтобы ему писали в личку, если есть какие-то вопросы лично к нему, но я не уверен. Признаться, я был ошарашен.
Можно сказать, Александр был моим кумиром, и вдруг - вот он, в чатике, где с ним можно поболтать так же,
как с моими друзьями. В тот момент я понял, что не общался с ним до сих пор лишь по той причине, что почему-то решил, что он не станет со мной общаться. И по той же причине я не участвовал в жизни Yii.</p>
<p>Тогда я решился на авантюру. Мне было страшно, но я это сделал: написал ему. Казалось бы, где Александр, а где я, но
я убеждал себя, что в первую очередь мы оба - люди, а значит - нам не чуждо общение, да и вообще - за спрос не бьют.
Я написал что-то вроде этого:</p>
<blockquote>
<p>Привет! Меня зовут Виктор, я PHP-разработчик. Мне очень нравится фреймворк Yii, и я хотел бы помочь
в выпуске новой, третьей версии. Подскажи, пожалуйста, с чего можно начать?</p>
</blockquote>
<p>И он поблагодарил меня за энтузиазм и дал несколько ссылок на тикеты на гитхабе, за которые я вскоре принялся.
Тикеты действительно были на уровне &quot;с чего начать&quot;. Не помню, что именно, но это были мелочи. Постепенно я втянулся
в этот процесс и стал делать все более сложные вещи. Например, я написал первую версию библиотеки <code>yiisoft/validator</code>.
Не полностью, proof of concept. А потом я сменил основную работу и сколько-то месяцев не мог заниматься OpenSource-разработкой.
Но ребята взяли мой код, доработали и превратили в полноценную библиотеку. Сейчас у нее уже совершенно другие архитектурные
подходы, но лично для меня это много значит: это был мой первый серьезный вклад в популярный и, ИМХО, лучший фреймворк.</p>
<h3>Что я вынес из OpenSource-разработки</h3>
<p>Конечно, я вынес много опыта. Я продолжаю с радостью заниматься OpenSource в свободное время
и продолжаю получать все новый опыт. Но если вычленить самые яркие приобретения, я назову эти пункты:</p>
<h4>Глубокое понимание принципов SOLID и в целом архитектуры приложений и библиотек.</h4>
<p>В коммерческой разработке у тебя
обычно нет возможности спокойно сесть
и проанализировать различные архитектурные решения, написать несколько вариантов и выбрать один из них, или даже
полностью переработать первоначальный вариант, проведя глубокий рефакторинг. В OpenSource другие ценности:
если в коммерческой разработке один из основных критериев выбора чего угодно - это соотношение цены/качества, то
здесь ставка всегда на качество. И полученный в OpenSource опыт создания SOLID'ных архитектур я теперь применяю
в коммерческой разработке, где важнее не следовать принципам до конца, а понимать, где и в какой степени им
следовать стоит, и какова будет цена их соблюдения и нарушения.</p>
<h4>Тестирование.</h4>
<p>До Yii у меня было мало опыта в написании тестов. В основном - то, чему я научился сам. А здесь
мы начали применять комплексный подход:</p>
<ul>
<li><a href="https://psalm.dev" target="_blank" rel="noopener noreferrer">Psalm</a>, инструмент статической проверки типов.</li>
<li><a href="https://phpunit.readthedocs.io/ru/latest/" target="_blank" rel="noopener noreferrer">PHPUnit</a>, фреймворк для написания автоматических тестов.</li>
<li><a href="https://infection.github.io/" target="_blank" rel="noopener noreferrer">Infection</a>, инструмент проверки качества тестов, написанных на PhpUnit (мутационное тестирование).</li>
</ul>
<p>И при этом мы делаем максимально полное покрытие кода тестами. К релизу любого пакета Psalm выставляется на уровень 1,
а Infection показывает почти 100% MSI (например, <a href="https://github.com/yiisoft/assets" target="_blank" rel="noopener noreferrer">yiisoft/assets</a>.
Особенно ценно то, что я попал на новый виток развития фреймворка,
и эти инструменты и способы их использования мы выбирали вместе, вместе покрывали тестами первые пакеты. Надо сказать,
свой первый пакет я покрыл тестами так, что потом сам держался за голову: это было весьма сложно поддерживать, развивать. Но
это тот случай, когда на ошибках учатся. Хотя именно те тесты на момент написания этой статьи еще не отрефакторены 😆</p>
<h4>Опыт работы в самоуправляющемся коллективе.</h4>
<p>Да, в core-команде разработки фреймворка Yii все равны. Как правило, мы
придаем больше значения мнению Александра Макарова (<a href="https://github.com/samdark" target="_blank" rel="noopener noreferrer">@samdark</a>), но и он от остальных не отмахивается. Если у кого-то есть вопросы
или возражения - мы обсуждаем их до тех пор, пока не придем к какому-то общему решению.<br>
У нас тоже не без минусов, конечно.
Например, долгое время именно Александр выполнял всю менеджерскую и управленческую работу. Он фактически
был нашим начальником. Он принимал архитектурные решения, мержил PR и релизил пакеты, он же определял приоритеты команды.
И в какой-то момент на пару месяцев выпал из разработки фреймворка. Нам пришлось крутиться самим. И я очень рад, что мы выкрутились.
Не сразу, но мы наладили процессы так, что они не требовали присутствия Александра. Мы созвонились и обсудили самые острые вопросы,
договорились о компромиссах и приоритетах. А потом и @samdark подтянулся. Теперь он больше стал помогать остальной команде
налаживать процессы по новой схеме, чем непосредственно управлять нашей деятельностью. Он по-прежнему самый уважаемый член команды и
мейнтейнер фреймворка, но уже не является бас-фактором. <em>Это чудесный, замечательный опыт: люди, объединенные одной
идеей, по своей инициативе скооперировались и делают общее дело.</em> И большинство из нас делает это бесплатно.
Что-то подобное, вероятно, можно получить в т.н. &quot;бирюзовых компаниях&quot;, но их на сегодня довольно мало, так что
OpenSource остается более простым способом получить подобный опыт.</p>
<p>Это тот опыт, который оказался самым ярким для меня. Для каждого он свой. Но самое главное - для каждого найдется много такого, что очень сложно получить в коммерческой разработке. Не говоря уже о том, насколько ценна строчка о том, что у вас есть опыт разработки популярного OpenSource-инструмента, в резюме.</p>
<h3>Как самому начать писать код в OpenSource-проекте</h3>
<p><img src="/tom_jerry_wait.jpg" alt></p>
<p>Не важно, какому именно продукту отдаете предпочтение лично вы, его команда будет рада вашей помощи.
Хорошая новость в том, что начать писать код для него довольно просто. Необходимые для этого шаги:</p>
<ol>
<li>Узнать, в чем именно нужна помощь.</li>
<li>Написать требуемый код и отдать его на ревью.</li>
<li>Поправить код по замечаниям, которые появятся во время ревью.</li>
</ol>
<p>Это действительно все. После этого вашим кодом начнут пользоваться сотни/тысячи/миллионы людей.
Остаётся лишь продолжать попеременно выполнять пункты из этого списка.</p>
<p>Однако, самое сложное - это начать. Надо откуда-то узнать, чем именно можно помочь. Но тут тоже все просто: если вы не видите тикетов, которыми могли бы заняться прямо сейчас, задайте вопрос в сообществе продукта. Сейчас, наверное, у всех OpenSource-продуктов есть свои чаты или хотя бы форумы. Например, у Yii3 это телеграм-чаты <a href="https://t.me/yii3ru" target="_blank" rel="noopener noreferrer">@yii3ru</a> и <a href="https://t.me/yii3en" target="_blank" rel="noopener noreferrer">@yii3en</a>. Алгоритм просто: заходим в чат, выражаем желание помочь и просим подкинуть тикет, которым стоит заняться. Если это для вас впервые - стоит попросить что-нибудь самое простое, чтобы на паре мелких задач прочувствовать весь процесс от чтения тикета до мержа вашей задачи в мастер.</p>
<h3>Что мешает заняться разработкой OpenSource-продукта</h3>
<p>Алгоритм выше действительно до безобразия прост, его выполнение доступно каждому. Но почему же так мало людей его выполняет?</p>
<h4>1. Нет времени</h4>
<p>Скорее всего, у вас есть фултайм-работа, может быть - семья. И 24 часа в сутки заняты, а то и все 25. Однако, секрет прост: можно заниматься OpenSource в свободное рабочее время. Может показаться, что &quot;свободное&quot; и &quot;рабочее&quot; - вещи несовместимые, однако это не так. Дело в том, что действительно продуктивны мы всего 2-4 часа из всего рабочего дня. Все остальное время занимают другие вещи: ревью кода, текучка, бюрократия и т.п. Но это - в идеале, а обычно все это время занято прокрастинацией. Подумайте: действительно ли ваши задачи для занимать столько времени на реализацию, сколько они занимают? Нет? Тогда предлагаю эксперимент: занимайтесь 2 часа в день своей работой с максимальным погружением в нее, не отвлекаясь ни на что. Подготовьте четкий план действий, переключите все чаты и почту на беззвучный, и - вперёд. Вторая часть плана - обязательно посвятить часть времени тому, что приносит удовольствие, чему вы искренне рады. Лично я порой это время трачу на разработку фреймворка 😃 Ну и значительную часть времени - на вялотекущие рабочие задачи, не требующие большой концентрации. И не стоит забывать про обед, где вы не будете заниматься работой ни в каком виде в течение часа. И ещё желательно устраивать себе прогулку или какие-то физ. упражнения в перерывах от работы.<br>
Бьюсь об заклад, что при таком режиме ваша эффективность вырастет, а часть освободившегося времени можно будет отдать любимому OpenSource-проекту.</p>
<h4>2. Я неопытен</h4>
<p>Значит, OpenSource - для вас! Это превосходное место для наработки опыта. Как правило, задач хватает самой разной сложности: не только по разработке новых архитектурных подходов и оптимизации производительности, но и различные простые, но нужные мелочи: изменить стиль кода, дописать документацию, переименовать файлы и классы и т.п. И вы очень поможете, если возьмёте эти вещи на себя. При этом вы вольны брать и то, в чем ещё надо будет разобраться для реализации, и даже просить помощи у основных разработчиков продукта. Вход в OpenSource-разработку возможен на любом этапе профессионального развития, если вы уже хорошо знаете применяемый в продукте язык программирования.</p>
<h4>3. Боюсь, что меня засмеют</h4>
<p>Для меня очень странно было услышать эту причину. Я ещё не встречал настолько токсичных разработчиков OpenSource-инструментов, чтобы они как-то высмеивали тех, кто хочет им помочь. Да и за что? Единственное, что приходит в голову - что знакомые, услышав, что вы решили сделать свой вклад в OpenSource, из зависти и неуверенности в себе могут попытаться как-то оскорбить вас, остановить, чтобы ни в коем случае не оказалось, что вы &quot;лучше их&quot;. Бросайте таких знакомых, если они у вас есть: не стоит идти ко дну вместе с ними. Наш с вами путь - только вперёд!</p>
<h4>4. Мне страшно.</h4>
<p>Конечно, страшно. Как и любому человеку, который собирается заняться чем-то новым. Это нормально. Без страха нет развития, а смелый человек - не тот, кто не боится, а кто действует, несмотря на страх. А потому лучшее решение - бояться и сделать 😊 Найти новых коллег, и кто знает, может быть - друзей и призвание?</p>
]]></content>
    <category term="Разработка: процессы"/>
    <published>2022-06-10T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Как запускать на компьютере PHP, не устанавливая его (Linux)</title>
    <id>https://viktorprogger.name/ru/posts/how-to-use-php-without-installing.html</id>
    <link href="https://viktorprogger.name/ru/posts/how-to-use-php-without-installing.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Ответ прост и очевиден: я использую Docker 😃 Преимущества, которые я получаю от такого использования PHP:</p>
<ul>
<li>Не надо морочаться с настройкой локального окружения, переключением версии PHP, установкой дополнительных библиотек и разрешением конфликтов</li>
<li>Достаточно одного текстового файла с алиасами командной строки, чтобы любая актуальная версия PHP работала на любом компьютере. А отсюда
<ul>
<li>Легкость переноса данных между машинами</li>
<li>Можно спокойно снести систему на своем основном компьютере, восстановление работы с PHP будет просто как <code>git pull</code></li>
</ul>
</li>
</ul>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Эта статья - про разбор использования докера в одном специфичном сценарии, так что если вы с этой технологией уже хорошо знакомы - можно смело пропускать. Остальных прошу под кат.</p>
</div>
<img src="/php-docker.png" style="width: 100%" alt="Как запускать на компьютере PHP, не устанавливая его">
]]></summary>
    <content type="html"><![CDATA[<p>Ответ прост и очевиден: я использую Docker 😃 Преимущества, которые я получаю от такого использования PHP:</p>
<ul>
<li>Не надо морочаться с настройкой локального окружения, переключением версии PHP, установкой дополнительных библиотек и разрешением конфликтов</li>
<li>Достаточно одного текстового файла с алиасами командной строки, чтобы любая актуальная версия PHP работала на любом компьютере. А отсюда
<ul>
<li>Легкость переноса данных между машинами</li>
<li>Можно спокойно снести систему на своем основном компьютере, восстановление работы с PHP будет просто как <code>git pull</code></li>
</ul>
</li>
</ul>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Эта статья - про разбор использования докера в одном специфичном сценарии, так что если вы с этой технологией уже хорошо знакомы - можно смело пропускать. Остальных прошу под кат.</p>
</div>
<img src="/php-docker.png" style="width: 100%" alt="Как запускать на компьютере PHP, не устанавливая его">
<!-- more -->
<h3>Что такое алиасы</h3>
<p>Алиас - это дополнительная или переопределенная команда в терминале, при вызове которой на самом деле вызывается другая команда. Например, если вы для вывода списка файлов в папке всегда пишете <code>ls -la</code>, имеет смысл сделать алиас <code>alias ls='ls -la'</code>. Таким образом каждый раз, когда вы будете набирать в терминале команду <code>ls</code>, фактически будет вызываться <code>ls -la</code>.<br>
Создание алиаса - это команда для терминала. Т.е. вы можете в терминале написать <code>alias ls='ls -la'</code>, и это будет работать. Но только в этой сессии терминала. Чтобы это работало всегда - надо запускать такую команду во всех сессиях. Это делается по-разному в разных shell-оболочках. Я расскажу про то, как это делается в bash, который стоит по умолчанию в большинстве linux-дистрибутивов. Если вы используете ZShell или другую оболочку - будем считать, что вы в курсе, как в ней создаются алиасы.</p>
<p>Итак, bash. Любой пользователь баша может создать в своем домашнем каталоге файл <code>.bashrc</code>, который выполняется всегда при старте терминала. Убунту создает его по умолчанию и по умолчанию же делает в нем такую запись:</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">if</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> [ </span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">-f</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> ~/.bash_aliases ]; </span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">then</span></span>
<span class="line"><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2">    .</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379"> ~/.bash_aliases</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">fi</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Т.е. подключает файл <code>.bash_aliases</code> в домашнем каталоге, если он существует. Все свои алиасы я пишу именно в <code>.bash_aliases</code>, чтобы держать их в отдельном файле. А теперь - приступим к изучению его содержимого.</p>
<h3>PHP</h3>
<p>Для PHP я создал набор вот таких алиасов:</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">alias</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> php</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'docker run --rm -it -u `id -u`:`id -g` --volume `pwd`:/app -w /app ghcr.io/mileschou/xdebug:8.1 php  $@'</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">alias</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> php81</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'docker run --rm -it -u `id -u`:`id -g` --volume `pwd`:/app -w /app ghcr.io/mileschou/xdebug:8.1 php  $@'</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">alias</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> php8</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'docker run --rm -it -u `id -u`:`id -g` --volume `pwd`:/app -w /app ghcr.io/mileschou/xdebug:8.0 php  $@'</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">alias</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> php74</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'docker run --rm -it -u `id -u`:`id -g` --volume `pwd`:/app -w /app ghcr.io/mileschou/xdebug:7.4 php  $@'</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>В итоге я пишу в терминале команды вроде <code>php -a</code> для интерактивной работы или <code>php vendor/bin/phpunit</code> для запуска тестов, и все работает так, будто php стоит локально на мой машине. А когда для какого-то проекта мне нужна другая версия php, я пишу, например, <code>php74 vendor/bin/psalm</code>.</p>
<p>Итак, айда разбирать приведенные выше команды. Для каждой версии у меня написано одно и то же, меняется только собственно версия.</p>
<ul>
<li><code>docker run</code> запускает новый контейнер на основе указанного образа (он - в конце команды). <a href="https://docs.docker.com/engine/reference/commandline/run/" target="_blank" rel="noopener noreferrer">Документация по <code>docker run</code></a>. Все опции этой команды описаны в документации на той же странице.</li>
<li><code>--rm</code> - этот параметр говорит о том, что контейнер должен быть удален после того, как закончит работу. Если его не указать, то при каждом <code>docker run</code> будет создаваться новый контейнер, после остановки он никуда не денется, и вскоре у вас закончится место на жестком диске.</li>
<li><code>-it</code> - этот параметр нужен для работы в интерактивной сессии командной строки. Если его не указать, то не будут работать те команды php, которые ждут какой-то реакции от пользователя (например, <code>php -a</code> или скрипты, просящие ввести данные).</li>
<li><code>-u</code> запускает контейнер от имени определенного пользователя. По умолчанию контейнер запускается от имени суперпользователя root, что может привести к проблемам с правами доступа к файлам, которые монтируются с локальной машины и изменяются/создаются в контейнере. <code>`id -u`</code> и <code>`id -g`</code> возвращает id пользователя и его группы соответственно, и в большинстве случаев это будет <code>1000</code>. Эти команды заключены в обратные кавычки для того, чтобы они выполнились до вызова алиаса. В итоге команда в алиасе вызовется с таким значением параметра пользователя: <code>-u 1000:1000</code>, если ваши id пользователя и группы равны <code>1000</code>.</li>
<li><code>--volume `pwd`:/app</code> - этот параметр монтирует текущий путь в файловой системе (<code>pwd</code>) в контейнер по пути <code>/app</code>. Другими словами, ваши файлы из текущей папки будут доступны во контейнере по пути <code>/app</code>.</li>
<li><code>-w /app</code> - устанавливаем путь <code>/app</code> в качестве рабочей папки в контейнере.</li>
<li><code>ghcr.io/mileschou/xdebug:8.1</code> - название запускаемого образа контейнера и его тег (версия). Я использую этот образ вместо официального php, т.к. в официальном нет XDebug, а порой бывает нужна отладка или вычисление покрытия кода тестами. Тег всегда указывается после двоеточия, в этом примере - это <code>8.1</code></li>
<li><code>php $@</code> - это команда, запускаемая внутри контейнера. При этом <code>$@</code> - это переменная bash, указывающая на строку после алиаса. Покажу на примере. Если я напишу в терминале <code>php vendor/bin/phpunit --foo --bar</code>, в переменной <code>$@</code> окажется <code>vendor/bin/phpunit --foo --bar</code>.</li>
</ul>
<h4>PhpStorm</h4>
<p>Шторм прекрасно работает с контейнерами. Для того чтобы была возможность кнопкой запускать тесты, статический анализ или другие скрипты из Шторма, надо добавить контейнер в качестве интерпретатора. <a href="https://www.jetbrains.com/help/phpstorm/configuring-remote-interpreters.html#487450a9" target="_blank" rel="noopener noreferrer">Документация</a>.
<img src="/phpstorm-settings-php-interpreter-docker.png" alt="phpstorm-settings-php-interpreter-docker.png"></p>
<p>Однако, на этом не приключения заканчиваются. Есть еще один частый кейс использования php в терминале, composer.</p>
<h3>Composer</h3>
<p>С композером все максимально похоже. Однако, команда тут значительно длиннее, т.к. добавляются переменные окружения и тома. Нужны они для двух целей: хранить настройки и кеш композера между запусками контейнера и прокидывать данные для ssh-авторизации с хост-машины в контейнер. У меня для него два алиаса на случай, если еще придется работать с проектом, где жестко вшита первая версия. Тут я уже использую официальные образы.</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">alias</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> composer</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'docker run --rm -it --tty -u 1000:1000 -e COMPOSER_HOME=/composer-data/.composer -e COMPOSER_CACHE_DIR=/composer-data/.composer/cache -e SSH_AUTH_SOCK=/ssh-auth.sock --volume `pwd`:/app --volume /home/`whoami`/.composer:/composer-data/.composer --volume /run/user/1000/keyring/ssh:/ssh-auth.sock --volume /etc/passwd:/etc/passwd:ro --volume /etc/group:/etc/group:ro composer:2 $@'</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">alias</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> composer1</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'docker run --rm -it --tty -u 1000:1000 -e COMPOSER_HOME=/composer-data/.composer -e COMPOSER_CACHE_DIR=/composer-data/.composer/cache -e SSH_AUTH_SOCK=/ssh-auth.sock --volume `pwd`:/app --volume /home/`whoami`/.composer:/composer-data/.composer --volume /run/user/1000/keyring/ssh:/ssh-auth.sock --volume /etc/passwd:/etc/passwd:ro --volume /etc/group:/etc/group:ro composer:1 $@'</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>Кроме параметров, которые я уже разобрал выше, тут используется еще несколько. Не буду повторяться и остановлюсь только на них.</p>
<ul>
<li><code>-e</code> - этот параметр задает переменную окружения внутри контейнера. Я задаю их несколько:
<ul>
<li><code>COMPOSER_HOME=/composer-data/.composer</code> - где композер будет искать и хранить свои данные (например, токены доступа на GitHub)</li>
<li><code>COMPOSER_CACHE_DIR=/composer-data/.composer/cache</code> - где композер будет искать и хранить свой кеш. Это нужно для оптимизации. Если не хранить кеш на хосте, он будет каждый раз заново скачивать все пакеты. Если же в кеше есть пакет нужной версии - композер просто скопирует файлы из кеша в проект.</li>
<li><code>SSH_AUTH_SOCK=/ssh-auth.sock</code> - путь к авторизационному сокету для ssh-агента</li>
</ul>
</li>
<li>Плюс я подключаю еще несколько томов с хоста. Это пары к указанным выше переменным окружения.
<ul>
<li><code>--volume /home/`whoami`/.composer:/composer-data/.composer</code> - сохраняю данные композера на хосте между запусками контейнеров</li>
<li><code>--volume /run/user/`id -u`/keyring/ssh:/ssh-auth.sock</code> - прокидываю в контейнер авторизационный сокет для ssh-агента</li>
<li><code>--volume /etc/passwd:/etc/passwd:ro</code> и <code>--volume /etc/group:/etc/group:ro</code> прокидывают в контейнер учетные записи пользователей с хоста в режиме &quot;только для чтения&quot; (за это отвечает <code>:ro</code> в конце). Стоит сразу заметить, что это небезопасно, и делать так стоит, только если вы понимаете, что и зачем делаете. Лично я считаю, что официальные образы композера достойны доверия, но это лишь мое личное мнение. А без этих томов не будет работать авторизация на гитхабе по SSH с ключами с хост-машины.</li>
</ul>
</li>
</ul>
<p>Стоит помнить, что в контейнере - минимально необходимая для работы композера сборка. Это значит, что порой вам придется дописывать флаг <code>--ignore-platform-reqs</code> к вызываемой команде: <code>composer i --ignore-platform-reqs</code>.</p>
<p>Если остались какие-то вопросы или есть предложения по улучшению алиасов - <a href="https://t.me/viktorprogger_channel_ru" target="_blank" rel="noopener noreferrer">заходите в чат блога</a>, пообщаемся.</p>
]]></content>
    <category term="Разработка: техники"/>
    <published>2022-08-19T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Public или private по умолчанию, что выбрать?</title>
    <id>https://viktorprogger.name/ru/posts/public-or-private-by-default-what-to-choose.html</id>
    <link href="https://viktorprogger.name/ru/posts/public-or-private-by-default-what-to-choose.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>На одном из проектов я встретил интересное мнение. Весь код в проекте должен быть максимально открыт по умолчанию. Т.е. мы делаем все методы и поля <code>public</code>, а <code>final</code> нигде не ставим. Мотивировано это было сложностями дальнейшей поддержки:</p>
<blockquote>
<p>Если я захочу наследовать класс, а там метод <code>private</code>, мне придется изучать код, чтобы понять, зачем его сделали <code>private</code> и могу ли я сделать его публичным.</p>
</blockquote>
<p>Это мнение было очень необычным для меня, и я решил выяснить: действительно ли приватные поля и методы принесут больше проблем, чем публичные.<br>
Давайте напишем класс с лишним публичным методом и посмотрим, насколько просто его будет поддерживать.</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">interface</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> FooInterface</span><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B"> {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> foo</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Foo</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> implements</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> FooInterface</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> foo</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() {</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">    $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() {</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div>]]></summary>
    <content type="html"><![CDATA[<p>На одном из проектов я встретил интересное мнение. Весь код в проекте должен быть максимально открыт по умолчанию. Т.е. мы делаем все методы и поля <code>public</code>, а <code>final</code> нигде не ставим. Мотивировано это было сложностями дальнейшей поддержки:</p>
<blockquote>
<p>Если я захочу наследовать класс, а там метод <code>private</code>, мне придется изучать код, чтобы понять, зачем его сделали <code>private</code> и могу ли я сделать его публичным.</p>
</blockquote>
<p>Это мнение было очень необычным для меня, и я решил выяснить: действительно ли приватные поля и методы принесут больше проблем, чем публичные.<br>
Давайте напишем класс с лишним публичным методом и посмотрим, насколько просто его будет поддерживать.</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">interface</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> FooInterface</span><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B"> {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> foo</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Foo</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> implements</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> FooInterface</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> foo</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() {</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">    $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() {</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><!-- more -->
<p>Условимся, что на момент написания кода метод <code>bar</code> нигде больше не используется, и технически нам все равно, делать его публичным или приватным.</p>
<h2>Private</h2>
<p>Если мы сделаем его приватным, то столкнемся с уже озвученными трудностями. Прежде, чем сделать логику этого метода публичной, программист должен будет ознакомиться с кодом метода и выяснить, нет ли в нем мест, специфичных для этого конкретного класса. Например, работы с состоянием.</p>
<h2>Public</h2>
<p>Если сделать метод публичным по умолчанию, мы избежим вероятности того, что нам понадобится выполнять описанную выше работу в будущем. Шучу, конечно не избежим 😄</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Никто ведь не гарантирует, что программист напишет метод строго таким образом, чтобы его можно было переиспользовать из любого другого места приложения. Тем более, что изначально метод не планируется быть использованным повторно.</p>
</div>
<p>Давайте подумаем, с какими ещё проблемами мы сможем столкнуться через несколько лет после написания такого кода. Мы думаем о будущем, поэтому давайте представим, что мы не меняли этот метод года 3. И теперь нам надо удалить его или изменить сигнатуру. Первый вопрос, который встанет: используется ли этот метод в других местах проекта? Кто угодно из разработчиков мог вызвать публичный метод из любого другого места, и их все надо найти. Вы можете сказать, что современные IDE решают этот вопрос мгновенно, но что, если использовалась черная магия фреймворков?</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Все эти <code>__get()</code> и <code>__call()</code>, вызов метода по произвольному строковому индентификатору, поведения в Yii2, фасады в Laravel и т.п. Наличие таких подходов в проекте делает задачу нетривиальной.</p>
</div>
<p>Усложним ее ещё немного: мы нашли несколько мест, где метод <code>bar</code> использовался, но не вполне понятно, зачем, а авторы того кода уже не работают в вашей компании. Что делать в этом случае?</p>
<p>Добавим в копилку ещё одну потенциальную проблему. Какой-то разработчик решил немного изменить функционал <code>bar</code>, но не стал глубоко искать вызовы этого метода. А они были, но спрятанные.</p>
<h2>Выводы</h2>
<p>Публичный метод не решает проблему, озвученную для приватного, но добавляет ворох новых. Потому я однозначно рекомендую все поля, константы и методы классов делать приватными, если они не используются вне класса, в котором объявлены, и не предназначены для этого явно.</p>
<h2>Пример рефакторинга из private в public</h2>
<p>Итак, в нашем проекте есть версия класса <code>Foo</code> с приватным методом <code>bar</code>. В какой-то момент стало очевидно, что функционал этого метода нужен ещё в паре мест проекта. В большинстве случаев можно вынести <code>bar</code> в отдельный класс, который можно будет использовать где угодно в проекте с помощью любимого DI-контейнера. Итого полный код будет выглядеть так:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() {</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">interface</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> FooInterface</span><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B"> {</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> foo</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Foo</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> implements</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> FooInterface</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> {</span></span>
<span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">  __construct</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">private</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Bar</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">) {}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">  public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> foo</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">() {</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">    $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">bar</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">();</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // do stuff</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">  }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>В этом случае публичный контракт на функционал <code>bar</code> будет заключён ровно в тот момент, когда он стал нужен. Соответственно, и отношение к нему будет соответствующее: как к сознательно реализованному контракту, а не &quot;еще одному публичному методу, который навряд ли где-нибудь используется&quot;. Потому что в таком проекте ничего публичного просто так не делается.</p>
]]></content>
    <published>2023-04-30T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text"></title>
    <id>https://viktorprogger.name/ru/posts/ru.html</id>
    <link href="https://viktorprogger.name/ru/posts/ru.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>initialcontent</p>
]]></summary>
    <content type="html"><![CDATA[<p>initialcontent</p>
]]></content>
    <published>2026-08-29T21:02:16.000Z</published>
  </entry>
  <entry>
    <title type="text">Stateless сервисы в PHP</title>
    <id>https://viktorprogger.name/ru/posts/stateless-services-in-php.html</id>
    <link href="https://viktorprogger.name/ru/posts/stateless-services-in-php.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Представьте, что вы разработали сервис для обработки заказов, который хранит информацию о текущем пользователе и собирает товары в корзину. Всё работает отлично в простом веб-приложении, где каждый запрос обрабатывается отдельным PHP-процессом. Но однажды требования к проекту меняются.</p>
<img src="/stateless-services-php.png" style="width: 100%" alt="Stateless сервисы в PHP">
]]></summary>
    <content type="html"><![CDATA[<p>Представьте, что вы разработали сервис для обработки заказов, который хранит информацию о текущем пользователе и собирает товары в корзину. Всё работает отлично в простом веб-приложении, где каждый запрос обрабатывается отдельным PHP-процессом. Но однажды требования к проекту меняются.</p>
<img src="/stateless-services-php.png" style="width: 100%" alt="Stateless сервисы в PHP">
<!-- more -->
<h3>Ловушка долгоживущих процессов</h3>
<p>Ваше приложение растёт, и вы решаете оптимизировать его производительность, переведя на современный веб-сервер вроде <strong>RoadRunner</strong>, <strong>Swoole</strong> или <strong>FrankenPHP</strong>. Теперь PHP-процессы не умирают после каждого запроса, а <em>переиспользуются</em>. И тут начинается самое интересное: данные одного пользователя случайным образом &quot;просачиваются&quot; в запросы другого пользователя, потому что состояние сервиса сохраняется между запросами.</p>
<p>Или представьте, что вы решили обрабатывать заказы асинхронно через очередь сообщений. Воркер, обрабатывающий очередь, живёт долго и обрабатывает тысячи сообщений за один виток жизненного цикла. Если ваш сервис хранит состояние, каждое необработанное сообщение может повлиять на обработку следующих, создавая запутанные и сложные для отладки ошибки.</p>
<div class="hint-container info">
<p class="hint-container-title">Инфо</p>
<p>В таких долгоживущих процессах каждая строчка кода, сохраняющая состояние в свойствах класса, становится потенциальной проблемой:</p>
<ul>
<li>Состояние незаметно накапливается, потребляя всё больше памяти</li>
<li>Параллельные операции начинают конфликтовать друг с другом</li>
<li>Отладка становится настоящим квестом, потому что поведение сервиса зависит от его предыдущих вызовов</li>
</ul>
</div>
<h3>Эволюция использования</h3>
<p>Даже если ваш сервис сейчас используется в простом контексте, без этих модных долгоживущих состояний, его применение может эволюционировать:</p>
<ul>
<li>Код, который вызывался один раз, начинают вызывать в цикле</li>
<li>Простой обработчик HTTP-запроса переносят в консольную команду</li>
<li>Синхронные операции становятся асинхронными</li>
<li>Появляется необходимость масштабирования и параллельного выполнения</li>
</ul>
<p>В каждом из этих случаев наличие внутреннего состояния становится источником проблем и требует переработки кода.</p>
<h2>Решение: Сервисы без состояния</h2>
<p><strong>Stateless сервис</strong> - это класс, который не хранит внутреннего состояния между вызовами своих методов. Каждый метод работает только с теми данными, которые были переданы ему в качестве параметров.<br>
Такой подход позволяет абстрагироваться от контекста использования сервиса.
Stateless сервис может быть вызван и разово, и в цикле, и в long running приложении.
Ему это не важно.</p>
<p>Какие же есть альтернативы? Это сервисы с состоянием, или <em>stateful</em>. Рассмотрим пример, который я часто вижу в разных проектах.</p>
<h3>Пример сервиса с состоянием</h3>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderProcessor</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> PaymentGateway</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> ?</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">User</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $currentUser</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66"> null</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    </span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2"> __construct</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">PaymentGateway</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">)</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> setUser</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">User</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">void</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">currentUser</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> processOrder</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">array</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $items</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">void</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">        $total</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">calculateTotal</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$items</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">);</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">charge</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">$this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">currentUser</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$total</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">);</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> calculateTotal</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">array</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $items</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">float</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">        $subtotal</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2"> array_sum</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2">array_map</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">            fn</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$item</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">)</span><span style="--shiki-light:#383A42;--shiki-dark:#61AFEF"> => </span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$item</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">[</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'price'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">]</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> *</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $item</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">[</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'quantity'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">],</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">            $items</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        ));</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        </span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        return</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $subtotal</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> *</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> (</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">1</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> +</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">);</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>В этом примере обязательно нужно указывать корректного пользователя в качестве состояния сервиса. И результат выполнения метода <code>processOrder()</code> полностью от него зависит. Если по какой-то причине не был вызван метод <code>setUser()</code>, будет попытка списать деньги с другого человека. Что еще может пойти не так? Что угодно. Основная причина - человеческий фактор: можно быть уверенным, что рано или поздно какой-то из программистов проекта не уследит за корректной установкой пользователя в сервис, что приведет к серьезной ошибке. А вот, что может произойти технически:</p>
<ul>
<li>Метод забыли вызвать</li>
<li>После установки корректного пользователя установили другого (например, в момент обработки цепочки событий между вызовами <code>setUser()</code> и <code>processOrder()</code>)</li>
<li>После установки корректного пользователя была подмена инстанса сервиса</li>
</ul>
<div class="hint-container info">
<p class="hint-container-title">Инфо</p>
<p>Когда проект поддерживается годами, любая вероятность подобной ошибки возрастает практически до 100%. А между тем, исправить ее было бы очень легко.</p>
</div>
<h3>Пример stateless реализации</h3>
<p>Для того, чтобы избавиться от состояния в примере выше, достаточно убрать поле с пользователем и соответствующий метод, а самого пользователя добавить в параметры того метода, который действительно от него зависит:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderProcessor</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> PaymentGateway</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    </span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2"> __construct</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">PaymentGateway</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">)</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    </span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> processOrder</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">User</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">array</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $items</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">void</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">        $total</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">calculateTotal</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$items</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">);</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">charge</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$total</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">);</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> calculateTotal</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">array</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $items</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">float</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">        $subtotal</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2"> array_sum</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2">array_map</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">            fn</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$item</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">)</span><span style="--shiki-light:#383A42;--shiki-dark:#61AFEF"> => </span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$item</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">[</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'price'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">]</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> *</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $item</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">[</span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'quantity'</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">],</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">            $items</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        ));</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        </span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        return</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $subtotal</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> *</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> (</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">1</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> +</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">);</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3>Преимущества такого подхода</h3>
<ol>
<li>
<p>Предсказуемость:</p>
<ul>
<li>Поведение методов зависит только от входных параметров</li>
<li>Один и тот же ввод всегда даёт один и тот же результат (в этом примере в качестве результата мы всегда вызываем  <code>paymentGateway-&gt;charge()</code> с одними и теми же аргументами)</li>
<li>Нет неявных зависимостей между вызовами</li>
</ul>
</li>
<li>
<p>Простота тестирования:</p>
<ul>
<li>Не нужно заботиться о начальном состоянии</li>
<li>Тесты становятся проще и понятнее</li>
<li>Можно тестировать методы изолированно</li>
</ul>
</li>
<li>
<p>Потокобезопасность:</p>
<ul>
<li>Можно безопасно использовать в многопоточной среде</li>
<li>Нет проблем при масштабировании</li>
<li>Отсутствуют race conditions</li>
</ul>
</li>
</ol>
<h2>Разбираемся с разными типами данных в сервисах</h2>
<p>Не всё, что хранится в свойствах класса, является вредным состоянием. Давайте разберем три принципиально разных типа данных:</p>
<h3>1. Настройки (Configuration)</h3>
<p>Это неизменяемые значения, определяющие поведение сервиса:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderProcessor</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> readonly</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> readonly</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $minimumOrderAmount</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    </span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2"> __construct</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">, </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">float</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $minimumOrderAmount</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">)</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $taxRate</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">minimumOrderAmount</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $minimumOrderAmount</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Настройки допустимы в свойствах класса, потому что:</p>
<ul>
<li>Не меняются во время жизни объекта</li>
<li>Определяют базовое поведение сервиса</li>
<li>Являются частью конфигурации приложения</li>
<li>Не зависят от контекста выполнения</li>
</ul>
<h3>2. Зависимости (Dependencies)</h3>
<p>Это другие сервисы или ресурсы, необходимые для работы:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderProcessor</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2"> __construct</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        private</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> readonly</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderRepository</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $orderRepository</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">,</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        private</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> readonly</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> PaymentGateway</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $paymentGateway</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">,</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        private</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> readonly</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> LoggerInterface</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $logger</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    ) {}</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Зависимости можно хранить в свойствах, так как они:</p>
<ul>
<li>Являются инструментами для выполнения операций</li>
<li>Обычно сами stateless</li>
<li>Внедряются через конструктор</li>
<li>Не меняются во время работы</li>
</ul>
<h3>3. Эфемерное состояние (Ephemeral State)</h3>
<p>Это и есть то, чего следует избегать - данные, меняющиеся в процессе работы:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">// Анти-паттерн!</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> BadOrderProcessor</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    private</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> ?</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">User</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $currentUser</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#986801;--shiki-dark:#D19A66"> null</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    </span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // Избегайте таких методов!</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> setUser</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">User</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">): </span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B">void</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">        $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">currentUser</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2"> =</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2>Рекомендации по проектированию</h2>
<ol>
<li>
<p>Настройки:</p>
<ul>
<li>Передавайте через конструктор</li>
<li>Делайте неизменяемыми (<code>readonly</code>)</li>
<li>Используйте value objects для сложных конфигураций</li>
</ul>
</li>
<li>
<p>Зависимости:</p>
<ul>
<li>Внедряйте через конструктор</li>
<li>Используйте интерфейсы</li>
<li>Не меняйте после создания объекта</li>
</ul>
</li>
<li>
<p>Эфемерное состояние:</p>
<ul>
<li>Передавайте через параметры методов</li>
<li>Избегайте методов <code>set*</code></li>
<li>Используйте DTO для группировки параметров</li>
<li>Храните в БД или кэше при необходимости</li>
</ul>
</li>
</ol>
<h2>Заключение</h2>
<p>Проектирование сервисов без состояния - это не просто модное архитектурное решение. Это необходимость в современном PHP-разработке, где приложения должны быть готовы к масштабированию, параллельному выполнению и работе в долгоживущих процессах.</p>
<p>Правильное понимание различий между настройками, зависимостями и эфемерным состоянием позволяет создавать чистые и поддерживаемые сервисы, которые легко тестировать и безопасно использовать в любом контексте.</p>
<p>Помните: сервис, который сегодня обрабатывает один запрос, завтра может оказаться частью сложного асинхронного процесса. Проектируя его без состояния с самого начала, вы защищаете себя от множества потенциальных проблем в будущем.</p>
]]></content>
    <category term="Разработка: техники"/>
    <published>2025-04-21T00:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Два типа классов для вашего проекта</title>
    <id>https://viktorprogger.name/ru/posts/two-class-types-for-your-project.html</id>
    <link href="https://viktorprogger.name/ru/posts/two-class-types-for-your-project.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Вопрос о зоне ответственности того или иного класса в вопросах архитектуры не менее важен, чем ее стратегическое планирование. Сегодня я расскажу о том, как облегчить себе эту работу. Мы с вами решим довольно большое количество вопросов, из бесконечного многообразия классов оставив всего 2 их типа: ДТО и Сервис. Эти типы классов покрывают 90-99% всего необходимого в любом проекте. Итак, разберём, что они из себя представляют, и чем же так хорошо оставить только их.</p>
<img src="/chaos.jpg" style="width: 100%" alt="Два типа классов для вашего проекта">
]]></summary>
    <content type="html"><![CDATA[<p>Вопрос о зоне ответственности того или иного класса в вопросах архитектуры не менее важен, чем ее стратегическое планирование. Сегодня я расскажу о том, как облегчить себе эту работу. Мы с вами решим довольно большое количество вопросов, из бесконечного многообразия классов оставив всего 2 их типа: ДТО и Сервис. Эти типы классов покрывают 90-99% всего необходимого в любом проекте. Итак, разберём, что они из себя представляют, и чем же так хорошо оставить только их.</p>
<img src="/chaos.jpg" style="width: 100%" alt="Два типа классов для вашего проекта">
<!-- more -->
<div class="hint-container info">
<p class="hint-container-title">Инфо</p>
<p>Здесь и дальше в качестве примера будем использовать интернет-магазин.</p>
</div>
<p>Существует два основных подхода к организации работы с сущностями: так называемые &quot;богатая&quot; и &quot;анемичная&quot; модели. Богатая модель подразумевает, что сущность сама знает, что и как с ней можно сделать. Итак, в интернет-магазине есть такая сущность, как &quot;заказ&quot;. В богатой модели предметной области его интерфейс будет выглядеть примерно так:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">interface</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderInterface</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getId</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderId</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getProducts</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> array</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getCustomer</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Customer</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> getStatus</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderStatus</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> create</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> void</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> update</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> void</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> delete</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">():</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> void</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> moveTo</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">OrderStatus</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $status</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">):</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> void</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF"> removeProduct</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B">Product</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $product</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">):</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> void</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">    // etc.</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#E5C07B">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>То есть он содержит методы, не только сообщающие информацию о состоянии заказа (геттеры), но и каким-то образом его изменяющие: сохраняющие в БД, изменяющие статус и пр.</p>
<p>Логика подсказывает, что у такого подхода будут проблемы с принципами Single Responsibility и Interface Segregation из SOLID, а связанность (coupling) кода будет крайне высокой (вы только представьте, сколько действий в других подсистемах надо совершить, чтобы создать заказ: платежи, логистика, склад и пр.). Кроме того, я ни разу не видел успешного применения этого подхода. Не исключаю вариант, что просто не умею готовить богатую модель. Так что если у вас будет пример проекта, в котором она оправданно применена, буду рад увидеть его код.</p>
<p>Итак, мы подробнее остановимся на &quot;анемичной модели&quot;. Называется она так потому, что у сущности нет никаких дополнительных обязанностей и знаний о чем бы то ни было, кроме ее собственного состояния. То есть, если брать пример выше, у Заказа останутся только геттеры. А начиная с php8.1, можно даже без них:</p>
<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">class</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> Order</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">{</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">    public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> function</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2"> __construct</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> readonly</span><span style="--shiki-light:#C18401;--shiki-dark:#E5C07B"> OrderId</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $id</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic">        /** </span><span style="--shiki-light:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#C678DD;--shiki-dark-font-style:italic">@var</span><span style="--shiki-light:#C18401;--shiki-light-font-style:italic;--shiki-dark:#E5C07B;--shiki-dark-font-style:italic"> Product</span><span style="--shiki-light:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#E5C07B;--shiki-dark-font-style:italic">[]</span><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic"> */</span></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">        public</span><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD"> readonly</span><span style="--shiki-light:#A626A4;--shiki-dark:#E5C07B"> array</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75"> $products</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        public readonly Customer $customer;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">        public readonly OrderStatus $status;</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    ) {</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    }</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Итак, сущность в анемичной модели предметной области является DTO (Data Transfer Object, объектом для передачи данных).</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Есть и третий подход, встречающийся в 99% случаев. Это комбинированный подход. Когда нет четкого разделения на роли классов, и в сущностях встречается бизнес-логика, а в сервисах - состояние. Это самый неприятный вариант, т.к. в поддержке он становится все дороже и дороже с каждым днем. Развивать систему становится все сложнее, удовольствия от работы с ней у программистов больше не появляется, а поделать с этим без глобального рефакторинга ничего нельзя.</p>
</div>
<h2>Data Transfer Object</h2>
<p>DTO - это представление данных, которое кочует из одного места в системе в другое. В идеале DTO создается на входе в систему и подается на выход из нее. Например, веб-контроллер: пользователь присылает данные, в контроллер они приходят в виде <code>$_POST</code>-массива, мы тут же формируем из них соответствующий DTO. И уже его отдаем внутренним сервисам системы. И внутри системы данные ходят исключительно в виде DTO.</p>
<p>Ключевыми особенностями DTO являются два нюанса:</p>
<h3>Только представление данных, и ничего больше</h3>
<p>В DTO по определению отсутствует какая бы то ни было логика. Это исключительно представление данных.<br>
Такой подход позволяет использовать DTO для сквозной передачи данных между разными подсистемами, в том числе, когда эта передача осуществляется по сети: через API, очереди и т.п. Поскольку это лишь данные, их легко сериализовывать и десериализовывать. А отсутствие в них бизнес-логики гарантирует отсутствие сайд-эффектов от передачи объекта в очередной сервис или подсистему.</p>
<h3>Иммутабельность</h3>
<p>DTO по сути неизменяемы, и это их главное преимущество. Зачем? А представим, что DTO может измениться.<br>
Допустим, у нас есть такой код: <code>$service-&gt;foo($dto);</code>, и мы знаем, что методу <code>foo</code> вообще незачем изменять состояние переданной в него DTO. Но время идет, код меняется, и где-то ниже по цепочке вызовов, не в самом <code>foo</code> появилось нечто подобное: <code>$dto-&gt;bar = 'baz';</code>. Как на это отреагирует код, который идет после вызова <code>foo</code>? Когда вы заметите, что поведение изменилось? Сколько часов уйдет на отладку?<br>
Либо мы можем сразу сделать объект иммутабельным любым из этих способов:</p>
<ul>
<li>пометить все поля <code>readonly</code></li>
<li>сделать их <code>private</code> и обеспечить доступ через геттеры</li>
<li>пометить класс <code>@psalm-immutable</code>, если Psalm используется в CI</li>
</ul>
<p><em>Как же тогда внести изменения в данные?</em>
Никак. В этом и суть. Если бизнес-логика подразумевает изменение данных - необходимо создать новый объект с этими новыми данными. Иногда DTO может само подразумевать подобные операции. Для этого в соглашениях команды YiiSoft <a href="https://github.com/yiisoft/docs/blob/master/010-code-style.md#immutable-methods" target="_blank" rel="noopener noreferrer">есть <code>with</code>-методы</a>, возвращающие новый объект того же класса с измененными данными. Лично мне приятнее называть переменную <code>$instance</code>, а не <code>$new</code>, но это уже мелочи.</p>
<h2>Сервис</h2>
<p>Второй тип классов, необходимых в любом проекте. В противовес DTO, сервисы, наоборот, не имеют состояния и реализуют какую-то логику. Это репозитории, контроллеры, фабрики и прочие виды классов, которые <em>что-то делают</em>. Часто сервисы выполняют некоторую работу над данными, то есть над передаваемыми им DTO.<br>
Каких же ошибок стоит избегать при написании сервиса и какую выгоду мы при этом получим?</p>
<h3>Хранение состояния</h3>
<p>Это единственная по-настоящему грубая ошибка при проектировании любого сервиса. Если сервис хранит некое состояние, есть большие шансы получить неожиданные сайд-эффекты при его повторном использовании. Если сервис не хранит никакого состояния, его легко можно использовать снова в другом месте. Либо в том же, в рамках того же php-процесса. А это не только облегчает дальнейшую разработку, но и даёт возможность использовать инструменты вроде RoadRunner и Swoole.<br>
Итак, неправильно: <code>$service-&gt;setData($data)-&gt;foo()</code>, правильно: <code>$service-&gt;foo($data)</code>. Если вам все же кажется необходимым в вашем сервисе оставить состояние для работы каких-то отдельных методов, вполне вероятно, что вам стоит разделить этот сервис на два.</p>
<p>Тут стоит сделать ремарку насчёт того, что такое состояние у класса. В его полях может быть различная полезная нагрузка: зависимости, настройки, кеш и состояние.</p>
<ul>
<li>Зависимости - это другие сервисы, позволяющие текущему делать его работу. Просто следуем принципу dependency Inversion и доставляем классу его зависимости в конструктор.</li>
<li>Настройки - значения, позволяющие сервису работать. Данные для подключения к БД у репозитория, веб-адрес для API-клиента, префикс у кеша и т.п.</li>
<li>Рантайм-кеш хранит в себе уже вычисленные значения в случае, если эти значения не изменяются со временем, а их повторное вычисление ресурсозатратно. Выглядит его использование примерно так:<div class="language-php line-numbers-mode" data-highlighter="shiki" data-ext="php" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-php"><span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">if</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> (</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">!</span><span style="--shiki-light:#0184BC;--shiki-dark:#56B6C2">isset</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">$this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">cache</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">[</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$id</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">]) {</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B">  $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">cache</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">[</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$id</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">] </span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">build</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">(</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$id</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">);</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A626A4;--shiki-dark:#C678DD">return</span><span style="--shiki-light:#E45649;--shiki-dark:#E5C07B"> $this</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">-></span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">cache</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">[</span><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">$id</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">];</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div></li>
<li>Состояние же не задаётся глобально для всего проекта/модуля, а изменяется в зависимости от контекста. Это единственный вид данных в сервисе, который может меняться в течение выполнения программы. Именно над ним или с его помощью сервис выполняет какую-то работу.</li>
</ul>
<p>Таким образом, &quot;хорошие&quot; сервисы могут иметь зависимости, настройки и рантайм-кеш, а DTO - только состояние.</p>
<p>Конечно, в мире разработки есть и другие архитектурные шаблоны, не укладывающиеся в эти две категории. Но бывают нужны они значительно реже. Например, Строитель мне понадобилось реализовать лишь однажды за 10 лет работы.</p>
<p>А какие шаблоны проектирования, не являющиеся ни DTO, ни сервисом, нужны были вам? Рассказать и подискутировать всегда можно в <a href="https://t.me/viktorprogger_channel_ru" target="_blank" rel="noopener noreferrer">моем телеграм-чате</a>.</p>
]]></content>
    <category term="Разработка: техники"/>
    <published>2022-06-16T21:00:00.000Z</published>
  </entry>
  <entry>
    <title type="text">Пример настройки xDebug в докере</title>
    <id>https://viktorprogger.name/ru/posts/xdebug-docker-config-example.html</id>
    <link href="https://viktorprogger.name/ru/posts/xdebug-docker-config-example.html"/>
    <updated>2026-08-29T21:02:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Когда-то давно я собрал рабочий комбайн из PHP, xDebug, Docker и PhpStorm. С тех пор я таскаю его из проекта в проект и горя не знаю. Для тех, у кого настройка локального окружения с докером и xDebug вызывает сложности, выкладываю этот конфиг с пояснениями. Ниже мы напишем образ контейнера с установленным в нем xDebug, настроим PhpStorm и разберем рабочую конфигурацию Docker Compose.</p>
<img src="/xdebug-settings-example.png" style="width: 100%" alt="Пример настройки xDebug в докере">
]]></summary>
    <content type="html"><![CDATA[<p>Когда-то давно я собрал рабочий комбайн из PHP, xDebug, Docker и PhpStorm. С тех пор я таскаю его из проекта в проект и горя не знаю. Для тех, у кого настройка локального окружения с докером и xDebug вызывает сложности, выкладываю этот конфиг с пояснениями. Ниже мы напишем образ контейнера с установленным в нем xDebug, настроим PhpStorm и разберем рабочую конфигурацию Docker Compose.</p>
<img src="/xdebug-settings-example.png" style="width: 100%" alt="Пример настройки xDebug в докере">
<!-- more -->
<h2>Dockerfile</h2>
<p>Начнем с Dockerfile. Установка xDebug в поддерживаемых версиях php стала совсем тривиальной. Надо лишь установить PECL и вызвать <code>pecl install</code>, после чего - включить xDebug в настройках php. Для образа, основного на Alpine (например, <code>php:8.2-fpm-alpine</code>), эта команда выглядит вот так:</p>
<div class="language-dockerfile line-numbers-mode" data-highlighter="shiki" data-ext="dockerfile" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-dockerfile"><span class="line"><span style="--shiki-light:#4078F2;--shiki-dark:#61AFEF">RUN</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> apk add --no-cache linux-headers  \</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; apk add --update --no-cache --virtual .build-dependencies $PHPIZE_DEPS </span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">\ </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> </span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; pecl install xdebug </span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">\ </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> </span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; docker-php-ext-enable xdebug </span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">\ </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> </span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; pecl clear-cache </span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">\ </span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF"> </span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">    &#x26;&#x26; apk del .build-dependencies</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Готово, образ с xDebug собран!</p>
<h2>PhpStorm</h2>
<p>Открываем окно настроек, идем в PHP -&gt; Servers и добавляем новый сервер, обязательно указывая корректные маппинги.</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Маппинг - это сопоставление локальных файлов и файлов на сервере. В нашем случае сервер - это контейнер, внутри которого находится приложение.</p>
</div>
<p><img src="/server-settings.png" alt></p>
<p>Самые важные части на скриншоте выделены.</p>
<ul>
<li>
<p><strong>Absolute path on the server</strong>: указываем, по какому пути доступны файлы проекта в контейнере В примере <code>docker-compose.yml</code> выше я прокинул их томом в контейнер по пути <code>/var/www</code></p>
</li>
<li>
<p><strong>Name</strong>. Да, название сервера в данном случае играет <em>ключевую</em> роль, хотя обычно это поле - исключительно для ввода человекопонятного названия. Для корректной работы с <code>xDebug</code> оно должно полностью совпадать с тем, что мы написали в переменную окружения <code>PHP_IDE_CONFIG</code>. В конфиге выше написано <code>serverName=docker</code>, поэтому сервер в настройках PhpStorm я назвал тоже <code>docker</code>. Нужно это для того, чтобы PhpStorm смог сопоставить локальные файлы с файлами на сервере (в контейнере).</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Есть и другой вариант их сопоставления: вместо <code>PHP_IDE_CONFIG</code> можно использовать сопоставление по полям <code>Host</code> и <code>Port</code>, но тогда не будет работать отладка скриптов, запускаемых из консоли.</p>
</div>
</li>
</ul>
<h2>docker-compose.yml</h2>
<p>Минимальный конфиг <code>docker-compose</code> у меня выглядит обычно вот так:</p>
<div class="language-yml line-numbers-mode" data-highlighter="shiki" data-ext="yml" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-yml"><span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">version</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"3.8"</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">services</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">  php</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    build</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      dockerfile</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">.docker/php/Dockerfile</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      context</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">./</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    user</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"${USER_ID:-1000}:${GROUP_ID:-1000}"</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    restart</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">unless-stopped</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    volumes</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">./:/var/www</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    working_dir</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">/var/www</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    environment</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      PHP_IDE_CONFIG</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${PHP_IDE_CONFIG:-serverName=docker}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      XDEBUG_MODE</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">${XDEBUG_MODE:-off}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      XDEBUG_CONFIG</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">"client_host=host.docker.internal"</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">      XDEBUG_TRIGGER</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">: </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">'yes'</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    extra_hosts</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#50A14F;--shiki-dark:#98C379">host.docker.internal:${HOST_IP:-172.17.0.1}</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">    dns</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">:</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">1.1.1.1</span></span>
<span class="line"><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">      - </span><span style="--shiki-light:#986801;--shiki-dark:#D19A66">8.8.8.8</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Настройка <code>xDebug</code> здесь происходит через переменные окружения, спасибо Дерику Ретансу за столь удобный способ. Все возможные настройки xDebug и соответствующие переменные окружения можно посмотреть на <a href="https://xdebug.org/docs/all_settings" target="_blank" rel="noopener noreferrer">специальной странице</a>.</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Нотация <code>${VARIABLE:-someValue}</code> принесена в конфиги <code>docker-compose</code> из <code>bash</code>. Значит она следующее: &quot;вернуть значение переменной окружения <code>VARIABLE</code>, но если она не установлена или пуста, вернуть <code>someValue</code>&quot;. Другими словами, <code>someValue</code> - значение по умолчанию. Чтобы оно сработало, нужно указать символы <code>:-</code> между названием переменной окружения и этим значением.</p>
</div>
<p>Изменять значение переменных окружения при локальной разработке очень удобно через файл <code>.env</code>. Он может содержать что-то вроде такого:</p>
<div class="language-dotenv line-numbers-mode" data-highlighter="shiki" data-ext="dotenv" style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-dotenv"><span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic"># Изменим название сервера PHP в настройках PhpStorm с docker на project-server</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">PHP_IDE_CONFIG</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">serverName=project-server</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic"># Включим xDebug, ведь по умолчанию он выключен</span></span>
<span class="line"><span style="--shiki-light:#E45649;--shiki-dark:#E06C75">XDEBUG_MODE</span><span style="--shiki-light:#383A42;--shiki-dark:#56B6C2">=</span><span style="--shiki-light:#383A42;--shiki-dark:#ABB2BF">develop,debug,coverage</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Минус этого подхода: чтобы debug-сессия не запускалась, надо либо убрать переменную окружения <code>XDEBUG_TRIGGER</code>, либо убрать <code>debug</code> из <code>XDEBUG_MODE</code>. В первом случае вы оставите себе возможность запускать дебаг-сессию через get-параметры и cookie, например с помощью <a href="https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc" target="_blank" rel="noopener noreferrer">расширения для Chrome</a>.</p>
<p>Отдельного внимания заслуживает настройка <code>extra_hosts</code> в конфиге <code>docker-compose.yml</code>. Она добавляет в <code>hosts</code>  контейнера указанные в ней записи, в данном случае - что домен <code>host.docker.internal</code> следует искать по IP <code>172.17.0.1</code>. Обычно хост-машина доступна именно по этому адресу, но бывают и исключения, поэтому конфигурируется IP тоже через переменную окружения: чтобы ее можно было изменить.</p>
<div class="hint-container note">
<p class="hint-container-title">Заметка</p>
<p>Настройка <code>extra_hosts</code> нужна только для Linux, на Windows и MacOS внутри контейнера по умолчанию доступен хост <code>host.docker.internal</code></p>
</div>
<h2>Готовый проект</h2>
<p>Простой проект с указанными настройками я выложил на гитхаб по адресу https://github.com/viktorprogger/xdebug-sample. Чтобы проверить на нем, что указанные настройки работают, склонируйте его к себе и выполните следующие шаги:</p>
<ol>
<li>Настроить PhpStorm как я <a href="#phpstorm">описывал выше</a></li>
<li>Создать файл <code>.env</code> со следующим содержимым:<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#383A42;--shiki-dark:#abb2bf;--shiki-light-bg:#FAFAFA;--shiki-dark-bg:#282c34"><pre class="shiki shiki-themes one-light one-dark-pro vp-code"><code class="language-"><span class="line"><span>XDEBUG_MODE=debug</span></span>
<span class="line"><span>XDEBUG_TRIGGER=yes</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div></li>
<li>Поставить брейкпоинт в файле <code>index.php</code></li>
<li>Включить прослушивание коннектов от xDebug в PhpStorm</li>
<li>Удалить <code>extra_hosts</code> из <code>docker-compose.yml,</code>если вы используете Windows или MacOS</li>
<li>Запустить команду <code>docker-compose up -d</code>. Поднимать сервисы нам ни к чему, а вот сеть создать надо.</li>
<li>Запустить команду <code>docker-compose run --rm php php index.php</code>. Перед первым запуском соберется образ контейнера, это займет несколько минут.</li>
<li>Вуаля, выполнение скрипта остановилось на вашем брейкпоинте!</li>
</ol>
<p><img src="/xdebug-running.png" alt></p>
]]></content>
    <category term="Разработка: техники"/>
    <published>2023-01-21T00:00:00.000Z</published>
  </entry>
</feed>