Published Apr 29, 2023
Last Updated Aug 27, 2023
Caching Laravel configs that use objects
Categories: Laravel
Tags: #Caching
Real quick: If you’re using spatie/laravel-markdown
and having this issue (how I ran into this), it was partially fixed with PR#53 recently.
This headline might be a bit of a head-scratcher for you. It comes from an issue that I ran into recently—and that I didn’t dig into right away.
Basically, running artisan config:cache
started giving me errors about not being able to serialize my configs.
Rewind
If you haven’t used Laravel before, one of its directories in the project root is config/
. This directory houses PHP files for project settings—mostly Laravel’s, but you can create and register your own too. Some installed packages also let you publish their configs to that directory for you to customize.
The combined application config is normally rebuilt for every request. To speed this up, Laravel has a config:cache
command to generate 1 config file. This includes any values that might’ve been resolved dynamically, like from function calls. And it usually “just works”.
When “just works”, doesn’t
Sometimes you might need an object in your config, like if you’re using Spatie’s Laravel Markdown package and creating custom renderers.
For example:
<?php
return [
// ...
'block_renderers' => [
['class' => BlockQuote::class, 'renderer' => new BlockQuoteRenderer(), 'priority' => 1],
['class' => Heading::class, 'renderer' => new HeadingRenderer(), 'priority' => 1],
['class' => ListItem::class, 'renderer' => new ListItemRenderer(), 'priority' => 1],
],
//...
];
Now, this doesn’t look bad. But if you do this as-is and then run config:cache
, you'll get an error similar to this:
LogicException
Your configuration files are not serializable.
at /path/to/project/vendor/laravel/framework/src/Illuminate/Foundation/Console/ConfigCacheCommand.php:80
76▕ require $configPath;
77▕ } catch (Throwable $e) {
78▕ $this->files->delete($configPath);
79▕
➜ 80▕ throw new LogicException('Your configuration files are not serializable.', 0, $e);
81▕ }
82▕
83▕ $this->info('Configuration cached successfully!');
84▕ }
1 /path/to/project/bootstrap/cache/config.php:<line-number>
Error::("Call to undefined method App\Markdown\Renderers\BlockQuoteRenderer::__set_state()")
2 /path/to/project/vendor/laravel/framework/src/Illuminate/Foundation/Console/ConfigCacheCommand.php:76
require()
What causes this?
The error message might need clarifying. For 1, it says “not serializeable”. That might lead you to believe config:cache
is calling serialize()
under the hood, but it’s not.
It’s also not pointing out which config it’s having trouble with.
But with some digging through laravel/framework
, we find this section of code:
<?php
//...
$configPath = $this->laravel->getCachedConfigPath();
$this->files->put(
$configPath, '<?php return '.var_export($config, true).';'.PHP_EOL
);
try {
require $configPath;
} catch (Throwable $e) {
// ...
}
// ...
In there, there’s a call to var_export()
. If we look at the PHP manual for that function, it shows examples of different types of output.
But look specifically at Example #3: Exporting Classes. It has this sample output:
A::__set_state(array(
'var' => 5,
))
You might remember that that static method, __set_state()
, appears in the stack trace from earlier.
If we read PHP’s magic methods page, we’ll see that this static method allows parsing an exported object string back into an object while automatically providing it with its old exported data!
And rereading that snippet from laravel/framework
, what’s happening makes sense now. When calling config:cache
:
- Laravel is reading in the full, generated config and generating a combined PHP file from that. Then,
- it’s immediately reading in this pre-built PHP file, triggering calls to that magic method.
So it seems this error is just a side effect of how config caching works.
So how do we get around this?
We can implement __set_state()
for classes we “new up” in our configs. The return
value should be an instance of that class too.
For my renderers, I wrote a trait:
<?php
trait Resumeable
{
public static function __set_state(array $state_array): static
{
$object = new static();
foreach ($state_array as $prop => $state) {
if (!property_exists($object, $prop)) continue;
$object->{$prop} = $state;
}
return $object;
}
}
I tested this on PHP 8.1. Since it’s being called from within the class it affects, it also works on private
and protected
properties.
Good thing too, because var_export()
exports data from private
and protected
properties.
Summary
You may not need this very often. And if you’re using spatie/laravel-markdown
, PR#53 did get merged pretty quickly, so you can just use class strings now (Thanks erikn69 and freekmurze!)
But this is 1 way to get around config caching issues you may run into.
Thanks for reading!
6 days ago
JSn1nj4 opened Issue #216 at JSn1nj4/ElliotDerhay.com
6 days ago
JSn1nj4 opened Issue #215 at JSn1nj4/ElliotDerhay.com
6 days ago
JSn1nj4 opened Issue #214 at JSn1nj4/ElliotDerhay.com
1 week ago
JSn1nj4 opened Issue #1 at JSn1nj4/laundry-list
1 week ago
JSn1nj4 created main at JSn1nj4/laundry-list