Public channel ID 70941834150135

Махровый fullstack

А у нас в канале:

- Бэкенд, фронтенд и случайные мысли - в произвольных сочетаниях.
- Иногда литература, иногда всё, что взбредёт в голову.

Постится как душе просится.

Освежает разум. Возможны побочные эффекты.

Запаски:
- https://vk.ru/ordo_code
- https://t.me/ordo_code

Channels in category
1 765
This channel's rank
№ 1 256
Open category →
Channels in category
679
This channel's rank
№ 496
Open category →
Open in MAX

Posts for the period

By reactions

Search and the whole feed — after free registration. Custom dates — with a subscription; the channel owner sees them for free. Sorting by reactions — with a subscription. Sign up

Loading…

Как перенести дамп в 20Гб и не потерять ни бита
— Когда mysqldump уже не вывозит —


TL;DR
mydumper + myloader — интересная альтернатива классическому mysqldump для больших баз. Параллельная выгрузка, дробление таблиц, многопоточный импорт и возможность возобновить восстановление.

Для многих вынос дата-центров яндекса в начале октября обернулся неудобствами. Для кого-то финансовыми потерями и потерей данных. И мне досталось рикошетом и небольшим опытом...

В инфраструктуре яндекса лежала БД с весьма чувствительными данными. И как только сервис стал доступен встала задача извлечь данные из облака и скачать на флешку и положить под матрац и перебраться на резервную площадку. В моём случае — перенести какие-то жалкие 20Гб.

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

Двое из ларца – mydumper/myloader

— mydumper — снимает дамп в несколько потоков, разбивая таблицы на отдельные файлы. Полноценного автоматического продолжения прерванного дампа нет, но если данные гарантированно не изменялись, можно сохранить полностью выгруженные таблицы и отдельно доснять недостающие.
— myloader — параллельно восстанавливает базу и умеет продолжать прерванный импорт через --resume при наличии файла состояния.

Первая часть марлезонского балета

Снимаем дамп — синтаксис максимально похож на привычный mysqldump:

mydumper \
--host=<DB_HOST> \
--port=3306 \
--user=<DB_USER> \
--ask-password \
--ssl \
--ssl-mode=VERIFY_IDENTITY \
--ca=CA.pem \
--database=<DB_NAME> \
--skip-ddl-locks \
--no-backup-locks \
--sync-thread-lock-mode=LOCK_ALL \
--outputdir=./backup-sql \
--threads=4 \
--rows=100000 \
--trx-tables \
--verbose=3


Из интересных параметров:
--threads=4 — четыре параллельных потока выгрузки.
--rows=100000 — ориентир на 100 тысяч строк в одном фрагменте большой таблицы.
--trx-tables — оптимизация блокировок для транзакционных таблиц, например InnoDB.
--skip-ddl-locks — отключает дополнительные DDL-блокировки.
--no-backup-locks — запрещает использовать backup-locks.
--sync-thread-lock-mode=LOCK_ALL — синхронизирует потоки с помощью блокировок таблиц вместо глобального FLUSH TABLES WITH READ LOCK.

Облачный mysql оказался довольно ревнивым к своим привилегиям: сначала не дал BACKUP_ADMIN, потом отказал в FLUSH TABLES. Поэтому пришлось подбирать режимы синхронизации эмпирически.

Вторая часть марлезонского балета

Когда дамп наконец снялся, выяснилось, что myloader на macOS категорически не желает отключать SSL, которого на целевом сервере не было.

Попытки уговорить его через волшебный --ssl-mode=DISABLED закончились ничем. Оказалось, моя сборка использовала MariaDB Connector/C, с которым возникли проблемы при отключении SSL.

Пришлось доставать docker-образ с mysql-клиентом:

docker pull mydumper/mydumper:latest

И дальше дело пошло веселее. Для импорта через Docker команда получилась такой:

