Throwing Your Own Library Exceptions in PHP

sarfraznawaz2005

Sarfraz Ahmed

Posted on January 28, 2020

Throwing Your Own Library Exceptions in PHP

In the previous post, we saw the difference between errors and exceptions, how exceptions can be useful and created our custom exception handler. In this post, we will look how we can create custom exceptions specific to our application, library or company. We also talk about SPL Exceptions that should be used in your code where possible as best practice.

Background

Let's start with bit of background. If you worked on PHP's DOM, PDO, MySQLi or some other extensions or frameworks such as Symphony, Laravel, Slim or any other third party libraries, you might have noticed usually they throw their own exceptions:

$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

$dbh = new PDO($dsn, $user, $password);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Enter fullscreen mode Exit fullscreen mode

In case of wrong database connection, it would throw PDOException:

PDOException: SQLSTATE[HY000] [1045] Access denied for user 'dbuser'@'localhost'
Enter fullscreen mode Exit fullscreen mode

Where did PDOException come from ? Or how did PDO gave that exception ? It is simple. As I had pointed out in my previous post, Exception is a class like any other normal class that can be extended. That's exactly what PDO does here, it simply extends the Exception class so PDO extension should be doing something like this under the hood to be able to throw PDOException:

class PDOException extends Exception {}
Enter fullscreen mode Exit fullscreen mode

And that's usually it. And then PDO should now be throwing that new exception instead of Exception:

// something went wrong
throw new PDOException('message here');
Enter fullscreen mode Exit fullscreen mode

And then user of PDO sees those PDOExceptions.

Now that we know PDOException actually extends Exception, we can come to conclusion that we can catch exceptions thrown by PDO either by using PDOException or Exception through nested catch blocks. If exception is NOT caught for PDOException, it will fall back to base Exception:

try{
    // any PDO code here
}
catch(PDOException $e){
    // handle PDOException
}
catch(Exception $e){
    // handle Exception
}
Enter fullscreen mode Exit fullscreen mode

Throwing Your Own Exceptions

As said above, we can throw our own exceptions by extending the Exception class:

class MyAwesomeLibraryException extends Exception {}
Enter fullscreen mode Exit fullscreen mode

or

class MyCompanyException extends Exception {}
Enter fullscreen mode Exit fullscreen mode

That's easy but one might ask why throw custom exceptions? I can see these reasons:

  • It makes it easy to recognize which thing (library, class, extension, etc) generated exception in code hierarchy
  • This helps developer of orginal library easily spot problems in their code
  • It brands your library exceptions like PDO, DOM, etc do.

Now as developer of some library/application, any time we find we need to throw exception, we simply throw our own custom exceptions:

// something wrong, throw our custom exception
throw new MyAwesomeLibraryException('some message');
Enter fullscreen mode Exit fullscreen mode

Of course you can have many exception types as well for your application if you want.

Prioritizing Your Own Exceptions

Imagine we want to create our own ORM library called SuperORM and it uses PDO under the hood. We create our custom exception first:

class SuperORMException extends PDOException {}
Enter fullscreen mode Exit fullscreen mode

And now we throw SuperORMException exception from whole of our ORM where needed. But since we are using PDO under the hood, we get its PDOException as well and we don't want to show this directly to the consumers of our SuperORM library, we want to be able to first show them our own exception type. This is how we can do that:

class SuperORMException extends PDOException {}

class SuperORM {
    public function connect($dsn, $user, $password) {
        // try connecting to database
        try {
            $dbh = new PDO($dsn, $user, $password);
            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }
        catch (PDOException $e) {
            throw new SuperORMException($e->getMessage(), null, $e);
        }        
    }
}

$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

$superORM = new SuperORM;
$superORM->connect($dsn, $user, $password);
Enter fullscreen mode Exit fullscreen mode

And that would result in our SuperORMException:

SuperORMException: SQLSTATE[HY000] [1045] Access denied for user 'dbuser'@'localhost'

And consumer of our SuperORM can catch our exception now:

try {
    $dsn = 'mysql:dbname=testdb;host=127.0.0.1';
    $user = 'dbuser';
    $password = 'dbpass';

    $superORM = new SuperORM;
    $superORM->connect($dsn, $user, $password);
}
catch (SuperORMException $e) {
    echo $e->getMessage();
}
Enter fullscreen mode Exit fullscreen mode

This makes sure consumer of library will now know that SuperORM will always throw exception of type SuperORMException.

We see that SuperORMException extends PDOException and PDOException extends Exception, this gives consumer the opportunity to catch exceptions in those types like so:

try {
    $dsn = 'mysql:dbname=testdb;host=127.0.0.1';
    $user = 'dbuser';
    $password = 'dbpass';

    $superORM = new SuperORM;
    $superORM->connect($dsn, $user, $password);
}
catch (SuperORMException $e) {
    // code to catch exception
}
catch (PDOException $e) {
    // code to catch exception
}
catch (Exception $e) {
    // code to catch exception
}
Enter fullscreen mode Exit fullscreen mode

So that's how you can throw your own custom exceptions. In fact that's how some of the ORMs or database libraries from various framework do and throw their own exceptions for consumers to catch.

Don't Always Throw Your Custom Exceptions

By this I mean there are certain exceptions types that are part of Standard PHP (SPL) known as SPL Exceptions. They are made to be thrown for specific reasons and considered best practice. You should throw SPL Exceptions where applicable instead of your own custom exceptions. Here they are:

As can be seen, those exceptions can be broadly divided into two main categories: LogicException and RuntimeException. All of those exceptions are self-explanatory from their names. For example LogicException represent any errors caused by the logical errors in your code, RuntimeException represent any errors that are caused after script has run eg runtime errors; similarly BadMethodCallException exception should be thrown if a method in your class doesn't exist and so on.

These SPL exceptions are excellent addition to PHP core because previously if call was made to some method which didn't exist, you usually communicated that to user through a message like:

throw new Exception('Method does not exist!');
Enter fullscreen mode Exit fullscreen mode

So you communicated through those messages for different types of problems. The problem here was that we needed specific exceptions; if method didn't exist, we needed BadMethodCallException exception. This clearly tells developer what type of exception it is and why it might have come about. Another benefit is that such exceptions are useful even for Non-English speaking developers who can also easily spot the problem. Therefore, you must use SPL exceptions for different situations they are made for.

đź’– đź’Ş đź™… đźš©
sarfraznawaz2005
Sarfraz Ahmed

Posted on January 28, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related