Введение

В мире разработки бизнес-приложений, особенно построенных по принципам Domain-Driven Design (DDD), важным элементом архитектуры является обработка ошибок. Неправильно реализованная стратегия может привести к логическому хаосу и плохому пользовательскому опыту. Представьте, если ошибка базы данных попадёт напрямую в UI — это не только некрасиво, но и опасно.

Слоистая архитектура предполагает чёткое разделение ответственности:

  • Infrastructure Layer — работа с внешними системами.

  • Domain Layer — бизнес-логика.

  • Application Layer — координация операций.

  • Presentation Layer — API, UI, CLI.

Ошибки могут возникать на любом уровне, но очень важно правильно их перехватывать, трансформировать и логировать. В этой статье разберёмся, как настроить обработку ошибок в соответствии с принципами DDD и практиками промышленной разработки.

Рассмотрим общий флоу обработки на примере: вы делаете запрос на получение сущности из базы данных. Произошёл сбой соединения. Что дальше? Если ошибка SqlException напрямую попадёт в клиент, вы нарушили все уровни абстракции. Поэтому важно, чтобы каждый слой делал свою работу.

Вот как выглядит данный флоу:

  1. Ошибка возникает на уровне Infrastructure — например, IOException, HttpRequestException, SqlException.

  2. Infrastructure Layer перехватывает низкоуровневую ошибку, при необходимости логирует, и выбрасывает обобщённую исключительную ситуацию, понятную Domain Layer — например, StorageUnavailableException.

  3. Domain Layer принимает исключение и преобразует его, если нужно, в доменное: InvalidOrderStateException, BusinessRuleViolationException.

  4. Application Layer принимает доменное исключение, принимает решение:

    • Повторить попытку?

    • Записать в аудит?

    • Вернуть Result.Failure(...)?

    • Пробросить вверх?

  5. Presentation Layer получает итог: либо Result.Success(...), либо ошибку. Преобразует в стандартизированный HTTP-ответ: ApiError, HttpException.

Такой подход:

  • Сохраняет изоляцию слоёв

  • Даёт гибкость в интерпретации

  • Позволяет централизованно логировать и трассировать ошибки


Паттерн последовательной обработки исключений

За годы работы с обработкой ошибок в програмировани я перепробовал множество подходов: от громоздких try-catch-блоков до избыточных логов, которые только маскировали проблемы вместо их решения. Методом проб и ошибок у меня сформировался паттерн которым я хочу с вами поделиться. С помощу него у нас получаеться: 

  • Минимизирует дублирование кода — исключения обрабатываются на нужном уровне, без лишних вложенных проверок.

  • Обеспечивает прозрачность — каждая ошибка либо логируется, либо преобразуется в понятный ответ, либо передаётся выше по стеку вызовов.

  • Сохраняет контекст — даже при каскадных сбоях система не теряет важные детали, помогая быстро находить корень проблемы.

Infrastructure Layer

  => throw low-level Exception (например, SqlException, IOException)

  => log error (тех детали, например, ошибки от third party service)

Domain Layer

  => catch low-level exception

  => map to DomainException (например, DomainRuleViolationException)

  => throw DomainException (для бизнес логики)

Application Layer

  => catch DomainException

  => enrich error context (например, id пользователя, use-case)

  => take design: применить патерны - повторить, отменить транзакцию, бросить доменное исключение

  => log error (если это ошибка бизнес-логики или неожиданная ошибка)

  => throw ApplicationException (с кодом ошибки)

Presentation Layer (API, UI)

  => catch ApplicationException

  => map to HTTP response (например, 400, 409, 500)

  => включить уникальный код ошибки в ответ


Ошибки на уровне Infrastructure level

Infrastructure Layer — это всё, что взаимодействует с "внешним миром": БД, файловая система, API, очереди сообщений. Здесь ошибки случаются часто и почти всегда технические. Типичные примеры:

  • SqlException — ошибка SQL-запроса или подключения

  • IOException — проблемы с файлами

  • HttpRequestException — сбои при вызове внешних сервисов

  • TimeoutException — превышено время ожидания

Основная задача на этом уровне — перехват низкоуровневых технических исключений и их преобразование в более абстрактные ошибки, понятные с точки зрения нашей системы.

public function findById(UserId $userId): UserInterface
{
    return $this->repository->find($userId->toRfc4122()) 
        ?? throw new UserNotFoundException($userId);
}

Тут мы бросаем доменную ошибку, так как Repository патерн это больше все же чать домена. Также одним из подходов выброса ошибки может быть более обобщенное исключение, которое можно использовать для всех типов сущностей например EntityNotFoundException, которая будет пеехватываться на уровне Domain и уже там преобразововаться в UserNotFoundException при необходимосте. Но с точки зрения чистого DDD, более правильным подходом будет использование UserNotFoundException. Исключения должен быть определен в домене, а не в инфраструктуре, поскольку это исключение относится к бизнес-правилам и семантике домена.

Также этот слой являеться идеальное место для логирования технических деталей. Вы здесь знаете всё: стек вызовов, соединения, таймауты. Логировать тут: безопасно, полезно для поддержки, не влияет на бизнес-логику. Но слудеет следовать определенным принципам:

  1. Логировать технические детали: Статус-коды, время ответа, технические ошибки подключения

  2. Не дублировать бизнес-логику: Основная интерпретация ошибок должна происходить выше

  3. Соблюдать уровни логирования:

    • DEBUG: Детали запросов/ответов

    • INFO: Успешные операции

    • WARN: Повторные попытки, временные сбои

    • ERROR: Критические ошибки на уровне инфраструктуры

Пример:

