eboreum/exceptional

Create and format PHP exceptions easily. Automatically unravel method arguments. Ensure that sensitive strings like passwords, tokens, PHPSESSID, etc. are being masked and thus will instead appear as e.g. "******" in the resulting text.

1.0.0 2022-04-22 10:00 UTC

This package is auto-updated.

Last update: 2024-03-24 16:48:19 UTC


README

license build Code Coverage PHPStan Level

Create and format PHP exceptions easily. Automatically unravel method arguments. Ensure that sensitive strings like passwords, tokens, PHPSESSID, etc. are being masked and thus will instead appear as e.g. "******" in the resulting text.

When a method is called, and somehow that leads to an exception/throwable being raised, wouldn't it be nice knowing all arguments a method was called with? Exceptional can unravel that for you and present these arguments with their respective names in a concise and meaningful way. Additionally, the integration with Eboreum/Caster (https://packagist.org/packages/eboreum/caster) allows revealing of information about the object within which the exception/error occured. This is sometimes valuable and crucial information, and it is superb for debugging.

Requirements

"php": "^8.1",
"eboreum/caster": "^1.0"

For more information, see the composer.json file.

Installation

Via Composer (https://packagist.org/packages/eboreum/exceptional):

composer install eboreum/exceptional

Via GitHub:

git clone git@github.com:eboreum/exceptional.git

Fundamentals

Exception message generation

Example 1: The basics

Example:

<?php

use Eboreum\Exceptional\ExceptionMessageGenerator;

class Foo377464ece90d4b918254101d596d90a8
{
    /**
     * @throws \RuntimeException
     */
    public function bar(int $a, bool $b, ?string $c = null): string
    {
        throw new \RuntimeException(ExceptionMessageGenerator::getInstance()->makeFailureInMethodMessage(
            $this,
            new \ReflectionMethod(self::class, __FUNCTION__),
            func_get_args(),
        ));
    }
};

$foo = new Foo377464ece90d4b918254101d596d90a8;

try {
    $foo->bar(42, true);
} catch (\RuntimeException $e) {
    echo $e->getMessage() . PHP_EOL;
}

Output:

Failure in \Foo377464ece90d4b918254101d596d90a8->bar($a = (int) 42, $b = (bool) true, $c = (null) null) inside (object) \Foo377464ece90d4b918254101d596d90a8

Notice how each argument is paired with its respective values from the func_get_args() function. The argument $c has even received its default value, which func_get_args() will not return.

Example 2: Providing more arguments than there are named arguments

Example:

<?php

use Eboreum\Exceptional\ExceptionMessageGenerator;

class Foo1ff07b0e563e4efbb5a5280f7fe412d8
{
    /**
     * @throws \RuntimeException
     */
    public function bar(int $a, bool $b): string
    {
        throw new \RuntimeException(ExceptionMessageGenerator::getInstance()->makeFailureInMethodMessage(
            $this,
            new \ReflectionMethod(self::class, __FUNCTION__),
            func_get_args(),
        ));
    }
};

$foo = new Foo1ff07b0e563e4efbb5a5280f7fe412d8;

try {
    $foo->bar(42, true, null, 'hello');
} catch (\RuntimeException $e) {
    echo $e->getMessage() . PHP_EOL;
}

Output:

Failure in \Foo1ff07b0e563e4efbb5a5280f7fe412d8->bar($a = (int) 42, $b = (bool) true, {2} = (null) null, {3} = (string(5)) "hello") inside (object) \Foo1ff07b0e563e4efbb5a5280f7fe412d8

Notice how $a and $b are named, but the unnamed arguments have received their respective indexes, {2} and {3}.

Example 3: A constant as default value

Example:

<?php

use Eboreum\Exceptional\ExceptionMessageGenerator;

class Fooaea91664ed3d4467aeb2dfabb2623b53
{
    const SOME_PARENT_CONSTANT = 42;
}

class Fooc261bae9da674d679de77a943ae57779 extends Fooaea91664ed3d4467aeb2dfabb2623b53
{
    const SOME_CONSTANT = 3.14;

    /**
     * @throws \RuntimeException
     */
    public function bar(
        float $a = self::SOME_CONSTANT,
        int $b = self::SOME_PARENT_CONSTANT,
        int $c = PHP_INT_MAX
    ): void {
        throw new \RuntimeException(ExceptionMessageGenerator::getInstance()->makeFailureInMethodMessage(
            $this,
            new \ReflectionMethod(self::class, __FUNCTION__),
            func_get_args(),
        ));
    }
};

$foo = new Fooc261bae9da674d679de77a943ae57779;

try {
    $foo->bar();
} catch (\RuntimeException $e) {
    echo $e->getMessage() . PHP_EOL;
}

Output:

Failure in \Fooc261bae9da674d679de77a943ae57779->bar($a = (float) 3.14, $b = (int) 42, $c = (int) 9223372036854775807) inside (object) \Fooc261bae9da674d679de77a943ae57779

Argument $a has received its default value from the class constant Fooc261bae9da674d679de77a943ae57779::SOME_CONSTANT, $b has received its default value from the class constant Fooaea91664ed3d4467aeb2dfabb2623b53::SOME_PARENT_CONSTANT, and $c has received its default value from the global constant GLOBAL_CONSTANT_25b105757d32443188cca9c7646ccfe6.

Example 4: Static method call

Example:

<?php

use Eboreum\Exceptional\ExceptionMessageGenerator;

class Foo1a7c13d6ce9f4646a120041e36717d5a
{
    /**
     * @throws \RuntimeException
     */
    public static function bar(int $a): string
    {
        throw new \RuntimeException(ExceptionMessageGenerator::getInstance()->makeFailureInMethodMessage(
            static::class,
            new \ReflectionMethod(self::class, __FUNCTION__),
            func_get_args(),
        ));
    }
};


try {
    Foo1a7c13d6ce9f4646a120041e36717d5a::bar(42);
} catch (\RuntimeException $e) {
    echo $e->getMessage() . PHP_EOL;
}

Output:

Failure in \Foo1a7c13d6ce9f4646a120041e36717d5a::bar($a = (int) 42) inside (class) \Foo1a7c13d6ce9f4646a120041e36717d5a

Notice how instead of $this, static::class is used.

Example 5: Making object descriptions verbose using caster

Wouldn't it be nice if we, in addition to the method argument snitching, could get additional information about the object within which the method failed? We can do just that using the Eboreum\Caster\Caster integration.

Example:

<?php

use Eboreum\Caster\Attribute\DebugIdentifier;
use Eboreum\Caster\Collection\Formatter\ObjectFormatterCollection;
use Eboreum\Caster\Contract\CasterInterface;
use Eboreum\Caster\Contract\TextuallyIdentifiableInterface;
use Eboreum\Caster\Contract\DebugIdentifierAttributeInterface;
use Eboreum\Caster\Formatter\Object_\DebugIdentifierAttributeInterfaceFormatter;
use Eboreum\Caster\Formatter\Object_\TextuallyIdentifiableInterfaceFormatter;
use Eboreum\Exceptional\Caster;
use Eboreum\Exceptional\ExceptionMessageGenerator;

// Using TextuallyIdentifiableInterface

class Foo1990801ff8324df1b73e323d7fca71a8 implements TextuallyIdentifiableInterface
{
    protected int $id = 42;

    /**
     * @throws \RuntimeException
     */
    public function bar(int $a): string
    {
        $caster = Caster::getInstance();
        $caster = $caster->withCustomObjectFormatterCollection(new ObjectFormatterCollection([
            new TextuallyIdentifiableInterfaceFormatter(),
        ]));


        $exceptionMessageGenerator = ExceptionMessageGenerator::getInstance()->withCaster($caster);

        throw new \RuntimeException($exceptionMessageGenerator->makeFailureInMethodMessage(
            $this,
            new \ReflectionMethod(self::class, __FUNCTION__),
            func_get_args(),
        ));
    }

    /**
     * {@inheritDoc}
     */
    public function toTextualIdentifier(CasterInterface $caster): string
    {
        return sprintf(
            'My ID is: %d',
            $this->id,
        );
    }
};


$foo = new Foo1990801ff8324df1b73e323d7fca71a8;

try {
    $foo->bar(7);
} catch (\RuntimeException $e) {
    echo $e->getMessage() . PHP_EOL;
}

/**
 * Using DebugIdentifierAttributeInterface
 */

class Foo31eda25b57e8456fb2b3e8158232b5e5 implements DebugIdentifierAttributeInterface
{
    #[DebugIdentifier]
    protected int $id = 42;

    /**
     * @throws \RuntimeException
     */
    public function bar(int $a): string
    {
        $caster = Caster::getInstance();
        $caster = $caster->withCustomObjectFormatterCollection(new ObjectFormatterCollection([
            new DebugIdentifierAttributeInterfaceFormatter(),
        ]));

        $exceptionMessageGenerator = ExceptionMessageGenerator::getInstance()->withCaster($caster);

        throw new \RuntimeException($exceptionMessageGenerator->makeFailureInMethodMessage(
            $this,
            new \ReflectionMethod(self::class, __FUNCTION__),
            func_get_args(),
        ));
    }
};


$foo = new Foo31eda25b57e8456fb2b3e8158232b5e5;

try {
    $foo->bar(7);
} catch (\RuntimeException $e) {
    echo $e->getMessage() . PHP_EOL;
}

Output:

Failure in \Foo1990801ff8324df1b73e323d7fca71a8->bar($a = (int) 7) inside (object) \Foo1990801ff8324df1b73e323d7fca71a8: My ID is: 42
Failure in \Foo31eda25b57e8456fb2b3e8158232b5e5->bar($a = (int) 7) inside (object) \Foo31eda25b57e8456fb2b3e8158232b5e5 {$id = (int) 42}

Notice how we now get useful information from the above object, its ID being 42 (and argument $a is 7).

You must use $this as the argument in the makeFailureInMethodMessage call (and not static::class) for the above to work.

Exception formatters

Example 1: Default formatter

Class: Eboreum\Exceptional\Formatting\DefaultFormatter

A plain text formatter. Contains line breaks and indentation.

<?php

use Eboreum\Exceptional\Caster;
use Eboreum\Exceptional\Formatting\DefaultFormatter;

$caster = Caster::getInstance();
$defaultFormatter = new DefaultFormatter($caster);

$throwable = new \Exception('foo');

$result = $defaultFormatter->format($throwable);

echo $result;

Output:

\Exception
Message:
    foo
File: /some/file/path/script/misc/readme/formatter/example-1-defaultformatter.php
Line: 13
Code: 0\nStacktrace:\n    #0 /path/to/some/file.php:34: fake_function()\nPrevious: (None)

Example 2: HTML5 <table> formatter

Class: Eboreum\Exceptional\Formatting\HTML5TableFormatter

Formats the throwable as HTML5 <table>.

<?php

use Eboreum\Caster\CharacterEncoding;
use Eboreum\Exceptional\Caster;
use Eboreum\Exceptional\Formatting\HTML5TableFormatter;

$caster = Caster::getInstance();
$characterEncoding = new CharacterEncoding('UTF-8');
$html5TableFormatter = new HTML5TableFormatter($caster, $characterEncoding);
$html5TableFormatter = $html5TableFormatter->withIsPrettyPrinting(true);

$throwable = new \Exception('foo');

$result = $html5TableFormatter->format($throwable);

echo $result;

Output:

<table>
  <tbody>
    <tr>
      <td colspan="2">
        <h1>\Exception</h1>
      </td>
    </tr>
    <tr>
      <td>Message:</td>
      <td>foo</td>
    </tr>
    <tr>
      <td>File:</td>
      <td>/some/file/path/script/misc/readme/formatter/example-2-html5tableformatter.php</td>
    </tr>
    <tr>
      <td>Line:</td>
      <td>16</td>
    </tr>
    <tr>
      <td>Code:</td>
      <td>0</td>
    </tr>
    <tr>
      <td>Stacktrace:</td>
      <td>
        <pre>#0 /path/to/some/file.php:34: fake_function()</pre>
      </td>
    </tr>
    <tr>
      <td>Previous:</td>
      <td>(None)</td>
    </tr>
  </tbody>
</table>

Example 3: JSON formatter

Class: Eboreum\Exceptional\Formatting\JSONFormatter

Formats the throwable as JSON.

<?php

use Eboreum\Caster\CharacterEncoding;
use Eboreum\Exceptional\Caster;
use Eboreum\Exceptional\Formatting\JSONFormatter;

$caster = Caster::getInstance();
$characterEncoding = new CharacterEncoding('UTF-8');
$jsonFormatter = new JSONFormatter($caster, $characterEncoding);
$jsonFormatter = $jsonFormatter->withFlags(JSON_PRETTY_PRINT);

$throwable = new \Exception('foo');

$result = $jsonFormatter->format($throwable);

echo $result;

Output:

{
    "class": "\\Exception",
    "file": "\/some\/file\/path\/script\/misc\/readme\/formatter\/example-3-jsonformatter.php",
    "line": "16",
    "code": "0",
    "message": "foo",
    "stacktrace": "#0 \/path\/to\/some\/file.php:34: fake_function()"
    "previous": null
}

Example 4: Oneline formatter

Class: Eboreum\Exceptional\Formatting\OnelineFormatter

Formats the throwable as string with all its contents on a single line. Great for (improved) output in error logs, which do not allow line breaks.

<?php

use Eboreum\Caster\CharacterEncoding;
use Eboreum\Exceptional\Caster;
use Eboreum\Exceptional\Formatting\OnelineFormatter;

$caster = Caster::getInstance();
$onelineFormatter = new OnelineFormatter($caster);

$throwable = new \Exception('foo');

$result = $onelineFormatter->format($throwable);

echo $result;

Output:

\Exception. Message: foo. File: /some/file/path/script/misc/readme/formatter/example-4-onelineformatter.php. Line: 14. Code: 0. Stacktrace: #0 /path/to/some/file.php:34: fake_function(). Previous: (None)

Example 5: XML formatter

Class: Eboreum\Exceptional\Formatting\XMLFormatter

Formats the throwable as XML.

<?php

use Eboreum\Caster\CharacterEncoding;
use Eboreum\Exceptional\Caster;
use Eboreum\Exceptional\Formatting\XMLFormatter;

$caster = Caster::getInstance();
$characterEncoding = new CharacterEncoding('UTF-8');
$xmlFormatter = new XMLFormatter($caster, $characterEncoding);
$xmlFormatter = $xmlFormatter->withIsPrettyPrinting(true);

$throwable = new \Exception('foo');

$result = $xmlFormatter->format($throwable);

echo $result;

Output:

<?xml version="1.0" encoding="UTF-8"?>
<exception>
  <class>\Exception</class>
  <file>/some/file/path/script/misc/readme/formatter/example-5-xmlformatter.php</file>
  <line>16</line>
  <code>0</code>
  <message>foo</message>
  <stacktrace>#0 /path/to/some/file.php:34: fake_function()</stacktrace>
  <previous/>
</exception>

Test/development requirements

"nette/neon": "^3.2",
"phpstan/phpstan": "^1.4",
"phpunit/phpunit": "^9.5",
"sebastian/diff": "^4.0"

Running tests

For all unit tests, first follow these steps:

cd tests
php ../vendor/bin/phpunit

License & Disclaimer

See LICENSE file. Basically: Use this library at your own risk.

Contributing

We prefer that you create a ticket and or a pull request at https://github.com/eboreum/exceptional, and have a discussion about a feature or bug here.

Credits

Authors