I've been wondering why would I use Exceptions in my PHP. Let's take a look at a simple example:
class Worker
{
public function goToWork()
{
return $isInThatMood ?
// Okay, I'll do it.
true :
// In your dreams...
false;
}
}
$worker = new Worker;
if (!$worker->goToWork())
{
if (date('l',time()) == 'Sunday')
echo "Fine, you don't have to work on Sundays...";
else
echo "Get your a** back to work!";
}
else
echo "Good.";
Is there a reason for me to use Exceptions for that kind of code? Why? How would the code be built?
And what about code that may produce errors:
class FileOutputter
{
public function outputFile($file)
{
if (!file_exists($file))
return false;
return file_get_contents($file);
}
}
Why would I use Exceptions in the above case? I have a feeling that Exceptions help you to recognize the type of the problem, which occured, true?
So, am I using Exceptions appropriately in this code:
class FileOutputter
{
public function outputFile($file)
{
if (!file_exists($file))
return throw new Exception("File not found.",123);
try
{
$contents = file_get_contents($file);
}
catch (Exception $e)
{
return $e;
}
return $contents;
}
}
Or is that poor? Now the underlying code can do this:
$fo = new FileOutputter;
try
{
$fo->outputFile("File.extension");
}
catch (Exception $e)
{
// Something happened, we could either display the error/problem directly
echo $e->getMessage();
// Or use the info to make alternative execution flows
if ($e->getCode() == 123) // The one we specified earlier
// Do something else now, create "an exception"
}
Or am I completely lost here?
When should I use an exception?
You use an exception to indicate an exceptional condition; that is, something which prevents a method from fulfilling its contract, and which shouldn't have occurred at that level.
For example, you might have a method, Record::save(), which saves changes to a record into a database. If, for some reason, this can't be done (e.g. a database error occurs, or a data constraint is broken), then you could throw an exception to indicate failure.
How do I create custom exceptions?
Exceptions are usually named such that they indicate the nature of the error, for example, DatabaseException. You can subclass Exception to create custom-named exceptions in this manner, e.g.
class DatabaseException extends Exception {}
(Of course, you could take advantage of inheritance to give this exception some additional diagnostic information, such as connection details or a database error code, for example.)
When shouldn't I use an exception?
Consider a further example; a method which checks for file existence. This should probably not throw an exception if the file doesn't exist, since the purpose of the method was to perform said check. However, a method which opens a file and performs some processing could throw an exception, since the file was expected to exist, etc.
Initially, it's not always clear when something is and isn't exceptional. Like most things, experience will teach you, over time, when you should and shouldn't throw an exception.
Why use exceptions instead of returning special error codes, etc?
The useful thing about exceptions is that they immediately leap out of the current method and head up the call stack until they're caught and handled, which means you can move error-handling logic higher up, although ideally, not too high.
By using a clear mechanism to deal with failure cases, you automatically kick off the error handling code when something bad happens, which means you can avoid dealing with all sorts of magic sentinel values which have to be checked, or worse, a global error flag to distinguish between a bunch of different possibilities.
I'm not a PHP programmer, but this looks similar to C#.
Typically you'd want to throw errors if it is a point of no return. Then you would be able to log something to show that the impossible happened.
If you can tell that the file doesn't exist then you could just say that. No real need in my mind to also throw an exception.
Now if the file was found and you are processing it, and say only half the file was uploaded and you had no way of telling that without an exception, then it'd be nice to have around.
I would say it's a good design practice to avoid the catch all exceptions "catch (Exception $e)" and design by contract instead, just like you seem to be doing in the prior example. I would at first be more specific with the type of exception being thrown and then work from there if needed. Otherwise, stay away from the try->catch and exceptions.
In no way can you assume that a file exists just because you called file_exists()! The function file_exists doesn't open the file, so the file can potentially be deleted or renamed or moved at any time!
class FileOutputter
{
public function outputFile($file)
{
if (!file_exists($file))
return false;
///<--- file may be deleted right here without you knowing it
return file_get_contents($file);//<-- then this will throw an error
//if you don't catch it, you're program may halt.
}
}
I believe this it better:
class FileOutputter
{
public function outputFile($file)
{
try{
if (!file_exists($file))
return false;
return file_get_contents($file);
}catch(Exception e){
check_what_went_wrong_and_go_to_plan_B();
}
}
}
(Edit: And it's probably even better to actually try to open the file from the beginning. If you succeed then you have a 'lock' on the file and it won't just disappear. If not, then catch the exception and see what went wrong.
Then again, you might feel that this level of reduncancy is just silly. In that case I don't think you need to worry about try/catch at all :)
Just a note really, your code for file outputter is invalid, since file_get_contents($file)
does not throw an exception. It will however raise a warning if the file doesn't exist, or can't be accessed for some reason. Additionally, you are returning an exception from outputFile, when you should probably be just letting the error propagate up the call stack.
However, you can register the error handler in php to throw exceptions when an error occurs:
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");
That way, any standard function calls that cause an error will throw an exception.
So, I would change FileOutputter to this (with that snippet above added):
class FileOutputter
{
public function outputFile($file)
{
if (!file_exists($file))
throw new Exception("File not found.",123);
return file_get_contents($file);
}
}
The calling code is then basically the same.
Basically rule is not to return an Exception when an error occurs - you can catch it if you want, and throw a custom exception, but let the exception go up the call stack until you want to use it.
Related
So the question is the same as in subject:
How to simulate and properly write test with error_get_last() function in phpunit
Sample code to write test to:
$lasterror = error_get_last();
if ($lasterror !== null) {
//do something
}
Thanks for asking this question, it was not clear to me at the very beginning where the problem resulted from writing a test for it. That is due to the nature of error and exception handling, especially in testing these things can be considered a bit harder and there are normally no ready-made "easy" solutions because at the end of the day you need to write the test yourself.
I hope this could be clarified in comments and the following can show how to approach them in a way.
NOTE: As we're writing test-code, I use assert() statements in the examples, and this is by intention as assert is a more pure language feature. It makes the examples more portable, too.
To make use of it, execute php having assertions in development mode and throwning (zend.assertions=1 and assert.exception=1) and run the example as it evolves and have later phpunit run with these settings as well.
You can certainly port this into a concrete test-case to implement it Phpunit specific, for the examples the concrete assertion library should not be of interest, but the assertions. They are expected in the examples to bring PHP to halt if they aren't fullfilled and this is how I wrote the examples (some people say those are expectations).
In general testing code that tests for the side-effect of there being an error:
$lasterror = error_get_last();
if ($lasterror !== null) {
//do something
}
and then in your test-suite you get not errors but exceptions:
#trigger_error('Oh no'); // ErrorException: Oh no
which will prevent further code to run (the code you want to put under test).
What to do now? Well, the naked error-trigger would make phpunit highlight you've got an ErrorException, what you can do is to assert the fixture-setup is correct (fixture is the "Oh no" error) and just declare it so.
What? Yes, we turn the obstacle in front of us into the good condition just by saying so:
try {
#trigger_error('Oh no');
} catch (ErrorException $good) {
assert('Oh no' === $good->getMessage(), get_class($good));
} finally {
assert(isset($good), 'an expected exception was thrown');
}
This example helps to better describe what is going on and what we know to deal with, however the original problem still remains: Due to some error handler turning errors into exceptions, error_get_last() returns NULL as there was no error, but an exception.
Execute this code. It will show it passes.
So what is to assert is the opposite. Let's do this, turn the good condition into this bad, bad failure by negating it:
try {
#trigger_error('Oh no');
} catch (ErrorException $fail) {
assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
assert(!isset($fail), 'there must not have been any exception');
}
I hope you can follow so far. First flip.
Now the fixture will only be asserted properly set-up when the expected state is given, here asserting that no ErrorException is being thrown, as we learned that would be "bad" (ruin any test of code with the error_get_last() call).
This is normally what one would expect to be the first version. We actually have already done two test runs now. One green, one red.
As long as this code does not pass, we know the fixture was not set-up and hence there is no need to even run any further tests.
As you can see testing is as easy as programming. You write the test code as-if everything is already in order, pure magic. Even when it makes no sense at all (there is no test yet, right?!) and still this is what you want to achieve.
Why is something that simple so hard? One part might be that writing the code before the test can be irritating then when writing the test. The mind is filled with expectations about the code while it should be only concentrated on the test(-code).
Don't sweat, just don't get irritated that testing then does not work first of all, that would be the same otherwise, too. A good test starts with failing very soon at the beginning. And this is exactly where we are and what we did is just to turn condition red into green. Second flip.
In testing this is also called the Happy Path, that is where it goes happily ever after and the test just tests for that what when things are unspecifically just working and unspecifically just testing for it.
But this only to side-track you a bit.
So now let's make the fixture pass this happy-path (remove the existing error handler, set the error, restore the old handler, I know, you have read this up from the manual already, right? So I can spare the links, sorry for being lazy):
try {
set_error_handler(null); // <--- this
#trigger_error('Oh no');
restore_error_handler(); // <--- restore original runtime configuration
} catch (ErrorException $fail) {
assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
assert(!isset($fail), 'there must not have been any exception');
}
Et voilá, the happy path is green!
But this is only the happy path. Good tests test for others paths, too. And rigorously.
Didn't we have one? A right, yes, the one with the first assertion that ErrorException were triggered, we have that test already. What once was good despite a failure is now good again (but despite the name, on the unhappy path). Third flip.
try {
set_error_handler(null);
#trigger_error('Oh no');
restore_error_handler();
try {
#trigger_error('Oh no');
} catch (ErrorException $good) {
assert('Oh no' === $good->getMessage(), get_class($good));
} finally {
assert(isset($good), 'an expected exception was thrown');
}
} catch (ErrorException $fail) {
assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
assert(!isset($fail), 'there must not have been any exception');
}
Wow, now this certainly escalated as the story evolved. And it's not even complete yet. You've spotted the one or other problem with it, right?
The initial expectation that there is the ErrorException throwing error-handler on the happy path is missing. Let's put it on top as an early check (and unset the check variable afterwards as there is the re-use for the post-assertion).
And even more importantly, ensure error_get_last() returns NULL as otherwise the test could be easily tainted for the result:
try {
#trigger_error('Oh no');
} catch (ErrorException $good) {
assert('Oh no' === $good->getMessage(), get_class($good));
} finally {
assert(isset($good), 'an expected exception was thrown');
}
unset($good);
assert(NULL === error_get_last()); // <-- this easy to miss
try {
set_error_handler(null);
#trigger_error('Oh no');
restore_error_handler();
try {
#trigger_error('Oh no');
} catch (ErrorException $good) {
assert('Oh no' === $good->getMessage(), get_class($good));
} finally {
assert(isset($good), 'an expected exception was thrown');
}
} catch (ErrorException $fail) {
assert(false, sprintf('the "%s" exception must not be thrown', get_class($fail)));
} finally {
assert(!isset($fail), 'there must not have been any exception');
}
// continue here...
It is verbose but shows all the things that are done:
awareness that triggering an error alone does not work and why
the precondition that error_get_last(); returns NULL before setting an error
that there is an ErrorException error-handler that needs to get out of the way to trigger a ture PHP error
that this error handler is re-instated as likely the system under test needs it (or should be tested with it)
Thanks to assert() statements it documents its own expectations.
Wrap it in a function and give it a useful name and a short description for what it is good for. You can - but must not - safe-guard the assertions. I normally do that inside the Phpunit test-suite so that the expected runtime-configuration is not getting lost.
Here I come to an end which hopefully is another beginning for you.
This is only one way how you could approach such hard problems, and these problems are really hard to test because it's full of side-effects here. Not only would error_get_last() pose a problem for a straight forward handling.
Yes, error handling is hard, often forgotten or attacked with a "burn-it-with-fire" attiture. At the end of the day this is not helpful when you actually need to deal with it.
And by the definition of it, writing a test for that, is.
The full example incl. simulation of an error exception throwing error handler and some of the earlier flips on 3v4l.org: https://3v4l.org/8fopq
It should go without saying that turning all errors into exceptions is some of the stupidest idea some developers have fallen for. It is just another example that error handling is hard.
Errors and exceptions are a major source of complexity and bugs
If you want get know something talk with the another human.
Thank you a lot #hakre!
And compressed above answer:
protected function generateSafelySimulatedError(\Closure $errorGeneratingFn, string $generatedErrorMsg = null): void
{
try {
set_error_handler(null);
set_exception_handler(null);
$errorGeneratingFn();
restore_error_handler();
restore_exception_handler();
try {
$errorGeneratingFn();
} catch (\ErrorException $good) {
if ($generatedErrorMsg !== null) {
$this->assertTrue($generatedErrorMsg === $good->getMessage(), get_class($good));
}
} finally {
$this->assertTrue(isset($good), 'An expected exception was thrown.');
}
} catch (\ErrorException $fail) {
$this->assertTrue(false, sprintf('The "%s" exception must not be thrown.', get_class($fail)));
} finally {
$this->assertTrue(!isset($fail), 'There must not have been any exception.');
}
}
And sample usage:
public function testCriticalErrors(): void
{
$testObject = $this->getObjectUnderTest();//Here is creation of object and also here is set_error_handler() and other similar.
$this->generateSafelySimulatedError(function (): void {
#trigger_error('Oh no');
}, 'Oh no');
$testObject->criticalErrors();//tested method like:
/*
$lasterror = error_get_last();
if ($lasterror !== null) {
//do something
}
*/
//Below other assertions if needed.
$this->expectOutputString('<html class="error"><body><h1>500</h1>Error 500</body></html>');
}
I am trying to determine what is best practice for PHP exception handling.
For example, a basic function which checks validation:
public function myValidationChecker(MyForm $form, $dateFrom, $dateTo) {
try {
$start = $dateFrom->format('Y-m-d'); // could cause exception
$finish = $dateTo->format('Y-m-d'); // could cause exception
// lots more logic here
$diff = $dateTo->diff($dateFrom); // could cause different exception
// ... more non-exception causing logic
} catch (\Exception $e) {
$this->log($e->getMessage());
// attach error to $form so that code stops executing
// & display generic error message to user
}
}
Should I potentially be catching a specific exception rather than just \Exception, e.g. UnexpectedValueException or InvalidArgumentException and have the catch surrounding just that specific, scary area of code?
Or equally, is it best to surround the entire function, in the case that there is an exception that I have not planned for? Either way, in this catch block, the code is prevented from being executed, as after this function is run, $form is checked for an error message.
You should only ever catch the exception you know how to handle. Don't just catch, because you want it, catch them only when you need to. Most of the time the exception should bubble up and be caught by the general logger, preferably the one built-in to PHP.
If you know what to do with the exception, then you know what you need to catch. Catch the exception you need to handle and let the others bubble up.
I access a function in an included class Environment which can throw an Alert which needs to direct the users back to a page with some error information. What is the best way for me to reference the page so it can be used wherever I access the function from anywhere?
Is there any better way to do what I am trying to do?
public function checkEnvironment() {
try {
// If status is false
if (!$this->getStatus()) {
// Generate a new special exception with code
throw new Alert(6);
} else {
$connection = Gateway::checkInstance();
return $connection->getData('SELECT * FROM control_environment WHERE subdomain = ?', array($this->subdomain[0]));
}
} catch (Alert $alert) {
$_SESSION['error'] = $alert->getData();
if (!headers_sent()) {
header('Location: my/file/here.php');
exit;
}
}
}
You're badly abusing exceptions. There is absolutely no reason to put a single if statement in a try block, which can only throw one type of exception which is guaranteed to be caught immediately after that block. You've added nothing to your code but clutter, there is literally no advantage to doing this over simply doing an if/else with no exceptions/catching.
The point of exceptions is that you throw them up out of the current scope, to some place where they can actually be handled in a meaningful way.
Pick a real exception class, one that communicates something about the error that's occurred. Alert(6) doesn't tell anybody anything. Then, handle that (and probably many other) exceptions up above this, where you can be more sure that redirecting is the correct course of action. Your low-level database code shouldn't have any concept of browsers or http or redirection.
Your code also shouldn't have an else branch when the purpose of the if branch is to throw an exception. The else is redundant.
The whole function should look like this.
public function checkEnvironment() {
if (!$this->getStatus())
// Generate a new special exception with code
throw new StatusException;
$connection = Gateway::checkInstance();
return $connection->getData('SELECT * FROM control_environment WHERE subdomain = ?', array($this->subdomain[0]));
}
Should caught exceptions be re-thrown directly, or should they be wrapped around a new exception?
That is, should I do this:
try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
throw $e;
}
or this:
try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
throw new Exception("Exception Message", 1, $e);
}
If your answer is to throw directly please suggest the use of exception chaining, I am not able to understand a real world scenario where we use exception chaining.
You should not be catching the exception unless you intend to do something meaningful.
"Something meaningful" might be one of these:
Handling the exception
The most obvious meaningful action is to handle the exception, e.g. by displaying an error message and aborting the operation:
try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
echo "Error while connecting to database!";
die;
}
Logging or partial cleanup
Sometimes you do not know how to properly handle an exception inside a specific context; perhaps you lack information about the "big picture", but you do want to log the failure as close to the point where it happened as possible. In this case, you may want to catch, log, and re-throw:
try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
logException($e); // does something
throw $e;
}
A related scenario is where you are in the right place to perform some cleanup for the failed operation, but not to decide how the failure should be handled at the top level. In earlier PHP versions this would be implemented as
$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
$connect->insertSomeRecord();
}
catch (Exception $e) {
$connect->disconnect(); // we don't want to keep the connection open anymore
throw $e; // but we also don't know how to respond to the failure
}
PHP 5.5 has introduced the finally keyword, so for cleanup scenarios there is now another way to approach this. If the cleanup code needs to run no matter what happened (i.e. both on error and on success) it's now possible to do this while transparently allowing any thrown exceptions to propagate:
$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
$connect->insertSomeRecord();
}
finally {
$connect->disconnect(); // no matter what
}
Error abstraction (with exception chaining)
A third case is where you want to logically group many possible failures under a bigger umbrella. An example for logical grouping:
class ComponentInitException extends Exception {
// public constructors etc as in Exception
}
class Component {
public function __construct() {
try {
$connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
}
}
}
In this case, you do not want the users of Component to know that it is implemented using a database connection (maybe you want to keep your options open and use file-based storage in the future). So your specification for Component would say that "in the case of an initialization failure, ComponentInitException will be thrown". This allows consumers of Component to catch exceptions of the expected type while also allowing debugging code to access all the (implementation-dependent) details.
Providing richer context (with exception chaining)
Finally, there are cases where you may want to provide more context for the exception. In this case it makes sense to wrap the exception in another one which holds more information about what you were trying to do when the error occurred. For example:
class FileOperation {
public static function copyFiles() {
try {
$copier = new FileCopier(); // the constructor may throw
// this may throw if the files do no not exist
$copier->ensureSourceFilesExist();
// this may throw if the directory cannot be created
$copier->createTargetDirectory();
// this may throw if copying a file fails
$copier->performCopy();
}
catch (Exception $e) {
throw new Exception("Could not perform copy operation.", 0, $e);
}
}
}
This case is similar to the above (and the example probably not the best one could come up with), but it illustrates the point of providing more context: if an exception is thrown, it tells us that the file copy failed. But why did it fail? This information is provided in the wrapped exceptions (of which there could be more than one level if the example were much more complicated).
The value of doing this is illustrated if you think about a scenario where e.g. creating a UserProfile object causes files to be copied because the user profile is stored in files and it supports transaction semantics: you can "undo" changes because they are only performed on a copy of the profile until you commit.
In this case, if you did
try {
$profile = UserProfile::getInstance();
}
and as a result caught a "Target directory could not be created" exception error, you would have a right to be confused. Wrapping this "core" exception in layers of other exceptions that provide context will make the error much easier to deal with ("Creating profile copy failed" -> "File copy operation failed" -> "Target directory could not be created").
Well, it's all about maintaining the abstraction. So I'd suggest using exception chaining to throw directly. As far as why, let me explain the concept of leaky abstractions
Let's say you're building a model. The model is supposed to abstract away all of the data persistence and validation from the rest of the application. So now what happens when you get a database error? If you rethrow the DatabaseQueryException, you're leaking the abstraction. To understand why, think about the abstraction for a second. You don't care how the model stores the data, just that it does. Likewise you don't care exactly what went wrong in the underlying systems of the model, just that you know that something went wrong, and approximately what went wrong.
So by rethrowing the DatabaseQueryException, you're leaking the abstraction and requiring the calling code to understand the semantics of what's going on under the model. Instead, create a generic ModelStorageException, and wrap the caught DatabaseQueryException inside of that. That way, your calling code can still try to deal with the error semantically, but it doesn't matter the underlying technology of the Model since you're only exposing errors from that abstraction layer. Even better, since you wrapped the exception, if it bubbles all the way up and needs to be logged, you can trace to the root exception thrown (walk the chain) so you still have all the debugging information that you need!
Don't simply catch and rethrow the same exception unless you need to do some post-processing. But a block like } catch (Exception $e) { throw $e; } is pointless. But you can re-wrap the exceptions for some significant abstraction gain.
IMHO, catching an Exception to just rethrow it is useless. In this case, just don't catch it, and let earlier called methods handle it (aka methods that are 'upper' in the call stack).
If you rethrow it, chaining the caught exception into the new one you'll throw is definitely a good practise, as it will keep the informations that the caught exception contains. However, rethrowing it is only usefull if you add some information or handle something to the caught exception, may it be some context, values, logging, freeing resources, whatever.
A way to add some information is to extend the Exception class, to have exceptions like NullParameterException, DatabaseException, etc. More over, this allow the developper to only catch some exceptions that he can handle. For example, one can catch only DatabaseException and try to solve what caused the Exception, like reconnecting to the databse.
You usually think of it this way.
A class might throw many types of exceptions that will not match. So you create an exception class for that class or type of class and throw that.
So the code that uses the class only has to catch one type of exception.
We have try/catch which could produce exceptions - because
make logical decisions in your app - you might want to change the Type of the exception if you make logical decisions based on the exception type. But the exception type thrown are most of the time specific enough. So this is more a reason not to do anything to the exception just re-throw it.
show different things to users - in this case just re-throw as it is and decide what to do at the highest level - in the Controller. You need to change the message - to log the real technical message and show to user a friendly one.
DB transactions - it can fall in any of the 2 types above - if you make some logical decisions or you just need to tell something to the user.
So, unless you got very good reasons, handling exceptions should be made in only one place (otherwise it becomes confusing) - and that place must be the very top - in the controller.
All the other places should be treated as intermediary and you should just bubble the exception - unless you have a very good reason not to.
Few days ago I deal with errors like this...
exit( 'Error!' );
or exit( 'Error!' );
Doh, right? =] Now I'm trying to learn Exceptions. This is how far I got...
http://pastie.org/1555970
Is that correct use of them? It would be cool that I can have more info about 'em. Like file where exception is thrown and line of that file. I know that there are build-in methods (in Exception class), but I want to somehow extend it so I don't need to write...
throw new My_Exception( '...' );
catch( My_Exception $e ) {
echo $e->my_method();
}
...but use old syntax.
throw new Exception( '...' );
catch( Exception $e ) {
echo $e->getMessage();
}
...or maybe you have any greater thought of Exceptions? How to deal with them? Help me! =]
In general you only need to echo/log exceptions, that cannot be otherwise handled. This pretty much means, that if you put your entire application within try block, there's only one place where you need to put echoing/logging logic (i.e. the catch block associated with the outermost try block).
If on the other hand the exception can be handled without stopping the application (in your example this could be providing a default numeric value, instead of incorrect value), then there's usually no need to echo/log it.
If you do want to log such exceptions (for debugging for example), then your application should contain a logging framework, so that it's enough to do in your catch blocks something like
} catch (Exception $e) {
ExceptionLogger::log($e); //static method == ugly, but it's for simplicity in this example
// do whatever needs to be done
}
log() method above would check if the logging is enabled, and if it is savenecessary data to a file.
Exceptions should be typed based upon the error that you find. The Spl Exceptions are a good start, but you really should be creating your own exceptions as well. Some common ones that I use:
FileNotFoundException extends RuntimeException <- self explanatory
HTTPException extends RuntimeException <- Used for http classes when a non-200 result is encountered
DatabaseQueryException extends LogicException <- Used for database query errors
Now, by typing them specifically, it lets you handle the errors in your code. So let's say that you want to fetch a HTTP resource. If that fails with anything but a 404, you want to try a backup URL. You could do that with:
try {
return getHttp($url1):
} catch (HttpException $e) {
if ($e->getCode() != 404) {
try {
return getHttp($url2);
} catch (HttpException $e2) {
//It's ok to ignore this, since why know it's an HTTP error and not something worse
return false;
}
} else {
return false;
}
}
As far as your example code that you posted, I would change a few things:
Change the thrown exception to InvalidArgumentException since it has more semantic meaning (I almost never throw a raw exception).
You should try to avoid catch(Exception $e) at all costs. You have no idea what exception was thrown, so how can you possibly handle it?
Only catch exceptions that you are reasonably sure you know how to handle (and outputting an error/logging is not handling, it's removing the usefulness of the exception). You should never see something like catch($e) { logerror($e); } or catch($e) { print $e->getMessage(); } since netiher is actually handling the exception.
If you don't fix or workaround the cause of the exception in your catch block, you should re-throw it. Let the code above you in the stack try to handle it. This is especially true with libraries and classes that are reused all over the place.
Now, with user interfaces, it may be acceptable to catch the exception and show the user an error message. So your example where you print the exception's message might be ok, but you'd really need to think about the use-cases of it. Are you calling it from a model or controller? If so, it may be ok display an error message. Are you calling it from a library? If so, it's probably better to let the exception bubble up.
Also, don't use a global try{} catch() {} block. Instead, install an Exception Handler to handle it for you. It's cleaner, and more semantically correct (since any try{}catch{} implies that you know how to handle the exception that you caught, whereas the exception handler is precisely meant for exceptions that weren't handled because you didn't know how to handle them.
Exceptions are for exceptional circumstances. Do not use them for all error conditions. If a user submits a password that's too short, don't throw an exception, handle that in validation. But if your hash function is expecting sha256 to be available and it isn't, that's a time for an exception. Exceptions are useful for program errors (when a condition that is unexpected happens, such as invalid input to a function), state errors (when the application enters a state that is unknown or unstable, such as if the requested view does not exist) and runtime errors (when the application encounters an error that can only be detected at runtime, such as a file-not-found error).
There is an entire page of the PHP manual devoted to extending exceptions and that page also gives you a lot of information on the methods to identify file/line number, backtrace etc. where the exception was thrown. This is the type of information that is extremely useful for debugging.