<?php
    private function request(string $method, string $endpoint, array $options = []): array
    {
       $url = "{$this->baseUri}{$endpoint}"
        $this->logger->debug('API request initiated', [
            'method' => $method,
            'url' => $url,
       ]);
        $startTime = microtime(true);
        try {
            $response = $this->httpClient->request($method, $url, $options);
            $duration = microtime(true) - $startTime;
            $statusCode = $response->getStatusCode();
            
            $this->logger->info('API request completed', [
                'method' => $method,
                'url' => $url,
                'status_code' => $statusCode,
                'duration_ms' => round($duration * 1000),
            ]);
            
            $body = $response->getBody()->getContents();
            $data = json_decode($body, true);
            
            if (json_last_error() !== JSON_ERROR_NONE) {
                $this->logger->warning('Failed to parse API response', [
                    'error' => json_last_error_msg(),
                    'body_preview' => mb_substr($body, 0, 100) . (mb_strlen($body) > 100 ? '...' : ''),
                ]);
                
                throw new ExternalApiException('Invalid JSON response from API', $statusCode);
            }
            
            return $data;
            
        } catch (GuzzleException $e) {
            $duration = microtime(true) - $startTime;
            $statusCode = $e->getCode();
            $context = [
                'method' => $method,
                'url' => $url,
                'status_code' => $statusCode,
                'duration_ms' => round($duration * 1000),
                'exception' => get_class($e),
                'message' => $e->getMessage(),
            ];

            // Логируем с разным уровнем в зависимости от типа ошибки
            if ($statusCode >= 500) {
                $this->logger->error('API server error', $context);
            } elseif ($statusCode >= 400) {
                $this->logger->warning('API client error', $context);
            } else {
                $this->logger->error('API connection error', $context);
            }

            // Преобразуем низкоуровневое исключение в доменное
            throw new ExternalApiException(
                message: "API request failed: {$e->getMessage()}",
                code: $statusCode ?: 0,
                previous: $e
            );
        }
   }

Ошибки на уровне Domain level

В любой нетривиальной системе, доменный слой (Domain Layer), содержащий основную бизнес-логику, неизбежно взаимодействует с внешними зависимостями через инфраструктурный слой (Infrastructure Layer). Эти зависимости могут включать базы данных, файловые системы, внешние API, очереди сообщений и т.д. В процессе взаимодействия с инфраструктурой могут возникать ошибки, специфичные для этой инфраструктуры (например, проблемы с подключением к базе данных, ошибки файловой системы, сетевые тайм-ауты). Неконтролируемое распространение таких "инфраструктурных" исключений внутрь доменного слоя является антипаттерном, нарушающим принцип инкапсуляции и делающим доменный слой зависимым от деталей реализации инфраструктуры. Условно можно разделить все ошибки в домене на те что пришли к нам с уровня ниже (Infrastructure) и созданые нами во время работы внутри домена (VO, Agreggate, Domain Services).

Обработка инфраструктурных ошибок

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

1. Перехват на границе домена: Инфраструктурные исключения не должны пересекать границу доменного слоя в своем первозданном виде. Они должны быть перехвачены в инфраструктурном слое или на самом стыке между слоями. Это гарантирует, что доменный слой оперирует только понятиями и ошибками, релевантными его бизнес-логике.

Тут нужно остановиться и прояснить что значит “на стыке”.

Для этого разьером несколько терминов:

  • Инфраструктурное исключение — ошибка, связанная с внешними системами (БД, API, файловая система и т.д.). Пример: PDOException, GuzzleHttp\Exception\RequestException.

  • Граница домена — условная линия между бизнес-логикой (домен) и внешним миром (инфраструктура).

  • "На стыке" — код, который находится между слоями и отвечает за преобразование исключений (адаптер, слой приложения). Преобразование исключений при переходе между слоями.

  • "Не на стыке" — глубоко внутри слоя (например, в репозитории или доменном сервисе). Обработка внутри слоя без проброса наружу.

Пример: Перехват на стыке (границе домена)

// Domain/Exception/PersistenceException.php
class PersistenceException extends \RuntimeException {}

// Domain/UserService.php
class UserService {
    public function createUser(string $name) {
        $user = new User($name);
        $this->repository->save($user); // Получает только PersistenceException
    }
}

Если исключение можно обработать сразу в инфраструктуре без проброса в домен. Когда ошибка БД не критична для бизнес-логики (например, кэширование).

Пример: Перехват не на стыке (в инфраструктуре)

// Infrastructure/UserRepository.php
class UserRepository {
    public function save(User $user): void {
        try {
            $this->pdo->prepare('INSERT ...')->execute(...);
        } catch (\PDOException $e) {
            // Логируем и возвращаем null вместо исключения
            $this->logger->error("DB error", [$e]);
            return null;
        }
    }
}

2. Трансформация в доменные исключения: После перехвата инфраструктурное исключение должно быть преобразовано (трансформировано) в исключение, специфичное для доменного слоя. Эти доменные исключения должны выражать причину ошибки в терминах бизнес-логики или предметной области, а не в терминах технических деталей инфраструктуры. Например, DatabaseConnectionException из инфраструктуры может быть трансформирован в RepositoryUnavailableException или DataAccessException в домене.

3. Сохранение необходимого технического контекста: Несмотря на трансформацию, важно не потерять весь технический контекст исходного исключения. Определенная часть технической информации (например, код ошибки базы данных, сообщение об ошибке от внешней службы, детали запроса) может быть критически важна для отладки или логирования на более высоких уровнях приложения (например, на уровне презентации или логирования). Этот контекст может быть сохранен внутри доменного исключения (например, в виде свойств) или передан вместе с ним.

Пример:

// Инфраструктурный слой (репозиторий)
class UserRepository {
    public function save($data) {
        try {
            // Может упасть PDOException
            $this->pdo->query('INSERT...'); 
        } catch (PDOException $e) {
            // Просто оборачиваем в доменное исключение
            throw new DomainException("Save failed", 0, $e);
        }
    }
}

Стратегии обработки инфраструктурных исключений

Существует несколько стратегий для трансформации и обработки инфраструктурных исключений:

  • Прямая трансформация: Самая простая стратегия, при которой каждое конкретное инфраструктурное исключение напрямую маппится на соответствующее доменное исключение. Например, FtpConnectionException маппится на FileStorageUnavailableException, а HttpClientTimeoutException на ExternalServiceTimeoutException. Эта стратегия подходит, когда существует четкое одно-к-одному соответствие между инфраструктурными и доменными ошибками.

  • Обогащение контекстом: При трансформации доменное исключение обогащается дополнительным контекстом, который может быть полезен для обработки ошибки дальше по стеку вызовов. Этот контекст может включать исходное инфраструктурное исключение (для логирования), параметры операции, которая привела к ошибке, или любые другие релевантные технические детали. Это позволяет сохранить информацию для отладки, не загрязняя доменный слой инфраструктурными типами исключений.

  • Агрегация ошибок: В некоторых случаях одна операция в домене может взаимодействовать с несколькими компонентами инфраструктуры, и каждая из них может сгенерировать ошибку. В таких ситуациях может быть полезно агрегировать несколько инфраструктурных ошибок в одно доменное исключение (например, MultiFileUploadFailedException, которое содержит список отдельных ошибок для каждого файла). Это упрощает обработку множественных сбоев на уровне домена.

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

