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

Передача информации в исключении

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

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

public function completeOrder(Order $order): void
{
	...
	if (!$order->isFilledFio()) {
		throw new Exception('Не заполнено ФИО');
	}
	...
}
// Где-то выше по стеку
try {
	$this->completeOrder($order);
} catch (Exception $e) {
	$order->fail($e->message);
	
	return false;
}

return true;

Почему это плохо?

Это плохо как и любое использование чего-либо не по назначению.

Блоки try/catch уродливы, замусоривают код и усложняют его понимание и отладку.

К тому же, что будет в ситуации когда будет выброшено не наше “фейковое” исключение а настоящее? Тогда код выше его подавит? Нужно ещё и это учитывать.

Неисключительная ситуация

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

Просто не прошла одна из проверок.

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

Но мы можем вернуть только одно значение, как же нам передать “обратно” данные при неуспешном выполнении?

Используем свойства объекта

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

public function completeOrder(Order $order): bool
{
	...
	if (!$order->isFilledFio()) {
		$this->errorReason = 'Не заполнено ФИО';
		
		return false;
	}
	...
}
// Где-то выше по стеку
if (!$this->completeOrder($order)) {
	$order->fail($this->errorReason);
	
	return false;
}

return true;

Здесь записываем свойство, возвращаем false как указание того что операция не выполнена и выше по стеку читаем из свойства если метод вернул false.

Получается довольно наглядно и относительно безопасно.

Используем объект результата

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

Этот приём я тоже широко использую.

public function completeOrder(Order $order): CompleteOrderResult
{
	...
	if (!$order->isFilledFio()) {
		return CompleteOrderResult::fail('Не заполнено ФИО');
	}
	
	...
	return CompleteOrderResult::success();
}
// Где-то выше по стеку
$result = $this->completeOrder($order);

if (!$result->isOk()) {
	$order->fail($result->getErrorReason());
	
	return false;
}

return true;