Библиотека собеса по PHP | вопросы с собеседований
3.16K subscribers
192 photos
6 videos
130 links
Вопросы с собеседований по PHP и ответы на них.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/9f3affba

Для обратной связи: @proglibrary_feeedback_bot
Download Telegram
Чем отличается Optimistic Lock от Pessimistic Lock?

Pessimistic Lock — блокируем строку в БД на время транзакции. Никто другой не может её изменить до снятия блокировки.

  SELECT * FROM orders WHERE id = 1 FOR UPDATE;


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

Optimistic Lock — блокировки нет. У записи есть поле version. При обновлении проверяем, что версия не изменилась:

  UPDATE orders SET status = 'paid', version = 6
WHERE id = 1 AND version = 5;


Если affected_rows = 0 — кто-то успел раньше, делаем retry или возвращаем ошибку.

Применять когда: конфликты редки, операции долгие (нельзя держать блокировку), высокий параллелизм.
Минус: нужен retry-механизм, сложнее реализовать корректно.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5👍2
Объясни разницу между индексом B-Tree и Hash?

B-Tree (сбалансированное дерево) — стандартный тип индекса в InnoDB. Узлы дерева хранят ключи в отсортированном порядке.

Поддерживает:

— точный поиск: WHERE id = 5
— диапазоны: WHERE id BETWEEN 5 AND 10
— сортировку: ORDER BY id
— префиксный поиск: WHERE name LIKE 'Ali%'

Hash-индекс — хранит хэш значения ключа. Поиск O(1), но только точное равенство. Не поддерживает диапазоны, сортировку, LIKE.
Please open Telegram to view this post
VIEW IN TELEGRAM
3👍1
Что такое Covering Index и как он ускоряет запрос?

Covering Index — индекс, который содержит все столбцы, необходимые для выполнения запроса. MySQL отвечает прямо из индекса, не обращаясь к основной таблице (heap/clustered index).

  CREATE INDEX idx_user_status ON orders(user_id, status, created_at);

SELECT status, created_at FROM orders WHERE user_id = 5;


Все три поля запроса есть в индексе → MySQL читает только индекс. В EXPLAIN увидишь: Using index.

Без covering index: MySQL находит строки через индекс, затем делает дополнительный lookup в основную таблицу за остальными столбцами (random I/O). При большом количестве строк это медленно.

Правило проектирования индексов: сначала столбцы из WHERE и JOIN, потом из ORDER BY, потом из SELECT. Порядок в составном индексе критичен — MySQL использует индекс слева направо и останавливается на первом неиспользованном столбце.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍32🔥2
Что такое CQRS? Почему его часто используют вместе с Event Sourcing?

CQRS (Command Query Responsibility Segregation) — разделение модели на две:

🔹 Command side (запись) — принимает команды (CreateOrder, CancelOrder), изменяет состояние, не возвращает данные (или только ID).

🔹 Query side (чтение) — принимает запросы, возвращает данные, никогда не изменяет состояние.

Зачем разделять: модели чтения и записи имеют разные требования. Write-модель — богатый домен с инвариантами и валидацией. Read-модель — денормализованные данные, оптимизированные под конкретный экран/API.

Связь с Event Sourcing: когда command side сохраняет событие, это событие обновляет read-модели (проекции). Проекции — денормализованные таблицы или документы, заточенные под конкретные запросы.

OrderCreated → обновить проекцию "список заказов"
OrderCreated → обновить проекцию "аналитика по дням"

Без Event Sourcing CQRS тоже применяется: просто два разных репозитория — один для записи (доменные объекты), другой для чтения (DTO, raw SQL).
Please open Telegram to view this post
VIEW IN TELEGRAM
👍32
✔️ PHP-тест: Exception handling + PDO транзакции + молчаливая потеря данных

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

📦 Задание

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

// src/Payment/PaymentService.php
class PaymentService
{
public function __construct(
private PDO $pdo,
private Logger $logger,
private Notifier $notifier,
) {}

public function process(PaymentDTO $dto): bool
{
try {
$this->pdo->beginTransaction();

$paymentId = $this->insertPayment($dto);
$this->updateBalance($dto->userId, $dto->amount);
$this->insertAuditLog($paymentId, $dto);

$this->pdo->commit();

$this->notifier->sendReceipt($dto->userId, $paymentId);

return true;

} catch (NotificationException $e) {
$this->logger->warning('Receipt failed', ['error' => $e->getMessage()]);
return true;

} catch (Throwable $e) {
$this->logger->error('Payment failed', ['error' => $e->getMessage()]);
$this->pdo->rollBack();
return false;
}
}

private function insertPayment(PaymentDTO $dto): int
{
$stmt = $this->pdo->prepare(
'INSERT INTO payments (user_id, amount, status) VALUES (?, ?, ?)'
);
$stmt->execute([$dto->userId, $dto->amount, 'pending']);
return (int) $this->pdo->lastInsertId();
}

private function updateBalance(int $userId, float $amount): void
{
$stmt = $this->pdo->prepare(
'UPDATE balances SET amount = amount - ? WHERE user_id = ?'
);
$stmt->execute([$amount, $userId]);

if ($stmt->rowCount() === 0) {
throw new \RuntimeException("Balance record not found for user $userId");
}
}

private function insertAuditLog(int $paymentId, PaymentDTO $dto): void
{
// Пишем в отдельную audit БД через отдельное соединение
$this->auditPdo->prepare(
'INSERT INTO audit_log (payment_id, user_id, amount) VALUES (?, ?, ?)'
);
// ... execute
}
}