Антипаттерны

Неправильная обработка инфраструктурных ошибок может привести к появлению антипаттернов, которые ухудшают качество кодовой базы и усложняют ее поддержку:

  • Проброс "голых" инфраструктурных исключений: Самый распространенный антипаттерн. Когда инфраструктурное исключение беспрепятственно пробрасывается через границу домена, доменный слой становится явно зависимым от конкретной используемой инфраструктуры. Это нарушает принципы слабой связанности и инкапсуляции, делая доменный слой хрупким к изменениям в инфраструктурном слое.

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

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

Основные типы доменных ошибок

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

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

Нарушение инвариантов агрегатов

Агрегат (Aggregate) в Domain-Driven Design (DDD) представляет собой кластер связанных объектов домена, который рассматривается как единое целое для целей изменений данных. У агрегатов есть инварианты — правила, которые всегда должны выполняться, чтобы агрегат находился в согласованном (валидном) состоянии. Инварианты агрегата гарантируют целостность бизнес-данных внутри его границ.

Нарушение инварианта агрегата происходит, когда операция пытается перевести агрегат в состояние, которое противоречит одному или нескольким из этих правил. Такие нарушения являются фундаментальными доменными ошибками, поскольку они означают, что бизнес-объект находится в некорректном состоянии с точки зрения предметной области.

Пример: В домене электронной коммерции агрегат Order может иметь инвариант: "Суммарная стоимость товаров в заказе должна соответствовать сумме позиций заказа за вычетом примененных скидок". Если попытка добавить или изменить позицию заказа приводит к нарушению этого правила, это является нарушением инварианта агрегата Order, что должно быть сигнализировано доменной ошибкой (например, InvalidOrderStateException).

Попытка создать недопустимое значение

Многие сущности и объекты-значения Value Objects (VO) в домене имеют ограничения на допустимые значения своих свойств. Например, email-адрес должен иметь определенный формат, номер телефона — соответствовать определенному паттерну, количество товара в корзине не может быть отрицательным. Попытка создать объект домена или присвоить свойству значение, которое не соответствует этим ограничениям, является доменной ошибкой.

Этот тип ошибки часто связан с валидацией входных данных внутри доменного слоя, в конструкторах или фабричных методах объектов домена, а не на уровне пользовательского интерфейса или API. Валидация на входе в домен гарантирует, что внутри домена оперируют только корректными, валидными данными с точки зрения бизнес-логики.

Пример: Создание объекта-значения EmailAddress. Его конструктор должен проверить, соответствует ли переданная строка формату email. Если формат неверный, конструктор должен выбросить доменное исключение (например, InvalidEmailFormatException).


Ошибки на уровне Application Layer

В слоеной архитектуре (Layered Architecture) и Domain-Driven Design (DDD) каждый слой выполняет свою специфическую роль. Доменный слой содержит чистую бизнес-логику и инварианты,уровень представления (Presentation Layer) занимается взаимодействием с пользователем (HTTP API, UI). Между ними находится Уровень Приложения (Application Layer). Уровень Приложения действует как оркестратор или координатор. Он принимает входные данные из уровня представления (часто в виде DTO), организует выполнение определенного варианта использования use case (Clear Architecture)  путем вызова методов доменных сервисов, агрегатов и репозиториев, а также взаимодействует с инфраструктурным слоем. Результатом выполнения use case может быть успешное изменение состояния системы или ошибка.

Обработка ошибок является прерогативой Application Layer

Почему Application layer, а не, скажем, контроллера или самого домена?

  • Изоляция домена: Доменный слой должен быть сфокусирован исключительно на бизнес-логике. Он не должен знать о том, как обрабатываются ошибки на системном уровне (логирование, повторные попытки, формирование ответов для пользователя). Доменные объекты и сервисы сигнализируют об ошибках путем выбрасывания доменных исключений, описывающих нарушение бизнес-правил.

  • Изоляция инфраструктуры: Инфраструктурный слой, в свою очередь, выбрасывает исключения, специфичные для конкретной технологии (например, DatabaseConnectionException, CurlException). Доменный слой также не должен зависеть от этих технических деталей.

  • Роль координатора: Application Layer - это первый уровень, который осознает весь контекст выполнения операции: какой use case был вызван, какие входные данные были переданы, кто (какой пользователь) его вызвал. Именно здесь есть вся информация, необходимая для принятия решения о том, как реагировать на ошибку, произошедшую либо в домене, либо в инфраструктуре при выполнении данного use case. Уровень представления не должен содержать этой бизнес-логики обработки ошибок, он лишь отображает результат работы Application Layer.

Таким образом, Application Layer выступает в роли барьера и переводчика ошибок. Он перехватывает исключения из нижних слоев (домена и инфраструктуры), обрабатывает их, обогащает контекстом и преобразует в унифицированный формат, понятный для уровня представления.

Флоу обработки ошибок

Стандартный сценарий обработки ошибки в типичном use case или command/query (в случае CQRS) выглядит следующим образом:

Перехват исключений: Код use case оборачивается в блок try...catch. Перехватываются ожидаемые доменные исключения (например, UserNotFoundException, InvalidOrderStateException, ProductOutOfStockException), а также, возможно, инфраструктурные исключения, которые не были полностью трансформированы на границе с инфраструктурой (хотя предпочтительнее, чтобы инфраструктура трансформировала их в свои "граничные" исключения, которые Application Layer уже перехватит). Также перехватываются неожиданные исключения (\Throwable).

Обогащение контекстом: При перехвате исключения крайне важно собрать максимум полезной информации для отладки и анализа. Этот контекст может включать:

  • Идентификатор пользователя, совершающего действие.

  • Название выполняемого use case.

  • Входные данные, переданные use case.

  • Уникальный идентификатор запроса/трассировки (Trace ID/ Request ID).

  • Состояние системы, которое могло повлиять на ошибку.

  • Исходное исключение ($exception->getPrevious()).

