Я рад бы написать что “эта статья предназначена для новичков”, но это не так. Большинство php-разработчиков, имея опыт 3, 5 и даже 7 лет, абсолютно не понимают как правильно использовать эксепшены. Нет, они прекрасно знают о их существовании, о том что их можно создавать, обрабатывать, и т.п., но они не осознают их удобность, логичность, и не воспринимают их как абсолютно нормальный элемент разработки.

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

Почему мы не умеем пользоваться эксепшенами:


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

PHP — это чрезмерно любящая мать. И при отсутствии строгого отца (например, Java) или самодисциплины, разработчик вырастет эгоистом, которому плевать на все правила, стандарты и лучшие практики. И вроде бы E_NOTICE пора включать, а он все на мать надеется. Которая, между прочим, стареет — ей уже E_STRICT c E_DEPRICATED нужны, а сынуля все на шее висит.

Виноват ли PHP — предмет дискуссий, но то, что с самого начала PHP не приучает нас к эксепшенам — это факт: его стандартные функции не создают эксепшены. Они либо возвращают false, намекая что что-то не так, или записывают куда-то код ошибки, который не всегда додумаешься проверить. Или впадают в другую крайность — Fatal Error.

И пока наш начинающий разработчик пытается написать свою первую быдло-cms, он ни разу не встретиться с механизмом эксепшенов. Вместо этого, он придумает несколько способов обработки ошибок. Я думаю, все понимают о чем я — эти методы, возвращающие разные типы (например, объект при успешном выполнении, а при неудаче — строка с ошибкой), или запись ошибки в какую-либо переменную/свойство, и всегда — куча проверок чтоб передать ошибку вверх по стеку вызовов.

Затем он начнет использовать сторонние библиотеки: попробует, например, Yii, и впервые столкнется с эксепшенами. И вот тогда…

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

Затем он научиться отлавливать и обрабатывать их. И на этом его знакомство c эксепшенами закончиться. Ведь надо работать, а не учиться: знаний ему и так хватает (сарказм)!

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

Преимущества эксепшенов


На самом деле использование эксепшенов — крайне лаконичное и удобное решение создания и обработки ошибок. Приведу наиболее значимые преимущества:

Контекстная логика


Прежде всего, хотелось бы показать что эксепшн — это не всегда только ошибка (как обычно ее воспринимают разработчики). Иногда он может быть частью логики.

Например, есть у нас функция чтения JSON объекта из файла:

/**
 * Читает объект из JSON файла
 * @param string $file
 * @throws FileNotFoundException    файл не найден
 * @throws JsonParseException       не правильный формат json
 * @return mixed
 */
public function readJsonFile($file)
{
  ...
}

Допустим мы пытаемся прочитать какие-то ранее загруженные данные. При такой операции эксепшн FileNotFoundException не является ошибкой и вполне допустим: возможно, мы ни разу не загружали данные, поэтому файла и нет. А вот JsonParseException — это уже признак ошибки, ибо данные были заргужены, обработаны, сохранены в файл, но почему-то сохранились не правильно.

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

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

Упрощение логики и архитектуры приложения


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

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

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

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

Вот пример информативного интерфейса, который дополнен знаниями об эксепшенах:

interface KladrService
{
	/**
	 * Определяет код КЛАДР по адресу
	 * @param Address $address
	 * @return string код для адреса
	 * @throws AddressNotFoundException     адрес не найден в базе адресов
	 * @throws UnresoledAddressException    адрес найден, но для него не существует код КЛАДР
	 */
	public function resolveCode(Address $address);

	/**
	 * Определяет адрес по коду КЛАДР
	 * @param string $code
	 * @return Address
	 * @throws CodeNotFoundException    не найлен код КЛАДР
	 */
	public function resolveAddress($code);
}

Следует упомянуть что разрабатывая классы эксепшенов мы должны следовать принципу информативного интерфейса. Грубо говоря — учитывать их логический смысл, а не физический. Например, если адреса у нас храняться в файлах, то отсутствие файла адреса вызовет FileNotFoundException. Мы же должны перехватить его и вызвать более осмысленный AddressNotFoundException.

Использование объектов


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

if(Yii::app()->kladr->getLastError() == ‘Не найден адрес’){
	….
}

try{
	...
}
catch(AddressNotFoundException $e){
	...
}

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

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

/**
 * Адрес не найден в базе адресов
 */
class AddressNotFoundException extends Exception
{
	/**
	 * Не найденный адрес
	 * @var Address
	 */
	private $address;

	/**
	 * @param Address $address
	 */
	public function __construct(Address $address)
	{
		Exception::__construct('Не найден адрес '.$address->oneLine);
		$this->address = $address;
	}
	/**
	 * @return Address
	 */
	public function getAddress()
	{
		return $this->address;
	}
}

Как видим — эксепшн содержит адрес, который не удалось найти. Обработчик может его получить и выполнить на основании его какую-то свою логику.

Третье преимущество — это, собственно, все преимущества ООП. Хотя эксепшены, как правило, простые объекты, поэтому возможности ООП мало используются, но используются.

Например, у меня в приложении порядка 70 классов эксепшенов. Из них несколько — базовых — по одному классу на модуль. Все остальные — наследуются от базового класса своего модуля. Сделано это для удобства анализа логов.