docker run --rm -it \
--entrypoint myloader \
-v "$(pwd)/backup:/backup" \
mydumper/mydumper:latest \
--host=<DB_HOST> \
--port=3306 \
--protocol=tcp \
--user=<DB_USER> \
--ask-password \
--ssl-mode=DISABLED \
--database=<DB_NAME> \
--directory=/backup \
--threads=4


В итоге база переехала, данные остались целы.

Осталось только купить флешку и положить её под матрац.

Так...на всякий случай.

#mysql #mydumper #myloader #devops
Views: 5
Go to post Open in MAX Report an inaccuracy
Тысяча и одна ошибка

— Не будь как Шазерезада - экономь квоту —

TL;DR

Одна ошибка может случиться тысячу раз, но в Bugsnag достаточно сообщить о ней один раз за 15 минут.
Кто о чём, а вшивый о багах.

Подключил я намедни проект Laravel к Bugsnag для сбора ошибок в коде, дабы не проспать чего апокалиптического. Но апокалипсис устроил сам... 😈

Проект ворочает довольно приличными объёмами данных и в случае катаклизма Bugsnag быстро бы оброс репортами.

Сказано - сделано!

Буквально за ночь доступные квоты были израсходованы! Источник проблем оказался внешний, но лимиты от этого не увеличились. 🤔

Чтобы решить подобную проблему на будущее было решено немного притормозить отправку репортов.

Основные требования к решению:
— Единый механизм дросселирования для любых типов исключений;
— Легкое добавление нового типа репорта без изменения существующей логики;
— Возможность самостоятельно определять, какие ошибки считать одинаковыми;
— Единая точка настройки ограничений, без размазывания логики по всему проекту;
— Максимально использовать штатные возможности Laravel и избнжать вендорлок сервиса мониторинга;
— Ограничение частоты репортов не должно влиять на выполнение бизнес-логики или обработку самих исключений;

1. Готовим максимально простой интерфейс


interface ThrottledReport
{
public const int THROTTLE_MINUTES = 15;
public function throttleKey(): string;
public function throttleMinutes(): int;
}

Основная идея - это throttleKey. Именно он определяет, какие ошибки считать одинаковыми. А throttleMinutes - определяет на сколько притормозить отправку повторяющихся репортов.

2. Добавляем trait с реализацией таймера по умолчанию


trait HasReportThrottle
{
public function throttleMinutes(): int
{
return static::THROTTLE_MINUTES;
}
}

3. Далее подключаем обработку эксепшена в bootstrap


->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof ThrottledReport) {
return Limit::perMinutes($e->throttleMinutes(), 1)
->by($e::class . ':' . $e->throttleKey());
}

return null; // остальные исключения без ограничений
});
})

Тут есть важный нюанс: троттлинг работает на уровне Laravel Exception Handler, поэтому подавленные репорты не попадут не только в Bugsnag, но и в стандартные логи приложения. Но для моего решения — это приемлемо.

И ещё момент — важно учесть, что лимит хранится в кэше, поэтому важно иметь настроенный механизм кэширования. Например, redis.

4. Создаём своё исключение

final class DoNotSpamException extends RuntimeException implements ThrottledReport
{
use HasReportThrottle;

public const int THROTTLE_MINUTES = 60;

public function throttleKey(): string
{
return 'my-unique-key-for-error';
}
}



5. И используем
report(new DoNotSpamException('Something went wrong'));

‼️ Но тут важно помнить — это решение уменьшает частоту сообщений об ошибках, а не частоту возникновения самих ошибок. О количестве и качестве ошибок заботимся сами! ☕️

#php #laravel #exception #throttling #backend #web #разработка
Views: 10
Go to post Open in MAX Report an inaccuracy
Loading image…
Территориальный конфликт: война за 172.19.0.0/16

— Когда контейнер ищет внешний сервер у себя под носом —

TL;DR

Разбираемся, куда пропадают пакеты, при чём тут маршрутизация и как закончить войну за IP-адреса.