Принятие решения: На основе типа перехваченного исключения и обогащенного контекста Application Layer может принять решение:

  • Повторить операцию (Retry): Если ошибка временная и, возможно, связана с инфраструктурой (например, сетевой тайм-аут, временная недоступность сервиса). Это решение должно приниматься с учетом стратегии повторных попыток (сколько раз, с какой задержкой).

  • Откатить транзакцию (Rollback): Если операция включает изменение состояния в персистентности (например, через транзакцию базы данных), а ошибка произошла до ее завершения, необходимо откатить все изменения, чтобы сохранить целостность данных.

  • Сформировать новую ошибку: В большинстве случаев, после обработки и обогащения, исходное исключение трансформируется в исключение, специфичное для Application Layer (ApplicationException), или объект ошибки (ApplicationErrorDto), который будет передан уровню представления.

Логирование: На этом этапе производится логирование информации об ошибке.

  • Бизнес-ошибки: Логируются доменные ошибки, которые могут указывать на попытки некорректных действий пользователей или нарушения бизнес-процессов. Уровень логирования может быть INFO или WARNING.

  • Неожиданные исключения: Любые неперехваченные специфические исключения (доменные, инфраструктурные) или общие \Throwable должны логироваться с высоким уровнем критичности (ERROR, CRITICAL), поскольку они указывают на потенциальные проблемы в коде или инфраструктуре. Контекст, собранный ранее, передается в логгер.

Преобразование в ApplicationException или ApplicationError: Финальным шагом является формирование результата для уровня представления. Это может быть выброс ApplicationException или возврат объекта, содержащего информацию об ошибке (например, ApplicationError) внутри себя. Этот объект/исключение должен содержать унифицированную информацию:

  • Тип ошибки (например, BusinessError, ValidationError, TechnicalError).

  • Код ошибки (часто используется Enum).

  • Понятное для пользователя (или уровня представления) сообщение.

  • Возможно, дополнительный "payload" с деталями ошибки (например, список полей с ошибками валидации).

    Пример:

try {
            $user = $this->userService->changeEmail($userId, $newEmail);
            
            return ApplicationResult::success([
                'userId' => $user->getId(),
                'email' => $user->getEmail()
            ]);
} catch (DomainException $e) {
            // Обработка общего доменного исключения
            return ApplicationResult::failure(
                'domain_error',
                $e->getMessage(), 
                400
            );
            
} catch (Throwable $e) {
            // Логирование неожиданных ошибок
            // logger()->error($e->getMessage(), ['exception' => $e]);
            
            // Преобразование в ApplicationException для системных ошибок
            throw new ApplicationException(
                'Произошла внутренняя ошибка системы', 
                500, 
                $e
            );
}

ApplicationException служит для:

  • Инкапсуляции ошибок, произошедших в домене или инфраструктуре.

  • Предоставления адаптерам (например, контроллерам) информации об ошибке без раскрытия внутренних деталей.

  • Упрощения обработки ошибок на уровне пользовательского интерфейса.

Антипаттерны

  • Повторный throw без enrich: Простое перебрасывание пойманного исключения без добавления контекста затрудняет отладку.

  • Проглатывание исключений: Молчаливое игнорирование ошибок (пустой блок catch) скрывает проблемы и приводит к непредсказуемому поведению.

  • Использование \Exception вместо специализированных типов: Ловля и пробрасывание общих исключений делает код обработки ошибок неспецифичным и хрупким. Всегда старайтесь перехватывать и выбрасывать более конкретные типы исключений.

  • Логирование без контекста: Сообщение об ошибке без привязки к конкретному use case, пользователю или входным данным бесполезно для диагностики.

  • возврат объединенного типа (ApplicationResult|ApplicationError). Создания интерфейса (ApplicationResultInterfce) или в более новых версиях PHP 8.0 и выше делают возврат объединенного типа (ApplicationResult|ApplicationError). Такой подход имеет свои недостатки:

    1. Неопределенность типа возврата - при использовании union types (|) сложнее контролировать логику обработки результата, требуется дополнительная проверка типа:

      $result = $service->someMethod();
      if ($result instanceof ApplicationError) {
          // Обработка ошибки
      } else {
          // Обработка успешного результата
      }
    2. Ментальная нагрузка - разработчикам приходится помнить о возможных разных типах и обрабатывать их корректно, что повышает вероятность ошибок.

    3. Проблемы с статическим анализом - инструменты статического анализа не всегда хорошо работают с union types, особенно в более старых версиях PHP.

    4. Нарушение принципа единой ответственности - метод берет на себя ответственность возвращать разные структуры данных в зависимости от результата.

    5. Сложность типизации в базовых интерфейсах - если вы определяете интерфейсы для сервисов, union types могут усложнить их.

Вместо этого подхода я рекомендую использовать единый тип возврата ApplicationResult, как было показано в предыдущем примере. Такой подход имеет следующие преимущества:

  1. Единообразие - всегда возвращается один и тот же тип, что упрощает использование API.

  2. Предсказуемость - клиентский код точно знает структуру возвращаемого значения

    $result = $service->someMethod();
    if ($result->isSuccessful()) {
        $data = $result->getData();
    } else {
        $error = $result->getErrorMessage();
    }

    3.Инкапсуляция состояния - объект ApplicationResult инкапсулирует как успешное, так и неуспешное состояние:

    • Возможность расширения - легче добавить новые состояния или атрибуты результата не меняя сигнатуру методов.

    • Соответствие практикам DDD - на уровне приложения мы трансформируем доменные ответы в единообразный формат, который может быть легко преобразован в API-ответы.


Обработка в Presentation Layer

Presentation Layer (уровень представления), будь то HTTP API, веб-интерфейс или CLI-интерфейс, является точкой входа для внешнего мира и точкой выхода для результатов выполнения бизнес-логики. Его основная ответственность в контексте обработки ошибок — это перехватить ошибки, возникшие на нижележащих уровнях (преимущественно ApplicationException из Application Layer), и преобразовать их в формат, подходящий для потребления клиентом (браузером, мобильным приложением, другим сервисом).

Одна точка для перехвата всех ошибок

