Undabot logo
Undabot logo

PHP EXCEPTIONS

Blog post

In this short (and somewhat abstract) article, we'd like to show you a few tricks we have when it comes to designing and using exceptions.
The examples shown here are based on and inspired by some real business rules and situations we've come across, but are also somewhat simplified.

Here are some basic things you should know about our fictitious and soon to be globally popular company that we're dealing with:

  •  We don't like corona;
  • We like to travel;
  • When booking a travel ticket, your travel cannot end before it starts meaning you need to pay attention to the date picker when reserving your tickets :);
  • When a family travels, the number of passengers has to be between 2 and 6 - otherwise it's either a single person travel, or a group travel.

Exceptions 
For the most part, the online PHP tutorials teach you to 'new up' an exception in the same way as you would a normal class - by calling its constructor and passing in the string message:

```php
throw new \DomainException('some business rule got violated and we\'re supposed to describe the context and maybe
even provide some data here');
```

This approach works, but there are some problems with it. You're introducing noise at your call site, while you could just delegate it to the exception class itself.

You might be ok with that (and that's totally fine!), but there are a few other issues which I find more worrying:
 

  • The process of evolving the exception becomes much more involved
  • You're mixing concerns and blurring the actual violations that caused the exception to happen.

I won't be going over the misuse of the generic, language provided exceptions, such as \DomainException, \RuntimeException, \InvalidArgumentException and others. I'm going to assume you write your own 'user land' exceptions that reflect some of your business rules (or concerns, at least).

<obligatory links to external blogs on this subject>

By harnessing the great power of a quick copy/paste & some refactoring tools, we could imagine a situation like this:

```php
class TravelTicketRequestException extends \DomainException{}
```

The calling (throwing? :thinking face: :)) code for which could look like this:

```php
throw new \TravelTicketRequestException('some business rule got violated and we\'re supposed to describe the context and maybe
even provide some data here');
```

So, what's wrong with this?

The Issues

By tasking the call site with structuring the error message, you're coupling the code that's supposed to deal with the business processes and business rules with the code that's dealing with providing information on an exceptional situation.

The problem actually stems from the design of the exception class itself, but well get to that briefly. At this moment, the problem isn't very clear because this exception is being thrown in only one place, in only one circumstance, and it reflects only one single business rule.

Suppose the customer got their dates mixed up:

```php
$start = DateTimeImmutable::createFromFormat('Y-m-D','2021-6-15');
$end = DateTimeImmutable::createFromFormat('Y-m-D','2021-6-10');

new TravelTicketRequest(new TravelDuration($start, $end));
```

The code that's supposed to check if the provided dates are compliant with the business rule would throw an exception:

```php
//...
if($start > $end){
throw new TravelTicketRequestException('Start and end dates do not conform to business rules.');
}
```

It might not seem like a big problem, but this isn't the only business rule we're dealing with.
What happens when we need to check if the number of passengers really represents a family or a group?

```php
// our customer has a lot of cousins and they all want to go for a post-corona party vacation together...
$numberOfPassengers = 9;
new FamilyTravelPackage($numberOfPassengers);
```
When enforcing the business rule, we need to throw the exception if the rule is being violated:

```php
if(2 > $numberOfPassengers || $numberOfPassengers > 6){
  throw new TravelTicketRequestException('Number of passengers does not conform to the terms of Family travel.');

}
```

At this point, we have two different scenarios and two different places where we're throwing the same exception.

Making the problem a bit clearer - mixed concerns, multiple axes of evolution