🔹 Задачи

— Найти сценарий, при котором платёж коммитится в БД, но return true не доходит до контроллера — и данные считаются потерянными
— Объяснить проблему
— Предложить исправленную структуру

Ставьте → 🔥 если нравится формат. Если нет → 🌚

💬 Решения пишите в комменты под спойлер — сравним подходы.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15
😱 Если ваш продукт не умеет отдавать данные в формате, понятном AI-агенту, то вас просто не существует

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

Как адаптировать продукт и не исчезнуть из выдачи:

— интегрировать MCP и A2A-взаимодействие, чтобы агенты могли вас читать;
— научиться контролировать стоимость (лимиты, кэш, роутинг между моделями);
— настроить AgentOps: трейсинг, логирование и отлов регрессий.

Всё это ждёт вас на обновлённом курсе «Разработка AI-агентов». Мы специально сделали фокус на утилитарном инжиниринге и production-ready решениях.

Кстати, до 29 марта можно забрать курс с большой скидкой, и стоит поторопиться — мест на потоке всё меньше.

Зафиксировать цену и начать деплоить агентов без слива бюджета 👈
😁5
В чём основное отличие Docker от виртуальной машины?

Основное отличие в уровне виртуализации.

Виртуальная машина виртуализирует железо целиком: у неё есть собственная ОС со всеми компонентами, гипервизор, ядро. Это тяжеловесно — VM может весить гигабайты и стартовать минутами.

Docker виртуализирует только уровень приложения. Контейнеры используют ядро хостовой ОС, изолируясь через namespaces и cgroups. Они легковесны — образ может весить десятки мегабайт, запускается за секунды.

🔹 На практике это означает

— Docker быстрее и экономнее по ресурсам.
— VM даёт полную изоляцию и может запускать разные ОС на одном хосте.
— Для микросервисов обычно выбирают Docker, для полной изоляции окружений — VM.
Please open Telegram to view this post
VIEW IN TELEGRAM
🥰31👍1
Что такое DDD?

DDD (Domain-Driven Design) — подход к проектированию, при котором структура кода отражает структуру бизнес-домена.

Основные строительные блоки:

Entity — объект с уникальной идентичностью. Два объекта с одним ID — один и тот же объект, даже если остальные поля разные. Пример: User, Order.

Value Object — объект без идентичности, определяется своими атрибутами. Иммутабелен. Пример: Money(100, 'USD'), Email('alice@example.com'). Два Money(100, 'USD') — одно и то же значение.

Aggregate — кластер связанных сущностей с одним корнем (Aggregate Root). Все изменения внутри агрегата — только через корень. Граница транзакции = граница агрегата. Пример: Order содержит OrderItems, но только Order — корень.

Domain Service — бизнес-операция, которая не принадлежит ни одной сущности. Пример: TransferService(fromAccount, toAccount, amount).

Repository — абстракция доступа к хранилищу для агрегатов. Один репозиторий — один агрегат.

Domain Event — факт, произошедший в домене. OrderPlaced, PaymentFailed.

Bounded Context — явная граница, внутри которой модель имеет единое значение. User в контексте Billing ≠ User в контексте Shipping.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍54🤔1
Чем отличается Application Service от Domain Service в DDD?

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
1👍1
Symfony Notifier: уведомления по электронной почте и в браузере через единый программный интерфейс. Бесплатный урок курса «Symfony Framework»

Уведомления почти всегда выглядят просто только на старте проекта. Потом появляются электронная почта, уведомления в браузере, разные приоритеты, отдельные подключения, и быстро становится ясно, что система расползается по коду. То, что должно было быть «пара писем и одно всплывающее сообщение», превращается в отдельную архитектурную боль.

📅 На открытом уроке 2 апреля в 20:00 разберём:

— как Symfony Notifier помогает собрать централизованную систему уведомлений без хаоса в архитектуре.
— как из одного класса уведомлений отправлять сообщения по электронной почте и в браузер.
— как настраивать разные представления под разные каналы.
— как работает политика маршрутизации уведомлений по важности.
— и как использовать Mercure для всплывающих уведомлений напрямую в браузере без сложной инфраструктуры обмена в реальном времени.

Урок не для тех, кто хочет «быстро прикрутить уведомления» без понимания архитектуры, рассчитывает обойтись набором разрозненных библиотек или не работает с реальными проектами на Symfony.

👉 Записаться: https://clc.to/L-YbEA

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
Что такое Idempotency в контексте HTTP и очередей? Как обеспечить?

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

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
🔥3
Что такое OPcache и как он ускоряет PHP?

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
👍5🔥2
✔️ PHP-тест: Generators + Memory leak + Batch-обработка

«Мы перешли на генераторы, чтобы не грузить память» — но память всё равно растёт 👇

📦 Задание

Команда переписала импорт 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
🔥51👍1🌚1