Так же я использую несколько ИНТЕРФЕЙС-МАРКЕРОВ:

  • UnloggedInterface: По умолчанию у меня логируются все необработанные ошибки. Этим интерфейсом я помечаю эксепшены, которые не надо логировать вообще.
  • PreloggedInterface: Этим интерфейсом я помечаю эксепшены, которые необходимо логировать в любом случае: неважно, обработаны они или нет.
  • OutableInterface: Этот интерфейс помечает эксепшены, текст которых можно выдавать пользователю: далеко не каждый эксепшн можно вывести пользователю. Например, можно вывести эксепшн с текстом “Страница не найдена” — это нормально. Но нельзя выводить эксепшн с текстом “Не удалось подключиться к Mysql используя логин root и пароль 123”. OutableInterface помечает эксепшены которые выводить можно (таких у меня меньшинство). В остальных ситуация выводиться что то типа “Сервис не доступен”.

Обработчик по умолчанию, логирование


Обработчик по умолчанию — чрезвычайно полезная штука. Кто не знает: он выполняется когда эксепшн не удалось обработать ни одним блоком try catch.

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

Откат изменений: так как операция не выполнена до конца, необходимо откатить все сделанные изменения. В противном случае мы испортим данные. Например, можно в CController::beforeAction() открыть транзакцию, в CController::afterAction() коммитить, а в случае ошибки сделать роллбэк в обработчике по умолчанию.

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

public function addPosition(Position $position)
{
  try
  {
    ... выполнение операции ...	
  }
  catch(Exception $e)
  {
    ... откат изменений ...
    
    throw $e;   // Заново бросаем тот же эксепшн
  }
}

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

Логирование: так же обработчик по умолчанию позволяет нам выполнить какое-то кастомное логирование. Например, в моем приложении я все складываю в базу и использую собственное средство для анализа. На работе мы используем getsentry.com/welcome. В любом случае, эксепшн, дошедший до обработчика по умолчанию — скорее всего непредусмотренный эксепшн, и его необходимо логировать. Следует отметить, что в класс эксепшена можно добавить различную информацию, которую необходимо логировать для большего понимания причины возникновения ошибки.

Невозможность не заметить и перепутать


Огромным плюсом эксепшена является его однозначность: его не возможно не заметить и не возможно с чем-то спутать.

Из первого следует что мы всегда будем осведомлены о возникшей ошибке. И это замечательно — всегда лучше знать о проблеме, чем не знать.

Второй плюс становится очевиден в сравнении с кастомными методами обработки ошибки, например когда метод возвращает null если не нашла нужный объект и false в случае ошибки. В таком случае элементарно не заметить ошибку:

$result = $this->doAnything(); // null если не нашла нужный объект и false в случае ошибки

// Не заметит ошибки
if($result){ ... }

// Не заметит ошибки
if($result == null){ ... }

// Не заметит ошибки
if(empty($result)){ ... }

// Не заметит ошибки
if($result = null){ ... }

Эксепшн же невозможно пропустить.

Прекращение ошибочной операции


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

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

$this->doOperation();
if($this->getLastError() !== null)
{
    echo $this->getLastError(); 
    die;
}

Это требует определенной дисциплины от разработчиков. А далеко не все дисциплинированны. Далеко не все вообще знают что у вашего объекта есть метод getLastError(). Далеко не все понимают, почему вообще так важно проверит что все идет как надо, а если нет — откатить изменения и прекратить выполнение.

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

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

Когда следует вызывать эксепшены:


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

Встает вопрос: в каких ситуациях стоит вызывать эксепшн?

Если кратко — всегда! Если подробно: всегда, когда ты уверен что операция должна выполниться нормально, но что-то пошло не так, и ты не знаешь что с этим делать.

Посмотрим на простейший экшн добавления записи:

/**
 * Создает пост
 */
public function actionCreate()
{
  $post = \Yii::app()->request->loadModel(new Post());
  if($post->save())
  {
    $this->outSuccess($post);
  }
  else
  {
    $this->outErrors($post);
  }
}

Когда мы введем некорректные данные поста эксепшн не вызывается. И это вполне соответствует формуле:

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

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

/**
 * Отменяет заказа.
 * Отмена производиться путем смены статуса на STATUS_CANCEL.
 * @throws \Exception
 */
public function cancel()
{
  // Проверим, находиться ли STATUS_CANCEL в разрешенных
  if(!$this->isAllowedStatus(self::STATUS_CANCEL))
  {
    throw new \Exception('Cancel status not allowed');
  }

  // Сообственно смена статуса
  $this->status = self::STATUS_CANCEL;
  $isSaved = $this->save();

  // Проверка на то что все успешно сохранилось и что после сохранения статус остался STATUS_CANCEL
  if(!$isSaved|| $this->status !== self::STATUS_CANCEL)
  {
    throw new \Exception('Bad logic in order cancel');
  }
}

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

Первым делом идет предвалидация — мы проверяем действительно ли мы можем выполнить операцию. В теории все должно пройти успешно, но если isAllowedStatus вернет false — значит что-то пошло не так. Плюс, в пределах текущего метода, мы абсолютно не знаем как обработать эту ситуацию. Понятно, что нужно залогировать ошибку, вывести ее пользователю, и т.п… Но в контексте именно этого метода мы не знаем что с ней делать. Поэтому бросаем эксепшн.

Далее идет выполнение операции и сохранение изменений.