Кто‑то скажет — сделал из мухи слона! Да у меня каждый день такие приключения! Но для меня получился интересный кейс, и я лучше зафиксирую его для потомков.

Случилось страшное – из docker-контейнера недоступен удалённый хост. При этом с хост-машины никаких проблем с доступность нет – что надо пингуется, что пингуется, то роутится. Хост для определённости - 172.19.10.50.

WTF?!

Диагностика. Раунд первый

Для начала проверяем подсети, которые создал docker:


docker network inspect $(docker network ls -q) \
--format '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}} {{end}}'

В моём случае обнаружились:


bridge: 172.17.0.0/16
traefik: 172.19.0.0/16
app-backend: 172.23.0.0/16
app-notifications: 172.29.0.0/16

В целом, картина уже ясна – видим пересечение сетей docker и внешнего хоста.

Для пущей убедительности проверяем маршрут в сетевом пространстве docker:


docker run --rm --network host \
nicolaka/netshoot \
ip route get 172.19.10.50

Бинго!


172.19.10.50 dev br-caf585119744 src 172.19.0.1

Cреда docker считает, что внешний сервер находится непосредственно в локальной сети.

Первое лечение. Неудачное

Перенёс сеть traefik в другой диапазон – 10.250.10.0/24, предварительно убедившись, что подсеть не используется где-то ещё.


networks:
traefik:
name: traefik
driver: bridge
ipam:
config:
- subnet: 10.250.10.0/24

Тут надо сказать, что app-backend и app-notifications используют общие external-сети. Поэтому сети создавались отдельно, а не средствами docker compose.



docker network create app-backend
docker network create app-notifications

Остановил использующие сеть проекты, удалил старые сети, поднял сети заново.
И… внешний сервер снова недоступен.

WTF?!

Диагностика. Раунд второй

Смотрим подсети ещё раз:


traefik: 10.250.10.0/24
app-backend: 172.18.0.0/16
app-notifications: 172.19.0.0/16

Постоянству docker можно позавидовать – не отпускает подсеть 172.19.0.0/16.

Лечение. Теперь окончательное

После повторного конфликта стало понятно: доверять docker автоматическое распределение адресов конкретно в этом окружении — идея так себе.
И поэтому сети создаём с явным указанием подсетей:


docker network create \
--driver bridge \
--subnet 10.250.11.0/24 \
app-backend

docker network create \
--driver bridge \
--subnet 10.250.12.0/24 \
app-notifications

Проверяем итоговую адресацию:


docker network inspect traefik app-backend app-notifications \
--format '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}}{{end}}'

Получаем:


traefik: 10.250.10.0/24
app-backend: 10.250.11.0/24
app-notifications: 10.250.12.0/24


Проверяем доступность внешнего сервера:


docker run --rm \
--network container:notifications-php \
nicolaka/netshoot \
ping -c 3 172.19.10.50

И вот теперь всё заработало как надо!
Для кого-то это база, а для меня кирпичик опыта!

Полезное открытие: nicolaka/netshoot

У минималистичных docker-образов, таких как alpine, debian:slim часто отсутствуют даже базовые сетевые утилиты: ping, curl, nslookup, ip. Когда возникает необходимость разобраться с сетевыми проблемами, то удобнее использовать образ nicolaka/netshoot с целым арсеналом сетевых инструментов.

Но самое интересное — возможность подключиться к сетевому пространству уже работающего контейнера:


docker run --rm -it \
  --network container:notifications-php \
  nicolaka/netshoot

Теперь мы находимся в отдельном диагностическом контейнере, но используем сетевые интерфейсы, IP-адреса и маршруты notifications-php.
При этом исходный контейнер не пересобирается, а после выхода диагностический контейнер автоматически удалится благодаря --rm.

Отличная DevOps-выручалочка. Крайне рекомендую.

