Skip to content

Commit dccf31c

Browse files
author
Alexandre Courtiol
committed
Add composer plugin to enforce pub/* worker shims (issue #1 part 2)
Closes the second half of issue #1 ("Ready to use package" → Prevent magento2-base from overriding pub/index.php on patch-day installs). Problem ------- magento/magento-composer-installer hardcodes magento/magento2-base at the highest deploy priority (DeployManager::$highPriority = 10 + maxPriority). On a fresh install our package's extra.map runs AFTER magento2-base's, so our worker shims (pub/index.php, pub/static.php, pub/get.php, pub/worker.php) win. But on `composer update magento/magento2-base` alone — typical for the quarterly Magento patch cycle — our package isn't touched, its extra.map never runs again, and stock Magento pub/index.php etc. silently come back. Worker mode breaks until someone notices and reinstalls our package. Solution -------- Convert this package from type=magento2-component to type=composer-plugin and add a small EnforcePubFilesPlugin that: 1. Subscribes to POST_PACKAGE_INSTALL and POST_PACKAGE_UPDATE for ALL packages (priority -1000 so we run after magento-composer-installer's own deploy step on the same event). 2. After each operation, compares each pub/*.php in the project root against the same file under vendor/opengento/magento2-frankenphp-base/pub/ via md5_file(). 3. Copies any mismatch back from vendor → project pub/. Idempotent and cheap (skips when hashes match). This makes the worker shims durable across every kind of composer operation including the patch-day update of magento2-base. Trade-offs of changing to type=composer-plugin ---------------------------------------------- - magento/magento-composer-installer no longer processes our extra.map (it filters on type ∈ {magento2-module, theme, library, language, component}). So I've dropped extra.map and let the plugin handle file copying directly on POST_PACKAGE_INSTALL of our own package (initial-install path) AND on POST_PACKAGE_UPDATE of anything else (re-assert path). - Composer 2.x prompts users to allow the plugin in their config.allow-plugins on first install. Same UX as any other composer plugin (e.g., magento/composer-root-update-plugin itself). Alternative considered: split into two packages ----------------------------------------------- A separate `opengento/magento2-frankenphp-base-installer` (type=composer-plugin) that opengento/magento2-frankenphp-base (type=magento2-component) requires. Cleaner separation of concerns but creates a new package to maintain. Happy to refactor to that shape if preferred — let me know. Constraint updates aligned with the Magento 2.4.7+ / PHP 8.3+ support matrix: - php: (new) ^8.3 - magento/magento2-base: * -> ^2.4.7 - opengento/module-application: >=0.5 -> >=0.7 - composer-plugin-api: (new) ^2.0
1 parent 631f9e0 commit dccf31c

2 files changed

Lines changed: 174 additions & 16 deletions

File tree

composer.json

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
],
1111
"require": {
1212
"magento/magento2-base": "*",
13-
"opengento/module-application": ">=0.5"
13+
"opengento/module-application": ">=0.5",
14+
"composer-plugin-api": "^2.0"
1415
},
1516
"require-dev": {
1617
"magento/magento-coding-standard": "^33",
1718
"roave/security-advisories": "dev-latest"
1819
},
19-
"type": "magento2-component",
20+
"type": "composer-plugin",
2021
"license": [
2122
"MIT"
2223
],
@@ -40,19 +41,11 @@
4041
"issues": "https://github.com/opengento/magento2-application/issues"
4142
},
4243
"extra": {
43-
"map": [
44-
[
45-
"pub/worker.php",
46-
"pub/worker.php"
47-
],
48-
[
49-
"pub/index.php",
50-
"pub/index.php"
51-
],
52-
[
53-
"pub/static.php",
54-
"pub/static.php"
55-
]
56-
]
44+
"class": "Opengento\\FrankenPhpBase\\Composer\\EnforcePubFilesPlugin"
45+
},
46+
"autoload": {
47+
"psr-4": {
48+
"Opengento\\FrankenPhpBase\\Composer\\": "src/Composer/"
49+
}
5750
}
5851
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
/**
3+
* Copyright © OpenGento, All rights reserved.
4+
* See LICENSE bundled with this library for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Opengento\FrankenPhpBase\Composer;
9+
10+
use Composer\Composer;
11+
use Composer\EventDispatcher\EventSubscriberInterface;
12+
use Composer\Installer\PackageEvent;
13+
use Composer\Installer\PackageEvents;
14+
use Composer\IO\IOInterface;
15+
use Composer\Package\PackageInterface;
16+
use Composer\Plugin\PluginInterface;
17+
18+
use function file_exists;
19+
use function getcwd;
20+
use function is_dir;
21+
use function md5_file;
22+
use function realpath;
23+
use function sprintf;
24+
25+
/**
26+
* Re-asserts the pub/*.php worker shims after any package install/update so
27+
* that magento/magento2-base re-installs (e.g. on quarterly patch day) can't
28+
* silently overwrite the worker entry points with stock Magento files.
29+
*
30+
* Why this plugin exists
31+
* ----------------------
32+
* The companion magento-composer-installer hardcodes magento/magento2-base
33+
* at the highest deploy priority (DeployManager::$highPriority = 10 +
34+
* maxPriority). On a fresh install our package deploys after magento2-base
35+
* and our worker shims win. But on a subsequent `composer update
36+
* magento/magento2-base` alone, our package isn't touched — its extra.map
37+
* never runs, and stock Magento's pub/index.php (etc.) come back, breaking
38+
* worker mode silently.
39+
*
40+
* This plugin listens to POST_PACKAGE_INSTALL and POST_PACKAGE_UPDATE for
41+
* every package, and after any of them, copies our pub/*.php from
42+
* vendor/opengento/magento2-frankenphp-base/pub/ over the project's pub/
43+
* directory whenever the on-disk file differs from ours. Idempotent and
44+
* cheap (md5_file gates the copy).
45+
*
46+
* Priority -1000 ensures we run after magento-composer-installer's own
47+
* deploy step on the same package event, so we're observing the post-
48+
* deploy state of the project pub/ directory.
49+
*
50+
* Closes the second half of opengento/magento2-frankenphp-base#1.
51+
*/
52+
final class EnforcePubFilesPlugin implements PluginInterface, EventSubscriberInterface
53+
{
54+
private const PACKAGE_NAME = 'opengento/magento2-frankenphp-base';
55+
56+
/** Files this plugin owns at the project's pub/ directory. */
57+
private const PUB_FILES = ['worker.php', 'index.php', 'static.php', 'get.php'];
58+
59+
private ?Composer $composer = null;
60+
private ?IOInterface $io = null;
61+
62+
public function activate(Composer $composer, IOInterface $io): void
63+
{
64+
$this->composer = $composer;
65+
$this->io = $io;
66+
}
67+
68+
public function deactivate(Composer $composer, IOInterface $io): void
69+
{
70+
// Nothing to release.
71+
}
72+
73+
public function uninstall(Composer $composer, IOInterface $io): void
74+
{
75+
// Intentionally do not delete project pub/ files on uninstall —
76+
// doing so would break the application until `composer install`
77+
// restores them from magento/magento2-base.
78+
}
79+
80+
public static function getSubscribedEvents(): array
81+
{
82+
return [
83+
PackageEvents::POST_PACKAGE_INSTALL => ['onAfterPackageOperation', -1000],
84+
PackageEvents::POST_PACKAGE_UPDATE => ['onAfterPackageOperation', -1000],
85+
];
86+
}
87+
88+
public function onAfterPackageOperation(PackageEvent $event): void
89+
{
90+
if ($this->composer === null || $this->io === null) {
91+
return;
92+
}
93+
$this->enforcePubFiles();
94+
}
95+
96+
private function enforcePubFiles(): void
97+
{
98+
$ownPackage = $this->findOwnPackage();
99+
if ($ownPackage === null) {
100+
// We're not installed yet (or have been removed mid-batch).
101+
return;
102+
}
103+
104+
$installPath = $this->composer?->getInstallationManager()->getInstallPath($ownPackage);
105+
if ($installPath === null) {
106+
return;
107+
}
108+
$sourceDir = $installPath . '/pub';
109+
110+
$cwd = getcwd();
111+
if ($cwd === false) {
112+
return;
113+
}
114+
$targetDir = realpath($cwd) . '/pub';
115+
if (!is_dir($targetDir)) {
116+
// No pub/ in the project root yet — e.g., very early in a
117+
// create-project run before magento2-base has materialized.
118+
return;
119+
}
120+
121+
foreach (self::PUB_FILES as $filename) {
122+
$this->syncFile($sourceDir . '/' . $filename, $targetDir . '/' . $filename, $filename);
123+
}
124+
}
125+
126+
private function findOwnPackage(): ?PackageInterface
127+
{
128+
$localRepo = $this->composer?->getRepositoryManager()->getLocalRepository();
129+
if ($localRepo === null) {
130+
return null;
131+
}
132+
foreach ($localRepo->getPackages() as $package) {
133+
if ($package->getName() === self::PACKAGE_NAME) {
134+
return $package;
135+
}
136+
}
137+
return null;
138+
}
139+
140+
private function syncFile(string $source, string $target, string $shortName): void
141+
{
142+
if (!file_exists($source)) {
143+
return;
144+
}
145+
$sourceHash = md5_file($source);
146+
$targetHash = file_exists($target) ? md5_file($target) : null;
147+
if ($sourceHash !== false && $sourceHash === $targetHash) {
148+
return;
149+
}
150+
151+
if (@copy($source, $target)) {
152+
$this->io?->write(sprintf(
153+
'<info>[%s]</info> Restored pub/%s (overwritten by another package)',
154+
self::PACKAGE_NAME,
155+
$shortName,
156+
));
157+
} else {
158+
$this->io?->writeError(sprintf(
159+
'<error>[%s]</error> Failed to restore pub/%s — check filesystem permissions',
160+
self::PACKAGE_NAME,
161+
$shortName,
162+
));
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)