Затем идет поствалидация — мы проверяем, действительно ли все сохранилось, и действительно ли статус изменился. На первый взгляд это может показаться бессмысленным, но: заказ вполне мог не сохранится (например, не прошел валидацию), а статус вполне мог быть изменен (например, кто-то набыдлокодил в CActiveRecord::beforeSave). Поэтому эти действия необходимы, и, опять-таки, если что-то пошло не так — бросаем эксепшн, так как в пределах данного метода мы не знаем как обрабатывать эти ошибки.

Эксепшн vs возврат null


Следует отметить, что эксепшн следует бросать только в случае ошибки. Я видел как некоторые разработчики злоупотребляют ими, бросая их там, где не следовало бы. Особенно часто — когда метод возвращает объект: если не получается вернуть объект — бросается эксепшн.

Тут следует обратить внимание на обязанности метода. Например, СActiveRecord::find() не бросает эксепшн, и это логично — уровень его “знаний” не содержит информации о том, является ли ошибкой отсутствие результата. Другое дело, например, метод KladrService::resolveAddress() который в любом случае обязан вернуть объект адреса (иначе либо код неправильный, либо база не актуальная). В таком случае нужно бросать эксепшн, ибо отсутствие результата — это ошибка.

В целом же, описанная формула идеально определяет места, где необходимо бросать эксепшены. Но особо хотелось бы выделить 2 категории эксепшенов, которых нужно делать как можно больше:

Технические эксепшены


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

Вот несколько примеров:

// В нескольких if
if($condition1)
{
	$this->do1();
}
elseif($condition2)
{
	$this->do2();
}

...

else
{
	// Когда должен сработать один из блоков if, но не сработал - бросаем эксепшн
	throw new BadLogicException;
}

// То же самое в swith
switch($c)
{
	case 'one':
		return 1;

	case 'two'
		return 2;

		...

	default:
		// Когда должен сработать один из блоков case, но не сработал - бросаем эксепшн
		throw new BadLogicException;
}

// При сохранении связанных моделей
if($model1->isNewRecord)
{
	// Если первая модель не сохранена, у нее нет id, то строка $model2->parent_id = $model1->id
	// сделает битые данные, поэтому необходимо проверять
	throw new BadLogicException;
}

$model2->parent_id = $model1->id;

// Просто сохранении - очень часто разраотчики используют save и не проверяют результат
if(!$model->save())
{
	throw new BadLogicException;
}

/**
 * Cкоуп по id пользователя
 * @param int $userId
 * @return $this
 */
public function byUserId($userId)
{
	if(!$userId)
	{
		// Если не вызывать этот эксепшн, то при пустом userId скоуп вообще не будет применен
		throw new InvalidArgumentException;
	}

	$this->dbCriteria->compare('userId', $userId);
	return $this;
}

Технические эксепшены помогут не допустить или отловить, имхо, большую часть багов в любом проекте. И неоспоримым плюсом их использования является отсутствие необходимости понимать предметную область: единственное что требуется — это дисциплина разработчика. Я призываю не лениться и вставлять такие проверки повсеместно.

Эксепшены утверждений


Эксепшены утверждений (по мотивам DDD) вызываются когда мы обнаруживаем что нарушается какая-либо бизнес-логика. Безусловно, они тесно связанна с знаниями предметной области.

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

Например, есть метод добавления позиции в заказ:

/**
 * Добовляет позицию в заказ
 * @param Position $position
 * @throws \Exception
 */
public function addPosition(Position $position)
{
  $this->positions[] = $position;

  ... перерасчет стоимость позиций, доставки, скидок, итоговой стоимсоти ...

// проверям корректность рассчета
  if($this->totalCost != $this->positionsCost + $this->deliveryCost - $this->totalDiscounts)
  {
    throw new \Exception('Cost recalculation error');
  }

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

// проверям можем ли мы доставить заказа с новой позицеей
  if(!Yii::app()->deliveryService->canDelivery($this))
  {
    throw new \Exception('Cant delivery with new position')
  }

… прочие действия ...
}

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

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

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

Поэтому в критичных местах такие эксепшены нужны однозначно.

Изменение логики для избегания эксепшна


Как я уже говорил, PHP разработчики боятся эксепшенов. Они боятся их появления, и боятся бросать их самостоятельно.

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

Вот пример: необходимо просто отобразить страницу по id (чтоб вы понимали — это реальный код из известного проекта)

/**
 * Отображает страницу по id
 * @param int $id
 */
public function actionView($id = 1)
{
  $page = Page::model()->findByPk($id) ?: Page::model()->find();
  $this->render('view', ['page' => $page]);
}

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

  • если id не задан — берется id = 1. Проблема в том, что когда id не задан — это уже баг, ибо где-то у нас не правильно формируются ссылки.
  • Если страница не найдена — значит где-то у нас ссылка на несуществующую страницу. Это тоже, скорее всего, баг.

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

Еще один пример:

/**
 * Выдает код кладра города
 * @param mixed $region
 * @param mixed $city
 * @return string
 */
public function getCityKladrCode($region, $city)
{
  if($сode = ... получение кода для города... )
  {
    return $сode;
  }

  return ... получение кода для региона ...
}

Тоже из реального проекта, и мотивация такая-же: вернуть хоть что-то, но не вызывать эксепшн, несмотря на то, что метод явно должен возвращать код города, а не региона.

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