Ключевым принципом обработки ошибок на уровне представления является наличие единой точки перехвата. Это означает, что вместо того, чтобы оборачивать каждый контроллер или обработчик запроса в try...catch блоки, используется централизованный механизм, который перехватывает все необработанные исключения, "вылетевшие" из Application Layer (или других частей системы, если они достигли этого уровня).

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

  • Унификация: Все ошибки обрабатываются единообразно, гарантируя консистентный формат ответа для клиента.

  • Сокращение дублирования кода: Логика обработки ошибок (логирование, форматирование ответа, интернационализация) не повторяется в каждом обработчике запроса.

  • Упрощение поддержки: Изменение логики обработки ошибок (например, добавление нового поля в ответ ошибки) требует модификации только в одном месте.

  • Повышение надежности: Снижается вероятность пропустить необработанное исключение.

Стандартизация ответа

Когда ошибка перехвачена в единой точке, ее необходимо представить клиенту в стандартизированном формате. Для HTTP API это обычно означает:

  • Соответствующий HTTP статус-код: Ошибки валидации (ValidationError) могут маппиться на 400 Bad Request, ошибки доступа (BusinessRuleViolation типа "нет прав") на 403 Forbidden, отсутствие ресурса (NotFound) на 404 Not Found, технические ошибки (TechnicalError) на 500 Internal Server Error (или 503 Service Unavailable, если это временная проблема).

  • Структурированное тело ответа: Часто используется JSON. Тело ответа должно содержать информацию об ошибке в предсказуемой структуре. На основе ApplicationException (который содержит тип ошибки, код ошибки и сообщение) можно сформировать такой ответ.

Пример:

{
  "error": {
    "type": "BUSINESS_RULE_VIOLATION",               // Тип ошибки из ApplicationErrorType
    "code": "USER_BLOCKED",                          // Код ошибки из ApplicationErrorCode
    "message": "Ваша учетная запись заблокирована.", // Локализованное сообщение
    "details": {                                     // Опционально: дополнительные детали (например, поля валидации)
      "field": "email",
      "reason": "invalid_format"
    }
  }
}

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

В контексте шаблона Action-Domain-Responder (ADR) этот стандартизированный формат ошибок будет в первую очередь ответственностью компонента Responder.

Интернационализация сообщений об ошибках

Сообщения об ошибках, которые видит конечный пользователь, должны быть понятны и, по возможности, представлены на языке пользователя. Уровень представления — идеальное место для реализации интернационализации (i18n) сообщений об ошибках.

Флоу интернационализации:

  1. Application Layer возвращает код ошибки (Enum): Как мы видели в предыдущем разделе, ApplicationException содержит структурированный код ошибки (ApplicationErrorCode::USER_NOT_FOUND, ApplicationErrorCode::INVALID_EMAIL_FORMAT и т.д.). Этот код является языконезависимым идентификатором конкретной бизнес или валидационной проблемы.

  2. Presentation Layer определяет локаль пользователя: Локаль (предпочитаемый язык) пользователя может быть определена различными способами:

    • Из заголовка HTTP Accept-Language.

    • Из настроек профиля аутентифицированного пользователя.

    • Из параметра запроса.

    • Из поддомена или пути URL.

  3. Presentation Layer делает lookup локализованного текста: Используя код ошибки, полученный из ApplicationException, и определенную локаль пользователя, Presentation Layer обращается к системе локализации (например, gettext, Symfony Translation Component или простой массив соответствий код -> текст) для получения соответствующего локализованного сообщения.

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

Обработка ошибок на уровне Presentation Layer завершает цикл обработки ошибок в приложении. Создание единой точки перехвата, стандартизация формата ответа и правильная интернационализация сообщений об ошибках делают ваше приложение более удобным для пользователей и клиентов API, а также упрощают его поддержку и развитие. Использование кодов ошибок из Application Layer в качестве ключей для локализации обеспечивает четкое разделение ответственности и гибкость.


ErrorObject vs throw exception

При проектировании доменного слоя (Domain) и уровня приложения (Application) возникает фундаментальный вопрос: как сигнализировать о возникновении ошибок? Существуют два основных подхода: использование механизма исключений (throw Exception) или возврат специального объекта, инкапсулирующего информацию об ошибке (ErrorObject или ErrorObjectList, часто встречающиеся в концепциях VO или DTO типов). Выбор между этими подходами зависит от природы ошибки и ожидаемого поведения системы. При этом важно учитывать компромиссы между производительностью и удобством разработки, поскольку исключения при правильном использовании делают код более чистым и понятным в определенных сценариях

Throw Exception

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

  • Непредвиденные или невосстановимые ошибки: Это могут быть критические сбои, нарушения базовых инвариантов домена, или технические проблемы, которые делают невозможным дальнейшее выполнение текущей операции.

  • Нарушение контракта метода: Если метод имеет "контракт" (например, он обещает создать валидный объект, но входные данные делают это невозможным), нарушение этого контракта сигнализируется исключением.

  • Прерывание потока: Исключения эффективно "пробрасываются" вверх по стеку вызовов, прерывая текущий поток выполнения до тех пор, пока не будут пойманы подходящим обработчиком на соответствующем уровне (например, Application Layer или Presentation Layer).

ErrorObject

Этот подход используется, когда "ошибка" является ожидаемым и предсказуемым результатом операции, который является частью обычной бизнес-логики. Вместо прерывания потока, операция возвращает объект, который явно указывает на успешность или неуспешность выполнения, а в случае неуспешности содержит детали ошибки.

  • Ошибки валидации: Когда входные данные не соответствуют бизнес-правилам, но это не является "исключительной" ситуацией, а скорее ожидаемым сценарием, требующим информирования пользователя. Часто валидация может выявить несколько ошибок одновременно, и ErrorObjectList позволяет вернуть все из них.

  • Ожидаемые бизнес-отказы: Например, "пользователь не найден" при поиске пользователя в списке пользователей, "недостаточно средств на счете" при попытке списания (если это не критическое нарушение, а ожидаемый бизнес-сценарий).

  • Функциональный подход: В стилях программирования, ориентированных на функциональный подход, функции предпочитают всегда возвращать значение (либо результат, либо ошибку), вместо того чтобы выбрасывать исключения, что способствует более явному управлению потоком.

Railway-oriented programming: функциональный путь обработки ошибок (Updated 15.05.2025)

