startup optimizations

This commit is contained in:
Adrian Stephens 2026-06-24 19:01:39 +01:00
parent 77af1935b3
commit c5bfde12a6
14 changed files with 239 additions and 99 deletions

BIN
dist/feca_mailshots_plugin-1.1.6.zip vendored Normal file

Binary file not shown.

View File

@ -3,7 +3,7 @@
* Plugin Name: FECA Mailshots
* Plugin URI: https://fenedge.co.uk/
* Description: FECA mailshots plugin.
* Version: 1.1.5
* Version: 1.1.6
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: FECA

View File

@ -101,8 +101,7 @@ final class DataSourcesAdminPage
$dsl = (string) ($draft['dsl_text'] ?? $dsl);
}
$result = $this->consumeOptionArray(self::RESULT_OPTION_KEY);
$sourceFieldsMap = $this->service()->sourceFields();
$schemaList = $this->service()->listSchemas();
$builtInSources = $this->service()->knownSources();
echo '<div class="wrap feca-mailshots-admin">';
echo $this->renderAdminUiStyles();
@ -184,16 +183,13 @@ final class DataSourcesAdminPage
echo '<div class="feca-builder-grid">';
echo '<div class="feca-builder-col-left">';
echo '<h3 class="feca-modal-title">Data Sources</h3>';
foreach (array_keys($sourceFieldsMap) as $builtInSource) {
foreach ($builtInSources as $builtInSource) {
$escapedSource = htmlspecialchars((string) $builtInSource, ENT_QUOTES);
echo '<label><input type="checkbox" class="ds-source-built" value="' . $escapedSource . '"> ' . $escapedSource . '</label><br>';
}
echo '<br>';
echo '<strong>Add custom source</strong><br>';
echo '<label>Schema <select id="ds-builder-schema"><option value="">Select schema</option>';
foreach ($schemaList as $schema) {
echo '<option value="' . htmlspecialchars((string) $schema, ENT_QUOTES) . '">' . htmlspecialchars((string) $schema) . '</option>';
}
echo '</select></label> ';
echo '<label>Table <select id="ds-builder-table"><option value="">Select table</option></select></label> ';
echo '<button type="button" class="button button-small" id="ds-builder-add-custom">Add</button>';
@ -285,7 +281,7 @@ final class DataSourcesAdminPage
echo '</div></div>';
echo '<script>';
echo 'window.fecaDataSourcesBuilderConfig = ' . json_encode([
'sourceFields' => $sourceFieldsMap,
'sourceFields' => [],
'restBase' => '/wp-json/mailshots/v1/data-sources',
'adminPostApi' => $this->wp->adminUrl('admin-post.php?action=feca_mailshots_data_sources_api'),
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
@ -320,6 +316,9 @@ final class DataSourcesAdminPage
echo 'var customSources=[];';
echo 'var sourceOrder=[];';
echo 'var constraints=[];';
echo 'var sourceFieldRequests={};';
echo 'var schemasLoaded=false;';
echo 'var schemasLoading=false;';
echo 'var isDirty=false;';
echo 'function confirmDiscard(){if(!isDirty){return true;}return window.confirm("You have unsaved changes. Close without saving?");}';
echo 'if(editorForm){editorForm.querySelectorAll("input,select,textarea").forEach(function(el){el.addEventListener("input",function(){isDirty=true;});el.addEventListener("change",function(){isDirty=true;});});editorForm.addEventListener("submit",function(){isDirty=false;});}';
@ -329,7 +328,57 @@ final class DataSourcesAdminPage
echo 'function noteSourceDeselected(src){sourceOrder=sourceOrder.filter(function(v){return v!==src;});}';
echo 'function updateCustomList(){customList.innerHTML="";customSources.forEach(function(src,i){var li=document.createElement("li");li.textContent=src+" ";var b=document.createElement("button");b.type="button";b.className="button-link-delete";b.textContent="Remove";b.onclick=function(){noteSourceDeselected(src);customSources.splice(i,1);updateCustomList();renderRows();updateDsl();};li.appendChild(b);customList.appendChild(li);});}';
echo 'function fetchTables(schema){tableSel.innerHTML="<option value=\"\">Loading...</option>";tableSel.style.color="#1d2327";if(tableErr){tableErr.textContent="";}var pickLabel=function(t){if(typeof t==="string"){return t.trim();}if(t===null||t===undefined){return "";}if(typeof t==="number"){return String(t);}if(typeof t==="object"){var direct=[t.table_name,t.name,t.table,t.label];for(var i=0;i<direct.length;i++){var dv=String(direct[i]||"").trim();if(dv){return dv;}}var keys=Object.keys(t||{});for(var k=0;k<keys.length;k++){var key=String(keys[k]||"").trim();if(/^Tables_in_/i.test(key)){var vv=String(t[key]||"").trim();if(vv){return vv;}}}var vals=Object.values(t||{});for(var j=0;j<vals.length;j++){var v=String(vals[j]||"").trim();if(v){return v;}}}return "";};var fill=function(items){tableSel.innerHTML="<option value=\"\">Select table</option>";var invalid=0;(items||[]).forEach(function(t){var label=pickLabel(t);if(!label){invalid++;return;}var o=document.createElement("option");o.value=label;o.textContent=label;tableSel.appendChild(o);});if((items||[]).length===0&&tableErr){tableErr.textContent="No tables returned for this schema. Check DB grants: SELECT and SHOW VIEW on schema tables.";}if(invalid>0&&tableErr){tableErr.textContent="Received "+invalid+" table entries without names; showing only valid table names.";}};var showErr=function(msg){tableSel.innerHTML="<option value=\"\">Select table</option>";if(tableErr){tableErr.textContent=msg||"Table lookup failed. Resolve the error before continuing.";}if(err){err.textContent="Table lookup failed: "+(msg||"unknown error");}};var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=tables&schema="+encodeURIComponent(schema);fetch(u,{credentials:"same-origin"}).then(function(r){if(!r.ok){throw new Error("API "+r.status);}return r.json();}).then(function(j){if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}fill((j&&j.items)||[]);}).catch(function(e){showErr(e&&e.message?e.message:"Need MySQL grants on the selected schema.");});}';
echo 'function sourceFields(source){var map=cfg.sourceFields||{};if(map[source]){return map[source];}if(source&&source.indexOf(".")!==-1){var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=source_fields&source="+encodeURIComponent(source);fetch(u,{credentials:"same-origin"}).then(function(r){if(!r.ok){throw new Error("API "+r.status);}return r.json();}).then(function(j){if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}var fields=(j&&j.fields)||[];if(fields&&fields.length){map[source]=fields;cfg.sourceFields=map;renderRows();updateDsl();return;}if(err){err.textContent="Source field lookup returned no fields for "+source+".";}}).catch(function(e){if(err){err.textContent="Source field lookup failed for "+source+": "+(e&&e.message?e.message:"unknown error");}});}return [];}';
$lazyMetadataJs = <<<'JS'
function loadSchemas(){
if(schemasLoaded||schemasLoading||!schemaSel){return;}
schemasLoading=true;
var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=schemas";
fetch(u,{credentials:"same-origin"}).then(function(r){
if(!r.ok){throw new Error("API "+r.status);}
return r.json();
}).then(function(j){
if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}
schemaSel.innerHTML="<option value=\"\">Select schema</option>";
(j.items||[]).forEach(function(schema){
var value=String(schema||"").trim();
if(!value){return;}
var option=document.createElement("option");
option.value=value;
option.textContent=value;
schemaSel.appendChild(option);
});
schemasLoaded=true;
schemasLoading=false;
}).catch(function(e){
schemasLoading=false;
if(err){err.textContent="Schema lookup failed: "+(e&&e.message?e.message:"unknown error");}
});
}
function sourceFields(source){
var map=cfg.sourceFields||{};
if(Object.prototype.hasOwnProperty.call(map,source)){return map[source];}
if(!source||sourceFieldRequests[source]){return [];}
sourceFieldRequests[source]=true;
var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=source_fields&source="+encodeURIComponent(source);
fetch(u,{credentials:"same-origin"}).then(function(r){
if(!r.ok){throw new Error("API "+r.status);}
return r.json();
}).then(function(j){
delete sourceFieldRequests[source];
if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}
var fields=(j&&j.fields)||[];
map[source]=fields;
cfg.sourceFields=map;
if(fields.length){renderRows();updateDsl();return;}
if(err){err.textContent="Source field lookup returned no fields for "+source+".";}
}).catch(function(e){
delete sourceFieldRequests[source];
if(err){err.textContent="Source field lookup failed for "+source+": "+(e&&e.message?e.message:"unknown error");}
});
return [];
}
JS;
echo $lazyMetadataJs;
echo 'function mkSelect(options,value){var s=document.createElement("select");options.forEach(function(opt){var o=document.createElement("option");o.value=opt[0];o.textContent=opt[1];if(opt[0]===value){o.selected=true;}s.appendChild(o);});return s;}';
echo 'function filterArgType(name){for(var i=0;i<filters.length;i++){if(filters[i][0]===name){return filters[i][2]||"none";}}return "none";}';
echo 'function addConstraint(){constraints.push({kind:"filter",negate:false,filter:"selected-renewal",filterArg:"",lhsSource:"",lhsField:"",op:"=",rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""});renderRows();updateDsl();}';
@ -350,7 +399,7 @@ final class DataSourcesAdminPage
echo 'if(addCustomBtn){addCustomBtn.onclick=function(){if(!schemaSel.value||!tableSel.value){return;}var src=schemaSel.value+"."+tableSel.value;if(customSources.indexOf(src)===-1){customSources.push(src);}noteSourceSelected(src);updateCustomList();renderRows();updateDsl();};}';
echo 'builtChecks.forEach(function(c){c.onchange=function(){if(c.checked){noteSourceSelected(c.value);}else{noteSourceDeselected(c.value);}renderRows();updateDsl();};});';
echo 'if(addRowBtn){addRowBtn.onclick=addConstraint;}';
echo 'if(openBtn){openBtn.onclick=function(){resetBuilder();prefillFromDsl();modal.style.display="block";};}';
echo 'if(openBtn){openBtn.onclick=function(){loadSchemas();resetBuilder();prefillFromDsl();modal.style.display="block";};}';
echo 'if(cancelBtn){cancelBtn.onclick=function(){modal.style.display="none";};}';
echo 'if(applyBtn){applyBtn.onclick=function(){if(dslInput){dslInput.value=out.value;}modal.style.display="none";};}';
echo 'if(openNew){openNew.onclick=function(){if(!confirmDiscard()){return;}if(editorId){editorId.value="";}if(editorName){editorName.value="";}if(dslInput){dslInput.value="";}isDirty=false;editorModal.style.display="block";};}';

View File

@ -141,6 +141,7 @@ final class SetupAdminPage
$this->wp->updateOption(self::OPTION_KEY, $settings);
$this->wp->updateOption(self::DOWNLOAD_MEMORY_LIMIT_OPTION, $downloadMemoryLimit);
$this->wp->deleteOption(\FecaMailshots\Infrastructure\MailshotSchemaInstaller::STATE_OPTION_KEY);
if (!headers_sent()) {
$location = $this->wp->adminUrl('admin.php?page=feca-mailshots-setup&saved=1');

View File

@ -191,6 +191,12 @@ final class DataSourceService
return $result;
}
/** @return list<string> */
public function knownSources(): array
{
return $this->metadata->allKnownSources();
}
/** @return list<string> */
public function listSchemas(): array
{

View File

@ -8,6 +8,9 @@ use PDO;
final class MailshotSchemaInstaller
{
public const SCHEMA_VERSION = '1';
public const STATE_OPTION_KEY = 'feca_mailshots_schema_state';
private DatabaseRouter $router;
public function __construct(DatabaseRouter $router)
@ -26,6 +29,16 @@ final class MailshotSchemaInstaller
$this->ensureMailshotQueriesColumns($pdo);
}
/** @param array<string,string> $dbConfig @return array{version:string,config_hash:string} */
public static function migrationTarget(array $dbConfig): array
{
ksort($dbConfig);
return [
'version' => self::SCHEMA_VERSION,
'config_hash' => hash('sha256', json_encode($dbConfig, JSON_UNESCAPED_SLASHES) ?: ''),
];
}
/** @return list<string> */
private function createTableSql(): array
{

View File

@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace FecaMailshots\Infrastructure;
use PDO;
final class SchemaEnsuringDatabaseRouter implements DatabaseRouter
{
private DatabaseRouter $inner;
private bool $installed = false;
public function __construct(DatabaseRouter $inner)
{
$this->inner = $inner;
}
public function mailshotsPdo(): PDO
{
$this->installOnce();
return $this->inner->mailshotsPdo();
}
public function membersPdo(): PDO
{
return $this->inner->membersPdo();
}
public function mailshotsDbName(): string
{
return $this->inner->mailshotsDbName();
}
public function membersDbName(): string
{
return $this->inner->membersDbName();
}
public function fenDbName(): string
{
return $this->inner->fenDbName();
}
private function installOnce(): void
{
if ($this->installed) {
return;
}
(new MailshotSchemaInstaller($this->inner))->install();
$this->installed = true;
}
}

View File

@ -32,7 +32,6 @@ use FecaMailshots\Infrastructure\MailshotSchemaInstaller;
use FecaMailshots\Infrastructure\DatabaseSourceMetadataProvider;
use FecaMailshots\Infrastructure\PhpImapAppender;
use FecaMailshots\Infrastructure\PdoDatabaseRouter;
use FecaMailshots\Infrastructure\SchemaEnsuringDatabaseRouter;
use FecaMailshots\Infrastructure\WordPressSaltSecretKeyProvider;
use FecaMailshots\Repository\AttachmentRepository;
use FecaMailshots\Repository\LastRunRepository;
@ -54,7 +53,7 @@ final class Plugin
$c->set('logger', static fn() => new ErrorLogLogger());
$c->set(DatabaseRouter::class, static fn() => $router ?? new SchemaEnsuringDatabaseRouter(new PdoDatabaseRouter($dbConfig)));
$c->set(DatabaseRouter::class, static fn() => $router ?? new PdoDatabaseRouter($dbConfig));
$c->set(MailshotSchemaInstaller::class, static fn(Container $c) => new MailshotSchemaInstaller($c->get(DatabaseRouter::class)));
$c->set(DatabaseSourceMetadataProvider::class, static fn(Container $c) => new DatabaseSourceMetadataProvider($c->get(DatabaseRouter::class)));

View File

@ -2,21 +2,37 @@
declare(strict_types=1);
// Load Composer dependencies (Twig, Dompdf, etc.) when available.
$autoloadCandidates = [
dirname(__DIR__) . '/vendor/autoload.php', // Preferred: packaged/deployed plugin-local vendor
dirname(__DIR__, 2) . '/vendor/autoload.php', // Backward-compatible: workspace/shared vendor
];
foreach ($autoloadCandidates as $composerAutoload) {
if (is_file($composerAutoload) && feca_mailshots_composer_autoload_is_usable($composerAutoload)) {
try {
require_once $composerAutoload;
} catch (\Throwable $e) {
error_log('FECA Mailshots skipped unusable Composer autoload: ' . $e->getMessage());
/** @return object|null Composer class loader when runtime dependencies are available. */
function feca_mailshots_load_composer_dependencies(): ?object
{
static $attempted = false;
static $loader = null;
if ($attempted) {
return is_object($loader) ? $loader : null;
}
$attempted = true;
$autoloadCandidates = [
dirname(__DIR__) . '/vendor/autoload.php',
dirname(__DIR__, 2) . '/vendor/autoload.php',
];
foreach ($autoloadCandidates as $composerAutoload) {
if (!is_file($composerAutoload) || !feca_mailshots_composer_autoload_is_usable($composerAutoload)) {
continue;
}
break;
try {
$candidate = require $composerAutoload;
if (is_object($candidate)) {
$loader = $candidate;
return $loader;
}
} catch (\Throwable $e) {
error_log('FECA Mailshots skipped unusable Composer autoload: ' . $e->getMessage());
}
}
return null;
}
function feca_mailshots_composer_autoload_is_usable(string $composerAutoload): bool
@ -55,3 +71,15 @@ spl_autoload_register(static function (string $class): void {
require_once $path;
}
});
// Composer's generated function files are expensive to load. Load them only
// when Twig or Dompdf is first requested, then delegate that initial class load.
spl_autoload_register(static function (string $class): void {
if (strncmp($class, 'Twig\\', 5) !== 0 && strncmp($class, 'Dompdf\\', 7) !== 0) {
return;
}
$loader = feca_mailshots_load_composer_dependencies();
if ($loader !== null && method_exists($loader, 'loadClass')) {
$loader->loadClass($class);
}
});

View File

@ -3,25 +3,28 @@
declare(strict_types=1);
use FecaMailshots\Plugin;
use FecaMailshots\Infrastructure\MailshotSchemaInstaller;
use FecaMailshots\Infrastructure\PdoDatabaseRouter;
use FecaMailshots\WordPress\ProductionWordPressFacade;
require_once __DIR__ . '/autoload.php';
if (!function_exists('get_option')) {
throw new \RuntimeException('WordPress get_option() is unavailable while bootstrapping mailshots plugin.');
}
$setupErrors = [];
$getOption = static function (string $key) use (&$setupErrors): string {
if (!function_exists('get_option')) {
throw new \RuntimeException('WordPress get_option() is unavailable while bootstrapping mailshots plugin.');
}
$raw = get_option(\FecaMailshots\Admin\SetupAdminPage::OPTION_KEY, []);
if (!is_array($raw)) {
$setupErrors[] = 'Mailshots setup option is missing or invalid.';
return '';
}
if (!array_key_exists($key, $raw)) {
$rawSetup = get_option(\FecaMailshots\Admin\SetupAdminPage::OPTION_KEY, []);
if (!is_array($rawSetup)) {
$setupErrors[] = 'Mailshots setup option is missing or invalid.';
$rawSetup = [];
}
$readSetupValue = static function (string $key) use (&$setupErrors, $rawSetup): string {
if (!array_key_exists($key, $rawSetup)) {
$setupErrors[] = 'Missing setup configuration key: ' . $key;
return '';
}
$value = trim((string) $raw[$key]);
$value = trim((string) $rawSetup[$key]);
if ($value === '') {
$setupErrors[] = 'Empty setup configuration value: ' . $key;
}
@ -29,16 +32,37 @@ $getOption = static function (string $key) use (&$setupErrors): string {
};
$dbConfig = [
'MYSQL_HOST' => $getOption('db_host'),
'MYSQL_PORT' => $getOption('db_port'),
'MYSQL_USER' => $getOption('db_user'),
'MYSQL_PASSWORD' => $getOption('db_password'),
'MAILSHOTS_REMOTE_MYSQL_DB' => $getOption('mailshots_db_name'),
'MEMBERS_REMOTE_MYSQL_DB' => $getOption('members_db_name'),
'FEN_REMOTE_MYSQL_DB' => $getOption('fen_db_name'),
'MYSQL_HOST' => $readSetupValue('db_host'),
'MYSQL_PORT' => $readSetupValue('db_port'),
'MYSQL_USER' => $readSetupValue('db_user'),
'MYSQL_PASSWORD' => $readSetupValue('db_password'),
'MAILSHOTS_REMOTE_MYSQL_DB' => $readSetupValue('mailshots_db_name'),
'MEMBERS_REMOTE_MYSQL_DB' => $readSetupValue('members_db_name'),
'FEN_REMOTE_MYSQL_DB' => $readSetupValue('fen_db_name'),
];
$wp = new ProductionWordPressFacade();
if ($setupErrors === [] && function_exists('is_admin') && is_admin()) {
$target = MailshotSchemaInstaller::migrationTarget($dbConfig);
$state = get_option(MailshotSchemaInstaller::STATE_OPTION_KEY, []);
$sameTarget = is_array($state)
&& ($state['version'] ?? '') === $target['version']
&& ($state['config_hash'] ?? '') === $target['config_hash'];
if ($sameTarget && ($state['status'] ?? '') === 'failed') {
$setupErrors[] = 'Mailshots database schema setup failed: ' . (string) ($state['error'] ?? 'unknown error');
} elseif (!$sameTarget || ($state['status'] ?? '') !== 'complete') {
try {
(new MailshotSchemaInstaller(new PdoDatabaseRouter($dbConfig)))->install();
update_option(MailshotSchemaInstaller::STATE_OPTION_KEY, $target + ['status' => 'complete']);
} catch (\Throwable $e) {
update_option(MailshotSchemaInstaller::STATE_OPTION_KEY, $target + [
'status' => 'failed',
'error' => $e->getMessage(),
]);
$setupErrors[] = 'Mailshots database schema setup failed: ' . $e->getMessage();
}
}
}
if ($setupErrors !== []) {
$setupPage = new \FecaMailshots\Admin\SetupAdminPage($wp, array_values(array_unique($setupErrors)), true);
$setupPage->register();

View File

@ -84,6 +84,10 @@ Hard constraints for this task:
## Dependency Installation Scripts
Runtime Composer dependencies must be autoloaded lazily when Twig or Dompdf is first used. Ordinary WordPress/admin bootstrap requests must not load Composer-generated runtime function files.
Mailshot database schema installation/checking must run only when the schema version or database configuration changes. Normal page reads must not execute `CREATE TABLE`, `ALTER TABLE`, or `information_schema` migration checks.
- `scripts/install_dependencies.sh`
- Installs Composer dependencies (`vendor/`) for plugin runtime.
- Installs/bundles editor assets (Jodit + Ace) into `feca_mailshots_plugin/assets/vendor/`.

View File

@ -300,6 +300,8 @@ Optional:
Provide a UI to view, create, update, validate, and preview DSL sentences.
The initial Data Sources page GET must load only the saved query list and built-in source names. Schema enumeration and source-column discovery must be requested lazily within the Build DSL dialog when the user opens or uses the relevant controls; failures must be displayed inside that dialog.
### Mailshot data source page
Add a dedicated page "Data Sources" under FECA Mailshots admin page for managing data source sentences.

View File

@ -1,6 +1,43 @@
import { test, expect } from '@playwright/test';
import { adminPath, apiPost, cleanupByNames, ensureDataSource } from './helpers.mjs';
test('data sources: builder metadata is lazy and browser runtime is clean', async ({ page }) => {
const runtimeErrors = [];
const metadataRequests = [];
page.on('pageerror', error => runtimeErrors.push(error.message));
page.on('console', message => {
if (message.type() === 'error') {
runtimeErrors.push(message.text());
}
});
page.on('request', request => {
const url = request.url();
if (url.includes('op=schemas') || url.includes('op=source_fields')) {
metadataRequests.push(url);
}
});
await page.goto(adminPath('feca-mailshot-data-sources'));
await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible();
expect(metadataRequests).toHaveLength(0);
await page.getByRole('button', { name: 'New Data Source' }).click();
await page.locator('#ds-build-dsl-open').click();
await expect(page.locator('#ds-builder-modal')).toBeVisible();
await expect.poll(() => metadataRequests.filter(url => url.includes('op=schemas')).length).toBe(1);
await expect(page.locator('#ds-builder-schema option')).not.toHaveCount(1);
await page.locator('.ds-source-built[value="contacts"]').check();
await page.locator('#ds-builder-add-row').click();
const row = page.locator('.feca-builder-row').first();
await row.locator('select').first().selectOption('compare');
await row.locator('select').nth(1).selectOption('contacts');
await expect.poll(() => metadataRequests.filter(url => url.includes('op=source_fields')).length).toBe(1);
await expect(row.locator('select').nth(2).locator('option')).not.toHaveCount(1);
expect(runtimeErrors).toEqual([]);
});
test('data sources: create + preview + validation failure path', async ({ page, request }) => {
const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`;
const dsName = `e2e_ds_${uniq}`;

View File

@ -7,13 +7,11 @@ if (!defined('ABSPATH')) {
}
$GLOBALS['feca_bootstrap_actions'] = [];
$GLOBALS['feca_setup_option_reads'] = 0;
function get_option($key, $default = null)
{
if ($key !== \FecaMailshots\Admin\SetupAdminPage::OPTION_KEY) {
return $default;
}
return [
$config = [
'db_host' => 'invalid-host.local.test',
'db_port' => '3306',
'db_user' => 'mailshots',
@ -22,6 +20,28 @@ function get_option($key, $default = null)
'members_db_name' => 'members',
'fen_db_name' => 'fen',
];
if ($key === \FecaMailshots\Admin\SetupAdminPage::OPTION_KEY) {
$GLOBALS['feca_setup_option_reads']++;
return $config;
}
if ($key === \FecaMailshots\Infrastructure\MailshotSchemaInstaller::STATE_OPTION_KEY) {
$dbConfig = [
'MYSQL_HOST' => $config['db_host'],
'MYSQL_PORT' => $config['db_port'],
'MYSQL_USER' => $config['db_user'],
'MYSQL_PASSWORD' => $config['db_password'],
'MAILSHOTS_REMOTE_MYSQL_DB' => $config['mailshots_db_name'],
'MEMBERS_REMOTE_MYSQL_DB' => $config['members_db_name'],
'FEN_REMOTE_MYSQL_DB' => $config['fen_db_name'],
];
return \FecaMailshots\Infrastructure\MailshotSchemaInstaller::migrationTarget($dbConfig) + ['status' => 'complete'];
}
return $default;
}
function is_admin(): bool
{
return true;
}
function add_action(string $hook, callable $callback): void
@ -47,5 +67,15 @@ if (!isset($actions['admin_post_feca_mailshots_data_sources_api'], $actions['adm
fwrite(STDERR, "Expected normal handlers to register without touching the database\n");
exit(1);
}
if ($GLOBALS['feca_setup_option_reads'] !== 1) {
fwrite(STDERR, "Expected setup configuration to be read exactly once during bootstrap\n");
exit(1);
}
foreach (get_included_files() as $includedFile) {
if (strpos($includedFile, '/vendor/thecodingmachine/safe/') !== false) {
fwrite(STDERR, "Composer runtime function files should remain unloaded during admin bootstrap\n");
exit(1);
}
}
echo "Bootstrap full setup no DB touch regression test passed\n";