Мое мнение — это недопустимо. Просто когда ты работаешь с большими деньгами (а я с ними работал довольно долго), вырабатывается определенные правила, и одно из них — прерывать операцию в случае любого подозрения на ошибку. Транзакция на 10 млн баксов: согласитесь, ее лучше отменить, чем перечислить деньги не тому человеку.

Конечно, обычно мы имеем дело с менее рискованными операциями. И в случае бага, например, инвалид не сможет оставить заявку на установку пандуса в подъезде. И разработчик на раслабоне (его ведь даже не оштрафуют) пренебрегает этими элементарными правилами, мол, подумаешь, мелочь какая. Проблема в том, что когда ему доверят что-то критически важное — вряд ли его подход измениться. Ибо проблема не в знаниях, и не в риске, а в дисциплине и в отношении к делу. И получается, что после таких программистов у нас где-то трубы зимой лопаются, где-то нефть разливается тоннами, где-то люди умирают десятками, где-то деньги воруются миллионами. Подумаешь, мелочь какая!

Собачки


Я почему-то думал что собачками уже никто не пользуется. Но недавно столкнулся с коллективом разработчиков, которые используют их повсеместно вместо проверки isset, поэтому решил написать и про них.

Собачки вместо isset используют для лаконичности кода:

@$policy->owner->address->locality;

против

isset($policy->owner->address) ? $policy->owner->address->locality : null;

Действительно, выглядит намного короче, и на первый взгляд результат такой же. Но! опасно забывать что собачка — оператор игнорирования сообщений об ошибках. И @$policy->owner->address->locality вернет null не потому-что проверит существование цепочки объектов, а потому-что просто проигнорирует возникшую ошибку. А это совершенно разные вещи.

Проблем в том, что помимо игнорирования ошибки Trying to get property of non-object (которое и делает поведение собачки похожим на isset), игнорируются все другие возможные ошибки.

PHP — это магический язык! При наличии всех этих магических методов (__get, __set, __call, __callStatic, __invoke и пр.) мы не всегда можем сразу понять что происходит на самом деле.

Например, еще раз взглянем на строку $policy->owner->address->locality. На первый взгляд — цепочка объектов, если присмотреться пристально — вполне может быть и так:

  • policy — модель CActiveRecord
  • owner — релейшен
  • address — геттер, который, например, обращается к какому-либо стороннему сервису
  • locality — аттрибут у


То есть простой строкой $policy->owner->address->locality мы на самом деле запускаем выполнение тысяч строк кода. И собачка перед это строкой скрывает ошибки в любой из этих строк.

Таким образом, столь необдуманное использование собачки потенциально создает огромное кол-во проблем.

Послесловие


Программирование — потрясающее занятие. На мой взгляд, оно похоже на сборку огромного конструктора LEGO. В самом начале перед тобой инструкция и россыпь мелких деталей. И вот, ты берешь инструкцию, по которой методично собираешь их в небольшие блоки, затем объеденяешь их в нечто большее, еще больше… И ты ловишь кайф от этого чертовски увлекательного процесса, ты ловишь кайф от того, насколько все логично и продуманно устроенно, насколько все эти детальки подходят друг к другу. И вот — перед тобой уже целый трактор, или самосвал. И это потрясающе!

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

Но попробуйте собрать конструктор без инструкции… Эта мысль похожа на бред. Тем не менее, программисты без всех этих знаний прекрасно работают. Годами. И им это не кажется бредом — они даже не понимают, что что-то делают не так. Вместо этого они жалуются что им дали слишком мало времени.

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

Так что всем, кто прочел этот пост и подумал “что за бред”, “я все это знаю, но применять лень”, или “будет сосунок мне указывать” — я желаю совершить баг. Баг, за который оштрафуют или уволят. И тогда вы, возможно, вспомните этот пост, и задумаетесь: “возможно я и в правду что-то делаю не так”?

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