#docker #dockercompose #networking #devops
Views: 10
Go to post Open in MAX Report an inaccuracy
Loading image…
Великий исход из Bitbucket
– 347 репозиториев. Нормальный человек бы удалил –

tl;dr

Как перенести 347 репозиториев и не сойти с ума. Чудеса автоматизации с go, api и git –mirror

А помните в году так в 2023 пронеслась новость - Bitbucket заблокировал репозитории для разработчиков из России? Ох и испугался же я - ибо все существующие репозитории были на нём. Но делать нечего - взял и скачал все репозитории одним архивом на скорую руку в надежде в будущем перезалить куда-нибудь да и забросил его на антресоль.

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

Но тут что-то торкнуло заглянуть в аккаунт Atlassian.

А, Батюшки-святы! Аккаунт живой! Репозитории на месте! Дальше судьбу решил не искушать и таки перенести всё богатство.

Вооружившись Go, документацией к API и некоторым количеством терпения был создан скрипт, переносящий репозитории из Bitbucket -> GitFlic. Но получился небольшой миграционный конвейер.

Bitbucket API → git clone --mirror → GitFlic API → git push --mirror

Работает он в нескольких режимах: скачивание репозиториев и режим миграции.

По API Bitbucket скрипт получает полный список репозиториев с поддержкой постраничной загрузки и после чего каждый репозиторий клонируется локально.

Из интересного - git clone выполняется с флагом --mirror: создаётся bare-репозиторий, но без рабочей копии, но с полной историей, ветками, тегами и остальными git-особенностями, что заодно уменьшает объём промежуточных данных на диске.

Далее проверяется наличие проекта в GitFlic и если он не найден - создаётся через API GitFlic, после чего локальное зеркало отправляется командой git push --mirror.

Для безопасного повторного запуска уже скачанные зеркала пропускаются, а в консоль по ходу дела сыпется информация о процессе миграции.

И так несколько сотен раз.

На этом можно было бы и закончить, но тут появился Андрей (Андрей, привет! 🍺), заставивший пошебуршать по проектам, а меня дополнительно обуяло любопытство - что же у меня скопилось за почти 20 лет работы.

Скопилось 347 уникальных репозиториев — разной степени паршивости и полезности, конечно же, но всё же.

Содержат 29 133 уникальных коммитов, из которых 12 199 моих.

И, естественно, что коммиты не все мои - командную разработку никто не отменял. Но так или иначе, к каждому проекту были приложены собственные руки, мозги и нервы.

По языкам получился такой расклад:

PHP -- 395 293
JavaScript -- 141 132
CSS / SCSS -- 70 766
HTML -- 14 387
Dart -- 6 037
TypeScript -- 5 731
Swift -- 2 945
Ruby -- 2 666
Kotlin -- 1 847
Vue -- 1 545
Python -- 1 540


Также были найдены археологические слои: C/C++, Objective-C, голый SQL с триггерами и функциями, Java и 112 файлов на Go.

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

В итоге, несколько часов программирования и 20Гб архивов плавно переехали на новое, не подверженное политическим веяниям, безопасное хранилище, а на SSD освободилось 40Гб. Почему 40? А у меня, оказывается, два архива было. Для надёжности, наверняка, а не благодаря Альцгеймеру ;)

#git #bitbucket #gitflic #golang #автоматизация #миграция
Views: 18
Go to post Open in MAX Report an inaccuracy
Loading image…
Не помню IP. И больше не собираюсь!
– Как высвободить немного своей памяти –

tl;dr:

Давайте серверам имена. Перекладываем IP из своей памяти в ~/.ssh/config.

Я устал!

Спустя несколько лет вечных

ssh master@22.22.22.22
ssh master@33.33.33.33
ssh master@44.44.44.44

и ещё пяток других - я устал помнить IP, я устал открывать Obsidian со списком серверов, я устал копипастить.

Но, преодолев свой склероз, вспомнил, что можно ж и в алиасы!


