<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xml:base="https://graker.ru/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Laravel</title>
    <link>https://graker.ru/</link>
    <description>RSS домовой страницы</description>
    <language>ru</language>
    
    <item>
  <title>Интеграция Доктуриста с Точкой</title>
  <link>https://graker.ru/news/2026/06/09/tochka-bank-integration</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Турагенты могут вести десятки броней одновременно. Каждый день — новые оплаты от туристов, каждый день нужно вручную открыть банк, найти платёж, отметить в CRM оплату по туру (иначе как отслеживать что ничего не забыл?) Звучит как мелочь — но это копится ежедневно, сбивает с ритма и иногда порождает ошибки. И в целом это довольно нудно — переписывать руками из одного места в другое. А что нудно — нам делать не очень хочется.&lt;/p&gt;
&lt;p&gt;Поэтому мы добавили в &lt;a href=&quot;https://docturist.ru&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Доктурист&lt;/a&gt; возможность импорта платежей из банков и начали с интеграции API банка Точка. Здесь рассмотрим кейс о том, как подключили Точку к Доктуристу: сделали автоматические уведомления о платежах, умную привязку к бронированиям и создание платёжных ссылок прямо со страницы брони. И о нескольких нетривиальных вещах, с которыми пришлось разобраться по дороге. Технических и не очень.&lt;/p&gt;
&lt;h3&gt;Дано&lt;/h3&gt;
&lt;p&gt;Турагенты постоянно работают с деньгами, основных операций при этом две: принять (пред)оплату от туриста, отправить оплату оператору. Доктурист уже умеет вести историю платежей и показывать их в симпатичном таймлайне:&lt;/p&gt;&lt;/div&gt;
&lt;figure data-component-id=&quot;grakerru:figure&quot; class=&quot;figure figure--center figure--medium&quot;&gt;
      &lt;a href=&quot;/sites/default/files/uploads/2026-06/payments.png&quot; data-lightbox=&quot;node-2216&quot;&gt;
    &lt;img loading=&quot;lazy&quot; srcset=&quot;/sites/default/files/styles/content_medium/public/uploads/2026-06/payments.png.webp?itok=WkThDgBy 850w, /sites/default/files/styles/content_medium_2x/public/uploads/2026-06/payments.png.webp?itok=_Sug1HT4 1700w&quot; sizes=&quot;(min-width: 1200px) 850px, (min-width: 768px) 75vw, calc(100vw - 30px)&quot; width=&quot;850&quot; height=&quot;735&quot; src=&quot;/sites/default/files/styles/content_medium/public/uploads/2026-06/payments.png.webp?itok=WkThDgBy&quot; alt=&quot;payments.png&quot; /&gt;



&lt;/a&gt;
    &lt;/figure&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;&lt;strong&gt;Задача&lt;/strong&gt;: нужно сделать что-то более автоматизированное, чем старательно переписывать входящие платежи из банк-клиента. То есть получать входящие платежи в Доктуристе.&lt;/p&gt;