В функциональном программировании существует интересный подход к обработке ошибок, который часто называют "Railway-oriented programming" (ROP). Эта техника фокусируется на четком разделении успешных вычислений и потенциальных ошибок, направляя поток выполнения по одному из "рельсов" - либо по пути успеха, либо по пути неудачи.

Ключевой концепцией ROP является использование алгебраических типов данных (ADT), таких как Either или Result. Эти типы явно представляют результат операции, который может быть либо успешным значением, либо информацией об ошибке.

Тип Either (часто также называемый Result, особенно когда речь идет об операциях, которые могут завершиться ошибкой) обычно имеет две возможные формы:

  • Right(value): Представляет успешный результат, содержащий некоторое значение.

  • Left(error): Представляет неудачный результат, содержащий информацию об ошибке.

Идея заключается в том, чтобы все функции в цепочке вычислений принимали и возвращали объекты типа Either/Result. Это позволяет автоматически "пропускать" последующие шаги в случае возникновения ошибки.

Пример:

<?php

declare(strict_types=1);

namespace App\Result;

interface Result
{
    public function isSuccess(): bool;
    public function isFailure(): bool;
    public function getValue(): mixed;
    public function getError(): mixed;
    public function map(callable $fn): Result;
    public function flatMap(callable $fn): Result;
}

final readonly class Success implements Result
{
    public function __construct(private mixed $value) {}

    public function isSuccess(): bool
    {
        return true;
    }

    public function isFailure(): bool
    {
        return false;
    }

    public function getValue(): mixed
    {
        return $this->value;
    }

    public function getError(): mixed
    {
        throw new \BadMethodCallException('Cannot get error from a Success result.');
    }

    public function map(callable $fn): Result
    {
        return new Success($fn($this->value));
    }

    public function flatMap(callable $fn): Result
    {
        return $fn($this->value);
    }
}

final readonly  class Failure implements Result
{
    public function __construct(private mixed $error) {}

    public function isSuccess(): bool
    {
        return false;
    }

    public function isFailure(): bool
    {
        return true;
    }

    public function getValue(): mixed
    {
        throw new \BadMethodCallException('Cannot get value from a Failure result.');
    }

    public function getError(): mixed
    {
        return $this->error;
    }

    public function map(callable $fn): Result
    {
        return $this; // Если уже ошибка, ничего не меняем
    }

    public function flatMap(callable $fn): Result
    {
        return $this; // Если уже ошибка, ничего не меняем
    }
}

function success(mixed $value): Success
{
    return new Success($value);
}

function failure(mixed $error): Failure
{
    return new Failure($error);
}

Оценка потребления памяти

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

Причина в том, что при выбрасывании исключения большинство языков программирования (включая PHP) создают и захватывают стек вызовов (stack trace). Это относительно дорогая операция как по времени выполнения, так и по объему потребляемой памяти, поскольку она включает обход стека вызовов и сбор информации о каждой функции в нем. Если исключения выбрасываются очень часто (например, в цикле или при каждом провальном шаге валидации), это может значительно повлиять на производительность и объем используемой памяти. Также PHP выполняет дополнительную работу для обработки исключений, включая поиск блоков catch и unwinding (разматывание) стека.

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

Рекомендации для высоконагруженных систем

Если у вас высоконагруженная система, где ошибки возникают регулярно, использование обекта будет значительно более эффективным с точки зрения памяти

Что можно сделать:

  • В критичных к производительности частях кода (горячие пути выполнения) следует избегать исключений и использовать объекты ошибок.

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

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

Разница в потребляемой памяти может быть существенной:

  • Типичное исключение в PHP может потреблять от 4 до 10 КБ памяти или больше, в зависимости от глубины стека вызовов.

  • Обект обычно потребляет от 0.5 до 2 КБ, в зависимости от количества хранимой информации.

Разница в производительности становится заметной только при многократном и частом выбрасывании исключений. Для большинства бизнес-приложений, где исключения сигнализируют действительно исключительные ситуации (которые происходят редко), этот фактор не является решающим при выборе подхода.

Оптимизация работы с исключениями

Если же вы выбрали флоу throw Exception для высоконагруженного приложения или так случилось исторически или ваше приложения в момент стало высоконагруженым и вы столкнулись с вопросом экономии памяти, что является вполне обоснованным беспокойством, так как создание исключений, особенно захват стека вызовов (stack trace), действительно может быть ресурсоемким. Важно понимать, что основная рекомендация для экономии памяти в этом случае — это минимизировать количество выбрасываемых исключений в "горячих" путях, используя их строго по назначению (для исключительных ситуаций). Однако, если от этого флоу отказаться нельзя, существуют механизмы, которые могут помочь.

1.Ограничение информации в стеке вызовов (пользоваться с умом)

  • Использование Exception с отключенным стеком: Создание пользовательских исключений с минимальным потреблением памяти:

Пример:

final readonly class LightweightException extends Exception
{
  public function __construct(string $message, int $code = 0)
  {
    // null вместо предыдущего исключения
    parent::__construct($message, $code, null); 
  }
}
  • Установка лимита глубины стека (PHP 7.4+): Настройка параметра zend.exception_ignore_args и zend.exception_string_param_max_len в PHP.ini для ограничения размера сохраняемой информации в стеке.

2.Системные настройки PHP

  • Оптимизация OPCache: Правильная настройка OPCache может снизить общее потребление памяти, включая исключения.

  • Увеличение лимитов памяти: Стратегическое увеличение memory_limit для предотвращения аварийного завершения при пиковых нагрузках.

  • Настройка сборщика мусора: Оптимизация параметров zend.enable_gc и релевантных настроек для вашего приложения.

3.Рефакторинг

  • Пакетная обработка и агрегация ошибок.

  • Паттерн "Fail-fast" с разумным подходом: В некоторых случаях можно прерывать выполнение операции как можно раньше, чтобы избежать глубоких стеков вызовов и погружения в не нужные слои.

4.Инструменты профилирования

Наконец, самый важный "механизм" — это профилирование. Не стоит применять оптимизации памяти "вслепую". Используйте инструменты профилирования, такие как Xdebug (профилирование памяти), Blackfire или Tideways, чтобы точно определить, действительно ли исключения являются значительным источником потребления памяти в вашем высоконагруженном приложении. Возможно, узкое место находится совсем в другом месте.

