Skip to main content
Laravel, shipping fast.

Here's a trap that looks like good news at first: tagging a new release of webplo/templates does not automatically reach a single live customer site. Nothing changes on any already-provisioned instance the moment you push a tag.

That's deliberate, not a gap. If publishing a version instantly mutated every live site, one bad release would take the whole fleet down at once, no rollout, no canary. The isolation is the safety mechanism, but it comes with an obligation: reaching a live instance takes two explicit steps, and both have to happen, or "I shipped the fix" and "every customer has the fix" quietly stop being the same fact.

tag webplo/templates v1.1.85
    โ†“
bump the version constraint in the skeleton's composer.json
    โ†“
redeploy that instance (or run the installer again on it)
    โ†“
webplo:install re-bakes the theme + editable system on disk

Step one is publishing. Step two is every already-provisioned instance actually taking the version. template-base's composer.json pins the dependency by range:

// template-base/composer.json
"require": {
    "webplo/templates": "^1.1.84"
}

Tag 1.1.85 and that constraint is already satisfied on paper, ^1.1.84 allows it, but nothing installs it until something runs composer update webplo/templates and redeploys. On a fleet where every customer instance is its own repo, that second step is easy to lose track of: you tag the release, write the changelog entry, move on. Meanwhile some instance provisioned eight months ago, with no reason to redeploy since, is still running the version it was born with.

A normal app has one production environment, so "did the deploy work" is a single yes-or-no you answer once. Here it's a different yes-or-no for every customer. Treat a release as done the moment you tag it, and you'll eventually ship a fix, a security patch, a schema change to the per-site content contract, that a meaningful fraction of your live fleet never receives, because nobody redeployed the instances that weren't already scheduled to. A tag is a promise a version exists. A bump-and-redeploy is a promise a customer has it. Track both as separate facts, or don't bother tagging at all.

The Silent-Staleness Trap Behind a Persistent Cache

Here's the failure mode that costs the most hours to diagnose: you ship a real fix, to the right file, verified locally, and on the live instance it does nothing. Not an error. Not a partial change. Nothing. The old behavior keeps happening, and every instinct you have says the deploy must have failed, when the deploy actually worked perfectly.

The editable system writes two kinds of file directly to disk on a customer's box: config/webplo.php, the per-site content contract, and the active language's lang/{locale}/messages.php. Both are plain PHP files returning an array, written with var_export():

// app/Livewire/Editable.php (baked in by webplo:install-editable)
private function writeConfigFile(array $data, string $filePath): void
{
    $exportedData = var_export($data, true);
    $fileContent = "<?php\n\ndeclare(strict_types=1);\n\nreturn {$exportedData};\n";
    file_put_contents($filePath, $fileContent);

    if (function_exists('opcache_invalidate')) {
        opcache_invalidate($filePath, true);
    }
}

That last three lines are the whole lesson. PHP files get compiled by OPcache and cached as opcodes, and OPcache does not necessarily notice a file changed the instant file_put_contents() returns. It revalidates on its own schedule. So a config-loading change, save the customer's edit to config/webplo.php, then redirect straight back into the same page to show it, can appear to do nothing: the very next request compiles the old bytecode for a file that, on disk, already has the new content. You'd swear the write failed. It didn't. The write is fine. The read, milliseconds later, is the one still looking at the past.

This is a specific instance of a general trap, worth stating plainly because it will find you again in a different shape: any change to how a distributed package loads configuration has to assume a persistent runtime cache sits between the write and the next read, and invalidate that cache explicitly as part of the fix, not as an afterthought you hope the redeploy handles. A redeploy restarts the process and clears everything by accident. A save-and-reload within an already-running process does not. Test only the redeploy case and you'll ship it, watch it pass, then watch it fail silently in front of an actual customer, because the environment that broke it isn't the one you checked.

opcache_invalidate($filePath, true) is the entire fix, three lines, telling OPcache directly that this file is stale right after you write it. Finding out you needed it took considerably longer, because "nothing happened" doesn't point at a cache the way a stack trace points at a line number. Write the invalidation, and write a test that proves the next request after the write sees the new value, not just that the file on disk changed. The write was never the risk. The read was.