7min.

Leverage Symfony VarDumper Component to Enhance your Dumps

Symfony’s VarDumper Component is a game changer when it comes to debugging. It allows us to dump variables in a clean and efficient way. We hope you already use it in your projects. If not, you should definitely give it a try!

Today, we’ll see how we can extend it to dump our objects, or some vendor objects, in a more readable way.

Section intitulée the-problemThe problem

On a project which uses Stripe, when we dump a StripeObject, we get something like this:

A dump without the caster

As you may have guessed, it’s not very readable. There are lots of internal properties. If we expand them, we can see the raw HTTP response, the response parsed as an array, and the response parsed as an object.

This is too much information, with a lot of duplication. Moreover, the VarDumper component is only able to dump a limited quantity of objects. And in this very situation, we consume all the available variables dumped. So we miss some information!

Section intitulée the-solutionThe solution

The component comes with a powerful solution to mitigate this problem: Casters. It allows us to define how an object should be dumped. It’s a very powerful tool, but it’s also a bit tricky to use. Let’s see how we can use it to dump our StripeObject in a more readable way.

A dump with the caster

Section intitulée how-the-vardumper-component-worksHow the VarDumper component works

To understand how casters work, we need to understand how the VarDumper component works. It’s a bit complex, but it’s worth it!

In this article, we’ll not explain everything, because there are way too many subjects to cover. We’ll just focus on the most important parts.

The component is composed of two main parts:

  • The VarCloner, which is responsible for cloning the variable to dump. It’s a recursive process, and it’s able to clone any variable. The main goal is to snapshot variables, so we can dump them later;
  • The VarDumper, which is responsible for dumping the snapshot. It’s also recursive. The main goal is to display the variable in a readable way. There are three dumpers in Symfony: the HtmlDumper, the CliDumper, and the ServerDumper. The first one is used in the browser, the second one in the CLI, and the last one is used to send the dump to a connected client (vendor/bin/var-dump-server).

You guessed it right, we’ll focus on the VarCloner!

If we put it all together, we get something like this:

function dump_something($variable, $maxDepth = 3): string
{
    $cloner = new VarCloner();

    // $data is the snapshot of the $variable
    $data = $cloner->cloneVar($variable);

    $dumper = new HtmlDumper();
    $dumper->setDisplayOptions([
        'maxDepth' => $maxDepth,
    ]);

    $output = fopen('php://memory', 'r+b');

    $dumper->dump($data, $output);

    // It returns the dump as a string. It contains some HTML, CSS, and JavaScript
    return stream_get_contents($output, -1, 0);
}

Section intitulée the-varclonerThe VarCloner

The VarCloner has more complex mechanisms to handle some specific cases, but the main idea is to cast the variable into an array:

class Foobar
{
    public $public = 'foo';
    protected $protected = 'bar';
    private $private = 'baz';
}

var_dump((array) new Foobar());

The result is:

^ array:3 [
  "public" => "foo"
  "\x00*\x00protected" => "bar"
  "\x00Foobar\x00private" => "baz"
]

Note: To display the previous result we actually used dump() instead of var_dump() because it’s more readable, and var_dump() does not show hidden characters like \x00.

Then, the VarCloner has a list of casters, which are responsible for adding or removing some information from the array. For example, the DateCaster is responsible for casting a DateTime and can add the date in a human readable format with the timezone. It can also cast a DateInterval and add the interval in a human readable format:

$php > dump(new DateTime());
^ DateTime @1679497380 {#30
  date: 2023-03-22 16:03:00.646944 Europe/Paris (+01:00)
}

php > dump(new DateInterval('P1Y2M3DT4H5M6S'));
^ DateInterval {#30
  interval: + 1y 2m 3d 04:05:06.0
  +"y": 1
  +"m": 2
  +"d": 3
  +"h": 4
  +"i": 5
  +"s": 6
  +"f": 0.0
  +"invert": 0
  +"days": false
  +"from_string": false
}

Another good example is the DoctrineCaster: It removes the __cloner__ and __initializer__ properties from Doctrine proxies. Thanks to it, our dump is smaller.

So, to extend the VarCloner, we’ll use a Caster.

Section intitulée the-casterThe Caster

A blank caster looks like this:

namespace App\Bridge\VarDumper\Caster;

use Stripe\StripeObject;
use Symfony\Component\VarDumper\Cloner\Stub;

class StripeObjectCaster
{
    public static function castStripeData(
        StripeObject $object,
        array $a,
        Stub $stub,
        bool $isNested,
        int $filter = 0,
    ) : array {
        return $a;
    }
}

The castStripeData() method is called by the VarCloner. We can name it the way we want. It has 5 arguments:

  1. $object: our raw object (a StripeObject instance)
  2. $a: the array representation of the object (the result of (array) $object)
  3. $stub: an object used by the VarDumper to display the variable. We can attach some metadata to it, and it will be displayed in the dump
  4. $isNested: a boolean which indicates if the object is nested in another object
  5. $filter: a bit mask which indicates which information should be displayed

The method must return an array, which will be used to display the variable.

Now, we need to fill the castStripeData() method to remove internal properties.

Section intitulée removing-some-propertiesRemoving some properties

To remove the properties, we must know their name first. Do you remember? The naming is quite strange! This is how PHP works! We suggest that you use dump() to discover all properties:

dump(array_keys($a));

// StripeObjectCaster.php on line 15:
// array:57 [▼
//   0 => "\x00*\x00_opts"
//   1 => "\x00*\x00_originalValues"
//   2 => "\x00*\x00_values"
//   3 => "\x00*\x00_unsavedValues"
//   4 => "\x00*\x00_transientValues"
//   5 => "\x00*\x00_retrieveOptions"
//   6 => "\x00*\x00_lastResponse"
//   7 => "saveWithParent"
//   8 => "\x00~\x00id"
// ...

So now we can remove theses properties from the array:

unset($a["\x00*\x00_opts"], $a["\x00*\x00_originalValues"], ...);

There is a little drawback by doing that: we don’t know that some values have been removed! To fix that, we can tell the VarDumper to display the number of removed properties:

$stub->cut += 7; // 7 is the number of removed properties

Section intitulée adding-some-propertiesAdding some properties

We can also add some properties to the array. For example, we can add the virtual billing_cycle_anchor_as_date_time property:

if (isset($r->billing_cycle_anchor)) {
    $a += [
        Caster::PREFIX_VIRTUAL.'billing_cycle_anchor_as_date_time' => new \DateTime('@'.$r->billing_cycle_anchor),
    ];
}

Section intitulée all-togetherAll together

The following code is the final caster. It has been a bit optimized to not have to maintain the properties list:

class StripeObjectCaster
{
    private static array $propertiesToRemove;

    public static function castStripeData(StripeObject $r, array $a, Stub $stub, bool $isNested, int $filter = 0)
    {
        if (!isset(self::$propertiesToRemove)) {
            self::$propertiesToRemove = array_keys((array) new StripeObject());
        }

        foreach (self::$propertiesToRemove as $property) {
            if (array_key_exists($property, $a)) {
                unset($a[$property]);
                $stub->cut++;
            }
        }

        if (isset($r->billing_cycle_anchor)) {
            $a += [
                Caster::PREFIX_VIRTUAL.'billing_cycle_anchor_as_date_time' => new \DateTime('@'.$r->billing_cycle_anchor),
            ];
        }

        return $a;
    }
}

Section intitulée registering-the-casterRegistering the caster

If you don’t use the full stack framework, you can register the caster like this:

$cloner = new VarCloner();
$cloner->addCasters([
    StripeObject::class => StripeObjectCaster::castStripeData(...),
]);

In this array, the key is the class to cast, and the value is a callable to cast it.

If you use the full stack framework, you’ll need to register the caster manually too. Indeed, the VarCloner is not registered in the container! A good place to do so, is in the Kernel::boot() method:

class AppKernel extends Kernel
{
    public function boot()
    {
        parent::boot();

        AbstractCloner::$defaultCasters += [
            \Stripe\StripeObject::class => StripeObjectCaster::castStripeData(...),
        ];
    }
}

Section intitulée conclusionConclusion

🎉 Congratulations! You have learned how to extend the VarDumper to display your own objects in a better way!

By the way, did you know you can use the dump() function in the PHP REPL (php -a)? To do that, you should clone the VarDumper component somewhere, install the dependencies, and add the following line in the php.ini file:

auto_prepend_file = /path/to/var-dumper/vendor/autoload.php

And now, you can use dump() in the PHP REPL:

$ php -a
Interactive shell

php > dump(new \DateTimeZone('Europe/Paris'));
^ DateTimeZone {#23
  timezone: Europe/Paris (+01:00)
  +"timezone_type": 3
  +"timezone": "Europe/Paris"
}

And another tip we use quite often: a Twig extension to dump a variable in a template in production! Only in the admin, with sufficient permissions, don’t worry! Some will say it’s a bad practice, but we find it very useful to debug some data issues in production.

Here is the code
class DebugExtension extends AbstractExtension
{
    private readonly VarCloner $cloner;
    private readonly HtmlDumper $dumper;

    public function getFunctions(): array
    {
        yield new TwigFunction('debugProd', $this->debugProd(...), ['is_safe' => ['html']]);
    }

    public function debugProd(mixed $variable, int $maxDepth = 1): string
    {
        $data = $this->getCloner()->cloneVar($variable)
        $output = fopen('php://memory', 'r+');
        $dumper->getDumper()->dump($data, $output);

        return stream_get_contents($output, -1, 0);
    }

    private function getCloner(): VarCloner
    {
        if (!isset($this->cloner)) {
            $this->cloner = new VarCloner();
            $this->cloner->addCasters([
                // Some casters here
            ]);
        }

        return $this->cloner;
    }

    private function getDumper(int $maxDepth = 1): HtmlDumper
    {
        if (!isset($this->dumper)) {
            $this->dumper = new HtmlDumper();
            $this->dumper->setDisplayOptions([
                'maxDepth' => $maxDepth,
            ]);
        }

        return $this->dumper;
    }
}

Commentaires et discussions

Nos formations sur ce sujet

Notre expertise est aussi disponible sous forme de formations professionnelles !

Voir toutes nos formations

Ces clients ont profité de notre expertise