Несмотря на эти оптимизации, важно понимать, что использование исключений в высоконагруженных сценариях всегда будет более ресурсоёмким, чем использование ErrorObject (ROP). Рекомендуется применять смешанный подход и переходить на ErrorObject:

  • Использовать ErrorObject (ROP) для частых, предсказуемых ошибок в критических частях системы

  • Применять оптимизированные исключения для истинно исключительных ситуаций

  • Рассмотреть использование инструментов статического анализа для выявления мест необоснованного использования исключений

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


goto — это плохо, а throw - хорошо?

В мире PHP два оператора вызывают особенно жаркие споры: goto и throw. Первый считается "плохой практикой", второй — важной частью обработки ошибок. Но некоторые разработчики проводят между ними параллели, утверждая, что throw — это "цивилизованный goto". Так ли это? 

Как работают throw и goto в PHP

throw: Исключения и управление ошибками

throw используется для генерации исключения в PHP. Это позволяет прерывать нормальное выполнение программы, когда происходит ошибка, и передавать управление в ближайший блок catch.  throw «перепрыгивает» из функции в блок catch, минуя весь промежуточный код.

goto: Прямой прыжок в коде

goto позволяет программе немедленно перейти к заранее определённой метке. Его часто критикуют за ухудшение читаемости и усложнение отладки. goto перескакивает к метке end, игнорируя остальную часть тела цикла.

Почему throw напоминает goto

На первый взгляд, throw и goto выполняют схожую задачу: они оба немедленно прерывают текущее выполнение кода и перескакивают к другой части программы. Для некоторых разработчиков это вызывает аналогии.

Аргументы в пользу сходства:

  • Немедленный выход. Оба оператора мгновенно прекращают выполнение текущего блока.

  • Перенос управления. throw переносит выполнение в catch, минуя весь остальной код, что напоминает «прыжок» goto.

  • Неочевидность потока. Исключения могут нарушать линейную читаемость, так как обработка ошибки происходит в другом месте.

Сложность в чтении. Создание "невидимых" путей выполнения. А также возможное черезмерное использование для управления логикой.

Ключевые сходства и различия throw и goto

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

1. Цель использования

  • goto — простой переход в другую часть кода. Чаще всего используется для выхода из вложенных циклов или обхода кода.

  • throw — механизм обработки ошибок, предназначенный для работы с исключительными ситуациями.

2. Контекст и структура

  • throw требует строгой структуры try-catch, что добавляет архитектурную чёткость.

  • goto лишён структурного контекста и может увести выполнение в любое место, что затрудняет поддержку кода.

3. Читаемость

  • throw читается как «явный сигнал ошибки» и ожидаем в определённой части кода.

  • goto ломает предсказуемость, особенно при частом использовании.

4. Отладка и масштабирование

  • Исключения можно логировать, оборачивать, передавать дальше по цепочке.

  • goto не интегрируется с системами логирования и отладки ошибок.

5. Поддержка инструментов

  • Современные IDE умеют анализировать исключения, предлагать автозаполнение и выделять непойманные throw.

  • goto не анализируется большинством инструментов статической проверки как потенциально опасный участок кода.

6. Информативность и безопасность

goto просто перебрасывает выполнение в другое место, без контекста.

throw создаёт объект исключения, который содержит:

  • Тип ошибки (InvalidArgumentException, RuntimeException и др.)

  • Сообщение для разработчика

  • Полный стек вызовов

Насколько уместно сравнение?

Сравнение throw и goto уместно лишь на поверхностном уровне — в плане механизма «скачка» в другую часть программы. Однако их концептуальные различия слишком значительны, чтобы считать их равноценными с точки зрения дизайна кода.

Использование throw — часть архитектурного подхода к программированию, включающего декомпозицию, обработку исключений и управление ошибками. Тогда как goto почти всегда — симптом плохого проектирования.

Сообщество PHP склонно воспринимать throw как более зрелую и безопасную альтернативу управления потоком, в отличие от goto. В книге "Clean Code" Роберт Мартин подчёркивает важность структурированной обработки ошибок, и throw, будучи встроенным элементом исключений, соответствует этой философии.

Goto: Путь к изгнанию

Историческая эволюция отношения к goto

1960-е:

  • Первые языки (FORTRAN, COBOL, BASIC) активно использовали goto как основной способ управления потоком

  • В 1968 году Эдсгер Дейкстра публикует знаменитую статью "Go To Statement Considered Harmful", где называет goto: 

    • "Примитивным инструментом"

    • "Источником неструктурированного кода"

    • "Помехой для математически строгого программирования"

    1970-е - революция структурного программирования:

  • Появление альтернатив: циклов (while, for), условных блоков (if-else), подпрограмм

  • Языки Pascal (1970) и C (1972) формально поддерживают goto, но предлагают более структурированные альтернативы

  • Исследования показывают, что код с goto:

    • На 30-50% сложнее для понимания

    • На 25% дольше отлаживается

    • В 2 раза чаще содержит ошибки

1980-1990-е - постепенное изгнание:

  • Modern языки (Ada, Modula-2) начинают ограничивать goto

  • В объектно-ориентированных языках (C++, Java) goto становится анахронизмом

  • Появление исключений (exception handling) как цивилизованной альтернативы

2000-е - современное состояние:

  • В Python, Java, JavaScript goto отсутствует как концепция

  • В PHP, Perl, C сохраняется, но с жёсткими ограничениями

  • Go (Golang) сознательно исключил goto из синтаксиса

Причины негативного отношения:

  • Спагетти-код — сложность отслеживания потока выполнения

  • Нарушение инкапсуляции — переходы через границы логических блоков

  • Трудности рефакторинга — хрупкость кода при изменениях

В то время как goto сыграл важную роль в истории программирования, современная разработка практически полностью отказалась от него в пользу структурированных подходов. Исключения предоставляют все преимущества управления потоком выполнения без недостатков goto, делая код более надежным, поддерживаемым и безопасным.

Throw: Путь к структурированной обработке ошибок

Эволюционный путь:

