Skip to content

Commit

Permalink
Add new findOrigin functionality
Browse files Browse the repository at this point in the history
Adds Origin value object which is returned by Debug::findOrigin
to be able to determine the origin call without necessarily
creating an exception. We refactored the Debug class accordingly.
  • Loading branch information
iquito committed Dec 12, 2021
1 parent c162cb6 commit 139e84a
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 42 deletions.
111 changes: 71 additions & 40 deletions src/Debug.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class Debug
*
* @param class-string $exceptionClass
* @param class-string|list<class-string> $ignoreClasses Classes and interfaces to ignore in backtrace
* @param string|string[] $ignoreNamespaces Namespaces to ignore
* @param string|list<string> $ignoreNamespaces Namespaces to ignore
*/
public static function createException(
string $exceptionClass,
Expand All @@ -21,11 +21,65 @@ public static function createException(
string|array $ignoreNamespaces = [],
?\Throwable $previousException = null,
): \Throwable {
$ignoreClasses = self::convertToArray($ignoreClasses);
$ignoreNamespaces = self::convertToArray($ignoreNamespaces);
// Make sure the provided exception class inherits from Throwable or replace it with Exception
if (!\in_array(\Throwable::class, self::getClassInterfaces($exceptionClass), true)) {
$exceptionClass = \Exception::class;
}

$ignoreClasses = \array_filter($ignoreClasses, [Debug::class, 'isNotEmptyString']);
$ignoreNamespaces = \array_filter($ignoreNamespaces, [Debug::class, 'isNotEmptyString']);
// If we have no OriginException child class, we assume the default Exception class constructor is used
if (
!\in_array(OriginException::class, self::getClassParents($exceptionClass), true)
&& $exceptionClass !== OriginException::class
) {
/**
* @var \Throwable $exception At this point we know that $exceptionClass inherits from \Throwable for sure
*/
$exception = new $exceptionClass(
\str_replace("\n", ' ', $message),
( isset($previousException) ? $previousException->getCode() : 0 ),
$previousException,
);
} else {
$ignoreClassesArray = self::convertToArray($ignoreClasses);
// Ignore this class as we are doing another internal call to findOrigin below
$ignoreClassesArray[] = self::class;

$origin = self::findOrigin(
ignoreClasses: $ignoreClassesArray,
ignoreNamespaces: $ignoreNamespaces,
);

/**
* @var OriginException $exception At this point we know that $exceptionClass inherits from OriginException for sure
*/
$exception = new $exceptionClass(
originCall: $origin->getCall(),
originFile: $origin->getFile(),
originLine: $origin->getLine(),
message: \str_replace("\n", ' ', $message),
code: (isset($previousException) ? $previousException->getCode() : 0),
previous: $previousException,
);
}

return $exception;
}

/**
* Find origin of current position in code by backtracing and ignoring some classes/namespaces
*
* @param class-string|list<class-string> $ignoreClasses Classes and interfaces to ignore in backtrace
* @param string|list<string> $ignoreNamespaces Namespaces to ignore
*/
public static function findOrigin(
string|array $ignoreClasses = [],
string|array $ignoreNamespaces = [],
): Origin {
$ignoreClassesArray = self::convertToArray($ignoreClasses);
$ignoreNamespacesArray = self::convertToArray($ignoreNamespaces);

$ignoreClassesArray = \array_filter($ignoreClassesArray, [Debug::class, 'isNotEmptyString']);
$ignoreNamespacesArray = \array_filter($ignoreNamespacesArray, [Debug::class, 'isNotEmptyString']);

// Get backtrace to find out where the query error originated
$backtraceList = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
Expand All @@ -43,12 +97,12 @@ public static function createException(

$lastInstance ??= $backtrace;

if (self::isIgnoredClass($backtrace['class'], $ignoreClasses)) {
if (self::isIgnoredClass($backtrace['class'], $ignoreClassesArray)) {
$lastInstance = $backtrace;
continue;
}

if (self::isIgnoredNamespace($backtrace['class'], $ignoreNamespaces)) {
if (self::isIgnoredNamespace($backtrace['class'], $ignoreNamespacesArray)) {
$lastInstance = $backtrace;
continue;
}
Expand All @@ -65,39 +119,11 @@ public static function createException(
$parts = \explode('\\', $lastInstance['class'] ?? '');
$shownClass = \array_pop($parts);

// Make sure the provided exception class inherits from Throwable or replace it with Exception
if (!\in_array(\Throwable::class, self::getClassInterfaces($exceptionClass), true)) {
$exceptionClass = \Exception::class;
}

// If we have no OriginException child class, we assume the default Exception class constructor is used
if (
!\in_array(OriginException::class, self::getClassParents($exceptionClass), true)
&& $exceptionClass !== OriginException::class
) {
/**
* @var \Throwable $exception At this point we know that $exceptionClass inherits from \Throwable for sure
*/
$exception = new $exceptionClass(
\str_replace("\n", ' ', $message),
( isset($previousException) ? $previousException->getCode() : 0 ),
$previousException,
);
} else {
/**
* @var OriginException $exception At this point we know that $exceptionClass inherits from OriginException for sure
*/
$exception = new $exceptionClass(
originCall: $shownClass . ($lastInstance['type'] ?? '') . ($lastInstance['function'] ?? '') . '(' . self::sanitizeArguments($lastInstance['args'] ?? []) . ')',
originFile: $lastInstance['file'] ?? '',
originLine: $lastInstance['line'] ?? 0,
message: \str_replace("\n", ' ', $message),
code: (isset($previousException) ? $previousException->getCode() : 0),
previous: $previousException,
);
}

return $exception;
return new Origin(
call: $shownClass . ($lastInstance['type'] ?? '') . ($lastInstance['function'] ?? '') . '(' . self::sanitizeArguments($lastInstance['args'] ?? []) . ')',
file: $lastInstance['file'] ?? '',
line: $lastInstance['line'] ?? 0,
);
}

/**
Expand Down Expand Up @@ -156,6 +182,11 @@ public static function sanitizeData(mixed $data): string
return '[' . \implode(', ', $result) . ']';
}

/**
* @template T of string|class-string
* @psalm-param T|list<T> $list
* @psalm-return list<T>
*/
private static function convertToArray(string|array $list): array
{
if (\is_string($list)) {
Expand Down
31 changes: 31 additions & 0 deletions src/Origin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Squirrel\Debug;

/**
* @immutable
*/
final class Origin
{
public function __construct(
private string $call,
private string $file,
private int $line,
) {
}

public function getCall(): string
{
return $this->call;
}

public function getFile(): string
{
return $this->file;
}

public function getLine(): int
{
return $this->line;
}
}
34 changes: 32 additions & 2 deletions tests/DebugTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class DebugTest extends \PHPUnit\Framework\TestCase
{
private const DEBUG_CLASS_EXCEPTION_LINE = 90;
private const DEBUG_CLASS_EXCEPTION_LINE = 55;

public function testCreateException()
{
Expand Down Expand Up @@ -63,6 +63,36 @@ public function testInvalidExceptionClass()
$this->assertEquals(\Exception::class, \get_class($exception));
}

public function testFindOrigin()
{
$o = Debug::findOrigin();

$this->assertEquals(__FILE__, $o->getFile());
$this->assertEquals(__LINE__ - 3, $o->getLine());
$this->assertEquals('Debug::findOrigin()', $o->getCall());
}

public function testFindOriginNamed()
{
$o = Debug::findOrigin(
ignoreNamespaces: '',
);

$this->assertEquals(__FILE__, $o->getFile());
$this->assertEquals(__LINE__ - 4, $o->getLine());
$this->assertEquals('Debug::findOrigin([], \'\')', $o->getCall());
}

public function testFindOriginOutside()
{
$o = Debug::findOrigin(
ignoreNamespaces: 'Squirrel',
);

$this->assertStringStartsWith(\dirname(__DIR__, 1), $o->getFile());
$this->assertEquals('DebugTest->testFindOriginOutside()', $o->getCall());
}

public function testBaseExceptionClass()
{
$e = Debug::createException(OriginException::class, 'Something went wrong!');
Expand Down Expand Up @@ -95,7 +125,7 @@ public function testBaseExceptionClassStringBacktraceAndNamespaceClasses()
$this->assertEquals(__FILE__, $e->getFile());
$this->assertEquals(__LINE__ - 7, $e->getLine());
$this->assertEquals(self::DEBUG_CLASS_EXCEPTION_LINE, $e->getExceptionLine());
$this->assertEquals('Debug::createException(\'Squirrel\\\\Debug\\\\OriginException\', \'Something went wrong!\', [], [])', $e->getOriginCall());
$this->assertEquals('Debug::createException(\'Squirrel\\\\Debug\\\\OriginException\', \'Something went wrong!\', \'\', \'\')', $e->getOriginCall());
}

public function testBinaryData()
Expand Down

0 comments on commit 139e84a

Please sign in to comment.