Всем добра )

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


  1. Mendel
    10.08.2015 03:57
    +3

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

    Следует упомянуть что разрабатывая классы эксепшенов мы должны следовать принципу информативного интерфейса. Грубо говоря — учитывать их логический смысл, а не физический. Например, если адреса у нас храняться в файлах, то отсутствие файла адреса вызовет FileNotFoundException. Мы же должны перехватить его и вызвать более осмысленный AddressNotFoundException.

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

    Или вы имели ввиду, что КАЖДЫЙ адрес хранится в отдельном файле, и его отсутствие по бизнес-логике это отсутствие записи в базе?
    Или я под утро уже засыпаю и торможу, или вы чуть ушли от локаничности, за которую ратуете в статье.

    И тем не менее статья супер :)


    1. greabock
      10.08.2015 06:35

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

      Например, если адреса у нас хранятся в файлах

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


  1. Wedmer
    10.08.2015 04:27

    Как ни странно, но в C++ тоже очень часто или забывают про существование исключений, или наоборот перебарщивают с их использованием.
    Было бы здорово, если бы вы рассказали и про негативные последствия чрезмерного употребления исключений (если они есть).


    1. khim
      10.08.2015 11:16

      Собственно та же самая проблема, что и у в любом другом языке: потеря контроля над последовательностью исполнения. Исключение — это нелокальный GoTo. То есть в коде

      create_temporary_table();
      read_json();
      remove_temporary_table();
      при отсутствии исключений таблица будет уничтожена всегда, а вот если read_json кинет исключение — то всё «рассыпется».

      Грубо говоря написания кода без исключений и с исключениями — это сильно разные навыки. А если учесть, что нормальная поддержка исключений появилась в PHP только начиная с версии 5.6, которому меньше года от роду… стоит ли удивляться, что мало кто использует исключения в PHP???


      1. Mendel
        10.08.2015 12:46

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


        1. khim
          10.08.2015 13:13

          А это от архитектуры зависит. И от того, что мы в этих таблицах храним. Если там какая-нибудь статистика, то можно и без обработчика (вернее он будет внутри функции create_temporary_table). Вы правы в том смысле, что PHP во многих случаях спасается за счёт того, что за ним «убирает» DBMS (собственно именно этим разработчики PHP годами аргументировали своё нежелание реализовывать final), но это не значит, что это будет проиходить совсем уж всегда.


          1. Mendel
            10.08.2015 19:19

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


      1. KReal
        10.08.2015 19:51

        Я понимаю, что это синтетический пример, но…

        1. try catch finally
        2. using (var transaction = BeginTransaction())

        как-то так.


        1. khim
          10.08.2015 20:15

          finally как уже было сказано вменяемо работает начиная с PHP 5.6 — что очень многое говорит о языке во-первых, а также представляет некоторую практическую проблему во-вторых.

          Но главное не в том, как эту ситуацию обработать, а в том, что её нужно обрабатывать — а без исключений об этом не нужно было даже и задумываться.


  1. saggid
    10.08.2015 05:10

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

    В общем, еще раз спасибо за статью, да еще и с множеством примеров, да еще и по главам разбитую)


    1. TimsTims
      10.08.2015 07:42
      +1

      Если всё работает и никаких проблем нет — то не за что и ругать себя


      1. saggid
        11.08.2015 06:01

        Мм… Ну я вообще в корне не согласен с подобным утверждением. Для уровня начинающего кодера оно конечно вполне нормально, таким образом расценивать свою работу: нашлёпал какую-то кучу кода. Если всё работает — значит молодец.

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


        1. TimsTims
          15.08.2015 17:43

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


          1. khim
            15.08.2015 18:36

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


          1. saggid
            15.08.2015 19:13

            Я согласен, что в определённых ситуациях тактика написания обалтяйского кода полезна, на то она и тактика.

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

            Вот вам статья моя на данную тему, немного более широко разъясняющую мой посыл: believer.su/programmirovanie/chistiy-kod


          1. mitaichik
            16.08.2015 07:14

            Этот пост немного про другое. Тут архитектура не принципиальна — хоть в одну функцию все пишите. Тут про безбажность — как не допустить баг, а если допустил — не пропустить.

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


  1. greabock
    10.08.2015 06:38

    Прекрасный слог. Последовательно, обстоятельно. Мы бы с Вами написали много прекрасных статей. Жаль, что Вы из «другого лагеря» =)
    Послесловие, правда, несколько экспрессивное…


  1. Athari
    10.08.2015 06:53

    После продолжительного рассказа про иерархию исключений, документцию, интерфейсы и прочее показывать код, утыканный «throw new Exception» — это, как минимум, странно.

    Про обработку исключений мало сказано, среди примеров презираемый многими «catch (Exception)»…


    1. greabock
      10.08.2015 07:39

      я думаю, что catch «на месте» должен употребляться только для переброса исключений, как в примере с FileNotFound => AdressNotFound. Во всех прочих случаях исключения должны обрабатываться «общей пачкой» на уровне приложения.

      <? namespace App\Support\Exceptions\Handlers;
      
      use App\Supprot\Exceptions\Contracts\ExceptionHandlerContract;
      use App\Supprot\Exceptions\Handlers\DefaultHandler;
      
      class ExceptionHandler implments ExceptionHandlerContract {
            protected $defaultHandler = DefaultHandler::class;
                     
            protected $customHandlers = [];    
      
            public function handle(Exception $e)
            {
                   $exceptionType = get_class ($e);
      
                   if(array_key_exists($exceptionType, $this->customHandlers))
                   {
                         $handler = new $this->customHandlers[$exceptionType];
      
                         return $this->runHandler($handler, $e);
                   }
                   
                   return $this->handleDefault($e);
            }
      
            protected function handleDefault(Exception $e)
            {
                 $handler = new $this->defaultHandler;
      
                 return $this->runHandler($handler, $e);
            }
      
            public function addCustomHandler($exceptionClassName , $handlerClassName)
            {
                 $this->customHandlers[$exceptionClassName] = $handlerClassName;
            }
      
            public function runHandler(ExceptionHandlerContract $handler, Exception $e)
            {
                   return $handler->handle($e);
            }
      }
      

      Где-то в дебрях инициализации приложения:
            $app->exceptionHandler->addCustomHandler(CustomHandler::class);
      

      Ну и сам запуск приложения.
      rquire_once('../paths.php');
      
      rquire_once(VENDOR_AUTOLAD_PATH);
      
      $app = new App;
      
      try 
      {    
        $app->run();
      } 
      catch (Exception $e) 
      {
      
          $app->exceptionHandler->handle($e);
      }
      


      Простите, если где-то ошибся накатал прямо сейчас в браузере…


      1. jrip
        10.08.2015 12:06
        +2

        >Во всех прочих случаях исключения должны обрабатываться «общей пачкой» на уровне приложения.
        Это убивает весь смысл, получается тот же самый trigger_error.


      1. FanatPHP
        10.08.2015 13:39
        +2

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

        $app = new App;
        $app->run();
        

        При этом в случае выброшенного исключения метод exceptionHandler::handle() прекрасно отработает.
        Просто его надо зарегистрировать.


      1. FanatPHP
        10.08.2015 14:11
        +1

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

        К примеру, идет обработка картинок.
        Из ста картинок одна — битая.
        Все что нам надо — это запомнить имя этой картинки, если было брошено invalidImageFormat исключение.
        Писать и регистрировать хендлер из одной строчки будет избыточным.

        А вот если мы во время обработки картинок улетели по памяти — то да, здесь как раз пригодится стандартный обработчик.


        1. greabock
          11.08.2015 01:41
          +1

          Код был образный, и накатан прямо в браузере.
          Вот те раз. Умыли меня. Во всех моих приложениях есть обработчики на кучу различных «нефатальных» исключений. Например ModelNotFoundException в большинстве случаев преобразуется в PageNotFoundException, который в свою очередь обрабатывается на уровне приложения возвращает страницу 404 в текущем лэйауте. Если же запрос аяксовый, то возвращается json в формате соответствующем jsonapi. Есть также различные исключения, которые обрабатываются на уровне приложения. Например ValidationFailureException в при обычных запросах Делает
          redirect()->back()->withErrors($validator->errors);

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

          И что же я делаю не так? Как же этот код (хотя, еще раз подчеркиваю, не этот конкретно, но принципиально такой же) вдруг стал «для фатальных ошибок»?


          1. FanatPHP
            11.08.2015 14:17

            Признаю, здесь я был неправ. Инерция мышления меня же и подвела.


    1. mitaichik
      10.08.2015 14:49

      код, утыканный «throw new Exception» — это, как минимум, странно.

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

      Это все к мануалу, статья не про это, а про то что, как верно подметил FanatPhp — обработка исключений не заключается чисто на try catch
      среди примеров презираемый многими «catch (Exception)

      В статье только один пример с catch (Exception) и он вовсе не про обработку исключений, а про откат изменений. Поэтому там все верно — именно catch (Exception) там и должен быть.


      1. Athari
        10.08.2015 16:53

        Просто в контексте тех мест класс эксепшна не так важен как именно его наличие

        В обучающих материалах никакой «контекст» не может оправдать дурной пример. Вас же никто не заставляет детально описывать все классы исключений, вместо «throw new Exception» можно просто написать «throw new InvalidStatusException».

        Это все к мануалу

        Как бросать исключения — это тоже в мануале описано. Но смысл-то как раз в том, чтобы описать в статье, как правильно работать с исключениями, а ловля и обработка исключений — это тоже нетривиально.


        1. aprusov
          11.08.2015 08:37

          В большинстве случаев хватает стандартных spl-исключений, про них можно было бы более подробно упомянуть автору. Часто нет смысла городить лишнюю иерархию на каждый чих, не считаю это хорошей практикой, но есть места, где это действительно нужно и удобно.


          1. Athari
            11.08.2015 08:42

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


  1. Fedcomp
    10.08.2015 07:32
    -2

    > СActiveRecord::find() не бросает эксепшн, и это логично
    Не соглашусь, если я хочу открыть конкретную страничку с постом (допустим есть моделька с постами), то лучше бы этот метод кидал NotFound exception, в таком случае обработка дальше не пойдет и фреймворк кинет 404 (если он понимает этот эксепшн) или (нежелательно) 500.
    А вот CActiveRecord::where(['id' => 342342]) вполне может выдавать просто нули, так как подразумевается что данных может не быть.


    1. AlmazDelDiablo
      10.08.2015 08:34
      +7

      Поиск по БД не имеет ни какого отношения к какой-то там страничке, не путайте уровни. Если чего-то нет в БД — на глобальном уровне это штатная ситуация. Если же вам где-то надо показать 404 при отсутствии чего-либо в БД, то этим должен заняться контроллер, который сделает проверку на наличие данных в БД и кинет специальное исключение, которое интерпретируется, как 404. А вот ORM/ActiveRecord ну никак не может кидать HttpException.


    1. maximw
      10.08.2015 11:51

      Простой контрпример.
      Пустая корзина у пользователя это страница пустой корзины или 404?


      1. matiouchkine
        10.08.2015 17:55
        +2

        Ну тут все просто: нахрен нужны покупатели, которые только шарятся по магазину, жирными руками все прилавки заляпали, и ничего так и не купили? На выход, товарищи, на выход!

        Так что это 402, очевидно. А при повторной попытке — 503.


      1. Fedcomp
        10.08.2015 22:48
        -1

        Корзина должна при старте сессии создаться, так что да, если она не создалась то это ошибка приложения.


        1. maximw
          11.08.2015 00:37

          Не, ну отсутствие корзины и пустая корзина несколько разные вещи.


        1. skobkin
          17.08.2015 02:20

          Зачем? Можно при добавлении товара.


    1. jrip
      10.08.2015 12:12

      Исключения на то и называются именно исключениями, т.к. исключительные ситуации.
      Т.е. если СActiveRecord::find() должен всегда что-то вернуть, то он должен это что-то вернуть, иначе исключение.
      Можно еще с помощью исключений передавать данные обратно, но это спорный метод, это очень похоже на слабоуправляемое GOTO.


      1. slavcopost
        10.08.2015 18:42

        С чего вы решили что СActiveRecord::find() должен всегда что-то вернуть? мне кажется она должна «найти» или «не найти», обе ситуации не исключительные. Может и MySQL на SELECT должен бросать исключение при пустом результате?


        1. Fedcomp
          10.08.2015 22:47

          where подразумевает что выборка может быть и пуста, а допустим find()/find_by_id() подразумевает что такой элемент есть, иначе дальнейшая логика ломается.


          1. VolCh
            11.08.2015 20:48
            +1

            Логика может быть разная. Например, если элемент не найден, то создать его. Для ОРМ, как и для СУБД не найденная запись — норма. Если для бизнес-логики нет, то и бросайте исключение на её уровне.


        1. slavcopost
          10.08.2015 23:13

          СActiveRecord::find() также подразумевает наличие дополнительных условий установленные критериями, scopes и прочими. Так что все аналогично, find может вернуть найденную строку либо не найдя строку вернул null.


        1. jrip
          11.08.2015 03:09

          >С чего вы решили что СActiveRecord::find() должен всегда что-то вернуть
          Я как раз и говорю — если он должен вернуть -то ожидаем что вернет. Но ведь вы можете решить, что например отсутствие данных это для вас исключительная ситуация. Т.е. по идее можно и исключение заюзать.
          Но вот как по мне — тут главное некоторую грань не переступать, а то можно начать передавать данные через исключение.


          1. aprusov
            11.08.2015 08:47
            +2

            Очень удобно, когда есть два метода: один кидает исключение, например UnexistentEntityException и всегда возвращает объект, например ActiveRecord::getById(), а второй возвращает объект либо null, например ActiveRecord::findById().


  1. maximw
    10.08.2015 11:47

    Где должны располагаться описания собственных классов исключений?

    В том же файле, что и класс, кидающий свое специфическое исключение? Вроде удобно, но противоречит п. 3 PSR-1 — каждый класс должен быть в своем файле.

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


    1. jrip
      10.08.2015 12:17
      +1

      В отдельных файлах недалеко от класса.
      >Если они в разных директориях с классом, его кидающим (при модульности).
      В таком случае это какая-то неправильная модульность.


    1. mitaichik
      10.08.2015 14:39

      Да, однозначно в отдельных файлах.

      Что касается автозагрузки, то если у вас нормальный автозагрузчик (например как в Yii2) то проблем с автозагрузкой никогда не возникнет.

      Про папочки — это как душе угодно. Я обычно делаю папку exceptions в папке модуля/компонента и туда складываю все классы эксепшенов.
      Таким образом получается вот так (неймспейсы повторяют физическое размещение):

      kladr\KladrService
      kladr\exceptions\AddressNotFoundException
      kladr\exceptions\UnresoledAddressException


    1. gro
      10.08.2015 17:16
      -1

      Вот я не знаю.
      Читаешь бывает вопрос, а там «PSR-1», «пространстов имён», то есть человек, кажется, более шаристый чем 90% пхпшников.
      А сам вопрос какая-то глупость.
      В тех же PSR исчерпывающе же описан ответ на вопрос «в каких файлах что должно располагаться».


      1. jrip
        10.08.2015 17:51

        >а там «PSR-1», «пространстов имён», то есть человек, кажется, более шаристый чем 90% пхпшников.
        Экак вы с плеча кучу пехепешников обидели) Особенно учитывая то, что полностью PSR следует печально маленький процент даже успешных и красивых по коду проектов)


  1. Casus
    10.08.2015 12:21

    Про finally забыли расказать, и spl exceptions для общего развития стоило упомянуть.


    1. mitaichik
      10.08.2015 14:30

      Это же все в манулае можно прочитать. Я же хотел сосредоточиться на немого другом аспекте: как верно подметил FanatPHP — что обработка исключений не заканчивается на try catch


  1. FanatPHP
    10.08.2015 13:25
    +5

    Мне кажется, что самого главноего в статье не написано :)
    Что обработка исключений не заключается в коде вида

    try {
       // something
    } catch (Ecxeption $e) {
        die($e->message());
    }

    — в чем абсолютно железобетонно уверены 86% пользователей похапе


    1. mitaichik
      10.08.2015 14:27

      Совершенно верное замечание ) С вашего позволения, добавлю это в пост.


  1. slavcopost
    10.08.2015 18:38
    +2

    Браво, хорошая статья, и главное редко затрагиваемая тема.

    Хотел бы добавить пару моментов:

    1) Никогда не бросайте базовый Exception, лучше использовать собственные исключения, либо на крайний случай один из SPL. Т.к. базовое исключение сложно словить.

    2) В дополнении 1) Используйте Композицию вместо Наследования. В статье эта мысль присутствует, я лишь хочу уточнить. Вместо своего нового базового класса для исключения, используйте интерфейсы. Например

    class InvalidArgumentException extends \InvalidArgumentException implements MyApplicationExceptions {
    }
    

    теперь ваш обработчик ошибок и исключений будет проще.

    3) Обработчик ошибок и исключений, не обязательно должен быть один в приложении. Особенно если вы используйте слоистую систему, вполне возможно Вам будет полезно иметь отдельный обработчик в каком-то слое, например вы захотите для Presentation layer выловить все кроме Http exceptions и сделать их таковыми.

    4) Если Вам позволяет версия php, в 5.5+ не забывайте про finally, на ряду с try/catch является очень удобной, работая с исключениями.

    <чуть чуть сарказма>Автор, в 4 абзаце речь идет о стандартах и лучших практиках, как насчет PSR1,2? :)</чуть чуть сарказма>


    1. mitaichik
      11.08.2015 17:23

      <чуть чуть сарказма>Автор, в 4 абзаце речь идет о стандартах и лучших практиках, как насчет PSR1,2? :)</чуть чуть сарказма>


      Согласен. Просто я работаю в проекте который начали писать задолго до PSR. И переводить CodeStyle на PSR желаение нет: попробуйте смержить 2 больших файла, когда один разработчик переделал CodeStyle, а другой закоммитил +500 изменений — это может правреатиться в ад. Поэтому и не трогаем CodeStyle..., на качество кода как крути не влияет.


  1. PerlPower
    10.08.2015 22:51
    -1

    Лично я не использую эксцепшены для проверки ошибок потому что не вижу тут никакого удобства. Возврат кодов ошибок — механизм, который работает во всех языках, который понятен всем. Зачем создавать класс ради одной ошибки, которая обрабатывается в одном месте? Чтобы разработчик лишний раз поскакал по файлам туда и обратно при анализе кода?

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

    И вообще правило — исключения для системных вещей, типа кончилась память, отвалился диск, нет прав доступа, а коды ошибок для всего остального оно не просто так существует. Это позволяет четко определить в случае запутанных ошибок, куда нужно смотреть. А не искать какой именно из блоков catch сработает. Кстати в чем разница между лесенкой из if и лесенкой из catch?

    В общем что именно использовать для обработки ошибок — личное дело каждого, если иное явно не оговорено в проекте.


    1. lazycommit
      10.08.2015 23:02

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


      1. PerlPower
        10.08.2015 23:54

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


        1. maximw
          11.08.2015 00:46

          Вот типичный пример работы на разных уровнях.
          Допустим, есть ORM — слой ниже бизнес-логики.
          Как в продакшин режиме узнать, что что-то не так на уровне ORM?
          При этом ORM вообще не знает какой логгер ошибок вы используете, на каком уровне он работает и используется ли он вообще или ошибка будет обработана как-то иначе.
          Передачей кода ошибки очень сложно сделать такое разделение. Зато логгеру достаточно ловить все непойманные исключения.


          1. PerlPower
            11.08.2015 01:03

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


      1. jrip
        11.08.2015 03:22

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


        1. PerlPower
          11.08.2015 03:28

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


    1. jrip
      11.08.2015 03:17

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


      1. PerlPower
        11.08.2015 03:34

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


        1. Mendel
          11.08.2015 22:53

          Дело в том, что коды ошибок и гетЛастЕррор это неSOLIDно.
          Лишняя ответственность, неоправданный рост связности…
          Выкинули исключение. Если в биз.логике это не исключительная ситуация, то можно и не выкидывать, и будет обычное ветвление, или ловить сразу, тут-же.
          Или просто выкидывать, а когда потом, на будущем этапе развития появится воркараунд для этой ситуации, то прямо на месте, в контексте ответственности прописываем его перехват и обработку. Например переадресация на форму создания объекта которого может не быть, с плашкой о том, что не нашли, можете создать (к примеру как в википедии со статьями).
          Но изначально всё уходит вверх, на умолчания.
          В базовом алгоритме просто идет проверка на исключительность, и выброс исключения. Всё. Можно не разбираться дальше.
          Его потом подберут, и доложат админу.
          В свою очередь можно потом делать разбор по тому кому смс писать — девелоперу или админу. Если непонятная невыловленная ошибка, то деву, если там места нет, или файлик потерялся, то админу…
          Но всё это другая ответственность. Класс или метод где произошло исключение, не должен знать всё это.
          Отход от SOLID возможен. К примеру я не согласен с критиками Yii по поводу AR включающий и валидацию, и еще немножко шьющего. Так удобнее, и всё такое. Разумное обоснованное отклонение. Но отклоняться потому что «я так привык»… сомнительно.


          1. PerlPower
            11.08.2015 23:57

            Просто хоть убей не понимаю разницы — я могу выкинуть наверх исключение и поймать его через catch, а могу выкинуть наверх ошибку через return. В обоих случаях классу не нужно занть что будет с кинутой ошибкой уровнем выше. Казалось бы преимущество throw очевидно — можно поймать ошибку на самом верху, минуя промежуточные слои абстракции. Но ведь эти слои абстракции зачем-то создавались, там тоже есть какая-то логика, которая должна реагировать на ошибки уровнем ниже. И выходит что вся разница в том какие ключевые слова используются if/else или try/catch. При одинаковой архитектуре.


            1. Mendel
              12.08.2015 11:34
              +2

              Или должна или нет?
              Если должна, это частный случай.
              А если не должна?
              Все слои которые не должны были знать об исключении ВЫНУЖДЕНЫ о нем знать, просто чтобы его передать кому следует.


  1. iz0
    11.08.2015 21:40

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


  1. VolCh
    12.08.2015 08:15
    +1

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


    1. mitaichik
      13.08.2015 13:20

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