fa
Feedback
Библиотека собеса по PHP | вопросы с собеседований

Библиотека собеса по PHP | вопросы с собеседований

رفتن به کانال در Telegram

Вопросы с собеседований по PHP и ответы на них. По рекламе: @proglib_adv Учиться у нас: https://proglib.io/w/9f3affba Для обратной связи: @proglibrary_feeedback_bot

نمایش بیشتر
3 149
مشترکین
-124 ساعت
-57 روز
-630 روز
آرشیو پست ها
Что такое событийно-ориентированная архитектура? 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));
Зачем ✔️ Слабая связанность (UserService не знает о SendWelcomeEmail) ✔️ Легко добавить новый listener без изменения существующего кода (Open/Closed) ✔️ Можно делать асинхронные listeners (через очередь) В Laravel: Event / Listener, EventServiceProvider. В Symfony: EventDispatcher, декларация через атрибуты. Подводный камень: сложно трейсить цепочку — одно событие вызывает другое.

Что происходит при использовании команды cache:clear и как это влияет на кэшированные данные в случае использования Redis с тэгами? Команда php artisan cache:clear очищает весь кэш, включая данные в Redis, но если используется кэширование с тегами, то команда очистит только данные, связанные с общими ключами. Теги управляются отдельно, поэтому их нужно очищать вручную, используя cache:tags() для работы с конкретными группами данных.

💬 Обратная связь Текущий уровень сложности вопросов? 🔥 — Слишком просто, хочу сложнее 👍🏼 — В самый раз ❤️ — Иногда сложновато 😁 — Часто не понимаю

✔️ 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,
    ) {}
}
🔹 Задачи — Найти все источники роста памяти — Объяснить, почему проблема не воспроизводится на стейджинге — Предложить решение Ставьте → 🔥 если нравится формат. Если нет → 🌚 💬 Решения пишите в комменты под спойлер — сравним подходы.

Что такое 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.

Что такое 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.

Symfony Notifier: уведомления по электронной почте и в браузере через единый программный интерфейс. Бесплатный урок курса «Sy
Symfony Notifier: уведомления по электронной почте и в браузере через единый программный интерфейс. Бесплатный урок курса «Symfony Framework» Уведомления почти всегда выглядят просто только на старте проекта. Потом появляются электронная почта, уведомления в браузере, разные приоритеты, отдельные подключения, и быстро становится ясно, что система расползается по коду. То, что должно было быть «пара писем и одно всплывающее сообщение», превращается в отдельную архитектурную боль. 📅 На открытом уроке 2 апреля в 20:00 разберём: — как Symfony Notifier помогает собрать централизованную систему уведомлений без хаоса в архитектуре. — как из одного класса уведомлений отправлять сообщения по электронной почте и в браузер. — как настраивать разные представления под разные каналы. — как работает политика маршрутизации уведомлений по важности. — и как использовать Mercure для всплывающих уведомлений напрямую в браузере без сложной инфраструктуры обмена в реальном времени. Урок не для тех, кто хочет «быстро прикрутить уведомления» без понимания архитектуры, рассчитывает обойтись набором разрозненных библиотек или не работает с реальными проектами на Symfony. 👉 Записаться: https://clc.to/L-YbEA Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru

Чем отличается 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.

Что такое 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.

В чём основное отличие Docker от виртуальной машины? Основное отличие в уровне виртуализации. Виртуальная машина виртуализирует железо целиком: у неё есть собственная ОС со всеми компонентами, гипервизор, ядро. Это тяжеловесно — VM может весить гигабайты и стартовать минутами. Docker виртуализирует только уровень приложения. Контейнеры используют ядро хостовой ОС, изолируясь через namespaces и cgroups. Они легковесны — образ может весить десятки мегабайт, запускается за секунды. 🔹 На практике это означает — Docker быстрее и экономнее по ресурсам. — VM даёт полную изоляцию и может запускать разные ОС на одном хосте. — Для микросервисов обычно выбирают Docker, для полной изоляции окружений — VM.

😱 Если ваш продукт не умеет отдавать данные в формате, понятном AI-агенту, то вас просто не существует Скрипт не будет кликать по красивым кнопкам в браузере, он уйдёт к конкуренту с нормальным API. Перестроить архитектуру под машинных клиентов — это уже не хайп, а необходимое условие сохранения конкурентоспособности. Как адаптировать продукт и не исчезнуть из выдачи: — интегрировать MCP и A2A-взаимодействие, чтобы агенты могли вас читать; — научиться контролировать стоимость (лимиты, кэш, роутинг между моделями); — настроить AgentOps: трейсинг, логирование и отлов регрессий. Всё это ждёт вас на обновлённом курсе «Разработка AI-агентов». Мы специально сделали фокус на утилитарном инжиниринге и production-ready решениях. Кстати, до 29 марта можно забрать курс с большой скидкой, и стоит поторопиться — мест на потоке всё меньше. Зафиксировать цену и начать деплоить агентов без слива бюджета 👈

✔️ 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 не доходит до контроллера — и данные считаются потерянными — Объяснить проблему — Предложить исправленную структуру Ставьте → 🔥 если нравится формат. Если нет → 🌚 💬 Решения пишите в комменты под спойлер — сравним подходы.

