PHP SPL Exceptions Use Cases

An exception is used to immediately terminate the operation due to incorrect use of the code or incorrect data transferred for processing, or the inability to transfer data to another system.

LogicException (and inherited) is thrown before the operation is executed, due to incorrect code usage. For example, an unknown function is called, a variable is passed with the wrong type, value, or wrong string length.

RuntimeException (and inherited) is thrown during the execution of an operation, due to invalid data being passed or the inability to pass data to another system. For example, while processing the configuration file, it turned out that the value of the parameter was incorrect or the connection to the database was not established.

Throwing LogicException and RuntimeException results in a HTTP 500 (Internal Server Error) server response because the error is on the server side.

SPL Exceptions Class Tree

  • LogicException
    • BadFunctionCallException
      • BadMethodCallException
    • DomainException
    • InvalidArgumentException
    • LengthException
    • OutOfRangeException
  • RuntimeException
    • OutOfBoundsException
    • OverflowException
    • RangeException
    • UnderflowException
    • UnexpectedValueException

LogicException

class OrderCheckoutHandler
{
    private ?Notifier $notifier = null;
    
    public function checkout(): void
    {
        if (!$this->notifier) {
            throw new LogicException('For checkout, notifier must be specified.');
        }
    }
}

$orderCheckoutHandler = new OrderCheckoutHandler();
$orderCheckoutHandler->checkout();

BadFunctionCallException

function checkout() {
}

$function = 'purchase';
if (!is_callable($function)) {
    throw new BadFunctionCallException(
        sprintf('Function "%s" is not callable.', $function)
    );
}

BadMethodCallException

class OrderCheckoutHandler
{
    public function checkout(): void
    {
    }
}

$order = new OrderCheckoutHandler();
$method = 'purchase';

if (!method_exists($order, $method)) {
    throw new BadMethodCallException(
        sprintf('Method "%s" does not exist in class "%s".', $method, get_class($order))
    );
}

DomainException

interface ReaderInterface
{
}

class YamlReader implements ReaderInterface
{
}

class JsonReader implements ReaderInterface
{
}

class PhpReader implements ReaderInterface
{
}

class ReaderFactory
{
    public function create(string $reader): ReaderInterface
    {
        switch ($reader) {
            case 'YAML':
                return new YamlReader();
            case 'JSON':
                return new JsonReader();
            case 'PHP':
                return new PhpReader();
            default:
                throw new DomainException(
                    sprintf('The specified reader "%s" is not supported.', $reader)
                );
        }
    }
}

$factory = new ReaderFactory();
$reader = $factory->create('XML');

InvalidArgumentException

class Order
{
    public DateTime $createdAt;
    
    public function setCreatedAt(string $createdAt): self
    {
        $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $createdAt);
        if (!$dateTime) {
            throw new InvalidArgumentException('The createdAt must be in "Y-m-d H:i:s" format.');
        }
        
        $this->createdAt = $dateTime;
        return $this;
    }
}

$order = new Order();
$order->setCreatedAt('2023/02/13 13:39:00');

LengthException

function generateMd5HashWithSalt(string $password, string $salt): string
{
    if (strlen($salt) !== 12) {
        throw new LengthException('Salt should have 12 characters');
    }
    
    return crypt($password, $salt);
}

echo generateMd5HashWithSalt('password', '$1$test$');

OutOfRangeException

$orderStatuses = ['new', 'completed', 'shipped', 'delivered', 'canceled'];

class StatusesIterator implements Iterator
{
    private array $statuses;
    private int $position = 0;
    
    public function __construct(array $statuses)
    {
        $this->statuses = $statuses;
    }
    
    public function current(): string
    {
        if (!$this->valid()) {
            throw new OutOfRangeException('There are no more statuses');
        }
        
        return $this->statuses[$this->position];
    }

    public function next(): void
    {
        $this->position++;
    }

    public function key(): int
    {
        return $this->position;
    }

    public function valid(): bool
    {
        return isset($this->statuses[$this->position]);
    }

    public function rewind(): void
    {
        $this->position = 0;
    }
}

$statusesIterator = new StatusesIterator($orderStatuses);

foreach ($statusesIterator as $status) {
    echo $status . PHP_EOL;
}

$statusesIterator->next();
echo $statusesIterator->current();

RuntimeException

$connection = pg_connect('host=127.0.0.1 port=5432 dbname=app user=admin password=s3cr3t');
if (!$connection) {
    throw new RuntimeException('Could not connect to database');
}

OutOfBoundsException

class Dto
{
    public function __construct(private readonly array $data)
    {
    }
    
    public function get(int $index): string
    {
        if (!isset($this->data[$index])) {
            throw new OutOfBoundsException(sprintf('Index "%d" does not exist', $index));
        }
        
        return $this->data[$index];
    }
}

$dto = new Dto(range(0, 10));
$three = $dto->get(11);

OverflowException

class Dto
{
    private int $limit = 10;
    
    public function __construct(private array $data)
    {
    }
    
    public function add(string $item): self
    {
        if (count($this->data) === $this->limit) {
            throw new OverflowException(
                sprintf('The data array has reached its limit. The limit is "%d"', $this->limit)
            );
        }
        
        $this->data[] = $item;
        return $this;
    }
}

$dto = new Dto(range(1, 10));
$dto->add('new');

RangeException

class Wallet
{
    public function __construct(private int $balance)
    {
    }

    public function increase(int $amount): self
    {
        $this->balance += $amount;
        return $this;
    }
    
    public function decrease(int $amount): self
    {
        if ($this->balance - $amount < 0) {
            throw new RangeException('Cannot decrease the balance by this amount');
        }
        
        $this->balance -= $amount;
        return $this;
    }
}

$wallet = new Wallet(0);
$wallet->increase(1000);
$wallet->decrease(1500);

UnderflowException

class Dto
{
    public function __construct(private array $data = [])
    {
    }
    
    public function push(string $item): self
    {
        array_push($this->data, $item);
        return $this;
    }
    
    public function pop(): string
    {
        if (count($this->data) === 0) {
            throw new UnderflowException('Cannot extract item from empty data array');
        }
        
        return array_pop($this->data);
    }
}

$dto = new Dto();
$dto->push('one');
$dto->push('two');
$one = $dto->pop();
$two = $dto->pop();
$three = $dto->pop();

UnexpectedValueException

class HttpClient
{
    public function request(string $url): string|null
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        switch ($httpCode) {
            case 200:
                return $response;
            case 400:
            case 404:
            case 500:
                return null;
            default: throw new UnexpectedValueException(
                sprintf('Unexpected HTTP code "%d"', $httpCode)
            );
        }
    }
}

$httpClient = new HttpClient();
$response = $httpClient->request('www.google.com');
echo $response;