The two different contexts (parts of the code dealing with different business rules) throwing the same exception (but each with their own specific error messages) should be treated as a code smell (perhaps a Divergent Change? https://refactoring.guru/smells/divergent-change).

If you consider the exception class as a lower level service in comparison to its clients - the 'business' code, it makes sense to see the 'business' code as the driver or initiator of the changes in the lower level service.
Although this might seem counterintuitive and going against the Dependency Inversion Principle, it's really not - like stated earlier, the problem we're dealing with here stems from the poor design of the exception class itself.


The exception class is the one that's violating one of the SOLID principles, and it's the first one that's being broken.
The exception is trying to do too much, i.e., it's trying to serve too many clients. The glaring evidence of its misuse is seen in the different parts of the business code.

A higher perspective 
When developing the APIs that serve different clients, you can't possibly control the way the error messages will be presented to the end users. It's up to the client to decide how to treat the error message and what to do with it.
This, of course, holds only as long as - and rests on the presumption that - you are processing your exceptions (at the boundary of your system); one of the approaches used could be catching all unhandled exceptions and returning the extracted exception message in the response.

By setting the error message at the throwing site, you are effectively:
Mixing different concerns;

  • Code dealing with the business rules and processes should not care about the UI/UX concerns;
  • Removing the ability to introduce data elements to the exception, as needed by different clients;
  • Preventing the utilization of various design approaches to UI error representations;
  • Introducing an unnecessary risk of pointless refactoring of call sites when evolving the exception.

Evolving the Exception, pt. 1

The wrong way
Reusing the same Exception class to signal different exceptional situations might work for a while, but the trouble rears its ugly head when you need to use the exception's error message and it just "doesn't fit".

Instead of parsing the message text in order to extract the piece of data you're interested in (such as, for example, the values of offending dates selected, or the number of passengers that no longer constitute a family, but rather a group under the rules of the business we're dealing with), you could opt for passing these values as separate arguments to your exception.

```php
class TravelTicketRequestException extends \DomainException{
  public function __construct(string $message, int $numberOfPassengers){
  //...
  }
}
// ...
$numberOfPassengers = 9;
throw new TravelTicketRequestException('The number of passengers represent a group travel', $numberOfPassengers);
```

Doing this will cause a ripple effect across your code base - all uses of `TravelTicketRequestException` will have to be changed to account for this new parameter.
You could define this as an optional parameter, but that will just move the problem around a bit.

A better way

We can fix almost all of the problems stated above with a bit of design. Since Exceptions are just regular classes, there's no reason we can't treat them the same way as we would other objects in our system (to a degree, at least).

As we already established, all the issues we talked about stem from bad design of the Exception itself.
More specifically, the Exception is violating the SRP.
Even more specifically, it's the Exception's constructor that's violating the SRP.

The way to fix it is quite simple (although not as cheap in the short term) - introduce named constructors.

```php
class TravelTicketRequestException extends DomainException
{
  public static function endDateBeforeStartDate(DateTimeImmutable $startDate, DateTimeImmutable $endDate): self
  {
      $errorMessage = sprintf(
          'Travel must start %s before it ends %s',
          $startDate->format(DateTimeImmutable::ATOM),
          $endDate->format(DateTimeImmutable::ATOM)
      );

      return new self($errorMessage);
  }
 
      public static function incompatibleNumberOfPassengersForTravelType(string $travelType, int
  $potentialPassengers):
  self
  {
      $errorMessage = sprintf(
          'Travel type: %s does not cover %s passenger(s)',
          $travelType,
          $potentialPassengers
      );

      return new self($errorMessage);
  }
}
```
This approach solves almost all the issues we covered earlier (and some not covered). It will make your code more readable, more maintainable, and more expressive in terms of communicating specific business concepts and their rules.

It works well for relatively simple and straightforward business rules, but it tends to become a problem over time.


Although a significant improvement over the initial approach, it is not without its own flaws.


When a lot of closely related business rules are discovered, it's generally a good thing to make them explicit in the code. In such cases, the exception class tends to grow to accommodate them all:

```php
class TravelTicketRequestException extends DomainException
{
  public static function endDateBeforeStartDate(DateTimeImmutable $startDate, DateTimeImmutable $endDate): self
  {
      $errorMessage = sprintf(
          'Travel must start %s before it ends %s',
          $startDate->format(DateTimeImmutable::ATOM),
          $endDate->format(DateTimeImmutable::ATOM)
      );

      return new self($errorMessage);
  }

  public static function unknownTravelType(string $type): self
  {
      $errorMessage = sprintf(
          'Requested unknown Travel type: %s',
          $type
      );

      return new self($errorMessage);
  }

  public static function travelTypeDurationLimitExceeded(string $type, DateTimeImmutable $maxEndDate): self
  {
      $errorMessage = sprintf(
          'Travel type %s duration must end before %s',
          $type,
          $maxEndDate->format(DateTimeImmutable::ATOM)
      );

      return new self($errorMessage);
  }

  public static function incompatibleNumberOfPassengersTravelType(string $travelType, int
  $potentialPassengers):
  self
  {
      $errorMessage = sprintf(
          'Travel type: %s does not cover %s passenger(s)',
          $travelType,
          $potentialPassengers
      );

      return new self($errorMessage);
  }

  public static function invalidStartDateForTravel(
      string $travel,
      DateTimeImmutable $startDate,
      DateTimeImmutable $maxStartDate
  ): self {
      $errorMessage = sprintf(
          'Requested travel start: %s for travel %s exceeds allowed start date: %s',
          $startDate->format(DateTimeImmutable::ATOM),
          $travel,
          $maxStartDate->format(DateTimeImmutable::ATOM),
      );

      return new self($errorMessage);
  }
 
  public static function unknownBaggageInsuranceCoverage(string $requestedCoverage): self
  {
      $errorMessage = sprintf(
          'Requested coverage: %s unknown',
          $requestedCoverage
      );

      return new self($errorMessage);
  }
// ... a lot more business rules were discovered
}
```

This still works, but there's a lot going on in that class. Testing the business rules becomes a little harder every time you discover a new one:

```php
class FamilyTravelTypeTest extends TestCase
{
//...
  /** @dataProvider passengerNumberLimits */
  public function testTransportCoversBetweenTwoAndSixFamilyMembers(int $numberOfPassengers): void
  {
      $this->expectException(TravelTicketRequestException::class);
      $this->expectExceptionMessage(
          sprintf(
              'Travel type: %s does not cover %s passenger(s)',
              FamilyTravel::FAMILY_TRAVEL_CODE,
              $numberOfInsuredPersons
          )
      );
      new FamilyTravelPackage($numberOfPassengers);

      //...
  }
//    ...
}
```

Using this approach, we've coupled the tests to actual implementation details a bit too much.

If we need to, for whatever reason, change the error message for one of the business rule violations (but without actually changing the rules or behaviour), we're in for a bad time - we'll need to update all the tests that are referencing that specific case.
This might be a big and annoying problem - since we're just relying on the hardcoded string value of the error message, there's nothing providing our tools (IDE or static analyzers) with the info needed to pick up the references between the test and the exception class' named constructor.

This is a clear sign that the SRP violation has moved from the method level to the class level.

The “even better way” way™


To avoid this, we could try the following:


Create a base exception class representing the fundamental business rule*;
Extend the base exception with subtypes, each representing a specific violation, containing only one named constructor representing the specific violation;
Instantiate the new subtype with its own named constructor, instead of picking a specific one from the all-encompassing, higher level, exception class*;
Use the specific subtype in the exception expectations in tests.

Example:

```php
class TravelTicketRequestException extends DomainException {}

class ExceededFamilyTravelPassengerNumber extends TravelTicketRequestException {
  public static function incompatibleNumberOfPassengersForTravelType(int $potentialPassengers): self
  {
      $errorMessage = sprintf(
          'Family Travel does not cover %s passenger(s)',
          $potentialPassengers
      );

      return new self($errorMessage);
  }
}
```

Pros:

  • easier to test,
  • we no longer rely on exception messages,
  • easier to change / refactor,
  • more readable.

We no longer have to check for a specific exception message, since the exception type itself carries the context and reflects the target business rule. This makes the test a bit leaner and decoupled from the actual implementation:

```php
class FamilyTravelTypeTest extends TestCase
{
//...
  /** @dataProvider passengerNumberLimits */
  public function testTransportCoversBetweenTwoAndSixPassengers(int $numberOfPassengers): void
  {
      $this->expectException(ExceededFamilyTravelPassengerNumber::class);
      new FamilyTravelPackage($numberOfPassengers);
  }
//    ...
}
```

There are some downsides to this approach, tho.

  • You can expect to run into issues such as:
    Explosion in the number of files/classes that need to be written & maintained;
  • Naming might become an issue when business rules & concepts are verbose / have long names.

 

Similar blog posts

Get in touch