I would love to write that
“this article is intended for newbies” , but it’s not. Most php-developers, having experience of 3, 5 and even 7 years, absolutely do not understand how to properly use exceptions. No, they are well aware of their existence, the fact that they can be created, processed, etc., but they do not realize their convenience, consistency, and do not perceive them as an absolutely normal element of development.
In this article there will not be a manual for the exceptions - this is all perfectly described in the php documentation. Here I will tell you about the advantages of using exceptions, and about where, strictly speaking, they should be used. All examples will be for
Yii
, but this is not particularly important.
Why we do not know how to use exceptions:
I love
PHP
. This is an excellent language, as if it were not scolded. But for novice developers, he carries a certain danger: he forgives too much.
PHP
is an overly loving mother. And in the absence of a strict father (for example,
Java
) or self-discipline, the developer will grow into an egoist who doesn’t care about all the rules, standards and best practices. And it seems to
E_NOTICE
time to include
E_NOTICE
, and he hopes for his mother. Which, by the way, is getting old - it already
E_STRICT
with
E_DEPRICATED
, and everything is hanging around its neck.
')
Whether
PHP
is a matter of discussion is to blame, but the fact that from the very beginning
PHP
doesn’t teach us exceptions is a fact: its standard functions do not create exceptions. They either return
false
, hinting that something is wrong, or write down an error code somewhere that you don’t always think of to check. Or fall into another extreme -
Fatal Error
.
And while our novice developer is trying to write his first bydlo-cms, he never meets with the mechanism of execs. Instead, he will come up with several ways to handle errors. I think everyone understands what I am talking about - these methods returning different types (for example, an object when successfully executed, and if unsuccessful, a string with an error), or writing an error to some variable / property, and always a bunch of checks to pass error up the call stack.
Then he will begin to use third-party libraries: he will try, for example,
Yii
, and for the first time he will encounter exams. And then ...
And then nothing will happen. Nothing at all. He has already developed methods of error handling honed by months / years - he will continue to use them. Caused by someone (a third-party library), the exception will be perceived as a specific type of
Fatal Error
. Yes, much more detailed, logged in detail, and Yii will show a beautiful page, but no more.
Then he learn to catch and process them. And this is where his acquaintance with the exceptions 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 the exceptions is developed as something bad, undesirable, dangerous, which should not be, and which should be avoided by all means. This is absolutely not the right approach.
Advantages of exceptions
In fact, the use of exceptions is an extremely concise and convenient solution for creating and handling errors. I will give the most significant benefits:
Context logic
First of all, I would like to show that the execution is not always just a mistake (as usual, developers perceive it). Sometimes it can be part of the logic.
For example, we have a function for reading a
JSON
object from a file:
public function readJsonFile($file) { ... }
Suppose we are trying to read some previously loaded data. With such an operation, the FileNotFoundException
FileNotFoundException
not an error and is perfectly admissible: it is possible that we have never loaded data, therefore there is no file. But
JsonParseException
is already a sign of an error, because the data was loaded, processed, saved to a file, but for some reason it was not stored correctly.
It is quite another thing when we try to read a file that should always be: for such an operation,
FileNotFoundException
is also an error signal.
Thus, exceptions allow us to define the logic of their processing depending on the context, which is very convenient.
Simplify application logic and architecture
Try using exceptions, and you will see how your code will become more concise and understandable. All the crutch mechanisms will disappear, a bunch of nested ifs will be removed, various error transfer mechanisms go up the call stack, the logic will become simpler and more straightforward.
Places the calls of exceptions will help your colleague to better understand business logic and the subject area, for delving into your code, he will immediately see what is permissible and what is not.
And, if we consider any self-sufficient piece of code, for example, a component, then the list of exceptions thrown by it performs another important thing: it complements the interface of this component.
Using third-party components, we are accustomed to pay attention only to the positive side - that he knows how. In this case, we usually do not think about the exceptions that he can create in the process of work. The list of warnings immediately warns where, when, and what problems may arise. But forewarned is forearmed.
Here is an example of an informative interface that is supplemented with knowledge of the behavior:
interface KladrService { public function resolveCode(Address $address); public function resolveAddress($code); }
It should be mentioned that in developing classes of exceptions, we must follow the principle of an informative interface. Roughly speaking - to take into account their logical sense, and not physical. For example, if our addresses are stored in files, then the absence of an address file will cause a
FileNotFoundException
. We should intercept it and cause more sensible
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 variant, an elementary typo will break the whole logic for you, in the second one it is simply impossible to make a mistake.
The second advantage - the class of encapsulation encapsulates all the necessary data for its processing. For example,
AddressNotFoundException
might look like this:
class AddressNotFoundException extends Exception { private $address; public function __construct(Address $address) { Exception::__construct(' '.$address->oneLine); $this->address = $address; } public function getAddress() { return $this->address; } }
As you can see, the event contains an address that could not be found. The handler can get it and execute some logic of its own based on it.
The third advantage is, in fact, all the advantages of the PLO. Although, as a rule, simple objects, so the possibilities of OOP are used a little, but are used.
For example, I have about 70 classes of classes in the application. Of these, several - basic - one class per module. All the 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, I log all unprocessed errors. I mark this interface with exceptions that do not need to be logged at all.
- PreloggedInterface: I use this interface to mark exceptions that need to be logged anyway: it doesn't matter whether they are processed or not.
- OutableInterface: This interface marks exceptions, the text of which can be given to the user: not every event can be displayed to the user. For example, you can display an exception with the text “Page not found” - this is normal. But it is impossible to display an event with the text “Could not connect to Mysql using root login and password 123” .
OutableInterface
marks exceptions that can be displayed (I have such a minority). In the rest of the situation, something like “Service is not available” is displayed.
Default handler logging
The default handler is an extremely useful thing. Who does not know: it is executed when the operation could not be processed by any
try catch
.
This handler allows us to perform various actions before stopping the script. The most important thing to do is:
Rolling back changes: since the operation was not completed, it is necessary to roll back all the changes made. Otherwise, we will corrupt the data. For example, you can open a transaction in
CController::beforeAction()
commit it in
CController::afterAction()
, and make a rollback in the default handler in case of an error.
This is a rather rude way to rollback, plus often rolling back implies not only rolling back transactions, and knowledge of how to roll back properly must 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 exception that we continue to process it.
Logging: the default handler 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 exception that has reached the default handler is most likely an unintended exception and needs to be logged. It should be noted that you can add various information to the class of the exceptions that you need to log in order to better understand the cause of the error.
Impossibility not to notice and confuse
A huge advantage of the exception is its unambiguity: it is impossible not to notice and cannot be confused with something.
From the first it follows that we will always be aware of the error. And this is wonderful - it is always better to know about the problem than not to know.
The second plus becomes obvious in comparison with the custom error handling methods, for example, when the method returns
null
if it does not find the required object and false in case of an error. In this case, it is elementary not to notice the error:
$result = $this->doAnything();
Exception is impossible to miss.
Termination of an erroneous operation
But the most important thing, and the most important thing that makes the expection, is that it stops the further execution of the operation. An operation that has already gone wrong. And, therefore, the result is unpredictable.
A huge minus of self-made error handling mechanisms is the need to independently verify the occurrence of an error. 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 all are disciplined. Not everyone knows that your object has a
getLastError()
method. 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 execution.
As a result, checks are not done, and performing 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 produces a false result - I have seen this a dozen times.
Exception protects us from such problems: it either looks for the corresponding handler (its presence means that the developer has foreseen this situation, and everything is fine), or comes to the default handler, which can roll back all changes, log the error, and issue an appropriate warning to the user.
When to call exceptions:
With the benefits sort of sorted out. I hope I managed to show that exams are an extremely convenient mechanism.
The question is: in what situations is it worth to call an event?
In short - always! In detail: always, when you are sure that the operation should be performed normally, but something went wrong, and you do not know what to do with it.
Let's look at the simplest action to add a post:
public function actionCreate() { $post = \Yii::app()->request->loadModel(new Post()); if($post->save()) { $this->outSuccess($post); } else { $this->outErrors($post); } }
When we enter incorrect data, the post is not called. And it completely corresponds to the formula:
- At this step, we are not sure that the operation should be successful, because you cannot trust the data entered by the user.
- We know what to do with it. We know that in the case of incorrect data, we must display to the user a list of errors. It should be noted here that the knowledge of what to do is within the limits of the current method.
Therefore, in this case there is no need to use exceptions. 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:
public function 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 be successful (otherwise the button would not be displayed, and the user would not be able to click on it to call this method).
The first thing to do is prevalidation - we check whether we can really perform the operation. In theory, everything should be successful, but if
isAllowedStatus
returns
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 log a mistake, 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 throw an exception.
Next is the operation and saving changes.
Then there is a postvalidation - we check whether everything is really preserved and whether the status has really changed. At first glance, this may seem meaningless, but: the order could not be fully preserved (for example, it did not pass validation), and the status could well be changed (for example, someone could have a CAdbloader in
CActiveRecord::beforeSave
). Therefore, these actions are necessary, and, again, if something went wrong, we throw an exception, because within this method we do not know how to handle these errors.
Exclusion vs return null
It should be noted that the exception should be thrown only in case of an error. I’ve seen some developers abuse them, throwing them where they shouldn’t. Especially often - when the method returns an object: if it fails to return the object - an action is thrown.
Here you should pay attention to the responsibilities of the method. For example,
ActiveRecord::find()
does not throw an exception, and this is logical - the level of its “knowledge” does not contain information about whether the absence of a result is an error. Another thing, for example, is the method
KladrService::resolveAddress()
which in any case is obliged to return the address object (otherwise either the code is incorrect or the base is not up to date). In this case, you need to throw Exception, because the lack of a result is a mistake.
In general, the described formula ideally defines the places where it is necessary to throw exceptions. But I would especially like to highlight 2 categories of execs, which need to be done as much as possible:
Technical exceptions
These are exceptions that are completely unrelated to the subject area, and are necessary in order to technically prevent the execution of incorrect logic.
Here are some examples:
public function byUserId($userId) { if(!$userId) {
Technical descriptions will help prevent or catch, IMHO, most of the bugs in any project. And the indisputable advantage of their use is the lack of need to understand the subject area: the only thing required is the discipline of the developer. I urge not to be lazy and insert such checks everywhere.
Exception statements
Statements of statements (based on
DDD
) are called when we discover that some business logic is violated. Of course, they are closely related to the knowledge of the subject area.
They rush when we check a statement, and see that the result of the check does not match what was expected.
For example, there is a method for adding an item to an order:
public function addPosition(Position $position) { $this->positions[] = $position; ... , , , ...
. : , — .
:
, , — . ( , )
, . , , , .
.
,
PHP
. , .
: , , - , - .
: id ( — )
public function actionView($id = 1) { $page = Page::model()->findByPk($id) ?: Page::model()->find(); $this->render('view', ['page' => $page]); }
— .
, , :
- if
id
not specified, it is taken id = 1
. The problem is that when id is not set - this is already a bug, because somewhere we have not correctly formed links. - If the page is not found, it means that somewhere we have a link to a non-existent page. This is also most likely a bug.
This behavior does not benefit either the user or the developers. The motivation for such a realization is to show at least something, for an 404
exception is bad.One more example: public function getCityKladrCode($region, $city) { if($ode = ... ... ) { return $ode; } return ... ... }
Also from a real project, and the motivation is the same: to return at least something, but not to cause an exception, despite the fact that the method clearly must return the city code, not the region.And such changes of logic in the average project a huge amount. As long as you remember it, it seems harmless. But as soon as you forget, or another developer connects, the bug is provided. And an implicit bug floating.My opinion is inadmissible. Just when you work with big money (and I worked with them for a long time), certain rules are worked out, and one of them is to interrupt the operation in case of any suspicion of a mistake. Transaction for 10 million dollars: agree that it is better to cancel it than to transfer money to the wrong person., . , , . ( ) , , , . , - — . , , . , - , - , - , - . , !
Dogs
For some reason I thought that no one uses dogs anymore. But I recently encountered a team of developers who use them everywhere instead of checking isset
, so I decided to write about them.Dogs instead isset
used for brevity code: @$policy->owner->address->locality;
vs 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 ignoring error messages. And it @$policy->owner->address->locality
will return, null
not because it checks the existence of a chain of objects, but because it simply ignores the error that 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 dog’s behavior similar to isset
), all other possible errors are ignored.PHP
- this is a magic language! In the presence of all these magical methods ( __get, __set, __call, __callStatic, __invoke
and so forth), we cannot always immediately understand what is actually happening.For example, look at the line again $policy->owner->address->locality
. At first glance - a chain of objects, if you look closely - it may well be this:policy
- model CActiveRecord
owner
- relayaddress
- getter, which, for example, refers to any third-party servicelocality
- attribute
That is, with a simple string, $policy->owner->address->locality
we actually run the execution of thousands of lines of code. And the little dog in front of this line hides errors in any of these lines.Thus, such a rash use of a dog potentially creates a huge number of problems.Afterword
Programming is awesome. In my opinion, it looks like an assembly of a huge LEGO designer. Right at the beginning before you the instruction and a scattering of fine details. And so, you take the instruction, according to which you systematically assemble them into small blocks, then you merge them into something more, even more ... And you catch the buzz from this damn fascinating process, you catch the buzz on how logical and thoughtful everything is 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 knowledge of patterns, principles of class design, best practices of programming and building architectures. And when you soak it all up and learn how to put it into practice, you begin to catch the buzz from work, the same as when assembling a LEGO.But try building a constructor without an instruction ... This thought is like nonsense. However, programmers without all this knowledge work fine. For 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 post , - , . , , .
, “ ”, “ , ”, “ ” — . , . , , , : “ - ”?
. , , . Amen.
)