From a506f06f8dd35333d0275fc8f83427f129700d10 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:34:48 +0200 Subject: [PATCH 01/10] Fix `Plugin::autoload()` possibly prepending plugin autoloader to the stack --- formwork/src/Plugins/Plugins.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/formwork/src/Plugins/Plugins.php b/formwork/src/Plugins/Plugins.php index f1c1ef6b4..efafd199e 100644 --- a/formwork/src/Plugins/Plugins.php +++ b/formwork/src/Plugins/Plugins.php @@ -52,7 +52,12 @@ public function initialize(string $name): void throw new InvalidArgumentException(sprintf('Invalid plugin "%s"', $name)); } - $plugin->autoload()?->register(); + if ($autoload = $plugin->autoload()) { + // Requiring vendor/autoload.php always prepends the autoloader to the stack, + // so we need to unregister and re-register without prepending + $autoload->unregister(); + $autoload->register(prepend: false); + } foreach ($plugin->getEventListeners() as $eventName => $eventListener) { $this->eventDispatcher->on($eventName, $plugin->{$eventListener}(...)); From 36672cc20dcd60d98a85701c9b14ec1373c1e7da Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:36:35 +0200 Subject: [PATCH 02/10] Make `Plugin::autoload()` internal --- formwork/src/Plugins/Plugin.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/formwork/src/Plugins/Plugin.php b/formwork/src/Plugins/Plugin.php index c8e949d9d..35edca9d1 100644 --- a/formwork/src/Plugins/Plugin.php +++ b/formwork/src/Plugins/Plugin.php @@ -127,6 +127,8 @@ final public function isInitialized(): bool /** * Get the plugin autoloader + * + * @internal This method is called during plugin initialization. Since it can have side effects, it should not be called directly */ public function autoload(): ?ClassLoader { From 2774593d033b6df1861c64aafef297696ef4f6c6 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:38:42 +0200 Subject: [PATCH 03/10] Reserve "plugin" used as plugin id --- formwork/src/Plugins/Plugin.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/formwork/src/Plugins/Plugin.php b/formwork/src/Plugins/Plugin.php index 35edca9d1..7097d8da0 100644 --- a/formwork/src/Plugins/Plugin.php +++ b/formwork/src/Plugins/Plugin.php @@ -42,6 +42,10 @@ final public function __construct( ) { $this->id = basename($this->path); + if ($this->id === 'plugin') { + throw new InvalidArgumentException('Invalid plugin id "plugin". The plugin id "plugin" is reserved.'); + } + if (!preg_match('/^[a-z0-9-]+$/', $this->id)) { throw new InvalidArgumentException(sprintf('Invalid plugin id "%s". Plugin ids can only contain lowercase letters, numbers and hyphens.', $this->id)); } From 1a0eec2417fa25b7b37bfb8f63e004804c28864d Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:39:54 +0200 Subject: [PATCH 04/10] Throw `PluginInitializationException` on failure --- .../PluginInitializationException.php | 7 ++++++ formwork/src/Plugins/Plugins.php | 25 +++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 formwork/src/Plugins/Exceptions/PluginInitializationException.php diff --git a/formwork/src/Plugins/Exceptions/PluginInitializationException.php b/formwork/src/Plugins/Exceptions/PluginInitializationException.php new file mode 100644 index 000000000..316633f1c --- /dev/null +++ b/formwork/src/Plugins/Exceptions/PluginInitializationException.php @@ -0,0 +1,7 @@ +autoload()) { - // Requiring vendor/autoload.php always prepends the autoloader to the stack, - // so we need to unregister and re-register without prepending - $autoload->unregister(); - $autoload->register(prepend: false); + try { + if ($autoload = $plugin->autoload()) { + // Requiring vendor/autoload.php always prepends the autoloader to the stack, + // so we need to unregister and re-register without prepending + $autoload->unregister(); + $autoload->register(prepend: false); + } + } catch (Throwable $e) { + throw new PluginInitializationException(sprintf('Failed autoload for plugin "%s"', $name), $e->getCode(), previous: $e); } foreach ($plugin->getEventListeners() as $eventName => $eventListener) { $this->eventDispatcher->on($eventName, $plugin->{$eventListener}(...)); } - $plugin->initialize(); + try { + $plugin->initialize(); + } catch (Throwable $e) { + throw new PluginInitializationException(sprintf('Failed initialization for plugin "%s"', $name), $e->getCode(), previous: $e); + } } /** From 0703527cf73e3e128fc5b86fb044f7a61852500d Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:47:42 +0200 Subject: [PATCH 05/10] Avoid enabling plugins when initialization fails --- formwork/src/Panel/Controllers/PluginsController.php | 9 +++++++++ panel/src/ts/components/views/plugins.ts | 12 ++++++++---- panel/translations/de.yaml | 1 + panel/translations/el.yaml | 1 + panel/translations/en.yaml | 1 + panel/translations/es.yaml | 1 + panel/translations/fr.yaml | 1 + panel/translations/hu.yaml | 1 + panel/translations/it.yaml | 1 + panel/translations/nl.yaml | 1 + panel/translations/pl.yaml | 1 + panel/translations/pt.yaml | 1 + panel/translations/ro.yaml | 1 + panel/translations/ru.yaml | 1 + panel/translations/sv.yaml | 1 + panel/translations/tr.yaml | 1 + panel/translations/uk.yaml | 1 + 17 files changed, 32 insertions(+), 4 deletions(-) diff --git a/formwork/src/Panel/Controllers/PluginsController.php b/formwork/src/Panel/Controllers/PluginsController.php index 9eb7bfe3e..9aa818997 100644 --- a/formwork/src/Panel/Controllers/PluginsController.php +++ b/formwork/src/Panel/Controllers/PluginsController.php @@ -7,6 +7,7 @@ use Formwork\Http\Response; use Formwork\Http\ResponseStatus; use Formwork\Parsers\Yaml; +use Formwork\Plugins\Exceptions\PluginInitializationException; use Formwork\Plugins\Plugin; use Formwork\Plugins\Plugins; use Formwork\Router\RouteParams; @@ -109,6 +110,14 @@ public function enable(RouteParams $routeParams, Plugins $plugins): Response $this->togglePluginStatus($plugin, true); + try { + $plugins->initialize($name); + } catch (PluginInitializationException) { + $this->togglePluginStatus($plugin, false); + $this->panel->notify($this->translate('panel.plugins.plugin.cannotEnable.initializationError'), 'error'); + return JsonResponse::error($this->translate('panel.plugins.plugin.cannotEnable.initializationError'), ResponseStatus::InternalServerError); + } + $this->panel->notify($this->translate('panel.plugins.plugin.enabled'), 'success'); return JsonResponse::success($this->translate('panel.plugins.plugin.enabled')); } diff --git a/panel/src/ts/components/views/plugins.ts b/panel/src/ts/components/views/plugins.ts index 9a9e45ef5..8debf6417 100644 --- a/panel/src/ts/components/views/plugins.ts +++ b/panel/src/ts/components/views/plugins.ts @@ -3,11 +3,12 @@ import { app } from "../../app"; import { Notification } from "../notification"; import { Request } from "../../utils/request"; import { throttle } from "../../utils/events"; +import { TogglegroupInput } from "../inputs/togglegroup-input"; export class Plugins { constructor() { $$(".plugin-status-toggle").forEach((toggle) => { - const fieldset = toggle.closest(".form-togglegroup") as HTMLFieldSetElement; + const togglegroup = new TogglegroupInput(toggle.closest(".form-togglegroup") as HTMLFieldSetElement); const action = toggle.dataset.action; toggle.addEventListener("change", () => { @@ -15,7 +16,7 @@ export class Plugins { return; } - fieldset.disabled = true; + togglegroup.element.disabled = true; throttle(() => { new Request( @@ -25,12 +26,15 @@ export class Plugins { data: { "csrf-token": app.config.csrfToken as string }, }, (response) => { - if (response.status === "success" && !app.forms["plugin-form"]?.hasChanged()) { + if (!app.forms["plugin-form"]?.hasChanged()) { window.location.reload(); } else { const notification = new Notification(response.message, response.status); notification.show(); - fieldset.disabled = false; + if (response.status === "error") { + togglegroup.value = toggle.value === "1" ? "0" : "1"; + } + togglegroup.element.disabled = false; } }, ); diff --git a/panel/translations/de.yaml b/panel/translations/de.yaml index 4e14c863d..927b666e9 100644 --- a/panel/translations/de.yaml +++ b/panel/translations/de.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Seite anzeigen panel.panel: Administrationspanel panel.plugins.info: Info panel.plugins.nextPlugin: Nächstes Plugin +panel.plugins.plugin.cannotEnable.initializationError: Plugin kann nicht aktiviert werden. Beim Initialisieren des Plugins ist ein Fehler aufgetreten. panel.plugins.plugin.cannotSave.invalidFields: Plugin-Optionen können nicht gespeichert werden, einige Felder sind ungültig panel.plugins.plugin.disabled: Plugin deaktiviert panel.plugins.plugin.enabled: Plugin aktiviert diff --git a/panel/translations/el.yaml b/panel/translations/el.yaml index da9a452fa..a1e2f8e8b 100644 --- a/panel/translations/el.yaml +++ b/panel/translations/el.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Προβολή σελίδας panel.panel: Πίνακας διαχείρισης panel.plugins.info: Πληροφορίες panel.plugins.nextPlugin: Επόμενο πρόσθετο +panel.plugins.plugin.cannotEnable.initializationError: Δεν είναι δυνατή η ενεργοποίηση του πρόσθετου. Παρουσιάστηκε σφάλμα κατά την αρχικοποίηση του πρόσθετου. panel.plugins.plugin.cannotSave.invalidFields: Δεν είναι δυνατή η αποθήκευση των επιλογών του πρόσθετου, ορισμένα πεδία δεν είναι έγκυρα panel.plugins.plugin.disabled: Το πρόσθετο απενεργοποιήθηκε panel.plugins.plugin.enabled: Το πρόσθετο ενεργοποιήθηκε diff --git a/panel/translations/en.yaml b/panel/translations/en.yaml index 1b3f32be2..42f59d9c9 100644 --- a/panel/translations/en.yaml +++ b/panel/translations/en.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: View page panel.panel: Administration panel panel.plugins.info: Info panel.plugins.nextPlugin: Next plugin +panel.plugins.plugin.cannotEnable.initializationError: Cannot enable plugin. An error occurred while initializing the plugin. panel.plugins.plugin.cannotSave.invalidFields: Cannot save plugin options, some fields are invalid panel.plugins.plugin.disabled: Plugin disabled panel.plugins.plugin.enabled: Plugin enabled diff --git a/panel/translations/es.yaml b/panel/translations/es.yaml index 0e74d96d0..e8f8abb95 100644 --- a/panel/translations/es.yaml +++ b/panel/translations/es.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Ver página panel.panel: Panel de administración panel.plugins.info: Info panel.plugins.nextPlugin: Plugin siguiente +panel.plugins.plugin.cannotEnable.initializationError: No se puede activar el plugin. Se produjo un error al inicializar el plugin. panel.plugins.plugin.cannotSave.invalidFields: No se pueden guardar las opciones del plugin, algunos campos no son válidos panel.plugins.plugin.disabled: Plugin deshabilitado panel.plugins.plugin.enabled: Plugin habilitado diff --git a/panel/translations/fr.yaml b/panel/translations/fr.yaml index 2fedf19e6..0726f3acd 100644 --- a/panel/translations/fr.yaml +++ b/panel/translations/fr.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Voir la page panel.panel: Panneau d’administration panel.plugins.info: Info panel.plugins.nextPlugin: Plugin suivant +panel.plugins.plugin.cannotEnable.initializationError: Impossible d’activer le plugin. Une erreur s’est produite lors de l’initialisation du plugin. panel.plugins.plugin.cannotSave.invalidFields: Impossible d’enregistrer les options du plugin, certains champs sont invalides panel.plugins.plugin.disabled: Plugin désactivé panel.plugins.plugin.enabled: Plugin activé diff --git a/panel/translations/hu.yaml b/panel/translations/hu.yaml index 0735e92d3..9bae88068 100644 --- a/panel/translations/hu.yaml +++ b/panel/translations/hu.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Oldal megtekintése panel.panel: Adminisztrációs felület panel.plugins.info: Információ panel.plugins.nextPlugin: Következő bővítmény +panel.plugins.plugin.cannotEnable.initializationError: Nem lehet engedélyezni a bővítményt. Hiba történt a bővítmény inicializálása során. panel.plugins.plugin.cannotSave.invalidFields: Nem lehet menteni a bővítmény beállításait, egyes mezők érvénytelenek panel.plugins.plugin.disabled: Bővítmény letiltva panel.plugins.plugin.enabled: Bővítmény engedélyezve diff --git a/panel/translations/it.yaml b/panel/translations/it.yaml index 11dfdf061..aa87aee49 100644 --- a/panel/translations/it.yaml +++ b/panel/translations/it.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Visualizza pagina panel.panel: Pannello di amministrazione panel.plugins.info: Info panel.plugins.nextPlugin: Plugin successivo +panel.plugins.plugin.cannotEnable.initializationError: Impossibile abilitare il plugin. Si è verificato un errore durante l’inizializzazione del plugin. panel.plugins.plugin.cannotSave.invalidFields: Impossibile salvare le impostazioni del plugin, alcuni campi non sono validi panel.plugins.plugin.disabled: Plugin disabilitato panel.plugins.plugin.enabled: Plugin abilitato diff --git a/panel/translations/nl.yaml b/panel/translations/nl.yaml index 6cbb87444..42c7c32a7 100644 --- a/panel/translations/nl.yaml +++ b/panel/translations/nl.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Bekijk pagina panel.panel: Administratiepaneel panel.plugins.info: Info panel.plugins.nextPlugin: Volgende plugin +panel.plugins.plugin.cannotEnable.initializationError: Kan plugin niet inschakelen. Er is een fout opgetreden bij het initialiseren van de plugin. panel.plugins.plugin.cannotSave.invalidFields: Pluginopties kunnen niet worden opgeslagen, sommige velden zijn ongeldig panel.plugins.plugin.disabled: Plugin uitgeschakeld panel.plugins.plugin.enabled: Plugin ingeschakeld diff --git a/panel/translations/pl.yaml b/panel/translations/pl.yaml index 80a57bad7..1bbce0a09 100644 --- a/panel/translations/pl.yaml +++ b/panel/translations/pl.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Wyświetl stronę panel.panel: Panel administracyjny panel.plugins.info: Info panel.plugins.nextPlugin: Następna wtyczka +panel.plugins.plugin.cannotEnable.initializationError: Nie można włączyć wtyczki. Wystąpił błąd podczas inicjalizacji wtyczki. panel.plugins.plugin.cannotSave.invalidFields: Nie można zapisać opcji wtyczki, niektóre pola są nieprawidłowe panel.plugins.plugin.disabled: Wtyczka wyłączona panel.plugins.plugin.enabled: Wtyczka włączona diff --git a/panel/translations/pt.yaml b/panel/translations/pt.yaml index b67efc561..53879339c 100644 --- a/panel/translations/pt.yaml +++ b/panel/translations/pt.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Ver página panel.panel: Painel de administração panel.plugins.info: Info panel.plugins.nextPlugin: Plugin seguinte +panel.plugins.plugin.cannotEnable.initializationError: Não é possível ativar o plugin. Ocorreu um erro ao inicializar o plugin. panel.plugins.plugin.cannotSave.invalidFields: Não é possível guardar as opções do plugin, alguns campos são inválidos panel.plugins.plugin.disabled: Plugin desativado panel.plugins.plugin.enabled: Plugin ativado diff --git a/panel/translations/ro.yaml b/panel/translations/ro.yaml index 6793fe8ff..f3409f499 100644 --- a/panel/translations/ro.yaml +++ b/panel/translations/ro.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Vezi pagina panel.panel: Panoul de administrare panel.plugins.info: Info panel.plugins.nextPlugin: Plugin următor +panel.plugins.plugin.cannotEnable.initializationError: Pluginul nu poate fi activat. A apărut o eroare la inițializarea pluginului. panel.plugins.plugin.cannotSave.invalidFields: Nu se pot salva opțiunile pluginului, unele câmpuri sunt nevalide panel.plugins.plugin.disabled: Plugin dezactivat panel.plugins.plugin.enabled: Plugin activat diff --git a/panel/translations/ru.yaml b/panel/translations/ru.yaml index 062cb4898..e1725a07b 100644 --- a/panel/translations/ru.yaml +++ b/panel/translations/ru.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Просмотреть страницу panel.panel: Панель администрирования panel.plugins.info: Информация panel.plugins.nextPlugin: Следующий плагин +panel.plugins.plugin.cannotEnable.initializationError: Невозможно включить плагин. Произошла ошибка при инициализации плагина. panel.plugins.plugin.cannotSave.invalidFields: Не удалось сохранить настройки плагина, некоторые поля недействительны panel.plugins.plugin.disabled: Плагин отключён panel.plugins.plugin.enabled: Плагин включён diff --git a/panel/translations/sv.yaml b/panel/translations/sv.yaml index 83edd68b3..81f836e90 100644 --- a/panel/translations/sv.yaml +++ b/panel/translations/sv.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Visa sida panel.panel: Administrationspanel panel.plugins.info: Information panel.plugins.nextPlugin: Nästa plugin +panel.plugins.plugin.cannotEnable.initializationError: Kan inte aktivera pluginet. Ett fel uppstod när pluginet initierades. panel.plugins.plugin.cannotSave.invalidFields: Kan inte spara plugin-alternativ, vissa fält är ogiltiga panel.plugins.plugin.disabled: Plugin inaktiverad panel.plugins.plugin.enabled: Plugin aktiverad diff --git a/panel/translations/tr.yaml b/panel/translations/tr.yaml index 72ee976ff..8dd97e2d2 100644 --- a/panel/translations/tr.yaml +++ b/panel/translations/tr.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Sayfayı görüntüle panel.panel: Yönetim paneli panel.plugins.info: Bilgi panel.plugins.nextPlugin: Sonraki eklenti +panel.plugins.plugin.cannotEnable.initializationError: Eklenti etkinleştirilemiyor. Eklenti başlatılırken bir hata oluştu. panel.plugins.plugin.cannotSave.invalidFields: Eklenti seçenekleri kaydedilemiyor, bazı alanlar geçersiz panel.plugins.plugin.disabled: Eklenti devre dışı panel.plugins.plugin.enabled: Eklenti etkin diff --git a/panel/translations/uk.yaml b/panel/translations/uk.yaml index 53bd7675c..ac4c611d4 100644 --- a/panel/translations/uk.yaml +++ b/panel/translations/uk.yaml @@ -277,6 +277,7 @@ panel.pages.viewPage: Переглянути сторінку panel.panel: Панель адміністратора panel.plugins.info: Інформація panel.plugins.nextPlugin: Наступний плагін +panel.plugins.plugin.cannotEnable.initializationError: Неможливо ввімкнути плагін. Під час ініціалізації плагіна сталася помилка. panel.plugins.plugin.cannotSave.invalidFields: Не вдалося зберегти налаштування плагіна, деякі поля недійсні panel.plugins.plugin.disabled: Плагін вимкнено panel.plugins.plugin.enabled: Плагін увімкнено From 2acf74f109d741b2fae012cd3b5c778ce429a51a Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:51:19 +0200 Subject: [PATCH 06/10] Require that plugin schemes extend the base `plugins.plugin` --- formwork/schemes/plugins/plugin.yaml | 11 +++++ .../Panel/Controllers/PluginsController.php | 42 ++++++++----------- formwork/src/Schemes/Scheme.php | 15 +++++++ 3 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 formwork/schemes/plugins/plugin.yaml diff --git a/formwork/schemes/plugins/plugin.yaml b/formwork/schemes/plugins/plugin.yaml new file mode 100644 index 000000000..855a7ccf6 --- /dev/null +++ b/formwork/schemes/plugins/plugin.yaml @@ -0,0 +1,11 @@ +title: Plugin + +fields: + enabled: + label: Enabled + type: togglegroup + options: + 1: Enabled + 0: Disabled + default: 0 + visible: false diff --git a/formwork/src/Panel/Controllers/PluginsController.php b/formwork/src/Panel/Controllers/PluginsController.php index 9aa818997..d6057ddb4 100644 --- a/formwork/src/Panel/Controllers/PluginsController.php +++ b/formwork/src/Panel/Controllers/PluginsController.php @@ -2,7 +2,6 @@ namespace Formwork\Panel\Controllers; -use Formwork\Fields\FieldCollection; use Formwork\Http\JsonResponse; use Formwork\Http\Response; use Formwork\Http\ResponseStatus; @@ -56,19 +55,7 @@ public function plugin(RouteParams $routeParams, Plugins $plugins): Response return $this->forward(ErrorsController::class, 'notFound'); } - $scheme = $this->getPluginScheme($plugin); - - // If no scheme, just show plugin info - if ($scheme === null) { - return new Response($this->view('@panel.plugins.plugin', [ - 'title' => $plugin->manifest()->title() ?? $plugin->name(), - 'plugin' => $plugin, - 'fields' => new FieldCollection(), - ...$this->getPreviousAndNextPlugin($plugin), - ])); - } - - $fields = $scheme->fields(); + $fields = $this->getPluginScheme($plugin)->fields(); $fields->setValues($this->config->getArray("plugins.{$name}", [])); @@ -88,7 +75,7 @@ public function plugin(RouteParams $routeParams, Plugins $plugins): Response return new Response($this->view('@panel.plugins.plugin', [ 'title' => $plugin->manifest()->title() ?? $plugin->name(), 'plugin' => $plugin, - 'fields' => $form->fields(), + 'fields' => $form->fields()->filter(fn($field) => $field->isVisible()), ...$this->getPreviousAndNextPlugin($plugin), ]), $form->getResponseStatus()); } @@ -168,25 +155,32 @@ private function updatePluginsOptions(Plugin $plugin, array $options): void /** * Get scheme for a given plugin */ - private function getPluginScheme(Plugin $plugin): ?Scheme + private function getPluginScheme(Plugin $plugin): Scheme { $id = $plugin->id(); $schemes = $this->app->schemes(); if ($schemes->has("plugins.{$id}")) { - return $schemes->get("plugins.{$id}"); + $scheme = $schemes->get("plugins.{$id}"); + } else { + // Try to load scheme from plugin path + $path = FileSystem::joinPaths($plugin->path(), "schemes/plugins/{$id}.yaml"); + + if (FileSystem::exists($path)) { + $schemes->load("plugins.{$id}", $path); + $scheme = $schemes->get("plugins.{$id}"); + } } - // Try to load scheme from plugin path - $path = FileSystem::joinPaths($plugin->path(), "schemes/plugins/{$id}.yaml"); - - if (FileSystem::exists($path)) { - $schemes->load("plugins.{$id}", $path); - return $schemes->get("plugins.{$id}"); + // Require that the scheme extends the base plugin scheme, + // so that the `enabled` field is always present + if (isset($scheme) && !$scheme->extendsScheme('plugins.plugin')) { + // @phpstan-ignore argument.type + $scheme->extendWith($schemes->get('plugins.plugin')->toArray()); } - return null; + return $scheme ?? $schemes->get('plugins.plugin'); } /** diff --git a/formwork/src/Schemes/Scheme.php b/formwork/src/Schemes/Scheme.php index 00441987f..2d13a21a3 100644 --- a/formwork/src/Schemes/Scheme.php +++ b/formwork/src/Schemes/Scheme.php @@ -201,6 +201,21 @@ public function getExtendedScheme(): ?Scheme return isset($this->data['extend']) ? $this->schemes->get($this->data['extend']) : null; } + /** + * Check if the scheme extends another scheme + */ + public function extendsScheme(Scheme|string $scheme): bool + { + $id = $scheme instanceof Scheme ? $scheme->id() : $scheme; + while ($extendedScheme = $this->getExtendedScheme()) { + if ($extendedScheme->id() === $id) { + return true; + } + $extendedScheme = $extendedScheme->getExtendedScheme(); + } + return false; + } + /** * Translate a value */ From 67049b52b41eccb56f73061560cf45f1f5236f69 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:21:11 +0200 Subject: [PATCH 07/10] Fix `Scheme::extendsScheme()` recursion --- formwork/src/Schemes/Scheme.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/formwork/src/Schemes/Scheme.php b/formwork/src/Schemes/Scheme.php index 2d13a21a3..170bd2867 100644 --- a/formwork/src/Schemes/Scheme.php +++ b/formwork/src/Schemes/Scheme.php @@ -207,12 +207,16 @@ public function getExtendedScheme(): ?Scheme public function extendsScheme(Scheme|string $scheme): bool { $id = $scheme instanceof Scheme ? $scheme->id() : $scheme; - while ($extendedScheme = $this->getExtendedScheme()) { - if ($extendedScheme->id() === $id) { + + $extended = $this->getExtendedScheme(); + + while ($extended instanceof Scheme) { + if ($extended->id() === $id) { return true; } - $extendedScheme = $extendedScheme->getExtendedScheme(); + $extended = $extended->getExtendedScheme(); } + return false; } From f757032609c8f6a81dec45a77737b18fab134b91 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:23:36 +0200 Subject: [PATCH 08/10] Register plugin event listeners only on successful initialization --- formwork/src/Plugins/Plugins.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/formwork/src/Plugins/Plugins.php b/formwork/src/Plugins/Plugins.php index c02bf3f7b..315e208a5 100644 --- a/formwork/src/Plugins/Plugins.php +++ b/formwork/src/Plugins/Plugins.php @@ -66,15 +66,15 @@ public function initialize(string $name): void throw new PluginInitializationException(sprintf('Failed autoload for plugin "%s"', $name), $e->getCode(), previous: $e); } - foreach ($plugin->getEventListeners() as $eventName => $eventListener) { - $this->eventDispatcher->on($eventName, $plugin->{$eventListener}(...)); - } - try { $plugin->initialize(); } catch (Throwable $e) { throw new PluginInitializationException(sprintf('Failed initialization for plugin "%s"', $name), $e->getCode(), previous: $e); } + + foreach ($plugin->getEventListeners() as $eventName => $eventListener) { + $this->eventDispatcher->on($eventName, $plugin->{$eventListener}(...)); + } } /** From acda2875e5000a6d6ed86eb03b3316b413422033 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:23:02 +0200 Subject: [PATCH 09/10] Detect recursion in `Scheme::extendsScheme()` --- formwork/src/Schemes/Scheme.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/formwork/src/Schemes/Scheme.php b/formwork/src/Schemes/Scheme.php index 170bd2867..998483713 100644 --- a/formwork/src/Schemes/Scheme.php +++ b/formwork/src/Schemes/Scheme.php @@ -203,17 +203,29 @@ public function getExtendedScheme(): ?Scheme /** * Check if the scheme extends another scheme + * + * @throws RecursionException If there is recursion in scheme extension */ public function extendsScheme(Scheme|string $scheme): bool { - $id = $scheme instanceof Scheme ? $scheme->id() : $scheme; + $id = $scheme instanceof Scheme ? $scheme->id : $scheme; $extended = $this->getExtendedScheme(); + $visited = [$this->id => true]; + while ($extended instanceof Scheme) { - if ($extended->id() === $id) { + $extendedId = $extended->id; + + if (isset($visited[$extendedId])) { + throw new RecursionException(sprintf('Recursion in the extension of the scheme "%s". Extension chain: "%s"', $this->id, implode('" > "', array_keys($visited)))); + } + + if ($extendedId === $id) { return true; } + + $visited[$extendedId] = true; $extended = $extended->getExtendedScheme(); } From 344dd7b240d674c86681cddd8237b6e0dcbd2bc5 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:57:36 +0200 Subject: [PATCH 10/10] Explicitly exclude the `enabled` field --- formwork/src/Panel/Controllers/PluginsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formwork/src/Panel/Controllers/PluginsController.php b/formwork/src/Panel/Controllers/PluginsController.php index d6057ddb4..2ce68a2fc 100644 --- a/formwork/src/Panel/Controllers/PluginsController.php +++ b/formwork/src/Panel/Controllers/PluginsController.php @@ -75,7 +75,7 @@ public function plugin(RouteParams $routeParams, Plugins $plugins): Response return new Response($this->view('@panel.plugins.plugin', [ 'title' => $plugin->manifest()->title() ?? $plugin->name(), 'plugin' => $plugin, - 'fields' => $form->fields()->filter(fn($field) => $field->isVisible()), + 'fields' => $form->fields()->reject(fn($field) => $field->name() === 'enabled'), ...$this->getPreviousAndNextPlugin($plugin), ]), $form->getResponseStatus()); }