Skip to content

Commit

Permalink
Copy Debug class from doctrine/common
Browse files Browse the repository at this point in the history
This reduces our dependency to this shared library that now holds very
little code we use.
The class has not been copied verbatim:
- Unused parameters and methods have been removed.
- The class is final and internal.
- Coding standards have been enforced, including enabling strict_types,
  which lead to casting a variable to string before feeding it to
  explode().
- A bug found by static analysis has been addressed, where an INI
  setting obtained with ini_get() was compared with true, which is never
  returned by that function.
- Tests are improved to run on all PHP versions
  • Loading branch information
greg0ire committed Oct 12, 2023
1 parent c5137da commit 190df5f
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 8 deletions.
4 changes: 2 additions & 2 deletions lib/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

namespace Doctrine\ORM\Tools\Console\Command;

use Doctrine\Common\Util\Debug;
use Doctrine\ORM\Tools\Console\CommandCompatibility;
use Doctrine\ORM\Tools\Debug;
use LogicException;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
Expand Down Expand Up @@ -116,7 +116,7 @@ private function doExecute(InputInterface $input, OutputInterface $output): int

$resultSet = $query->execute([], constant($hydrationMode));

$ui->text(Debug::dump($resultSet, (int) $input->getOption('depth'), true, false));
$ui->text(Debug::dump($resultSet, (int) $input->getOption('depth')));

return 0;
}
Expand Down
164 changes: 164 additions & 0 deletions lib/Doctrine/ORM/Tools/Debug.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Tools;

use ArrayIterator;
use ArrayObject;
use DateTimeInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Persistence\Proxy;
use stdClass;

use function array_keys;
use function count;
use function end;
use function explode;
use function extension_loaded;
use function get_class;
use function html_entity_decode;
use function ini_get;
use function ini_set;
use function is_array;
use function is_object;
use function ob_end_clean;
use function ob_get_contents;
use function ob_start;
use function strip_tags;
use function var_dump;

/**
* Static class containing most used debug methods.
*
* @internal
*
* @link www.doctrine-project.org
*/
final class Debug
{
/**
* Private constructor (prevents instantiation).
*/
private function __construct()
{
}

/**
* Prints a dump of the public, protected and private properties of $var.
*
* @link https://xdebug.org/
*
* @param mixed $var The variable to dump.
* @param int $maxDepth The maximum nesting level for object properties.
*/
public static function dump($var, int $maxDepth = 2): string
{
$html = ini_get('html_errors');

if ($html !== '1') {
ini_set('html_errors', 'on');
}

if (extension_loaded('xdebug')) {
ini_set('xdebug.var_display_max_depth', (string) $maxDepth);
}

$var = self::export($var, $maxDepth);

ob_start();
var_dump($var);

$dump = ob_get_contents();

ob_end_clean();

$dumpText = strip_tags(html_entity_decode($dump));

ini_set('html_errors', $html);

return $dumpText;
}

/**
* @param mixed $var
*
* @return mixed
*/
public static function export($var, int $maxDepth)
{
$return = null;
$isObj = is_object($var);

if ($var instanceof Collection) {
$var = $var->toArray();
}

if (! $maxDepth) {
return is_object($var) ? get_class($var)
: (is_array($var) ? 'Array(' . count($var) . ')' : $var);
}

if (is_array($var)) {
$return = [];

foreach ($var as $k => $v) {
$return[$k] = self::export($v, $maxDepth - 1);
}

return $return;
}

if (! $isObj) {
return $var;
}

$return = new stdClass();
if ($var instanceof DateTimeInterface) {
$return->__CLASS__ = get_class($var);
$return->date = $var->format('c');
$return->timezone = $var->getTimezone()->getName();

return $return;
}

$return->__CLASS__ = ClassUtils::getClass($var);

if ($var instanceof Proxy) {
$return->__IS_PROXY__ = true;
$return->__PROXY_INITIALIZED__ = $var->__isInitialized();
}

if ($var instanceof ArrayObject || $var instanceof ArrayIterator) {
$return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1);
}

return self::fillReturnWithClassAttributes($var, $return, $maxDepth);
}

/**
* Fill the $return variable with class attributes
* Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075}
*
* @param object $var
*
* @return mixed
*/
private static function fillReturnWithClassAttributes($var, stdClass $return, int $maxDepth)
{
$clone = (array) $var;

foreach (array_keys($clone) as $key) {
$aux = explode("\0", (string) $key);
$name = end($aux);
if ($aux[0] === '') {
$name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private');
}

$return->$name = self::export($clone[$key], $maxDepth - 1);
}

return $return;
}
}
5 changes: 5 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
<exclude-pattern>tests/*</exclude-pattern>
</rule>

<rule ref="Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps">
<exclude-pattern>lib/Doctrine/ORM/Tools/Debug.php</exclude-pattern>
<exclude-pattern>tests/Doctrine/Tests/ORM/Tools/DebugTest.php</exclude-pattern>
</rule>

<rule ref="Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase">
<exclude-pattern>lib/Doctrine/ORM/Events.php</exclude-pattern>
<exclude-pattern>lib/Doctrine/ORM/Tools/ToolEvents.php</exclude-pattern>
Expand Down
7 changes: 1 addition & 6 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,8 @@
<ArgumentTypeCoercion>
<code>$class</code>
<code>$class</code>
<code><![CDATA[new $definition['class']()]]></code>
<code>$platformFamily</code>
<code><![CDATA[new $definition['class']()]]></code>
</ArgumentTypeCoercion>
<DeprecatedClass>
<code>new UuidGenerator()</code>
Expand Down Expand Up @@ -2406,11 +2406,6 @@
<code>getAllClassNames</code>
</PossiblyNullReference>
</file>
<file src="lib/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php">
<DeprecatedClass>
<code><![CDATA[Debug::dump($resultSet, (int) $input->getOption('depth'), true, false)]]></code>
</DeprecatedClass>
</file>
<file src="lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/AbstractCommand.php">
<InvalidNullableReturnType>
<code>int</code>
Expand Down
5 changes: 5 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@
<file name="lib/Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php"/>
</errorLevel>
</DuplicateClass>
<ForbiddenCode>
<errorLevel type="suppress">
<file name="lib/Doctrine/ORM/Tools/Debug.php"/>
</errorLevel>
</ForbiddenCode>
<InvalidArgument>
<errorLevel type="suppress">
<!-- Argument type changes in DBAL 3.2 -->
Expand Down
148 changes: 148 additions & 0 deletions tests/Doctrine/Tests/ORM/Tools/DebugTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Tools;

use ArrayIterator;
use ArrayObject;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Doctrine\ORM\Tools\Debug;
use Doctrine\Tests\DoctrineTestCase;
use stdClass;

use function ksort;
use function print_r;
use function strpos;
use function substr;
use function version_compare;

use const PHP_VERSION;

class DebugTest extends DoctrineTestCase
{
public function testExportObject(): void
{
$obj = new stdClass();
$obj->foo = 'bar';
$obj->bar = 1234;

$var = Debug::export($obj, 2);
self::assertEquals('stdClass', $var->__CLASS__);
}

public function testExportObjectWithReference(): void
{
$foo = 'bar';
$bar = ['foo' => & $foo];
$baz = (object) $bar;

$var = Debug::export($baz, 2);
$baz->foo = 'tab';

self::assertEquals('bar', $var->foo);
self::assertEquals('tab', $bar['foo']);
}

public function testExportArray(): void
{
$array = ['a' => 'b', 'b' => ['c', 'd' => ['e', 'f']]];
$var = Debug::export($array, 2);
$expected = $array;
$expected['b']['d'] = 'Array(2)';
self::assertEquals($expected, $var);
}

public function testExportDateTime(): void
{
$obj = new DateTime('2010-10-10 10:10:10', new DateTimeZone('UTC'));

$var = Debug::export($obj, 2);
self::assertEquals('DateTime', $var->__CLASS__);
self::assertEquals('2010-10-10T10:10:10+00:00', $var->date);
}

public function testExportDateTimeImmutable(): void
{
$obj = new DateTimeImmutable('2010-10-10 10:10:10', new DateTimeZone('UTC'));

$var = Debug::export($obj, 2);
self::assertEquals('DateTimeImmutable', $var->__CLASS__);
self::assertEquals('2010-10-10T10:10:10+00:00', $var->date);
}

public function testExportDateTimeZone(): void
{
$obj = new DateTimeImmutable('2010-10-10 12:34:56', new DateTimeZone('Europe/Rome'));

$var = Debug::export($obj, 2);
self::assertEquals('DateTimeImmutable', $var->__CLASS__);
self::assertEquals('2010-10-10T12:34:56+02:00', $var->date);
}

public function testExportArrayTraversable(): void
{
$obj = new ArrayObject(['foobar']);

$var = Debug::export($obj, 2);
self::assertContains('foobar', $var->__STORAGE__);

$it = new ArrayIterator(['foobar']);

$var = Debug::export($it, 5);
self::assertContains('foobar', $var->__STORAGE__);
}

/**
* @param array<string, int> $expected
*
* @dataProvider provideAttributesCases
*/
public function testExportParentAttributes(TestAsset\ParentClass $class, array $expected): void
{
$actualRepresentation = print_r($class, true);
$expectedRepresentation = print_r($expected, true);

$actualRepresentation = substr($actualRepresentation, strpos($actualRepresentation, '('));
$expectedRepresentation = substr($expectedRepresentation, strpos($expectedRepresentation, '('));

self::assertSame($expectedRepresentation, $actualRepresentation);

$var = Debug::export($class, 3);
$var = (array) $var;
unset($var['__CLASS__']);

self::assertSame($expected, $var);
}

/**
* @psalm-return array<string, array{TestAsset\ParentClass, mixed[]}>
*/
public function provideAttributesCases(): iterable
{
$differentAttributes = [
'parentPublicAttribute' => 1,
'parentProtectedAttribute:protected' => 2,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ParentClass:private' => 3,
'childPublicAttribute' => 4,
'childProtectedAttribute:protected' => 5,
'childPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ChildClass:private' => 6,
];
$sameAttributes = [
'parentPublicAttribute' => 4,
'parentProtectedAttribute:protected' => 5,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ParentClass:private' => 3,
'parentPrivateAttribute:Doctrine\Tests\ORM\Tools\TestAsset\ChildWithSameAttributesClass:private' => 6,
];
if (version_compare(PHP_VERSION, '8.1', '<')) {
ksort($differentAttributes);
ksort($sameAttributes);
}

yield 'different-attributes' => [new TestAsset\ChildClass(), $differentAttributes];

yield 'same-attributes' => [new TestAsset\ChildWithSameAttributesClass(), $sameAttributes];
}
}
15 changes: 15 additions & 0 deletions tests/Doctrine/Tests/ORM/Tools/TestAsset/ChildClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Tools\TestAsset;

final class ChildClass extends ParentClass
{
/** @var int */
public $childPublicAttribute = 4;
/** @var int */
protected $childProtectedAttribute = 5;
/** @var int */
private $childPrivateAttribute = 6;
}
Loading

0 comments on commit 190df5f

Please sign in to comment.