Что такое 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).

Что такое 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 использует индекс слева направо и останавливается на первом неиспользованном столбце.

Объясни разницу между индексом 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.

Чем отличается 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-механизм, сложнее реализовать корректно.

Как предотвратить SQL-инъекции? SQL-инъекция — подстановка вредоносного SQL через пользовательский ввод.
Плохо:
  $query = "SELECT * FROM users WHERE name = '$name'";
  // name = "' OR '1'='1" → сломает всё
Правильно — подготовленные выражения (prepared statements):
// PDO:
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);

// MySQLi:
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$stmt->execute();
Параметры никогда не попадают в SQL-текст — они передаются отдельно. Дополнительно: • Никогда не доверяй пользовательскому вводу • Принцип минимальных привилегий для DB-пользователя • ORM (Eloquent, Doctrine) использует prepared statements под капотом

Что такое сессии в PHP и какие у них уязвимости? Сессия — механизм хранения данных между HTTP-запросами. PHP создаёт уникальный session_id, передаёт его клиенту в cookie (PHPSESSID), а данные хранит на сервере (файлы, Redis, DB).
session_start();
$_SESSION['user_id'] = $user->id;
Уязвимости: ⚠️ Session Fixation — атакующий подсовывает жертве известный session_id. Решение: session_regenerate_id(true) после логина. ⚠️ Session Hijacking — кража session_id через XSS или сниффинг. Решение: HTTPS, httponly cookie, SameSite. ⚠️ Предсказуемый session_id — в старых PHP. В современных — криптографически безопасный генератор.

Начать рассказывать интервьюеру, как вы ловко дёргаете ручки API через базовый LangChain. Звучит как отличный план, да? Нет, это мгновенный отказ. В свежем отчёте по рынку GPU говорится, что 54% компаний стопают ИИ-внедрения тупо из-за конских затрат на инфраструктуру. На серверах более 70% стоимости — это видеокарты. Поэтому на собесах сейчас спрашивают не про красивые промпты, а про жёсткую экономику агентов. По сути, от вас ждут понимания, как лимитировать ресурсы на лету, роутить запросы и дебажить отказы через механизм time-travel в LangGraph. Если вы до сих пор собираете ботов в ноутбуках, гляньте обновлённый курс «Разработка ИИ-агентов» — фокус там смещён с игрушечных концепций на суровый энтерпрайз. Что требуют от мидлов и выше: — интеграция мультиагентных систем по стандарту MCP; — суровый AgentOps: метрики, трейсинг, защита от деградации пайплайнов; — локальный деплой Open Source под 152-ФЗ (без этого в финтех можно даже не стучаться). Прямо сейчас можно урвать курс с увесистой скидкой (49 000 ₽ 62 990 ₽ за базовый тариф и 99 000 ₽ 124 990 ₽ за продвинутый трек), но стоит поторопиться — на потоке осталось всего 5 мест. 👉 Подтянуть архитектуру до уровня прода

✔️ PHP-тест: Typed Properties + Lazy Initialization + Объект в статике Баг, который живёт в проде месяцами и проявляется только под нагрузкой 👇 📦 Задание Команда пишет модуль биллинга. Ты написал быстрый класс-обёртку для работы с тарифами. Код прошёл ревью, всё работало на стейджинге. В проде через неделю начались жалобы: у части пользователей неправильно считается стоимость. Причём только в пиковые часы.
// src/Billing/TariffCalculator.php
class TariffCalculator
{
    private static TariffConfig $config;
    private static array $cache = [];

    public static function init(array $rawConfig): void
    {
        self::$config = new TariffConfig($rawConfig);
    }

    public static function calculate(int $userId, int $units): float
    {
        $key = $userId . ':' . $units;

        if (isset(self::$cache[$key])) {
            return self::$cache[$key];
        }

        $price = self::$config->getBasePrice()
            * $units
            * self::$config->getUserMultiplier($userId);

        self::$cache[$key] = $price;

        return $price;
    }

    public static function resetCache(): void
    {
        self::$cache = [];
    }
}

// src/Billing/TariffConfig.php
class TariffConfig
{
    private float $basePrice;
    private array $multipliers;

    public function __construct(array $config)
    {
        $this->basePrice   = (float) $config['base_price'];
        $this->multipliers = $config['multipliers'] ?? [];
    }

    public function getBasePrice(): float
    {
        return $this->basePrice;
    }

    public function getUserMultiplier(int $userId): float
    {
        return $this->multipliers[$userId] ?? 1.0;
    }
}

// bootstrap.php — вызывается один раз при старте воркера
TariffCalculator::init(loadConfigFromDB());

// Где-то в обработчике запроса
$price = TariffCalculator::calculate($user->id, $request->units);
🔹 Задачи — Найти все архитектурные и логические проблемы в коде (их несколько) — Объяснить, почему баг проявляется только под нагрузкой и не воспроизводится на стейджинге — Предложить правильное решение Ставьте → 🔥 если нравится формат. Если нет → 🌚 💬 Решения пишите в комменты под спойлер — сравним подходы.