Proper use of Exception in PHP

    I would be glad to write that “this article is intended for beginners,” but it is not. Most php developers, having experience of 3, 5, and even 7 years, absolutely do not understand how to use executions correctly. No, they are well aware of their existence, that they can be created, processed, etc., but they do not realize their convenience, logic, and do not perceive them as an absolutely normal element of development.

    This article will not have a manual on executions - this is all well described in the php documentation. Here I will talk about the benefits of using executions, and where, in fact, they should be used. All examples will be for Yii, but this is not particularly important.

    Why we do not know how to use executions:


    I love you PHP. This is a wonderful language, no matter how scolded it is. But for beginner developers, it carries a certain danger: it forgives too much.

    PHP- This is an overly loving mother. And in the absence of a strict father (for example Java) or self-discipline, the developer will grow up to be an egoist who does not care about all the rules, standards and best practices. And it seems like it's E_NOTICEtime to turn it on, but he hopes all for his mother. Which, by the way, is getting old - she already needs E_STRICTc E_DEPRICATED, but her son is hanging on her neck.

    Whether PHPthe topic of discussion is to blame , but the fact that from the very beginning PHPdoes not accustom us to executions is a fact: its standard functions do not create executions. They either returnfalse, hinting that something is wrong, or write somewhere an error code that you can’t always guess at. Or go to the other extreme - Fatal Error.

    And while our novice developer is trying to write his first cattle-cms, he never once met with the mechanism of executions. Instead, he will come up with several ways to handle errors. I think everyone understands what I mean - these methods that return different types (for example, an object on successful execution, and on failure, a string with an error), or writing an error to some variable / property, and always a bunch of checks to pass an error up the call stack.

    Then he will start using third-party libraries: he will try, for example Yii, and for the first time he will encounter executions. And then ...

    And then nothing will happen. Nothing at all. He has already developed ways of processing errors that have been honed for months / years - he will continue to use them. The call caused by someone (a third-party library) will be perceived as a certain kind Fatal Error. Yes, much more detailed, but logged in detail, yes Yii will show a beautiful page, but no more.

    Then he learns to catch and process them. And on this his acquaintance with executions will end. After all, you have to work, not learn: he already has enough knowledge (sarcasm)!

    But the worst thing is that the attitude towards executions is being developed as something bad, undesirable, dangerous, which should not be, and which must be avoided by all means. This is absolutely not the right approach.

    Benefits of Executions


    In fact, the use of executions is an extremely concise and convenient solution for creating and processing errors. I will give the most significant advantages:

    Context logic


    First of all, I would like to show that execution is not always just a mistake (as developers usually perceive it). Sometimes it can be part of the logic.

    For example, we have a function to read an JSONobject from a file:

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

    Suppose we are trying to read some previously downloaded data. With such an operation, the execution is FileNotFoundExceptionnot an error and it is quite permissible: perhaps we have never downloaded data, therefore there is no file. And here JsonParseException- this is already a sign of an error, because the data was downloaded, processed, saved to a file, but for some reason it was not saved correctly.

    It is quite another matter when we try to read a file, which should always be: with such an operation FileNotFoundException, it is also an error signal.

    Thus, executions allow us to determine the logic of their processing depending on the context, which is very convenient.

    Simplify application logic and architecture


    Try using executions and you will see how your code becomes more concise and understandable. All crutch mechanisms will disappear, perhaps a bunch of nested ifs will be removed, various mechanisms for passing the error up the call stack, the logic will become simpler and more straightforward.

    Exception calls places will help your colleague better understand business logic and subject area, because delving into your code, he will immediately see what is permissible and what is not.

    And, if we consider some self-contained piece of code, for example, a component, then the list of executions thrown by it does one more important thing: it complements the interface of this component.

    Using third-party components, we are used to paying attention only to the positive side - that he knows how. However, we usually do not think about the exceptions that he can create in the process. The list of executions immediately warns where, when, and what problems may arise. And forewarned means armed.

    Here is an example of an informative interface, which is complemented by knowledge about executions:

    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);
    }
    

    It should be mentioned that when developing classes of actions we must follow the principle of an informative interface. Roughly speaking, consider their logical meaning, and not physical. For example, if our addresses are stored in files, then the absence of an address file will cause FileNotFoundException. We must intercept it and cause a more meaningful one AddressNotFoundException.

    Use of objects


    Using a specific class as an error is a very convenient solution. First, the class cannot be confused: take a look at 2 ways to handle the error:

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

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

    In the first version, an elementary typo will break your whole logic, in the second, it is simply impossible to make a mistake.

    The second advantage is that the execution class encapsulates all the necessary data for its processing. For example, AddressNotFoundExceptionit might look like this:

    /**
     * Адрес не найден в базе адресов
     */
    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;
    	}
    }
    

    As you can see - the reception contains an address that could not be found. The handler can receive it and execute some logic of its own based on it.

    The third advantage is, in fact, all the advantages of OOP. Although executions are, as a rule, simple objects, that is why OOP capabilities are not used much, but they are used.

    For example, I have about 70 classes of applications in my application. Of these, several - basic - one class per module. All others are inherited from the base class of their module. This is done for the convenience of log analysis.

    I also use several INTERFACE-MARKERS:

    • UnloggedInterface: By default, all unhandled errors are logged to me. With this interface I mark actions that do not need to be logged at all.
    • PreloggedInterface: With this interface I mark the actions that need to be logged in any case: it doesn't matter if they are processed or not.
    • OutableInterface: This interface marks the executions, the text of which can be displayed to the user: far from every implementation can be displayed to the user. For example, you can output an action with the text “Page not found” - this is normal. But you can’t get the execution with the text “Could not connect to Mysql using root login and password 123” . OutableInterfacemarks the executions which can be displayed (I have such a minority). In the rest, the situation displays something like “Service is not available .

    Default handler, logging


    The default handler is an extremely useful thing. Who does not know: it is executed when the execution could not be processed by any block try catch.

    This handler allows us to perform various actions before stopping the script. The most important thing to do is:

    Roll back changes: since the operation has not been completed to the end, it is necessary to roll back all changes made. Otherwise, we will spoil the data. For example, you can CController::beforeAction()open a transaction, CController::afterAction()commit, and in case of an error, make a rollback in the default handler.

    This is a rather crude way to roll back, plus often rollback involves not only rollback transactions, and knowledge of how to roll back correctly should be in the business logic code. In such situations, you should use this technique:

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

    It turns out that we rolled back the changes and threw the same reception that we would continue to process it.

    Logging: the default handler also allows us to perform some kind of custom logging. For example, in my application I put everything in the database and use my own analysis tool. At work, we use getsentry.com/welcome . In any case, the execution that reached the default handler is most likely an unexpected execution, and it needs to be logged. It should be noted that various information can be added to the reception class, which must be logged to better understand the cause of the error.

    The inability not to notice and confuse


    A huge advantage of the execution is its uniqueness: it is impossible not to miss it and it is not possible to confuse it with something.

    From the first it follows that we will always be aware of the error that has occurred. And this is wonderful - it’s always better to know about the problem than not to know.

    The second plus becomes obvious in comparison with custom error handling methods, for example, when the method returns nullif it did not find the desired object and false in case of an error. In this case, it is elementary not to notice the error:

    $result = $this->doAnything(); // null если не нашла нужный объект и false в случае ошибки
    // Не заметит ошибки
    if($result){ ... }
    // Не заметит ошибки
    if($result == null){ ... }
    // Не заметит ошибки
    if(empty($result)){ ... }
    // Не заметит ошибки
    if($result = null){ ... }
    

    The reception is impossible to miss.

    Stopping an erroneous operation


    But the most important, and the most important thing that the execution does is that it stops the further execution of the operation. An operation that has already gone wrong. And, therefore, the result of which is unpredictable.

    A huge minus of self-made error handling mechanisms is the need to independently check for errors. For example, after each operation we need to write something like:

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

    This requires a certain discipline from the developers. And not everyone is disciplined. Not everyone knows that your object has a method getLastError(). Not everyone understands why it is so important to check that everything is going as it should, and if not, roll back the changes and stop the execution.

    As a result, checks are not done, and the operation leads to completely unexpected results: instead of one user, everything is deleted, the money is sent to the wrong person, voting in the State Duma gives a false result - I saw this a dozen times.

    The action protects us from such problems: it either searches for the appropriate handler (its presence means that the developer has foreseen this situation, and everything is fine), or it reaches the default handler, which can roll back all changes, reserve the error, and issue a warning to the user.

    When should you call an exercise:


    I sort of figured out the advantages. I hope I managed to show that executions are an extremely convenient mechanism.

    The question arises: in what situations is it worth calling an execution?

    In short - always! If in detail: always when you are sure that the operation should be performed normally, but something went wrong and you don’t know what to do with it.

    Let's look at the simplest action of adding a record:

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

    When we enter incorrect post data, the execution is not called. And this is quite consistent with the formula:

    • At this step, we are not sure that the operation should succeed, because you cannot trust the data entered by the user.
    • We know what to do with it. We know that in case of incorrect data, we must display a list of errors to the user. It should be noted that the knowledge of “what to do” is within the current method.

    Therefore, in this case there is no need to use executions. But let's look at another example: There is an order page on which there is a button that cancels the order. The cancellation code is as follows:

    /**
     * Отменяет заказа.
     * Отмена производиться путем смены статуса на 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');
      }
    }
    

    The cancel button itself is shown only when the order can be canceled. Thus, when this method is called, I am sure that the operation should succeed (otherwise the button would not appear and the user could not click on it to call this method).

    The first step is pre-validation - we check whether we can actually perform the operation. In theory, everything should be successful, but if it isAllowedStatusreturns false, then something went wrong. Plus, within the current method, we absolutely do not know how to handle this situation. It is clear that you need to reserve the error, display it to the user, etc. ... But in the context of this particular method, we do not know what to do with it. Therefore, we drop the reception.

    Next is the operation and saving the changes.

    Then comes post-validation - we check whether everything is really preserved, and whether the status has really changed. At first glance, this may seem pointless, but: the order could not have been saved (for example, it didn’t pass validation), and the status could well have been changed (for example, someone had coded in CActiveRecord::beforeSave). Therefore, these actions are necessary, and, again, if something went wrong - drop the execution, because within this method we do not know how to handle these errors.

    Execution vs return null


    It should be noted that the reception should be thrown only in case of error. I have seen some developers abuse them by throwing them where they shouldn't. Especially often - when a method returns an object: if it is not possible to return the object, execution is thrown.

    Here you should pay attention to the responsibilities of the method. For example, he СActiveRecord::find()doesn’t give up the execution, and this is logical - the level of his “knowledge” does not contain information about whether the lack of result is a mistake. Another thing, for example, is a method KladrService::resolveAddress()that in any case is required to return an address object (otherwise either the code is incorrect or the database is not relevant). In this case, you need to quit execution, because the lack of result is a mistake.

    On the whole, the described formula ideally determines the places where it is necessary to quit executions. But I would especially like to distinguish 2 categories of executions, which need to be done as much as possible:

    Technical Actions


    These are executions that are absolutely not related to the subject area, and are necessary to prevent technically from executing incorrect logic.

    Here are some examples:

    // В нескольких 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;
    }
    

    Technical actions will help prevent or catch, IMHO, most of the bugs in any project. And the indisputable advantage of their use is the lack of the need to understand the subject area: the only thing required is the discipline of the developer. I urge you not to be lazy and to insert such checks everywhere.

    Acceptance statements


    The statements of actions (based on the motives DDD) are called up when we discover that any business logic is being violated. Of course, they are closely related to domain knowledge.

    They rush when we test a statement, and see that the result of the check does not match what was expected.

    For example, there is a method for adding a line item to an order:

    /**
     * Добовляет позицию в заказ
     * @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')
      }
    … прочие действия ...
    }
    

    In the process of adding a position, a bunch of different actions take place. And at the same time, various statements are periodically checked: that all the amounts agree, that the order can be delivered - this is the statement of claims.

    Here you can debate on the need for such actions:
    For example, you can write tests for methods of recalculating the cost of an order, and checking in the body of the method is nothing more than a duplication of the test. You can check the possibility of delivering an order with a new position before adding a position (to warn the user at least about this)

    But practice shows that it is far from always possible to write tests for all invariants of an object. And it is impossible to defend oneself from, for example, a new developer who can fill anything.

    Therefore, in critical places such actions are needed unambiguously.

    Change logic to avoid execution


    As I said, PHPdevelopers are afraid of executions. They are afraid of their appearance, and are afraid of throwing them on their own.

    And in this struggle against executions, many make a mistake: they deviate from the initially clear, understandable, straightforward logic towards any assumptions in order to somehow perform the operation.

    Here is an example: you just need to display the page by id (so you understand - this is the real code from a famous project)

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

    Despite the simplest and most understandable task, there is absolutely wild logic here.
    Not only can it show the user absolutely not what it needs, it also disguises our bugs:

    • if idnot set, it is taken id = 1. The problem is that when id is not set, this is already a bug, because somewhere in our links are not correctly formed.
    • If the page is not found, then somewhere we have a link to a page that does not exist. This is also most likely a bug.

    This behavior does not benefit either the user or the developers. The motivation for such an implementation is to show at least something, because 404execution is bad.

    One more example:

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

    It’s the same from a real project, and the motivation is the same: to return at least something, but not to call an execution, despite the fact that the method should obviously return the code of the city, not the region.

    And there are a huge number of such changes in logic in the average project. As long as you remember this - it seems harmless. But as soon as you forget, or another developer connects, the bug is provided. Moreover, an implicit bug is floating.

    My opinion is that this is unacceptable. It's just that when you work with big money (and I worked with them for quite some time), certain rules are worked out, and one of them is to interrupt the operation in case of any suspicion of an error. A transaction for 10 million bucks: you must agree, it is better to cancel it than transfer money to the wrong person.

    Of course, we usually deal with less risky operations. And in the case of a bug, for example, a disabled person will not be able to leave an application for installing a ramp in the entrance. And the developer at Raslabon (after all, he will not even be fined) neglects these elementary rules, they say, think, a trifle. The problem is that when something critical is entrusted to him, his approach is unlikely to change. For the problem is not in knowledge, and not in risk, but in discipline and in relation to business. And it turns out that after such programmers, somewhere in the winter pipes burst, somewhere oil spills in tons, somewhere people die in dozens, somewhere money is stolen by millions. Just think, what a trifle!

    Dogs


    For some reason, I thought that no one was using dogs . But recently I came across a team of developers who use them everywhere instead of checking isset, so I decided to write about them as well.

    Dogs issetare used instead for brevity:

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

    against

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

    Indeed, it looks much shorter, and at first glance the result is the same. But! It’s dangerous to forget that the dog is an operator of ignoring error messages. And it @$policy->owner->address->localitywill return nullnot because it checks the existence of a chain of objects, but because it simply ignores the error that has occurred. And these are completely different things.

    The problem is that in addition to ignoring the error Trying to get property of non-object(which makes the behavior of the dog look like isset), all other possible errors are ignored.

    PHPIs a magic language! In the presence of all these magical methods ( __get, __set, __call, __callStatic, __invokeetc.), we cannot always immediately understand what is really happening.

    For example, take another look at the line $policy->owner->address->locality. At first glance - a chain of objects, if you look closely - it could well be like this:

    • policy - model CActiveRecord
    • owner - relay
    • address - getter, which, for example, accesses a third-party service
    • locality - attribute


    That is, a simple line, $policy->owner->address->localitywe actually start the execution of thousands of lines of code. And the little dog before this line hides errors in any of these lines.

    Thus, such a rash use of the dog potentially creates a huge number of problems.

    Afterword


    Programming is awesome. In my opinion, it looks like building a huge LEGO constructor. At the very beginning, there is an instruction and a scattering of small parts. And so, you take instructions on which you methodically assemble them into small blocks, then combine them into something bigger, even more ... And you catch the buzz from this damn fascinating process, you catch the buzz from how everything is logical and thoughtfully arranged, how much all these parts fit together. And now - in front of you is a whole tractor, or a dump truck. And this is awesome!

    In programming, the same thing, only the role of instructions is performed by the knowledge of patterns, principles of designing classes, best programming practices and building architectures. And when you absorb all this and learn to put it into practice, you start to get the buzz from work, the same as when building LEGO.

    But try to assemble a constructor without instructions ... This thought is like nonsense. However, programmers work without all this knowledge. Over the years. And this does not seem to them nonsense - they do not even understand that they are doing something wrong. Instead, they complain that they were given too little time.

    And if in the afterword of the previous postI suggested thinking, hoping that someone would change their code for the better, now I have lost this hope. Apparently, people really appreciate only the experience for which they paid.

    So to everyone who read this post and thought “what nonsense”, “I know all this, but use laziness”, or “the sucker will tell me” - I want to commit a bug. A bug that will be fined or fired. And then you may recall this post, and think: “maybe I'm really doing something wrong”?

    Take it as soon as possible. For it’s better to make a mistake once, but to see clearly, than to live a bydlokodder all my life. Amen.

    Good to all)

    Also popular now: