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:
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.
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: theHtmlDumper
, theCliDumper
, and theServerDumper
. 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 ofvar_dump()
because it’s more readable, andvar_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:
-
$object
: our raw object (aStripeObject
instance) -
$a
: the array representation of the object (the result of(array) $object
) -
$stub
: an object used by theVarDumper
to display the variable. We can attach some metadata to it, and it will be displayed in the dump -
$isNested
: a boolean which indicates if the object is nested in another object -
$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 !
Symfony avancée
Découvrez les fonctionnalités et concepts avancés de Symfony
Ces clients ont profité de notre expertise
La société AramisAuto a fait appel à JoliCode pour développer au forfait leur plateforme B2B. L’objectif était de développer une nouvelle offre à destination des professionnels ; déjà testé commercialement, pro.aramisauto.com est la concrétisation de 3 mois de développement. Le service est indépendant de l’infrastructure existante grâce à la mise en…
Nous avons entrepris une refonte complète du site, initialement développé sur Drupal, dans le but de le consolider et de jeter les bases d’un avenir solide en adoptant Symfony. La plateforme est hautement sophistiquée et propose une pléthore de fonctionnalités, telles que la gestion des abonnements avec Stripe et Paypal, une API pour l’application…
JoliCode a formé l’équipe de développement d’Evaneos aux bonnes pratiques pour l’écriture de tests unitaires efficaces et utiles. Nous en avons également profité pour mettre en place une plateforme d’intégration continue pour accompagner l’évolution de la plateforme.