Domain Service — содержит бизнес-логику, которая не принадлежит конкретной сущности или Value Object. Работает только с объектами домена, ничего не знает об инфраструктуре.
class MoneyTransferService {
public function transfer(Account $from, Account $to, Money $amount): void {
if (!$from->hasSufficientFunds($amount)) {
throw new InsufficientFundsException();
}
$from->debit($amount);
$to->credit($amount);
}
}Application Service — оркестрирует выполнение use case. Загружает агрегаты из репозиториев, вызывает доменные сервисы, сохраняет результат, диспатчит события. Не содержит бизнес-логики.
class TransferMoneyHandler {
public function handle(TransferMoneyCommand $cmd): void {
$from = $this->accountRepo->findOrFail($cmd->fromId);
$to = $this->accountRepo->findOrFail($cmd->toId);
$this->transferService->transfer($from, $to, new Money($cmd->amount));
$this->accountRepo->save($from);
$this->accountRepo->save($to);
}
}Правило разделения: если в методе есть бизнес-решение ("можно ли это сделать?") — это доменный сервис. Если только координация ("загрузи, вызови, сохрани") — Application Service.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3❤1
Идемпотентность — повторный вызов с теми же параметрами даёт тот же результат без дополнительных побочных эффектов.
HTTP методы: GET, HEAD, PUT, DELETE — идемпотентны по спецификации. POST — нет.
Для POST-запросов применяется Idempotency Key: клиент генерирует UUID и передаёт в заголовке. Сервер кэширует результат под этим ключом. При повторном запросе возвращает закэшированный ответ, не выполняя операцию повторно.
В очередях: воркер может упасть после обработки задачи, но до подтверждения (ack). Брокер переотправит задачу. Обработчик должен быть идемпотентным — повторная обработка одной и той же задачи не должна создавать дублей.
Техники обеспечения идемпотентности:
— Хранить processed_ids и проверять перед обработкой
— Использовать INSERT IGNORE / ON DUPLICATE KEY в MySQL
— Использовать upsert (INSERT ... ON CONFLICT DO NOTHING в PostgreSQL)
— Проверять состояние перед изменением ("уже оплачен — пропустить")
Идемпотентность — обязательное требование для любого обработчика в распределённой системе с at-least-once delivery.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5
PHP — интерпретируемый язык. На каждый запрос без OPcache:
1. Читается PHP-файл с диска
2. Парсится в AST
3. Компилируется в opcode
4. Opcode выполняется Zend Engine
OPcache кэширует скомпилированный opcode в shared memory. При следующем запросе шаги 1-3 пропускаются.
Результат: ускорение в 2-10x, снижение нагрузки на CPU.
JIT (Just-In-Time) — следующий уровень: компилирует opcode в машинный код. Даёт прирост для CPU-интенсивных задач.
Сброс кэша при деплое: opcache_reset() или перезапуск PHP-FPM.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🔥3
«Мы перешли на генераторы, чтобы не грузить память» — но память всё равно растёт 👇
📦 Задание
Команда переписала импорт CSV на генераторы — раньше падало с OOM на файлах больше 500 МБ. После рефакторинга память перестала расти... на стейджинге. На проде с реальными файлами на 2M+ строк потребление всё равно ползёт вверх.
// src/Import/CsvImporter.php
class CsvImporter
{
private array $processedIds = [];
private array $errors = [];
private int $totalRows = 0;
public function import(string $filePath): ImportResult
{
foreach ($this->readRows($filePath) as $row) {
$this->totalRows++;
try {
$id = $this->processRow($row);
$this->processedIds[] = $id;
} catch (RowException $e) {
$this->errors[] = [
'row' => $this->totalRows,
'message' => $e->getMessage(),
'data' => $row,
];
}
}
return new ImportResult($this->processedIds, $this->errors, $this->totalRows);
}
private function readRows(string $filePath): \Generator
{
$handle = fopen($filePath, 'r');
$headers = fgetcsv($handle);
while (($raw = fgetcsv($handle)) !== false) {
yield array_combine($headers, $raw);
}
fclose($handle);
}
private function processRow(array $row): int
{
// Валидация, маппинг, вставка в БД
// Возвращает inserted ID
return $this->repository->upsert($row);
}
}
// src/Import/ImportResult.php
class ImportResult
{
public function __construct(
public readonly array $processedIds,
public readonly array $errors,
public readonly int $totalRows,
) {}
}
🔹 Задачи
— Найти все источники роста памяти
— Объяснить, почему проблема не воспроизводится на стейджинге
— Предложить решение
Ставьте → 🔥 если нравится формат. Если нет → 🌚
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥21🌚3❤1👍1
Текущий уровень сложности вопросов?
🔥 — Слишком просто, хочу сложнее
👍🏼 — В самый раз
❤️ — Иногда сложновато
😁 — Часто не понимаю
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥21👍5❤4😁4
Команда php artisan cache:clear очищает весь кэш, включая данные в Redis, но если используется кэширование с тегами, то команда очистит только данные, связанные с общими ключами. Теги управляются отдельно, поэтому их нужно очищать вручную, используя cache:tags() для работы с конкретными группами данных.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥2❤1
Event-driven architecture — это когда компоненты общаются через события, не зная друг о друге напрямую.
// Событие:
class UserRegistered {
public function __construct(public readonly User $user) {}
}
// Listener:
class SendWelcomeEmail {
public function handle(UserRegistered $event): void {
$this->mailer->send($event->user->email, 'Welcome!');
}
}
// Диспетчер:
$dispatcher->dispatch(new UserRegistered($user));
Зачем
В Laravel: Event / Listener, EventServiceProvider.
В Symfony: EventDispatcher, декларация через атрибуты.
Подводный камень: сложно трейсить цепочку — одно событие вызывает другое.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥2❤1
N+1 — классическая проблема производительности ORM.
// N+1: 1 запрос за постами + N запросов за автором каждого поста:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // запрос на каждой итерации!
}
100 постов = 101 запрос к БД.
Решение — Eager Loading:
// Laravel:
$posts = Post::with('author')->get(); // 2 запроса: posts + authors IN (...)
// Doctrine:
$posts = $em->createQuery('SELECT p, a FROM Post p JOIN FETCH p.author a')->getResult();
Как обнаружить
• Laravel Debugbar / Telescope — показывает все запросы
• Логирование медленных запросов в MySQL
• Профилировщик (Blackfire, Xdebug)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤2🔥1
Наследование — это механизм ООП, позволяющий создавать новый класс на основе уже существующего. Новый класс (подкласс) получает все свойства и методы родительского класса (суперкласса), что обеспечивает повторное использование кода и упрощает поддержку.
Наследование реализуется с помощью ключевого слова extends. Подкласс может расширять или переопределять поведение суперкласса, а также добавлять новые поля и методы. Важно помнить, что в PHP класс может наследоваться только от одного суперкласса.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥2❤1
Код прошёл нагрузочное тестирование. На проде деньги задвоились 👇
📦 Задание
Фича: пользователь может переводить бонусные баллы другу. Логика простая — проверить баланс, списать, начислить. Тесты зелёные, нагрузочное прогнали — всё ок. Через три дня после релиза: у нескольких пользователей баланс ушёл в минус, у других — задвоился.
// src/Bonus/BonusTransferService.php
class BonusTransferService
{
public function __construct(
private PDO $pdo,
private BonusRepository $repo,
) {}
public function transfer(int $fromId, int $toId, int $amount): void
{
$this->pdo->beginTransaction();
try {
$fromBalance = $this->repo->getBalance($fromId);
if ($fromBalance < $amount) {
throw new InsufficientFundsException();
}
$this->repo->debit($fromId, $amount);
$this->repo->credit($toId, $amount);
$this->pdo->commit();
} catch (Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
}
}
// src/Bonus/BonusRepository.php
class BonusRepository
{
public function __construct(private PDO $pdo) {}
public function getBalance(int $userId): int
{
$stmt = $this->pdo->prepare(
'SELECT balance FROM bonus_accounts WHERE user_id = ?'
);
$stmt->execute([$userId]);
return (int) $stmt->fetchColumn();
}
public function debit(int $userId, int $amount): void
{
$stmt = $this->pdo->prepare(
'UPDATE bonus_accounts SET balance = balance - ? WHERE user_id = ?'
);
$stmt->execute([$amount, $userId]);
}
public function credit(int $userId, int $amount): void
{
$stmt = $this->pdo->prepare(
'UPDATE bonus_accounts SET balance = balance + ? WHERE user_id = ?'
);
$stmt->execute([$amount, $userId]);
}
}
🔹 Задачи
— Объяснить, как именно происходит race condition в этом коде
— Почему транзакция здесь не защищает от проблемы
— Исправить getBalance так, чтобы устранить race condition
Ставьте → 🔥 если нравится формат. Если нет → 🌚
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17🤔3👍2
Основные проблемы многопоточности:
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5👍4🔥1
Стоит ли спойлерить часть ответа как тут, чтобы было время подумать самостоятельно?
🔥 — Да
😁 — Нет
🤔 — Без разницы
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥23😁4🤔3
Service Container — это
// bind() — каждый раз создаёт новый экземпляр
app()->bind(PaymentService::class, function ($app) {
return new PaymentService($app->make(HttpClient::class));
});
// singleton() — создаёт один раз, дальше отдаёт тот же объект
app()->singleton(CacheManager::class, function ($app) {
return new CacheManager(config('cache'));
});
🔹 Когда что использовать
→ bind() — если объект
→ singleton() — если объект
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2👍1🔥1
Оба инструмента для
Gate —
// Определяем в AuthServiceProvider
Gate::define('access-admin-panel', function (User $user) {
return $user->is_admin;
});
// Проверяем
if (Gate::allows('access-admin-panel')) { ... }
// или в контроллере
$this->authorize('access-admin-panel');
Policy —
// php artisan make:policy PostPolicy --model=Post
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->is_admin;
}
}
// Использование
$this->authorize('update', $post);
Правило выбора
— Нет модели →
— Есть модель, несколько действий →
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤1🔥1
Оба механизма для организации
Job Chaining — задачи выполняются
Bus::chain([
new ProcessPayment($order),
new SendInvoice($order),
new NotifyWarehouse($order),
])->dispatch();
Job Batching — задачи выполняются
$batch = Bus::batch([
new ImportRow($rows->chunk(100)[0]),
new ImportRow($rows->chunk(100)[1]),
new ImportRow($rows->chunk(100)[2]),
])
->then(fn (Batch $batch) => Log::info('Импорт завершён'))
->catch(fn (Batch $batch, Throwable $e) => Log::error('Ошибка'))
->finally(fn (Batch $batch) => Cache::forget('import-lock'))
->dispatch();
// Можно следить за прогрессом
$batch->progress(); // процент выполнения
Когда что
— Независимые задачи, нужна скорость и прогресс →
— Зависимость между задачами, порядок важен →
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👍3🔥1
У вас есть два класса:
class A {
public function __construct(private B $b) {}
public function doSomething(): string {
return $this->b->getValue();
}
}
class B {
public function getValue(): string {
return 'real';
}
}Создаём мок через PHPUnit, он реализует интерфейс (или наследует класс) и позволяет изолировать зависимость:
$mockB = $this->createMock(B::class);
$mockB->method('getValue')->willReturn('mocked');
$a = new A($mockB);
$this->assertSame('mocked', $a->doSomething());
Почему работает: createMock() генерирует анонимный класс, расширяющий B. PHP позволяет передать его туда, где ожидается B.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2👍1
PHP уничтожает объект, когда его refcount падает до 0. Но есть исключение —
Для этого существует
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🔥2🥱1
В первой части постов навалили жесткой базы, чтобы вправить мозги на место. Во второй дали конкретные инструменты, фреймворки и пошаговые инструкции, что нужно кодить прямо сейчас.
Часть 1. Введение, юзкейсы и реальность
Разбираемся с терминами, снимаем розовые очки и смотрим, где ИИ реально приносит бабки, а где только жжет нервы:
1. «Так что вообще считается AI-агентом?»
2. «Где тут бот, а где уже AI-агент?»
3. «Не надо пихать AI-агента в каждую задачу»
4. «Что уже можно спокойно делать через AI-агентов?»
5. «А что через AI-агентов пока лучше не трогать?»
Часть 2. Изнанка, ошибки и архитектура
Как всё это устроено под капотом, чтобы не слить бюджет и не наломать дров на старте:
6. «Можно ли просто сесть вечером и собрать себе AI-агента?»
7. «С чего вообще начать, если хочется попробовать AI-агентов»
8. «Почему AI-агент может внезапно начать творить дичь»
9. «Где AI-агенты реально экономят время, а где только добавляют возни»
10. «Почему они жрут столько денег?»
Часть 3. Хардкорная практика (Что делать руками)
Хватит теории. Открываем ноут, запускаем Cursor и делаем нормальные, отказоустойчивые системы:
11. «Почему одного промпта мало?»
12. «Почему AI-агенту мало просто “дать доступ к данным”»
13. «Если не следить за AI-агентом, он быстро начинает жить своей жизнью»
14. «Собрать демку легко. Но как же сделать нормально»
15. «Как сделать, чтобы это не развалилось через неделю?»
Please open Telegram to view this post
VIEW IN TELEGRAM
😁2
PHP array —
SplFixedArray —
// array: ~400 MB
$arr = array_fill(0, 1_000_000, 0);
// SplFixedArray: ~90 MB
$fixed = new SplFixedArray(1_000_000);
SplDoublyLinkedList / SplStack / SplQueue —
SplMinHeap / SplMaxHeap —
$heap = new SplMinHeap();
$heap->insert(5);
$heap->insert(1);
$heap->insert(3);
// Всегда достаёт минимум за O(log n)
echo $heap->extract(); // 1
echo $heap->extract(); // 3
⚠️ На что обратить внимание на практике
→ SplFixedArray не поддерживает строковые ключи и array_* функции — только числовые индексы
→ В PHP 8.1+ SplFixedArray реализует IteratorAggregate — работает в foreach без обёрток
→ Для задач "top-K элементов" или Dijkstra — SplMinHeap бьёт usort по всем фронтам
→ В большинстве бизнес-задач обычный array быстрее за счёт CPU-кэша — SPL оправдан при сотнях тысяч элементов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🔥2🤔1
Обычная Collection
// ❌ Загрузит ВСЕ записи в память
$users = User::all()->filter(...)->map(...);
// ✅ Lazy Collection — обрабатывает по одной записи через генератор
User::cursor()->filter(function (User $user) {
return $user->is_active;
})->each(function (User $user) {
ProcessUser::dispatch($user);
});
cursor() использует
Lazy Collection из файла:
// Обработка огромного CSV без OutOfMemoryError
$collection = LazyCollection::make(function () {
$handle = fopen('huge_file.csv', 'r');
while ($row = fgetcsv($handle)) {
yield $row;
}
});
$collection->skip(1)->chunk(100)->each(function ($rows) {
ImportBatch::dispatch($rows->toArray());
});
Когда использовать
—
—
Важно: методы типа count() и last() материализуют коллекцию. Их лучше избегать в lazy-контексте.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4❤1🔥1