ssh master@22.22.22.22 => ssh server-1
ssh master@33.33.33.33 => ssh server-2

И т.д.

Если вы спросите как, то я с радостью поделюсь секретом!

Можно в ~/.ssh/config просто добавить список серверов:


Host server-1
HostName 22.22.22.22
User master

Host server-2
HostName 33.33.33.33
User master

Но чтобы не захламлять конфиг, можно сделать круче!

Создаём в ~/.ssh/ папку config.d и там складываем конфиги серверов, сгруппированные хоть по именам, хоть по IP, хоть по натальной карте!


# ~/.ssh/config.d/22-servers.conf
# ~/.ssh/config.d/44-servers.conf

И инклудим всё это добро в основном конфиге:


Include ~/.ssh/config.d/*.conf

Маленькие грабли: Include лучше размещать до первого Host в основном конфиге. Потому что пустая строка блок Host не заканчивает.

Я, конечно же, узнал об этом исключительно из документации. Никаких «какого хрена» не было. ☺

А для отлова граблей - используем


ssh -G server-1 | grep -E '^(hostname|user|port) '

И никаких тебе больше мучительных воспоминаний и лишних пасов руками.

Ну красота же!

P.S.: Вишенка на торте - алиасы работают и с scp, rsync, sftp и прочими SSH-радостями.
P.P.S.: Про шаблоны и Match в конфигах поговорим как-нибудь потом ;)

#ssh #sshconfig #devops #linux #администрирование
Views: 45
Go to post Open in MAX Report an inaccuracy
Loading image…
Не долбимся как кролики с RabbitMQ и yii2-queue

– retry, delay, exponential backoff и немного jitter без лишнего насилия над очередями –

tl;dr

yii2-queue с amqp_interop может повторно отправлять упавшую задачу практически сразу. Добавляем DeliveryDelay - и получаем retry с паузой.

Ну что же, у нас классика на столе - yii2 и система нотификаций, реализованная через yii2-queue и rabbitmq в качестве брокера очередей.

У очередей настроены попытки - пусть 5 для определённости.

Как будто-бы всё хорошо: джоба падает → yii2-queue делает retry → жизнь продолжается.

Но есть нюанс.

При использовании amqp_interop повторная отправка сообщения идёт через redeliver(). И идёт она сразу.
То есть внешний сервис прилёг, API отвечает ошибкой - а мы такие:

— Не получилось?
— Не получилось.
— А сейчас?
— Нет.
— А сейчас?
— Всё ещё нет.
— А СЕЙЧАС?!


Не retry, а тревожный разработчик какой-то перед дедлайном. 😃

Улучшаем - добавляем паузу

Сам yii\queue\amqp_interop\Queue почти всё уже умеет. Но немножечко надо допилить метод redeliver и добавить отправку сообщений с задержкой.



protected function redeliver(AmqpMessage $message): void
{
$attempt = (int)$message->getProperty(self::ATTEMPT, 1);
$delay = $attempt * 60;

$newMessage = $this->context->createMessage(
$message->getBody(),
$message->getProperties(),
$message->getHeaders()
);
$newMessage->setDeliveryMode($message->getDeliveryMode());
$newMessage->setProperty(self::ATTEMPT, $attempt + 1);
$newMessage->setProperty(self::DELAY, $delay);

$producer = $this->context->createProducer();
$producer->setDeliveryDelay($delay * 1000);
$producer->send($this->context->createQueue($this->queueName), $newMessage);
}

Теперь между попытками появляется простенький linear backoff:
1 retry → 60 секунд
2 retry → 120 секунд
3 retry → 180 секунд
...


Если не нравится линейный вариант, то можно сделать exponential backoff:


$delay = 60 * (2 ** ($attempt - 1));

И тогда будет
attempt 1 → 60 сек
attempt 2 → 120 сек
attempt 3 → 240 сек
attempt 4 → 480 сек
...


Нет предела совершенству - добавляем jitter

Представим, что в очереди 10 000 сообщений. Все получили ошибку отправки уведомления через внешний сервис примерно одновременно и происходит retry для всей очереди.

Йеее - словили thundering herd problem, он же «эффект громового стада»! 😆

Чтобы не положить внешний сервис окончательно или не словить его rate limit, добавим немного случайности:

Full jitter - retry распределится по всему диапазону от 0 до $delay.


$jitter = random_int(0, $delay);

Equal Jitter - гарантированно ждём хотя бы половину задержки, а остальное отдаём на волю рандома.


$half = (int) ($delay / 2);
$jitter = $half + random_int(0, $half);

Decorrelated Jitter - cледующая задержка зависит от предыдущей (оставим для самостоятельного изучения 😄).

🚨 !!! ВАЖНО !!!

Уход на retry с новой попыткой срабатывает только тогда, когда джоба выбрасывает Exception.

Не возвращает false, null или что-нибудь ещё в духе "ну, не шмогла".

А именно падает:


throw new RuntimeException('Notification sending failed');

Иначе для yii2-queue джоба успешно завершилась и повторять там, собственно, нечего.

#php #yii2 #rabbitmq #yii2queue #queues #retry #amqp #backend #web #разработка
Views: 29
Go to post Open in MAX Report an inaccuracy
Loading image…
Неуникальная уникальность, и при чем тут дни рождения

– Ведь в мире их еще миллиарда четыре! А быть может и пять, как знать? –

tl;dr

Почему 2^32 вариантов — не так много, как кажется?
Немного математики о коллизиях и обманчивой уникальности.

Наткнулся на днях на интересное понятие - Парадокс дней рождений (birthday paradox). Это ситуация, когда интуиция нас подводит в оценке вероятностей.

В чём суть

Классический пример - кажется, что, раз в году 365 дней, то чтобы с вероятностью 50% у кого‑то в группе совпали дни рождения, то понадобится (на глазок) что-то около половины от 365 - то есть под две сотни человек. Но математика говорит другое: достаточно всего 23 человека, чтобы вероятность совпадения хотя бы у двоих была примерно 50,7%.

Так происходит потому, что сравнивается не “один человек со всеми датами”, а все пары людей между собой.

Для 23 человек пар будет: 23 * 22 / 2 = 253 пары

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

Зачастую мы генерируем всякие уникальные значения: UUID, slug, реферальный код и прочие идентификаторы каким-нибудь эдаким способом:

substr(sha1($orderKeyString), 0, 8)

Разберём

Один hex-символ хранит 4 бита. Значит 8 hex-символов - это 32 бита. А это, на минуточку, 2^32, примерно 4,29 млрд возможных значений. Дофига, не правда ли? На первый взгляд много, но из-за парадокса коллизии становятся вероятными намного раньше.

Вероятность хотя бы одной коллизии можно приблизительно оценить по формуле: p ≈ 1 - exp(-n(n - 1) / (2 × 2^32)).

Оценочно:
10 000 значений -> ~1.2% шанс хотя бы одной коллизии
50 000 значений -> ~25%
77 000 значений -> ~50%
100 000 значений -> ~69%


То есть проблема не в том, что 4 млрд быстро закончатся, а в том, что при случайном распределении два значения могут получить один и тот же 8-символьный slug уже на десятках тысяч записей.

Как исправить

Увеличить пространство значений, чтобы сделать коллизию практически невозможной.

Например, 16 hex-символов — это уже 64 бита. Для 100 000 записей вероятность коллизии составляет примерно 0,000000027%.

А если идентификатор обязан быть уникальным, то никакая теория вероятностей не заменит UNIQUE на поле и обработку коллизии при генерации.

—

Дурацкая математика! Вечно портит красивые решения!

#php #коллизии #хеширование #birthdayparadox #backend #web #разработка
Views: 38
Go to post Open in MAX Report an inaccuracy
Loading image…
Даже в Laravel не обойтись без политики

— Как проверить права доступа к модели через Policy —

Сегодня хочу поговорить о политике.
Чего я хуже остальных, что ли... ✊

tl;dr

Проверку доступа к модели удобно выносить в Policy и подключать в карте маршрутов.

Возникла задача добавить проверку при удаления файла - разрешить пользователю удалять только свои собственные файлы.

Самый простой и правильный способ сделать это в Laravel - использовать Policy.

Policy - специальные классы в Laravel, позволяющие определить логику авторизации для конкретной модели или ресурса.

Преимущества подхода с политиками:

Централизация логики доступа
Поведение задаётся в одном месте - в классе политики


public function delete(User $user, Attachment $attachment)
{
return $attachment->user_id === $user->id;
}

Переиспользование
Policy можно использовать в разных частях приложения
-- в контроллере:


$this->authorize('delete', $attachment);

-- в шаблонах:


@can('delete', $attachment)
<button>Delete</button>
@endcan

-- в маршрутах:


->can('delete', 'attachment')

Улучшение читаемости


$this->authorize('delete', $attachment);

более очевидно считывается, чем


if ($attachment->user_id !== auth()->id())

Масштабируемость
Логика доступа со временем может усложняться (роли, админы, shared-ресурсы и т.д.), но благодаря централизованному описанию политики изменения вносятся в одном месте.

Расширяемость
Можно добавлять собственные методы политик, а не ограничиваться только стандартными.

Тестируемость
Отдельный метод политики легко покрывается тестами.

Чёткое разделение ответственности
— Controller - обрабатывает запрос;
— Policy - определяет доступ;
— Model - определяет данные;

Policy обнаруживаются автоматически в проекте, если структура проекта стандартная:


app/
├─ Models/
│ └─ Attachment.php
└─ Policies/
└─ AttachmentPolicy.php

Laravel автоматически сопоставит:


Attachment → AttachmentPolicy

Никакой регистрации не требуется.
(Но при необходимости политику можно зарегистрировать вручную в AppServiceProvider.)
—————

Политики создаются командой:


php artisan make:policy AttachmentPolicy --model=Attachment

А дальше происходит магия соглашения именования и политика применяется к модели. ✨

Для своего случая я выбрал вариант проверки доступности на уровне обработки маршрута:


Route::post('/upload/{attachment}/delete', [UploadController::class, 'delete'])
->can('delete', 'attachment')
->name('upload.remove');

В итоге получается аккуратное, архитектурно правильное и читаемое решение.

Если файл принадлежит пользователю - удаляем, а нет - извините, политика. 📄

#laravel #policy #authorize #backend #web #разработка
Views: 42
Go to post Open in MAX Report an inaccuracy
Loading image…
Мутанты на проводе или Всевидящий MutationObserver

– Когда мир перестаёт быть реактивным, остаётся только наблюдать –

tl;dr

Иногда не нужно менять архитектуру. Достаточно наблюдать и DOM сам расскажет, что произошло.

Давненько у нас не было фронтовых баек, а пожалуй что вот оно!

Уютный мир: ламповый laravel, приятные vue-компоненты, встроенные в шаблоны. Всё живёт в безмятежном неведении надвигающейся катастрофы.

Но тут появляется внешний JS-скрипт, который начинает управлять состоянием vue-компонента извне. В частности - открывает попап, а vue-форма внутри должна менять своё поведение.

Должна, но не может!

Классические приемы не работают:
- Пропсы не пробросить - компонент уже смонтирован;
- События не проэмитить - нет родителя;
- Пересоздавать компонент с переносом логики - слишком жирно. Да и внешний скрипт менять нельзя.

Можно было бы отчаяться и уйти из IT на ферму кроликов разводить, но на помощь приходят мутанты-наблюдатели!

Да, MutationObserver - наше спасение. Всевидящее Око Саурона в мире DOM.

Создаём наблюдателя. Теперь он в засаде и зорко наблюдает за всеми изменениями у родителя:


const component = el.parentElement as HTMLDivElement | null;
let observer: MutationObserver | null = null;

onMounted(() => {
observer = new MutationObserver(() => {
if (component.classList.contains('popup_active')) {
// наша логика
}
})

observer.observe(component, {
attributes: true,
attributeFilter: ['class'],
});
});

Но что бы наблюдатель был не слишком наблюдателен, ограничим его кругозор через фильтр: attributeFilter и следим строго за классом.

Ну и, конечно, по-джентльменски освобождаем наблюдателя при смерти компонента.


onUnmounted(() => { 
observer?.disconnect(); 
observer = null;
});

Если состояние меняется вне зоны реактивности компонента - это не приговор.

Мутанты среди нас, и они всегда помогут! 

#vue #mutationobserver #javascript #frontend #web #разработка
Views: 38
Go to post Open in MAX Report an inaccuracy
Loading image…
Мамкин DevOps или CI/CD на коленке

Для всех, кто знает боль ручного деплоя.
Тем, кто делал git pull на проде в пятницу вечером.
Всем кто помнит адреса серверов наизусть.
Всем беспризорным душам посвящается.


tl;dr

GitLab webhook + простой серверный обработчик = автоматизированный git pull. Дёшево, сердито и без регистраций и смс.

Да, сегодня на повестке дня рутина - ручная накатка обновлений на сервер. Но и эту скучную работу можно и нужно автоматизировать.

Речь пойдёт о максимально простом варианте CI/CD. Без GitLab Runner, Kubernetes, Jenkins и прочих инструментов взрослых и серьёзных дядек. Сегодня нам понадобятся только GitLab, webhook и небольшой серверный обработчик.

Что такое webhook
Если отбросить умные определения, webhook — это обычный HTTP-запрос по событию. Произошёл push в репозиторий — GitLab отправил запрос на указанный URL. Для наших целей webhook будет выполнять одну простую задачу — запускать обновление проекта.

Как это работает
-- На сервере создаём endpoint для webhook.
-- В GitLab настраиваем webhook на этот URL.
-- GitLab получает push.
-- GitLab отправляет POST-запрос.
-- Сервер проверяет токен.
-- Сервер выполняет деплой.

Всё - один сервер вежливо сообщает другому, что пора обновляться.

Настраиваем GitLab
В проекте открываем: Settings → Webhooks
Далее указываем:
-- URL обработчика;
-- Secret Token;
-- событие Push Events;
-- при необходимости ограничиваем веткой main.

После сохранения GitLab начнёт отправлять запросы при каждом пуше в целевую ветку.

Настраиваем сервер
На сервере потребуется обработчик, который:
-- принимает только POST-запросы;
-- проверяет токен;
-- убеждается, что прилетел нужный branch;
-- запускает обновление проекта;
-- пишет лог.

В результате у нас не проект, а автономный зверь, который обновляется сам.

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

А главное — больше не придётся вспоминать, обновил ли ты этот сервер вчера или только собирался.

#cicd #devops #gitlab #webhook #deploy #разработка #backend #automation
Views: 49
Go to post Open in MAX Report an inaccuracy
The full feed — after free sign-up
VKontakte Telegram Odnoklassniki
Favorites at datool.ru

Follow the channel in your account

Sign up at datool.ru to add the channel “Махровый fullstack” to favorites and track its metrics.

Sign up
For channel owners

Verify your channel

Sign in or sign up at datool.ru, then add our bot to the channel following the instructions in your account. Once ownership is verified, the lock disappears and channel management becomes available.

Sign in or sign up

Report an inaccuracy

Describe what needs fixing. If you have a source, add a link.

You are leaving datool.ru

The link leads to an external website. datool.ru is not responsible for its content — check the address before entering passwords or payment details.

Redirecting in 10 s

Go