&lt;p&gt;Кроме того, агенты часто принимают оплату по ссылкам: создают в банковском приложении ссылку на оплату определенной суммы картой и/или СБП и отправляют туристу. Это такой облегченный эквайринг, когда можно не делать полноценную форму оплаты. И банк не против (если соблюсти определенные правила). &lt;strong&gt;Задача 2&lt;/strong&gt;: почему бы не дать пользователям возможность создавать ссылки на оплату прямо из интерфейса Доктуриста. Чтобы они могли копировать их и отправлять клиентам, а банковское приложение открывать было не нужно.&lt;/p&gt;
&lt;p&gt;Вот что конкретно мы научили Доктурист делать:&lt;/p&gt;
&lt;p&gt;— получать уведомления от банка о каждом входящем платеже;&lt;br&gt;
— автоматически привязывать платеж к бронированию (если это возможно);&lt;br&gt;
— давать пользователю привязывать платеж к бронированию вручную (если автоматически не вышло);&lt;br&gt;
— создавать ссылки на оплату картой и/или СБП.&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;С чем столкнулись в процессе&lt;/h3&gt;
&lt;p&gt;Для входящих платежей Точка отправляет уведомления на зарегистрированные вебхуки, причем вебхуки регистрировать можно программно. Пользователю нужно лишь добавить токен для интеграции и проставить несколько разрешений в банк-клиенте. Уведомления по вебхукам приходят практически мгновенно.&lt;/p&gt;
&lt;h4&gt;Вебхуки как подписанный JWT&lt;/h4&gt;
&lt;p&gt;Первое, с чем пришлось разобраться — формат входящих уведомлений от банка. В отличие от сервисов, которые присылают простой JSON с подписью в заголовке, Точка отправляет тело вебхука целиком как &lt;em&gt;подписанный JWT.&lt;/em&gt; Это значит, что нельзя просто прочитать JSON — нужно сначала верифицировать подпись публичным ключом банка.&lt;/p&gt;
&lt;p&gt;Контроллер читает сырое тело запроса и декодирует JWT через библиотеку &lt;code&gt;firebase/php-jwt&lt;/code&gt;:&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;$token = $request-&amp;gt;getContent();
$pem   = config(&amp;#039;services.tochka.jwt_public_key_pem&amp;#039;);
$data  = JWT::decode($token, new Key($pem, &amp;#039;RS256&amp;#039;));&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;При любой ошибке декодирования — логируем и тем не менее отвечаем http 200. Это важно: Точка интерпретирует любой не-200 как сбой и будет повторно доставлять уведомление. А так — бизнес-логика не выполняется, но и банк не уходит в цикл отправки.&lt;/p&gt;
&lt;p&gt;Тестирование этой механики потребовало отдельных настроек инфраструктуры: RSA-пара генерируется прямо в тестовом классе, публичный ключ подставляется в конфиг, и тогда мы подписываем тестовый пейлоад так же, как это делает Точка и расшифровываем как живое приложение.&lt;/p&gt;
&lt;h4&gt;Умная авто-линковка&lt;/h4&gt;
&lt;p&gt;Когда платёж приходит от банка, система пытается найти бронь автоматически — по тексту назначения платежа (например, «Оплата по бронированию ДЕМ-001»). Здесь возникает классическая проблема строкового поиска: если в агентстве есть брони &lt;code&gt;ДЕМ-10&lt;/code&gt; и &lt;code&gt;ДЕМ-100&lt;/code&gt;, то поиск &lt;code&gt;ДЕМ-10&lt;/code&gt; найдёт обе. Но мы не хотим ошибиться при связи броней с платежами, поэтому автоматически связываем только если найден ровно 1 результат.&lt;/p&gt;
&lt;p&gt;Если есть хотя бы еще одна бронь, чей номер начинается с найденного — линковка не происходит, платёж остаётся непривязанным. Пользователь увидит его в ленте и привяжет вручную. Для удобства привязки, поиск отфильтровывает старые, выполненные, оплаченные и отмененные брони.&lt;/p&gt;
&lt;h4&gt;Очень умная авто-линковка&lt;/h4&gt;
&lt;p&gt;Чтобы пользователи не забывали включать в назначение платежа номер брони, сделали, чтобы дефолтный текст при генерации платежной ссылки уже включал номер. Но боевое использование показало, что почти все платежи проходят мимо и ни с чем не линкуются.&lt;/p&gt;&lt;/div&gt;
&lt;figure data-component-id=&quot;grakerru:figure&quot; class=&quot;figure figure--center figure--medium&quot;&gt;
      &lt;a href=&quot;/sites/default/files/uploads/2026-06/payments_bank.png&quot; data-lightbox=&quot;node-2216&quot;&gt;
    &lt;img loading=&quot;lazy&quot; srcset=&quot;/sites/default/files/styles/content_medium/public/uploads/2026-06/payments_bank.png.webp?itok=YzndABJ9 850w, /sites/default/files/styles/content_medium_2x/public/uploads/2026-06/payments_bank.png.webp?itok=P7ZaOJBw 1470w&quot; sizes=&quot;(min-width: 1200px) 850px, (min-width: 768px) 75vw, calc(100vw - 30px)&quot; width=&quot;850&quot; height=&quot;697&quot; src=&quot;/sites/default/files/styles/content_medium/public/uploads/2026-06/payments_bank.png.webp?itok=YzndABJ9&quot; alt=&quot;payments_bank.png&quot; /&gt;



&lt;/a&gt;
    &lt;/figure&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;После некоторых исследований оказалось, что хотя платежи в банк-клиенте показываются с нормальным назначением типа &lt;em&gt;Оплата тура ФИО, номер брони&lt;/em&gt;, как мы задумали, в вебхук почему-то вместо этого прилетает &lt;em&gt;Оплата по QR-коду ID QRABCD12345678 …&lt;/em&gt;. Пришлось реализовать дополнительный метод из API, который при получении платежа с таким описанием дергает Точку еще раз, чтобы получить инфу о данном QR-коде. В этой информации назначение платежа уже правильно отражается и по нему можно линковать платеж к брони.&lt;/p&gt;
&lt;h4&gt;Валютная конвертация при привязке&lt;/h4&gt;
&lt;p&gt;Точка естественно присылает суммы в рублях, поскольку расчёты в России могут быть только рублевые. Но брони в Доктуристе могут быть заведены и в USD, и в EUR, так как туроператоры могут работать в этих единицах, а платежи принимать по курсу. Когда пользователь вручную привязывает рублёвый платёж к заявке в EUR, система должна рассчитать сумму в EUR, чтобы записать её в правильной валюте. При этом у каждого туроператора свой курс конвертации.&lt;/p&gt;
&lt;p&gt;Для этих случаев сделали два сценария:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Валютная заявка + override&lt;/strong&gt;&amp;nbsp;→ пользователь вводит вручную, какое количество USD/EUR получается из платежа (например, «150 EUR»), мы используем это значение в сохраняемом платеже по брони и вместе с ним сохраняем расчетный курс валюты.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Валютная заявка, без override&lt;/strong&gt;&amp;nbsp;→ берём последний курс ЦБ (он обновляется каждый день). В таком случае мы отмечаем связанный платеж как требующий внимания, поскольку авторасчет по курсу ЦБ это &lt;em&gt;примерный&lt;/em&gt; расчет, а курс у операторов всегда выше.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Дефолтные значения при создании платежных ссылок&lt;/h4&gt;
&lt;p&gt;В процессе использования интеграции оказалось, что во многих случаях пользователи хотят создавать платежные ссылки с одним и тем же пресетом, например «Карта выключена, СБП включен». Ошибка в выборе средства оплаты может привести к денежным потерям (комиссия банка на эквайринге и СБП очень разная), что конечно недопустимо. &lt;/p&gt;&lt;/div&gt;
&lt;figure data-component-id=&quot;grakerru:figure&quot; class=&quot;figure figure--center figure--medium&quot;&gt;
      &lt;a href=&quot;/sites/default/files/uploads/2026-06/payment_links.png&quot; data-lightbox=&quot;node-2216&quot;&gt;
    &lt;img loading=&quot;lazy&quot; srcset=&quot;/sites/default/files/styles/content_medium/public/uploads/2026-06/payment_links.png.webp?itok=BPA-XiWv 850w, /sites/default/files/styles/content_medium_2x/public/uploads/2026-06/payment_links.png.webp?itok=AW8FCDqT 1697w&quot; sizes=&quot;(min-width: 1200px) 850px, (min-width: 768px) 75vw, calc(100vw - 30px)&quot; width=&quot;850&quot; height=&quot;220&quot; src=&quot;/sites/default/files/styles/content_medium/public/uploads/2026-06/payment_links.png.webp?itok=BPA-XiWv&quot; alt=&quot;payment_links.png&quot; /&gt;



&lt;/a&gt;
    &lt;/figure&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Пришлось реализовать дефолтную настройку для пользователей, указывающую, какие способы оплаты по умолчанию включены и какие выключены, чтобы снизить вероятность ошибки.&lt;/p&gt;
&lt;h4&gt;Песочница&lt;/h4&gt;
&lt;p&gt;В самом начале разработки из документации на API Точки выяснилось, что у них нет полноценной песочницы, позволяющей проводить «игрушечные» платежи, есть только фиксированный набор тестовых запросов, которые, если сделаны верно, возвращают фиксированные ответы. Тестовые запросы конечно небесполезны, но без песочницы невозможно, например, провести полный тестовый цикл: генерация платежной ссылки → её оплата → получение платежа → разбор и линковка к брони. Только по частям.&lt;/p&gt;
&lt;p&gt;Собственно, создание ссылок пришлось по большей части тестировать наживую, поскольку оно работает только на аккаунтах, для которых уже активирован эквайринг, а для остальных аккаунтов кидает ошибку 403 &quot;Forbidden by consent&quot;.&lt;/p&gt;
&lt;p&gt;Но зато в банке есть линия поддержки по интеграции API, которая действительно отвечает по имейлу и даже дает подробную информацию, которой в документации не нашлось.&lt;/p&gt;
&lt;h3&gt;Результаты&lt;/h3&gt;
&lt;p&gt;100+ тестов на разные случаи жизни. Мы сделали верификацию JWT-подписи, дедупликацию, авто-линковку с граничными случаями, валютную конвертацию, права доступа. Интеграция API работает с Laravel Horizon, то есть все входящие и исходящие процессы (уведомления о платежах, создание платежной ссылки, регистрация пользователя в системе, регистрация вебхуков) запускаются асинхронно в очередях. А для уведомления ожидающих пользователей используются &lt;a href=&quot;/news/2026/01/20/laravel-reverb-broadcasting-and-websockets&quot;&gt;вебсокеты через Reverb&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Реализация позволила создать и протестировать логику для импорта платежей и создания платежных ссылок, которая не обязана привязываться к одному банку. Теперь необходимо добавить уровень абстракции между платежами/ссылками и API банка, и можно подключать другие банки к системе уже с меньшими трудозатратами. Следующий кандидат на интеграцию — Альфа.&lt;/p&gt;
&lt;p&gt;Интеграция уже работает в продакшене. Агенты создают платёжные ссылки прямо со страницы брони и отправляют их туристам — без переключения в банковское приложение. Входящие платежи появляются в системе автоматически и в большинстве случаев сами привязываются к нужному бронированию. Руками нужно разбирать только то, что система не смогла распознать однозначно.&lt;/p&gt;&lt;/div&gt;
</description>
  <pubDate>Mon, 08 Jun 2026 22:30:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2216 at https://graker.ru</guid>
    </item>
<item>
  <title>Про пакет Canvas в Laravel</title>
  <link>https://graker.ru/news/2026/04/16/laravel-canvas-package</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Когда готовил &lt;a href=&quot;https://docturist.ru&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Доктурист&lt;/a&gt; к открытию, решил что должна быть все-таки у проекта какая-никакая новостная лента. До этого я сделал самый примитивный вариант: вывод ленты заголовков и текстовых сообщений с датами.  &lt;/p&gt;
&lt;p&gt;Решил, что этого конечно мало и какой-никакой джентльменский набор функций для постинга надо иметь. Хотя бы:&lt;br&gt;
— редактор с форматированием и загрузкой картинок&lt;br&gt;
— рубрики и/или теги&lt;br&gt;
— редактируемые адреса страниц&lt;br&gt;
— минимальную админку, чтобы из панели Orchid туда заходить&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Конечно, писать я все это не хотел. Точнее я может и хотел бы, но каждый день старался сокращать число задач до запуска, а не увеличивать. Стал смотреть готовое.  &lt;/p&gt;
&lt;p&gt;Поиск и чат жопотэ присоветовали пакет &lt;a href=&quot;https://github.com/austintoddj/canvas/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Canvas&lt;/a&gt; для Laravel, его и взял. Редактор Quill внутри, небольшая админка на Vue.js с тёмной темой, есть немного лишних приблуд типа счётчика просмотров, есть базовое SEO, топики и теги. В общем, достаточно, чтобы не делать самому.  &lt;/p&gt;
&lt;p&gt;Прикрутил SSO, чтобы отдельно в эту админку не логиниться. Немного смутило, что в последнее время пакет не поддерживается. Но для ограниченного применения — ленты новостей и страниц документации — хватит. Стал пользоваться.  &lt;/p&gt;
&lt;p&gt;В процессе, правда, оказалось что админка почему-то не дружит с транслитом и не генерирует слаги с русскоязычных заголовков. Да вдобавок не даёт писать слаги для тегов и топиков самому: поле слага зачем-то блокируется во фронте, хотя бэк не против принять ручной ввод.  &lt;/p&gt;
&lt;p&gt;Взял Клода под мышку и пошёл это недоразумение исправлять. Клод говорит — смотри, эта фигня только во фронте, сейчас пару контролов попатчим, через композер патчи подключим, пересоберем и вуаля.  &lt;/p&gt;
&lt;p&gt;Стали делать — чото не работает, патченые контролы не собираются. И тут Клод такой: Je m&#039;en fous, не патчатся исходники — я тебе попатчу билд! И написал скрипт на питоне, который транслит и ручные слаги просто втыкает в минифицированный джаваскипт. И прописал в пост-инсталл скрипты композера.&lt;/p&gt;
&lt;p&gt;И я ведь как раз это недавно упоминал в &lt;a href=&quot;/news/2026/04/08/robots-code-we-take-responsibility&quot;&gt;статье про вайбкодинг&lt;/a&gt;. Машине всё равно, минимизирован код или нет, она его прочитает. Прочитает, воткнёт питоньим скриптом ещё нечитаемого кода, который через месяц никто не поймёт уже что делает, и будет такова. А для нас это неподдерживаемый код, которым ситуативно, в краткосрочной перспективе можно попользоваться (и я попользуюсь конечно). Но в долгосрочной надо форкать этот Canvas. Тем более редактор Quill в него так крепко встроен, что если я захочу кнопочек добавить в панель и плагинов всяких — всё равно придётся.&lt;/p&gt;
&lt;p&gt;А в остальном, пакет пригодный, если не хочется ставить целую CMS, или руками весь процесс публикаций создавать в админ-панели. Накатал в Канвасе уже несколько страниц документации для проекта, и продолжу пользоваться.&lt;/p&gt;
&lt;p&gt;P.S. Ещё недавно на другом проекте поработал с &lt;a href=&quot;https://www.bookstackapp.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BookStack&lt;/a&gt; — прикольным wiki-решением для документации. Но оно для моих целей чересчур. И к тому же не прокатывает как ещё одна зависимость: её надо на отдельный сабдомен втыкать. И композером не обновляется — авторы предлагают обновления через git pull ставить.&lt;/p&gt;&lt;/div&gt;
</description>
  <pubDate>Thu, 16 Apr 2026 07:00:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2206 at https://graker.ru</guid>
    </item>
<item>
  <title>Доктурист — CRM и документооборот для турагентов</title>
  <link>https://graker.ru/news/2026/03/29/docturist-touristic-crm-registration-open</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Ну что ж, прошло несколько лет, ковидов, и других катаклизмов — и я открыл-таки регистрацию в нашей туристической crmке, которая называется &lt;a href=&quot;https://docturist&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Доктурист&lt;/a&gt;. Ура!&lt;/p&gt;
&lt;p&gt;За последние месяцы была добавлена куча функций, без которых, как мне кажется, нельзя было запускать сервис. В моем списке таких функций еще очень много, но это ведь как ремонт — хочется сделать ещё вот это и вон то, а уж потом… В общем, в какой-то момент добавление фич можно было только приостановить для запуска. Последнее обновление, которое я выкатил сегодня ночью, готовилось 2 месяца и распухло до размеров небольшой планеты. В нём, в частности, была наконец открыта регистрация — так что теперь любой турагент может зайти и зарегистрироваться.&lt;/p&gt;
&lt;p&gt;Теперь можно вернуться в нормальный режим постепенной подготовки и накатывания обновлений. По чуть-чуть, но непрерывно, добавлять, улучшать, допиливать и даже переделывать. Как оно и должно было быть у нормального программного обеспечения. Continuous integration, continuous delivery. Continuous joy.&lt;/p&gt;
&lt;p&gt;В ближайшее время на очереди — открытие раздела статистики и подключение сервиса оплаты картами (сейчас пока можно только запросить счёт для оплаты по реквизитам).&lt;/p&gt;
&lt;p&gt;По технической части (у нас же здесь технический бложек) Доктурист — это Laravel, Vue.js, Redis / Horizon / Supervisor для очередей, Reverb для вебсокетов, Pulse для быстрого ревью чо-как в системе, Canvas для новостей и статей, и ~1000 тестов по методике TDD для лучшей поддерживаемости. Vue.js используется версии 2. Прежде чем пытаться апгрейдить фронт на 3 (серьезная работа) — нужно было довести до ума что есть. В конце концов, я только в прошлом году апнул &lt;a href=&quot;/news/2025/07/21/upgrade-from-laravel-5-to-laravel-12&quot;&gt;Laravel с 5 до 12&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;В общем, если вы турагент, добро пожаловать! На сайте &lt;a href=&quot;https://docturist.ru&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docturist.ru&lt;/a&gt; есть также ссылки на телеграм-канал и чатик. А если нет — можете просто за меня немножко порадоваться. Не каждый день удается отбросить сомнения и все-таки запустить проект, который &lt;a href=&quot;/news/2025/07/03/touristic-crm&quot;&gt;начал делать&lt;/a&gt; еще в прошлом десятилетии. Есть повод порадоваться.&lt;/p&gt;&lt;/div&gt;
</description>
  <pubDate>Sun, 29 Mar 2026 12:05:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2199 at https://graker.ru</guid>
    </item>
<item>
  <title>Laravel и тайна потерянного global scope</title>
  <link>https://graker.ru/news/2026/02/05/laravel-and-lost-global-scope-mystery</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Если у вас к модельке в Ларавеле был прикручен &lt;a href=&quot;https://laravel.com/docs/12.x/eloquent#global-scopes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;global scope&lt;/a&gt; и работал как часы, как вдруг в новой функции потерялся, или сломался — посмотрите, не используете ли вы raw-методы. Например, &lt;code&gt;whereRaw()&lt;/code&gt;, или &lt;code&gt;orderByRaw()&lt;/code&gt; в этой новой функции.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Global Scope — это класс, в котором можно один раз определить условия и затем применять их автоматически ко всем eloquent-запросам той или иной модели (то есть к запросам через &lt;code&gt;Model::&lt;/code&gt;). Например, можно написать global scope для ограничения моделей по автору, чтобы к любому запросу типа &lt;code&gt;Order::all()&lt;/code&gt; автоматически добавлялось &lt;code&gt;-&amp;gt;where(&#039;user_id&#039;, &#039;=&#039;, Auth::user()-&amp;gt;id)&lt;/code&gt;. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Так вот, оказывается, global scope чувствителен к наличию «сырых» запросов. Но это не значит, что их нельзя использовать вместе, можно. Всё дело в том, что билдер запроса не изолирует «сырые» условия от «нормальных». И если у вас есть например вот такое условие в whereRaw():&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;(DAYOFYEAR(date_of_birth) - DAYOFYEAR(?) BETWEEN 0 AND 30) OR (DAYOFYEAR(date_of_birth) - DAYOFYEAR(?) BETWEEN -365 AND -335)&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;то билдер не добавит за вас никаких скобочек и пристыкует условие из скоупа как есть:&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;(DAYOFYEAR(date_of_birth) - DAYOFYEAR(?) BETWEEN 0 AND 30) OR (DAYOFYEAR(date_of_birth) - DAYOFYEAR(?) BETWEEN -365 AND -335) AND (user_id = 3)&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;и как нетрудно заметить, приоритет выполнения условий тут будет совсем не такой, как мы задумывали.&lt;/p&gt;
&lt;p&gt;Решение, как теперь очевидно, простое: &lt;em&gt;всегда, когда требуется использовать &lt;code&gt;whereRaw()&lt;/code&gt; нужно всё условие целиком оборачивать в скобочки&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;А ещё можно пойти дальше, переписать класс билдера, добавив скобочки в &lt;code&gt;whereRaw()&lt;/code&gt; автоматически, и в своих моделях указать использовать этот новый билдер. Как &lt;a href=&quot;https://stackoverflow.com/questions/55758322/eloquent-query-builder-with-or-operator-neutralizes-my-global-scope-larave/55765899#55765899&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;вот здесь сделали&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;P.S. Собственно, это же может произойти и просто если использовать одновременно &lt;code&gt;whereRaw()&lt;/code&gt; и &lt;code&gt;where()&lt;/code&gt;. Так что, следите за скобочками.&lt;/p&gt;&lt;/div&gt;
</description>
  <pubDate>Thu, 05 Feb 2026 20:30:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2189 at https://graker.ru</guid>
    </item>
<item>
  <title>Laravel Reverb, броадкастинг и вебсокеты</title>
  <link>https://graker.ru/news/2026/01/20/laravel-reverb-broadcasting-and-websockets</link>
  <description>&lt;p&gt;В Ларавеле из коробки (ну, почти из коробки) есть поддержка отправки сообщений на вебсокетах, что бывает довольно полезно. Например, когда мы из фронта просим сервер выполнить длительную операцию, и не можем, или не хотим ждать её окончания. А вот узнать, когда она закончится — наоборот, очень хотим. Для чего нам нужно получить от сервера обратную связь. В эру палеолита для этого каждый клиент опрашивал сервер раз в секунду до получения ответа (или до полного изумления сервера). Но теперь у нас есть вебсокеты, с ними проще. Можно передать с сервера результат отложенной операции, можно сделать уведомления о действиях других юзеров, или просто чатик сделать даже. Почитать про это можно в документации на &lt;a href=&quot;https://laravel.com/docs/12.x/broadcasting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Laravel Broadcasting&lt;/a&gt;. А далее мы рассмотрим нюансы первоначальной настройки Laravel Reverb для этого самого броадкастинга.&lt;/p&gt;</description>
  <pubDate>Tue, 20 Jan 2026 10:20:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2185 at https://graker.ru</guid>
    </item>
<item>
  <title>Ночные тесты и Laravel Scout</title>
  <link>https://graker.ru/news/2025/12/15/night-tests-and-laravel-scout</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Вчера перед сном решил — а ну-ка сейчас за 20 минут добью большую фичу. Долго делал, чуть-чуть осталось. Сел, открыл, и правда — через 20 минут фича была готова. Но чёрт меня дернул запустить после этого все тесты. Пока фичу делал — смысла не было запускать весь пакет. А тут, в ночи, решил запустить.&lt;/p&gt;
&lt;p&gt;И конечно как посыпались ошибки! Пару простых исправил, и оказалось, что все остальные — потому что сломался поиск. У меня в проекте используется Laravel Scout для быстрого поиска-автодополнения по нескольким типам моделей одновременно, чтобы было удобно искать что угодно. И вот он сломался сразу весь. Абсолютно все поиски возвращались пустыми.&lt;/p&gt;
&lt;p&gt;Посмотрел на проде — всё еще работает. Стал разбираться, чего такого я мог сломать. Отключил несколько установленных библиотек — вдруг конфликт у трейта &lt;code&gt;Searchable&lt;/code&gt; с чем-нибудь. Включил обратно ту, которую наоборот — стёр за ненадобностью. Не работает.&lt;/p&gt;
&lt;p&gt;Откатил контроль версий на точку до начала разработки фичи. Не работает! Подозрительно как-то. Накатил обратно. Обновил зависимости. Не работает. &lt;/p&gt;
&lt;p&gt;Поотлаживал классы Скаута — увидел, что почему-то драйвер поиска определяется как null (то есть никакой)! Полез смотреть, оказывается у меня внезапно в &lt;code&gt;.env&lt;/code&gt; прописано &lt;code&gt;SCOUT_DRIVER=null&lt;/code&gt;! Хотя я просто полагался на дефолтное значение из конфига &lt;code&gt;scout.php&lt;/code&gt; (который не менялся), и в &lt;code&gt;.env&lt;/code&gt; вообще драйвер не прописывал.&lt;/p&gt;
&lt;p&gt;В общем, непонятно, кто такое сделал-то — то ли один из устанавливаемых пакетов в ходе &lt;code&gt;vendor:publish&lt;/code&gt; проявил инициативу. То ли я зачем-то выставил, хотя никакой пользы от этого не припоминаю. То ли нейросеть, которую тем более об этом не просили. &lt;/p&gt;
&lt;p&gt;В общем, пока нашёл ошибку и вернул нормальный драйвер, прошло уже конечно в разы больше 20 минут. Зато в конце увидел все тесты зелёными. На чем и ушел спать, вот только&lt;/p&gt;&lt;/div&gt;
&lt;figure data-component-id=&quot;grakerru:figure&quot; class=&quot;figure figure--center figure--medium&quot;&gt;
      
    &lt;img loading=&quot;lazy&quot; srcset=&quot;/sites/default/files/styles/content_medium_2x/public/uploads/2025-12/6e5976e46e9131843002978c5dd20091.jpg.webp?itok=L5JhQUBr 500w&quot; sizes=&quot;(min-width: 1200px) 850px, (min-width: 768px) 75vw, calc(100vw - 30px)&quot; width=&quot;500&quot; height=&quot;332&quot; src=&quot;/sites/default/files/styles/content_medium/public/uploads/2025-12/6e5976e46e9131843002978c5dd20091.jpg.webp?itok=BuVGoljt&quot; alt=&quot;6e5976e46e9131843002978c5dd20091.jpg&quot; /&gt;




    &lt;/figure&gt;
</description>
  <pubDate>Mon, 15 Dec 2025 16:00:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2178 at https://graker.ru</guid>
    </item>
<item>
  <title>Сортировка по json-полю в таблицах Laravel Orchid</title>
  <link>https://graker.ru/news/2025/11/22/laravel-orchid-sort-table-by-json-field</link>
  <description>&lt;p&gt;Спешно прикручиваю к своей туристической CRMке нормальную админку (которая для админов). Раньше была самописная с фронтом на Vue, из-за этого добавление любой новой страницы или функции требовало слишком много времени. Собрался с мыслями, решил наконец заменить её на Laravel Orchid. Дело пошло значительно быстрее, и в целом могу констатировать очевидное: как бы ни было прикольно играться с фронтом на Vue, какая бы небольшая ни была админка — лучше конечно сразу брать готовую панельку. А с фронтом играться в пользовательской части.&lt;/p&gt;
&lt;p&gt;В процессе переноса столкнулся с неожиданной трудностью. В Orchid есть вывод моделей в таблицы, который довольно легко и удобно собирается, при этом столбцы таблицы можно сделать фильтруемыми и сортируемыми — тоже легко. Вот, например, объявили колонку «Название» с сортировкой и фильтром (по фрагменту текста)…&lt;/p&gt;</description>
  <pubDate>Fri, 21 Nov 2025 22:30:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2170 at https://graker.ru</guid>
    </item>
<item>
  <title>О выборе между CMS и фреймворками и некоторых мифах этого выбора</title>
  <link>https://graker.ru/news/2025/11/14/some-myths-on-choosing-between-cms-and-frameworks</link>
  <description>&lt;p&gt;С каждым новым проектом в разработке и разработчикам, и порой заказчикам, приходится делать такой выбор. При этом часто можно услышать:&lt;/p&gt;
&lt;p&gt;— CMS только для несложных / шаблонных проектов;&lt;br&gt;
— если нужна нестандартная логика — поможет только фреймворк.&lt;/p&gt;
&lt;p&gt;Посмотрим, так ли это, всегда ли это так, а заодно рассмотрим несколько, возможно, странных, но настоящих мифов и возражений заказчиков.&lt;/p&gt;
&lt;blockquote&gt;CMS — Content Management System, система управления контентом. Популярные CMS — это Wordpress, Drupal, ModX, Joomla и, конечно же, 1С-Битрикс. В CMS предполагается наличие административного интерфейса и настроек.&lt;/blockquote&gt;
&lt;blockquote&gt;Фреймворк (framework) — это набор инструментов, библиотек и способов написания кода, который обычно не предполагает уже существующих настроек и конфигураций, а предполагает их программирование. Популярные фреймворки веб-разработки — это, например, Laravel, Symfony, Django, Yii, Ruby on Rails, Next.js.&lt;/blockquote&gt;</description>
  <pubDate>Fri, 14 Nov 2025 14:50:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2167 at https://graker.ru</guid>
    </item>
<item>
  <title>Почему не нужно писать тесты на визуал при разработке</title>
  <link>https://graker.ru/news/2025/11/11/why-we-should-not-test-visuals-in-development</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Вопрос касается не только TDD-разработки, но и написания качественных unit-тестов в целом.&lt;/p&gt;
&lt;p&gt;В &lt;a href=&quot;/news/2025/11/06/laravel-register-and-login-with-telegram-bots&quot;&gt;прошлой заметке&lt;/a&gt; был тест на проверку отправки юзеру в бот ссылки для входа на сайт. Вот код (я еще про &lt;code&gt;Argument::that()&lt;/code&gt; хотел отдельно написать):&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;/**
 * @test
 */
public function user_issues_login_command_and_gets_a_message_with_login_button(): void
{
    $user = User::factory()-&amp;gt;create([&amp;#039;telegram_username&amp;#039; =&amp;gt; &amp;#039;mr_phpunit&amp;#039;, &amp;#039;telegram_id&amp;#039; =&amp;gt; &amp;#039;123456789&amp;#039;]);
    $bot = Bot::factory()-&amp;gt;create([&amp;#039;user_id&amp;#039; =&amp;gt; $user-&amp;gt;id,]);
    $bot-&amp;gt;settings()-&amp;gt;create([&amp;#039;register&amp;#039; =&amp;gt; TRUE,]);
    $update = $this-&amp;gt;generateMessageUpdate(&amp;#039;mr_phpunit&amp;#039;, &amp;#039;&amp;#039;, &amp;#039;/login&amp;#039;);

    $webhooksService = resolve(TelegramBotWebhooksInterface::class);
    $webhooksService-&amp;gt;processUpdates($this-&amp;gt;api-&amp;gt;reveal(), $update); 

    // ожидаем что будет вызван метод sendMessage() где в тексте сообщения будет Please click the button below...
    // а ссылка в кнопке Login будет содержать роут быстрого логина
    $this-&amp;gt;api-&amp;gt;sendMessage(Argument::that(function ($params) use ($user) {
        return $params[&amp;#039;text&amp;#039;] === &amp;quot;Please click the button below to log in with your account. You will be automatically redirected to your dashboard.&amp;quot; &amp;amp;&amp;amp;
               strpos($params[&amp;#039;reply_markup&amp;#039;][&amp;#039;inline_keyboard&amp;#039;][0][0][&amp;#039;url&amp;#039;], route(&amp;#039;bot.quick-login&amp;#039;, [&amp;#039;user&amp;#039; =&amp;gt; $user-&amp;gt;id])) === 0;
    }))-&amp;gt;shouldHaveBeenCalled();
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Здесь мы проверяем, что пользователю в конце ушёл в виде сообщения определённый текст. Каковой текст и является визуалом, то есть &lt;em&gt;представлением&lt;/em&gt;. Это вывод текста и кнопки-ссылки в форме телеграм-сообщения. И этот текст тестировать не нужно.&lt;/p&gt;
&lt;p&gt;Почему? Потому что текст (представление) не является функциональной частью нашего решения. Мы можем написать какое угодно другое сообщение, но пока в нём содержится ссылка на логин, наша фича работает корректно. То есть конкретное содержимое текста совершенно не имеет значения.&lt;/p&gt;
&lt;p&gt;Почему ещё? Потому что в ходе развития проекта мы текст можем много раз улучшить и переписать. Что повлечёт необходимость менять соответствующие тесты. Безо всякой на то нужды, поскольку кнопку и роут входа мы не меняли.&lt;/p&gt;
&lt;p&gt;Иными словами, при написании тестов, будь то TDD, или просто тесты из общих соображений, мы тоже должны отделять бизнес-логику от представления. Тест — это не застывшее описание реализации, а спецификация поведения программы. А смена текста сообщения (представления) никак не меняет поведение (бизнес-логику). Поэтому тест не должен ломаться при смене текста.&lt;/p&gt;
&lt;p&gt;Текст был бы уместен, если бы кроме него никаких проверяемых результатов у нашей фичи бы не было. И то, тогда лучше проверить не наличие конкретных букв, а название строки из массива локализаций. Тогда проверку в упомянутом тесте можно было бы переписать так:&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;$this-&amp;gt;api-&amp;gt;sendMessage(Argument::that(function ($params) use ($user) {
    return $params[&amp;#039;text&amp;#039;] === __(&amp;#039;login_link.description&amp;#039;) &amp;amp;&amp;amp;
           strpos($params[&amp;#039;reply_markup&amp;#039;][&amp;#039;inline_keyboard&amp;#039;][0][0][&amp;#039;url&amp;#039;], route(&amp;#039;bot.quick-login&amp;#039;, [&amp;#039;user&amp;#039; =&amp;gt; $user-&amp;gt;id])) === 0;
}))-&amp;gt;shouldHaveBeenCalled();&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Возвращаясь к &lt;code&gt;Argument::that()&lt;/code&gt; — как вы уже могли заметить, этот статичный метод (из мок-библиотеки Prophecy) позволяет проверять в переданных аргументах только то, что действительно нужно проверить. А без него методы типа &lt;code&gt;shouldHaveBeenCalled()&lt;/code&gt; будут проверять все аргументы на точное совпадение. Что сделает наши тесты очень деревянными.&lt;/p&gt;
&lt;p&gt;Пример я взял из телеграм-бота просто потому что он был под рукой. Но абсолютно то же самое применимо и к веб-страницам. Не нужно, 10 раз не подумав, загонять в тесты конкретную верстку, или конкретные тексты. Нашим тестам от этого только вред: вывести фичу не поможет, а исправлять тесты потом придётся часто, причем безо всякой пользы для кода.&lt;/p&gt;
&lt;p&gt;А для тестирования визуала есть более подходящие способы: инструменты, сравнивающие референсные страницы с заранее сохраненными скринами попиксельно и поднимающие шухер, если вдруг что-то куда-то съехало. Ghost Inspector, например.&lt;/p&gt;&lt;/div&gt;
</description>
  <pubDate>Tue, 11 Nov 2025 07:50:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2166 at https://graker.ru</guid>
    </item>
<item>
  <title>Регистрация и логин через telegram-ботов в Laravel</title>
  <link>https://graker.ru/news/2025/11/06/laravel-register-and-login-with-telegram-bots</link>
  <description>&lt;p&gt;Недавно прикрутил к своей платформе телеграм-ботов возможность регистрироваться и входить в пользовательскую панельку сайта (ну или личный кабинет, если хотите). То есть, у тех ботов, для которых пользователю нужны веб-функции в личном кабинете, появляются кнопки регистрации и логина. Ну а если у бота такой «админки» нет, то не появляются.&lt;/p&gt;
&lt;p&gt;Далее немного расскажу, как это технически реализовано на Laravel (на котором написан Botopotamus.Ru).&lt;/p&gt;
&lt;p&gt;У Телеграма есть и свой механизм авторизации, но он подразумевает ручную настройку каждого бота, который должен это уметь. А я в данном случае рассматриваю код, который работает для всех ботов разом и его не нужно через Телеграм настраивать. Пользователь просто жмёт «Зарегистрироваться» в боте, а потом жмёт «Войти» чтобы получить ссылку на автологин.&lt;/p&gt;
&lt;p&gt;Под катом примеры с кодом.&lt;/p&gt;</description>
  <pubDate>Wed, 05 Nov 2025 22:00:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2163 at https://graker.ru</guid>
    </item>
<item>
  <title>Сериальный телеграм-бот TV Botopotamus</title>
  <link>https://graker.ru/news/2025/10/02/tvshows-telegram-bot-tv-botopotamus</link>
  <description>&lt;p&gt;Разнообразные сериалы я смотрю очень давно, когда-то писал, как так вышло. Ещё так вышло, что смотрю я обычно на английском. Что, кстати, за годы просмотра очень неплохо его прокачало — но об этом в другой раз.&lt;/p&gt;
&lt;p&gt;Со временем всё самое хорошее, что уже было снято, посмотрел и стал смотреть новое. В связи с чем встал вопрос отслеживания выхода новинок. Долгое время отслеживал с сайта addic7ed.com, где добрые люди неустанно изготавливают для сериалов субтитры. Причём, делают это крайне быстро, так что в списке последних добавленных сабов обычно и видны все новинки.&lt;/p&gt;
&lt;p&gt;Помню, когда-то таким образом отловил «Шерлока» — того что с Камбербетчем и Фриманом — и посмотрел через несколько часов после выхода. Понравилось, написал заметку, а через пару месяцев права на показ купил Первый канал, и я еще долго на эту заметку собирал трафик, поскольку написал про сериал чуть ли не первый.&lt;/p&gt;</description>
  <pubDate>Thu, 02 Oct 2025 08:25:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2149 at https://graker.ru</guid>
    </item>
<item>
  <title>Продолжаю работать над CRMкой</title>
  <link>https://graker.ru/news/2025/09/21/continue-coding-my-crm</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Есть такой сервис Qui-Quo, им турагенты пользуются, многие — если не все. Позволяет с помощью браузерного плагина организовывать удобные подборки предложений для отправки клиентам. Ну и еще всякое полезное.&lt;/p&gt;
&lt;p&gt;Вчера прикрутил к своей &lt;a href=&quot;/news/2025/07/03/touristic-crm&quot;&gt;агентской CRMке&lt;/a&gt; интеграцию с ним. Теперь, когда клиент выберет тур, агент может жмякнуть на кнопку отправки в CRM — и бронька сразу появится там, с подтянутыми данными: датами, отелем, ценами, оператором и графиком платежей.&lt;/p&gt;
&lt;p&gt;Под впечатлением от &lt;a href=&quot;https://t.me/graker_coding/268&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;доклада&lt;/a&gt; запилил сразу идемпотентность тоже. Если агент на радостях нажмёт кнопку заявки пять раз, бронь создастся только одна.&lt;/p&gt;
&lt;p&gt;Ляпота. Когда-нибудь я психану, перестану смотреть в бэклог, запилю CRMке какой-нибудь лендос и открою регистрацию.&lt;br&gt;
Да.&lt;br&gt;
Да.&lt;/p&gt;&lt;/div&gt;
</description>
  <pubDate>Sun, 21 Sep 2025 10:55:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2144 at https://graker.ru</guid>
    </item>
<item>
  <title>Да что такое этот ваш процентиль</title>
  <link>https://graker.ru/news/2025/09/16/on-percentiles</link>
  <description>&lt;p&gt;Добавил тут себе в &lt;a href=&quot;https://botopotamus.ru&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ботопотама для мониторинга сайтов&lt;/a&gt; вывод процентиля в графике значений пинга. Пользуясь случаем, рассказываю зафига они нужны, эти процентили, если вы вдруг не знаете.&lt;/p&gt;
&lt;p&gt;Для начала немного определений и полезного матана.&lt;/p&gt;
&lt;p&gt;Процентиль P — это такое число, что P% значений в наборе данных меньше или равны этому числу. Например, в наборе [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] число 8 — это 90й процентиль, т.к. 90% чисел в этом наборе меньше или равны 8. Из других примеров, 0й процентиль — может быть вам известен как &lt;em&gt;минимум&lt;/em&gt;, 100й процентиль — &lt;em&gt;максимум&lt;/em&gt;, а 50й процентиль вы можете знать под именем &lt;em&gt;медиана&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Процентили — это, кстати, частный случай квантилей. Квантили — примерно то же самое, только выражены не в процентах, а в долях от 0 до 1 (ну или в вероятностях, если так удобнее). Процентиль — это квантиль*100%.&lt;/p&gt;
&lt;blockquote&gt;Численный показатель (не)годности статьи или заметки в блог можно было бы назвать &lt;em&gt;(не)адеквантилем&lt;/em&gt;.&lt;/blockquote&gt;
&lt;p&gt;Итак, нафига нам эти процентили.&lt;/p&gt;</description>
  <pubDate>Tue, 16 Sep 2025 08:00:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2142 at https://graker.ru</guid>
    </item>
<item>
  <title>Как новичку попробовать в Test Driven Development</title>
  <link>https://graker.ru/news/2025/08/04/test-driven-development-tryout-for-newbies</link>
  <description>&lt;p&gt;Сегодня поговорим о Test Driven Development, то есть о разработке, движимой тестами. О том, с чего вообще она начинается, как начать писать код, и как продолжить. Для примеров будем использовать Laravel, в этом фреймворке многое заточено под тесты, примеры наглядные и лаконичные. Поехали.&lt;/p&gt;
&lt;p&gt;В литературе по тестированию и TDD в частности очень любят приводить в пример тест, что функция &lt;code&gt;sum(a, b)&lt;/code&gt; успешно складывает два числа. Сложно представить что-то более вредное для демонстрации методики. Этот пример:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;скучный — поскольку по сути ничего не проверяет и ничего не демонстрирует;&lt;/li&gt;
&lt;li&gt;практически бесполезный — поскольку изначально не нуждается в тестировании*;&lt;/li&gt;
&lt;li&gt;не представляет собой важную функцию в нашем проекте*.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;* если только мы не пишем реализацию математической библиотеки для какого-либо устройства, но там тесты будут совсем другие&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Попробуем сделать пример получше. Итак, чтобы удачно стартануть в проекте с TDD, нужно совершить очень важный первый шаг. Выбрать первую фичу, которую мы хотим реализовать, и написать…&lt;/p&gt;</description>
  <pubDate>Mon, 04 Aug 2025 07:45:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2131 at https://graker.ru</guid>
    </item>
<item>
  <title>Обновление с Laravel 5 до Laravel 12</title>
  <link>https://graker.ru/news/2025/07/21/upgrade-from-laravel-5-to-laravel-12</link>
  <description>&lt;p&gt;Недавно упоминал, что работаю над CRMкой для турагентств и турагентов, которую написал давно. Соответственно, Laravel уже очень старый. Я писал, что 6, а при проверке оказалось, что вообще 5, и это несколько сковывает мои возможности. В общем, решил осовременить систему, чтобы проще было работать и интегрировать более современные библиотеки (типа Horizon например). Поставил, короче, задачу обновиться за выходные. Ну и делюсь с вами полученным опытом.&lt;/p&gt;
&lt;p&gt;А то в чатах иногда пишут, что практических примеров кодинга с ИИ маловато. Вот — самый настоящий практический пример.&lt;/p&gt;
&lt;p&gt;Без лишних прелюдий, дано:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Legacy-код на Laravel 5;&lt;/li&gt;
&lt;li&gt;набор из 580 тестов (код разрабатывался по TDD);&lt;/li&gt;
&lt;li&gt;Cursor AI бесплатная версия — чтобы было видно, что с ней, на автовыборе модели, тоже можно.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Нужно: обновить код до Laravel 12 так, чтобы он не сломался.&lt;/p&gt;
&lt;blockquote&gt;
  TL/DR если не хочется читать технические подробности: 
  &lt;ol&gt;
&lt;li&gt;ИИшечка помогает, но есть нюансы (внутри заметки).&lt;/li&gt;
&lt;li&gt;Без ИИшечки я бы ещё очень долго это откладывал.&lt;/li&gt;
&lt;li&gt;А без пакета заботливо написанных при разработке тестов скорее всего вообще не стал бы.&lt;/li&gt;
&lt;li&gt;Задача была выполнена.&lt;/li&gt;
&lt;li&gt;Суммарное время выполнения составило около 8-9 часов.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;Итак, нам нужно обновить Laravel 5 до 12. Делаем мы это по шагам, то есть так: 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12, всего 7 обновлений. Для каждого разработчики Ларавела заботливо написали roadmap по апгрейду. Вот например для апгрейда до Laravel 6: &lt;a href=&quot;https://laravel.com/docs/6.x/upgrade&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://laravel.com/docs/6.x/upgrade&lt;/a&gt; &lt;/p&gt;</description>
  <pubDate>Mon, 21 Jul 2025 20:10:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2129 at https://graker.ru</guid>
    </item>
<item>
  <title>Laravel Livewire — вход по имени пользователя или электронной почте</title>
  <link>https://graker.ru/news/2025/07/15/laravel-livewire-login-by-name-or-email</link>
  <description>&lt;p&gt;Если поставить Laravel Starter Kit с Livewire внутри, который мы в прошлый раз &lt;a href=&quot;/news/2025/06/15/add-more-volts-to-your-livewire&quot;&gt;немного поизучали&lt;/a&gt;, то у нас сразу, из коробки будет поддержка аутентификации. Но — только по электронной почте и паролю. А заказчики могут хотеть (и хотят) чтобы можно было входить с помощью логина, либо вместо почты, либо в качестве альтернативы. Причины могут быть разные, не будем зацикливаться. Вместо этого давайте посмотрим, как сделать, чтобы можно было использовать и логин, и почту в качестве идентификатора пользователя при входе.&lt;/p&gt;
&lt;p&gt;Начнём как положено, с тестов. В &lt;code&gt;tests/Feature/Auth&lt;/code&gt; у нас есть &lt;code&gt;AuthenticationTest.php&lt;/code&gt;, в котором уже находятся дефолтные тесты аутентификации. Выберем главный опорный тест, на базе которого проведем изменения...&lt;/p&gt;</description>
  <pubDate>Tue, 15 Jul 2025 07:05:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2126 at https://graker.ru</guid>
    </item>
<item>
  <title>Макрос getOrFail в Laravel</title>
  <link>https://graker.ru/news/2025/07/05/getorfail-macro-in-laravel</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;В Ларавеле есть удобный хелпер-метод: &lt;code&gt;firstOrFail()&lt;/code&gt;, позволяющий получить по заданному запросу из БД первую найденную модель, либо вывалиться в 404, если моделей не найдено. Удобство в том, что использование хелпера позволяет избавиться от необходимости отдельно проверять наличие результата.&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;$page = ContentPage::where(&amp;#039;slug&amp;#039;, $slug)-&amp;gt;firstOrFail();&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;вместо&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;$page = ContentPage::where(&amp;#039;slug&amp;#039;, $slug)-&amp;gt;take(1)-&amp;gt;get();

if ($page-&amp;gt;isEmpty()) abort(404);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Очевидно, первый вариант лаконичнее и читабельнее.&lt;/p&gt;
&lt;p&gt;Так вот, хотелось бы, чтобы то же самое можно было делать, когда мы хотим загрузить &lt;em&gt;несколько&lt;/em&gt; моделей, или уйти в 404, если не нашли ни одной. То есть чтобы был хелпер-метод &lt;code&gt;getOrFail()&lt;/code&gt;, который делает то же, что и firstOrFail, но возвращает несколько моделей.&lt;/p&gt;
&lt;p&gt;Для этого можно написать макрос. В Laravel есть такой трейт, Macroable, и классы, которые его используют, можно расширять своими макро-методами. Расширим класс &lt;code&gt;Illuminate\Database\Eloquent\Builder&lt;/code&gt;. Добавим в &lt;code&gt;AppServiceProvider&lt;/code&gt;:&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;public function boot(): void
{
    // ...

    Builder::macro(&amp;#039;getOrFail&amp;#039;, function () {
        return tap(
            $this-&amp;gt;get(), 
            fn ($results) =&amp;gt; abort_unless($results-&amp;gt;count(), 404)
        );
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;теперь мы можем писать, например, так:&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;$contentPages = ContentPage::where(&amp;#039;slug&amp;#039;, $slug)-&amp;gt;getOrFail();&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;и получим либо коллекцию моделей ContentPage, либо ошибку 404, без необходимости отдельно проверять, что коллекция не пустая.&lt;/p&gt;
&lt;p&gt;P.S. Да, &lt;code&gt;slug&lt;/code&gt; в данном примере не уникальный, может быть несколько моделей с одинаковым &lt;code&gt;slug&lt;/code&gt;. Вместо этого у меня есть уникальный ключ &lt;code&gt;[&#039;slug&#039;, &#039;language&#039;]&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;
</description>
  <pubDate>Sat, 05 Jul 2025 11:25:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2121 at https://graker.ru</guid>
    </item>
<item>
  <title>CRM для туристических агентств</title>
  <link>https://graker.ru/news/2025/07/03/touristic-crm</link>
  <description>&lt;p&gt;В 2017 году уговорил жену открыть турагентство, взяли домен, собрал сайт на OctoberCMS, и так появилось туристическое агентство &lt;a href=&quot;https://alaytravel.ru&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AlayTravel&lt;/a&gt;. После чего, поскольку программировать я люблю, искать лёгких путей — нет, а жена вообще хотела всё записывать в эксель, сел писать для агентства систему CRM/документооборота. Взял Laravel и Vue.js, методику Test Driven Development, и через какое-то время написал. Жена начала пользоваться, а я — потихоньку дорабатывать первую «наивную» реализацию и думать, что с этим дальше делать. Ну, потому что писать целую CRM имея в виду работу одного агентства — это немножечко &lt;strike&gt;overkill&lt;/strike&gt; чересчур.&lt;/p&gt;
&lt;p&gt;Пока я думал и готовился, мир, аэропорты и туристический бизнес накрыло пандемией COVID-19. Все надели масочки, облились дезинфектором, и в аэропорты ездили, в основном, чтобы вернуть деньги за неиспользованные билеты. Стало сильно непонятно, что в такой ситуации произойдет с турбизнесом, но агентство мы не закрыли. И после пандемии туристы очухались, поправились, и снова стали летать.&lt;/p&gt;
&lt;p&gt;И только я опять хотел вернуться к своей цеэрэмке, как началась СВО. И полетные программы несколько подсократились, и понимание чего там дальше будет немножко размыло. Но, видимо помня короновирус, туристы очухались и полетели. И в принципе стало ясно, что будут летать дальше (тьфу-тьфу-тьфу). К этому моменту у меня уже на CRM было не очень много времени, так что она работала себе и работала — для одного агентства.&lt;/p&gt;
&lt;p&gt;Но несколько месяцев назад, когда на Ближнем Востоке активно летали ракеты, я подумал:&lt;/p&gt;</description>
  <pubDate>Thu, 03 Jul 2025 15:15:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2118 at https://graker.ru</guid>
    </item>
<item>
  <title>Вдарим вольтом по лайвваеру</title>
  <link>https://graker.ru/news/2025/06/15/add-more-volts-to-your-livewire</link>
  <description>&lt;p&gt;На днях начал работу над новым проектом, и выдался хороший случай попробовать Livewire и Volt с ним, с последней версией Laravel. Про Livewire в последних версиях Ларавела я раньше слышал и читал, но руки как-то не доходили. А теперь дошли и сейчас мы познакомимся с ним поближе.&lt;/p&gt;
&lt;h3&gt;Что такое Livewire&lt;/h3&gt;
&lt;p&gt;Что вообще такое Livewire? Это фреймворк, позволяющий разрабатывать динамичные интерфейсы прямо на PHP. Как гласит главная страница фреймворка:&lt;/p&gt;
&lt;blockquote&gt;Powerful, dynamic, front-end UIs without leaving PHP.&lt;/blockquote&gt;
&lt;p&gt;То есть «мощные, динамичные фронт-энд интерфейсы, не покидая PHP». Ничего не понятно, но очень интересно. Ну, я немного разобрался (прочитал документацию!) Этот фреймворк позволяет нам, путём вставки специальных директив типа &lt;code&gt;wire:model.live=&quot;filter&quot;&lt;/code&gt; прямо в blade-шаблон, в html какого-нибудь инпута, сделать этот инпут динамическим. Или, как раньше говорили, его аяксифицировать.&lt;/p&gt;
&lt;blockquote&gt;То есть заставить элемент отправлять запросы на сервер без перезагрузки страницы и обновлять соответствующий view по данным в ответе на запрос&lt;/blockquote&gt;
&lt;p&gt;И это — без необходимости писать JavaScript-код, вообще. И хотя мы в примере ниже немного JS-кода напишем, это, строго говоря, действительно необязательно.&lt;/p&gt;</description>
  <pubDate>Sun, 15 Jun 2025 13:30:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2112 at https://graker.ru</guid>
    </item>
<item>
  <title>Совпадения айдишников в тестах</title>
  <link>https://graker.ru/news/2025/05/04/false-matching-ids-in-tests</link>
  <description>&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Когда мы пишеем тесты на бэкенд-путь, который возвращает данные с отношениями, (&lt;code&gt;$order&lt;/code&gt;, в нём &lt;code&gt;$order→user&lt;/code&gt;, в нём &lt;code&gt;$order→user→plan&lt;/code&gt; и т.д.) — следует избегать проверять отсутсвие тех или иных id через &lt;code&gt;assertJsonMissing([&#039;id&#039; =&amp;gt; $order2-&amp;gt;id])&lt;/code&gt; и ему подобные методы. Когда тестов окажется много, и мы их запустим разом, инкремент-айдишники в таблицах побегут и в какой-то момент внутри &lt;code&gt;$order&lt;/code&gt;, который в json есть, внезапно окажется &lt;code&gt;$user&lt;/code&gt; у которого id такой же как у &lt;code&gt;$order2&lt;/code&gt;, и тест не пройдет.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Имеется в виду запуск всего пакета тестов, например, в Laravel с включенным трейтом RefreshDatabase: таблицы БД после каждого теста чистятся, но структура заново не пересоздается. Это и быстрее, и более похоже на «живую» среду.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;И мы будем голову ломать, почему вдруг этот тест сломался на ровном месте и почему это проявляется, только если сразу все тесты запустить. И ладно мы, мы-то быстро найдем почему. А вот если мы работаем с ИИ, он половину рабочего кода искромсает своми правками, пока допрёт, что это было. Если допрёт вообще — мой вот не допёр.&lt;/p&gt;
&lt;p&gt;В общем, проверять json ответа без декодирования в массивы/объекты не очень хорошо, т.к. может повлечь подобные ложные мэтчи.&lt;/p&gt;
&lt;p&gt;Неправильно:&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;  $response-&amp;gt;assertJsonMissing([&amp;#039;id&amp;#039; =&amp;gt; $order2-&amp;gt;id]);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;paragraph paragraph--text&quot;&gt;&lt;p&gt;Этот код будет искать по содержимому всего json и рано или поздно найдет вложенную зависимость со случайно совпавшим id.&lt;/p&gt;
&lt;p&gt;Правильнее будет например:&lt;/p&gt;&lt;/div&gt;
&lt;pre data-component-id=&quot;grakerru:code-block&quot; class=&quot;code-block language-none&quot;&gt;&lt;code class=&quot;language-none&quot;&gt;  $data = $response-&amp;gt;decodeResponseJson(&amp;#039;data&amp;#039;);
  $this-&amp;gt;assertNotContains($order2-&amp;gt;id, array_map(function ($order) {
    return $order[&amp;#039;id&amp;#039;];
  }, $data););
  // так мы явно показываем, на каком уровне вложенности тестируем
  // также можно использовать замыкания внутри assertJson(), позволяющие ходить по уровням структуры документа&lt;/code&gt;&lt;/pre&gt;
</description>
  <pubDate>Sun, 04 May 2025 08:25:00 +0000</pubDate>
    <dc:creator>graker</dc:creator>
    <guid isPermaLink="false">2098 at https://graker.ru</guid>
    </item>

  </channel>
</rss>