Ранние языки: коды ошибок и проверка возвращаемых значений

  • 1980-е: первые реализации исключений (Ada, ML)

  • 1990-е: массовое внедрение в C++, Java, Python

  • 2000-е: исключения как стандарт в большинстве языков

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

  • Отделение нормального потока от обработки ошибок

  • Гарантированная обработка через механизм try/catch

  • Богатый контекст — стек вызовов, типы исключений, сообщения

Fail first or fail fast (FF)

Принцип "Fail First, Fail Fast" (часто просто "Fail Fast") гласит, что система должна проверять все возможные условия сбоя как можно раньше и, в случае обнаружения ошибки, немедленно прекращать операцию, сигнализируя о проблеме. Вместо того чтобы пытаться продолжать работу с некорректными данными или в ошибочном состоянии, что может привести к каскадным сбоям, непредвиденному поведению и, что хуже всего, к порче данных, приложение должно "упасть" в контролируемой манере.

Ключевые идеи здесь:

  • Раннее обнаружение: Ошибки выявляются на самой ранней стадии, как только поступают неверные данные или возникает некорректное состояние.

  • Быстрое завершение: При обнаружении ошибки выполнение текущей операции прерывается, не допуская дальнейших вычислений или изменения состояния на основе невалидных предпосылок.

  • Четкое информирование: Система должна явно сообщить об ошибке, как правило, путем выбрасывания исключения.

Применение принципа "Fail Fast" приносит множество преимуществ:

  1. Упрощение отладки: Ошибка обнаруживается близко к своему источнику. Это значительно сокращает время на поиск и устранение проблемы, так как не приходится распутывать длинную цепь событий, чтобы понять, что и где пошло не так.

  2. Предотвращение каскадных сбоев: Быстрый отказ не позволяет ошибке распространиться по системе, вызывая новые, зачастую более сложные для диагностики, проблемы в других ее частях.

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

  4. Повышение предсказуемости: Система ведет себя более детерминированно. Если что-то идет не так, это становится очевидным сразу, а не проявляется в виде странного поведения спустя какое-то время.

  5. Экономия ресурсов: Предотвращается выполнение ненужных операций с некорректными данными, что экономит процессорное время, память и другие ресурсы.

  6. Более четкие контракты методов/функций: Функции, следующие этому принципу, явно определяют свои ожидания относительно входных данных и условий выполнения, делая код более понятным и простым в использовании.

Выбрасывание исключений (Throwing Exceptions)

Исключения – это основной механизм в PHP для сигнализации об ошибках в стиле "Fail Fast". Вместо возврата false, null или кодов ошибок, которые могут быть проигнорированы вызывающим кодом, выбрасывание исключения принуждает к обработке нештатной ситуации.

  • Используйте стандартные типы исключений SPL (InvalidArgumentException, RuntimeException, LengthException, DomainException, OutOfBoundsException и т.д.), когда это уместно.

  • Создавайте собственные классы исключений, наследуясь от Exception или его потомков, для специфических ошибок вашего приложения. Это позволяет более гранулярно обрабатывать ошибки в try-catch блоках.

Рекомендации

  • Не ловите все исключения подряд; обрабатывайте только ожидаемые.

  • Используйте строгую типизацию и специализированные классы исключений.

  • Избегайте "тихих" ошибок; всегда логируйте исключения.

  • Не раскрывайте внутренние детали ошибок пользователю.

  • Использование enum'ов для кодов ошибок: например, ErrorCode::USER_NOT_FOUND.

  • Интеграция с внешними системами: Sentry, Bugsnag и другие инструменты для мониторинга ошибок

  • Поддержуйте Retry/Backoff стратегий: повторные попытки выполнения операций при временных сбоях.

  • Добавляйте TraceId/RequestId в каждый лог

  • Разделяйте уровни логов: Info, Warn, Error, Critical, Debug

  • Испольуйте ErrorObject для критических мест с высокй нагруской

Следуя этим практикам, вы сможете создать надежную систему обработки ошибок, которая будет соответствовать принципам DDD и обеспечит высокое качество вашего PHP-приложения.

Комментарии (6)


  1. Dhwtj
    14.05.2025 16:31

    Где же railway?

    «Railway-oriented programming» — функциональный стиль обработки ошибок через ADT (Either/Result).

    Есть готовые библиотеки:

    • raphael-dms/monad,

    • functional-php/functional,

    • sergphp/fp-psrl.

    Раз уж вы на PHP


    1. dykyi_roman Автор
      14.05.2025 16:31

      А он частично описан у меня, я его назваю ErrorObject в статте. Я скоро внесу правки в чтобы не было путаницы и лучше распишу о нем. Спасибо


  1. cupraer
    14.05.2025 16:31

    Ваша LLM-ка к концу генерации забыла, о чем она говорила в начале.

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

    и

    Не ловите все исключения подряд; обрабатывайте только ожидаемые.

    — противоречат друг другу.

    Но самое главное, что исключения прибиты гвоздями к тому потоку, который обрабатывает данный кусок кода, и когда вы надумаете распараллелить его на несколько машин (да хотя бы на несколько потоков) — всё написанное выше, кроме either-монад, станет бессмысленным. А у нас тут 2025 на дворе, странно ориентироваться на один поток выполнения.


    1. dykyi_roman Автор
      14.05.2025 16:31

       ИИ такой бред не сгенерирует) Если бы тут поработал ИИ, у статьи был бы план, выводы и хоть какая-то структура)))

      То что `Ошибки могут возникать на любом уровне` - это про стратегию обработки ошибок и патерн которым я пользуюсь описан на начале статти,

      а то что `Не ловите все исключения подряд` - это про тактику обработки этих ошибок - имелось ввиду не использовать слепой catch (Throwable $e) а ловить ошибки только те что ожидаешь. FF - лучше раньше упасть и увидеть эту ошибку.


    1. dykyi_roman Автор
      14.05.2025 16:31

      Согласен. Сттаття больше ориентирована на однопоточную обработку. И советы в ней остаются актуальными для синхронной и асинхронной обработки. Что касаеться PHP то в нем нет настоящей параллельной обработки "из коробки"; Что касается других языков - там нужны другие подходы которые уже выходят за границы этой стати.


      1. cupraer
        14.05.2025 16:31

        Ну есть же всякие https://github.com/krakjoe/parallel и типа того.

        Но всё равно, вы, наверное, правы, и в PHP это не так востребовано, а у меня просто проф. деформация.