diff --git a/AGENTS.md b/AGENTS.md index 248d40e..296bd64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ wordpress-framework/ ├── phpstan.shared.neon PHPStan over packages/shared and tests/PHPStan/Shared, with no WordPress symbol known ├── phpstan.wordpress.neon PHPStan over packages/core and packages/settings, with the WordPress stubs and extensions ├── phpstan.woocommerce.neon PHPStan over packages/woocommerce/src, with the WordPress and WooCommerce stubs -├── phpstan.bootstrap.neon PHPStan over packages/bootstrap at the oldest PHP and WordPress it runs on, which its `phpVersion` and `WPCompat.requiresAtLeast` set +├── phpstan.bootstrap.neon PHPStan over packages/bootstrap and consumer-a's safeguard at the oldest PHP and WordPress the bootstrap package runs on, which its `phpVersion` and `WPCompat.requiresAtLeast` set ├── .wp-env.json developer environment, as local, on port 8800; .wp-env.tests.json the Integration environment, as production; both single sites with WooCommerce ├── .wp-env.multisite.json a network without WooCommerce for the `multisite` and `without-woocommerce` groups; .wp-env.php74.json a site on PHP 7.4 with WordPress 6.3 for the `Php74` suite; both as production └── composer.json one `packages/*` path repository @@ -60,7 +60,7 @@ composer test:unit composer fixtures:build # builds consumer-a, consumer-b, then the five personas: each a development install that scopes, its PHPStan, then its production install composer test:integration # starts .wp-env.tests.json, then runs test:integration:run: fixtures:build, then Integration tests without the multisite and without-woocommerce groups; WP_ENVIRONMENT_TYPE=production composer test:integration:multisite # starts .wp-env.multisite.json, then runs test:integration:multisite:run: fixtures:build, then the multisite and without-woocommerce groups -composer test:integration:php74 # starts .wp-env.php74.json, then runs test:integration:php74:run: the bootstrap persona's build, then the Php74 suite with the host's PHP +composer test:integration:php74 # starts .wp-env.php74.json, then runs test:integration:php74:run: consumer-a's and the bootstrap persona's builds, then the Php74 suite with the host's PHP composer test:integration:lowest # starts .wp-env.tests.json, then runs test:integration:lowest:run: prefer-lowest.sh on consumer-a, its build, then its rows; rewrites its manifest and lock, which git checkout restores composer audit --locked && npm audit --omit=dev --audit-level=high # every Composer package, but only npm's runtime packages, of which there are none actionlint && zizmor .github/ @@ -72,7 +72,7 @@ actionlint && zizmor .github/ 2. **Nodes and the container.** Node classes never extend, implement or `use` optional or late-loaded types. Never constructor-inject a gated component; inject ungated services. Every injected service that implements `ComponentInterface` is also a node, because only nodes run `register_hooks()`. Definitions are static closures, taking `ContainerInterface $c` only when they read the container, or autowiring, never PHP-DI helpers. Hold the built container as `ContainerInterface`. Container reads belong in the kernel and composition wiring: a class-entry read checks the entry's type before use, and a factory collecting contributors re-applies their `should_load()` before resolving them. Framework classes take their logger injected; the composition root chooses the logger. A failure is logged with its Throwable under `exception`, a `WP_Error`'s Throwable data included. The uninstall engine runs without a composition root, so it writes the jobs it could not unschedule to PHP's error log, one line per site. 3. **Constructors and `register_hooks()`.** Constructors take services only. Plugins boot on `plugins_loaded` at priority 15, after `woocommerce_loaded`, Action Scheduler's priorities 0 and 1 and `woocommerce_blocks_loaded` have fired, so work that waits for them hooks `init` or later. `register_hooks()` adds `array( $this, 'method' )` hooks, or constructs an `OptionsPage`, `ProductDataTab` or `ObjectForm` and calls its `register_hooks()`. It never fires actions, applies the plugin's own filters or reads a registry filled through them, because nodes later in the walk have not registered yet. Registration-time hooks register unconditionally and authorize in the callback. Scheduled-hook callbacks register on every request, because Action Scheduler runs actions in admin-ajax, WP-Cron and WP-CLI requests, and tolerate a duplicate or a missing invocation. 4. **Packages and `Shared\`.** Package edges stay as in the file map: a package requires only the layer below it, declaring a lower one only to raise its floor, and builds on everything beneath it, as woocommerce also builds on WooCommerce; a new edge needs the owner. Settings and WC helpers take data and a definition object, hosts take data, and neither implements a core type, because the kernel builds nodes by class-string. Every reachable `Shared\` symbol is API, and WordPress never enters the shared package. Shared holds kernel types that code without core can use; a primitive several packages need lives in the lowest of them, public only when it fills a gap that PHP, its extensions and PHPStan's typing leave, with its siblings decided together. A kernel type enters only when it would work unchanged in any PHP project, and on the owner's ruling. Result serves only an internal operation with two or more expected failure reasons a caller branches on, declared as `AbstractResult` under `#[\NoDiscard]`; WordPress and WooCommerce APIs, filters, REST and AJAX responses and v1 public functions use `X|WP_Error`. Carriers are `final readonly`; one whose identity is its values extends `AbstractValueObject`, while a snapshot of stored data does not. `Version` parses only versions the plugin controls, never on the boot path. Framework code throws the `Shared\Exception` mirrors. A runtime check stays where a violation fails silently, can lose or expose data, or breaks a standard the class implements. A violation the platform tolerates harmlessly gets no runtime check, at most a PHPStan type. -5. **Safeguards.** Everything a safeguard calls lives in `includes/safeguards.php` or files it requires. Relaxation happens only through the safeguard's own filter, after `{prefix}/boot/succeeded`, on an explicit `false`. +5. **Safeguards.** Everything a safeguard calls lives in `includes/safeguards.php` or files it requires. The entry loads that file before `load.php` and the floor gate, so a safeguard runs on any runtime that parses the plugin: it parses on PHP 7.4 and calls only WordPress APIs present at the oldest WordPress the bootstrap package runs on, which `phpstan.bootstrap.neon` sets, or guards them. Relaxation happens only through the safeguard's own filter, after `{prefix}/boot/succeeded`, on an explicit `false`. 6. **Shared logic.** `src/` holds a plugin's composition root and its components. Code that registers no hooks and is not part of the tree, such as option keys and readers, the footprint, migration steps and asset registrars, is prefixed functions and constants in `includes/`, which `functions.php` loads. Per-request state lives in a static holder or `WC()->session`. No service locator. 7. **Migration steps.** Append-only; a retired step keeps its code or throws. Converge from any partial state; verify writes and throw on failure, including a scheduler write that returns `WP_Error`. Role capabilities are granted by `Permission\CapabilityGrants`, never by a step. Create resources resumably (lookup marker, explicit `post_author`). Use WC CRUD for orders and products. A plain step finishes well under 10 minutes; bulk work is a `BatchedMigrationStepInterface` step, run only from the runner's scheduled hook, at about 1–2 s per batch. Steps run on `wp_loaded`, so every `init` registration exists. One linear list serves every build variant. 8. **Settings.** Full keys are the plugin's prefixed option constants; WC ids are flat. Boot-time defaults are constants; translated defaults come from the plugin's text reader after `init`. Validate on write, and guard every typed read: `filter_var( $raw, FILTER_VALIDATE_*, FILTER_NULL_ON_FAILURE ) ?? $default`, or a strict `in_array()` for a choice. Direct meta writes go through `wp_slash()`. The framework detects a failed query only where it runs the query itself; a WordPress or Action Scheduler API's failure is accepted as the API reports it. The uninstall engine is the exception, because it keeps consent until a pass runs clean. Framework components store data only under the consumer's `{prefix}_`, so the plugin's footprint covers it; a `TransientCache` prefix and any generation option it names start with it, so the footprint covers the generation option, and its entries, transients that all carry a timeout, expire on their own. An `ObjectCache` group starts with it too, which keeps it apart from core's and other plugins' groups. The bootstrap updater's release caches carry the text domain instead and expire on their own. WooCommerce multiselect option keys are truthy, because WooCommerce's save drops `'0'` and `''`. @@ -108,8 +108,8 @@ Releases follow SemVer on this contract from the first tag: a break of it ships 2. A request killed mid-migration holds the lock until 10 minutes after the step or batch it was running began, then the step re-runs and the error row names the step that did not finish. One primary database is assumed. While a plain step's failure persists in production, each request issues one `INSERT IGNORE` plus one `SELECT`, one more `SELECT` until a first step succeeds, and admin screens read the error row; in local and development environments the lock is released, so every request re-runs the failing step. While a batched step is pending, a request outside the runner's hook takes no lock and only looks the hook up in the scheduler. A WP-CLI process is one request, so once it has spent the runner's time budget, a long `wp action-scheduler run` only reschedules the runner's hook each time it comes due, and the next process continues the migration. After a rollback, older code runs on newer data until the plugin is upgraded again; the written-back revision then re-runs the newer steps, and `{option}_high_water` keeps the highest revision reached. Mixed-version rolling deploys may re-run steps, which converge. 3. Uninstall visits every site of a network and cleans each one that consents. The engine needs the plugin's PHP and WordPress floors, so where a site fell below them a web request deletes the plugin's files and leaves its data and cron events until the plugin is installed and deleted again on a compliant runtime, while WP-CLI fails and keeps the files. A throw from another plugin's hook ends that site's pass, and the rest of its footprint and its consent row wait for the next uninstall. A web request on a large network refuses and names `wp plugin uninstall`; on a network of thousands of sites, or on a single site whose footprint holds many rows, a web request can still exceed the server's time limit, and a retry or `wp plugin uninstall` completes it. Where the object cache cannot flush its runtime cache, every visited site's options stay in memory until the request ends. Network options and network-global user meta go only on a single site or on the consent constant. On an installation of several networks, the network pass deletes every network's options but clears only the current network's cached values. Action Scheduler actions stay queued when it is not loaded at uninstall, and fail once it runs them, since no callback is registered; on a site without its tables, unscheduling logs a database error and does nothing else; a job that cannot be unscheduled is logged and left. 4. Over WC REST, `validate` receives WooCommerce's normalized value: unknown multiselect choices are already dropped and text already sanitized. A rejected value is not stored, but the response echoes it. Grouped `option[key]` ids would bypass the bridge, hence flat ids. WC REST lists only WooCommerce's own field types, so custom-type and `checkboxes` fields are not reachable there. A product field's `validate` runs only on the classic product edit form; WC REST `meta_data`, CSV import and CRUD writes store what they are given. An object form field's `validate` runs only on its form; REST, imports, meta and order CRUD writes store what they are given, and so does a Custom Fields box for a key without a leading underscore: core's on a post type that supports `custom-fields`, orders in the posts table included, and WooCommerce's on the order tables' edit screen. WC-rendered controls carry WooCommerce's markup, and the row printed around a custom-type or `checkboxes` field on a WooCommerce settings screen or the product data panel imitates it. On a WooCommerce settings screen a field's `render` callable needs a type WooCommerce does not draw: on one of its own types WooCommerce draws its control and the callable never runs. The framework guarantees markup only for `Forms::render_control()`. `OptionsPage` and `ProductDataTab` report a rejected field in the page's notices, not beside the field, which after the redirect shows its stored value rather than the rejected input. `ObjectForm` reports it as a one-shot user notice through the plugin's `AdminNotices`, shown in the post editor after the save, or else on the user's next admin screen of the site it was saved on, the network and user admin counting as the main site's; a newer rejection of the same field, on any object, replaces it, and a rejection whose notice fails to save goes unreported. On the profile and in the block editor, which never validate the form, a number the browser cannot parse posts as empty and clears the stored value. With WooCommerce's beta `settings-ui` feature on, the sections a framework tab defines stay classic, and a framework section registered on any tab renders in its Settings UI, which saves through the same form post. A field the Settings UI's schema refuses, such as a `checkboxes` or custom-type field holding an array or a radio listing disabled choices, renders that section classic, with a `_doing_it_wrong` notice under `WP_DEBUG`; a field it has no control for, such as one of those types holding no array or WooCommerce's `color`, makes the page fail to render and offer WooCommerce's "Use classic settings" link, under which every field saves. On a WooCommerce settings screen a cleared checkbox or multiselect posts nothing, so one the section does not render at save is stored cleared. -5. Partial bootstrap package: if `load.php` exists but a file it requires is missing, the `Error` is thrown before the template's catch and the plugin fatals at include; its uninstall shim catches it, except on PHP 7.4, where a failed `require` is fatal. A missing `vendor-prefixed/` also unregisters the updater and skips the floor gate, so safeguards then load on whatever PHP runs (two faults). Core and woocommerce print their boot and migration notices through bootstrap's `Requirements`, so a plugin that boots them without requiring `load.php` fatals when one of those notices renders. Update offers come only from WordPress's own plugin update check. While its wordpress.org request fails, the check offers no Update URI release, so a new release waits for a later check that succeeds. A plugin active only on sites other than the main one is never offered a release, because WordPress checks only on the main site and in the network admin. -6. Deactivation, recovery mode and below-floor runtimes unload safeguards, as for any plugin. Plugins whose data would leak (IC) hide it at the data level. +5. Partial bootstrap package: if `load.php` exists but a file it requires is missing, the `Error` is thrown before the template's catch and the plugin fatals at include; its uninstall shim catches it, except on PHP 7.4, where a failed `require` is fatal. A missing `vendor-prefixed/` unregisters the updater. Core and woocommerce print their boot and migration notices through bootstrap's `Requirements`, so a plugin that boots them without requiring `load.php` fatals when one of those notices renders. Update offers come only from WordPress's own plugin update check. While its wordpress.org request fails, the check offers no Update URI release, so a new release waits for a later check that succeeds. A plugin active only on sites other than the main one is never offered a release, because WordPress checks only on the main site and in the network admin. +6. Deactivation and recovery mode unload safeguards, as for any plugin. Plugins whose data would leak (IC) hide it at the data level. 7. Scheduling and one-shot notices are best effort. Either scheduler can run an invocation twice or not at all, and work advances only with site traffic, Action Scheduler's admin dispatch, or a system cron or WP-CLI runner. On hosts that replace WP-Cron's store, the WP-Cron backend still reads only the `cron` option, so it may not see the host's events. Action Scheduler compares hook names and arguments under the database collation, so identities that differ only in letter case are one there. Action Scheduler stores a string argument holding invalid UTF-8 with those bytes replaced, which its callbacks then receive, while WP-Cron's write of it fails, because WordPress refuses invalid UTF-8 in the options table. Two concurrent renders can both show a one-shot notice, a request that dies after output shows it again, and a block-editor delivery whose response the editor never receives, as when the user leaves during it, loses its one-shots. Block-editor screens hide classic notices: the post editor fetches the site, network and user one-shots that name no screens as it loads and after each save that is not an autosave, while persistent and screen-limited notices wait for a classic screen, as every notice does on the other block editors. A dismissal, or a one-shot's showing, applies to the notice's id rather than to the copy the page showed, so a notice saved again under that id after the page showed it is deleted or hidden too. Two writes to one scope's notices, or to one viewer's dismissals, at the same moment can lose one of them: a notice added then never shows, a dismissal is undone, or a consumed one-shot shows once more. A notice whose write fails is lost, and logged only when the write throws. 8. Capability grants act on `wp_loaded` after the declaration changes or a declared role appears or is deleted. A granted capability removed or denied by hand stays so; a declared role deleted and created again is granted again, unless it was deleted while the plugin was inactive, which then reads as a hand removal. WP_Roles writes the whole roles row from the request's copy, so another request's role write in between is lost. A change the row does not keep is logged and made again on every request, which never ends on a site whose roles are defined in code (`$wp_user_roles`) or locked. 9. `TransientCache` and `ObjectCache` are best effort. On the options table a value holding invalid UTF-8 is refused and `set()` returns false, and a `delete()` whose database write fails still returns true, so later requests read the entry until it expires or is set again. An object whose class no longer loads reads back as `__PHP_Incomplete_Class`, so a plugin clears its caches when cached classes change. `getMultiple()` returns a decimal-string key as an integer, as PHP arrays do. `clear()` leaves earlier entries in place until they expire or the object cache evicts them, and behind a multi-node object cache that is not coherent, a node may read the old generation or salt until the new one reaches it. Without a persistent object cache, `ObjectCache` entries last one request, and an entry outlives its TTL until the request ends; some Memcached drop-ins keep a TTL over 30 days until eviction. Concurrent misses all recompute; nothing locks a key. diff --git a/README.md b/README.md index aafc80a..a8db4a5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ composer quality-check # Validation, PHPCS, PHPStan, Unit and Integration t | `test:integration:multisite` | Starts the multisite environment, then runs `test:integration:multisite:run` | | `test:integration:multisite:run` | Builds the fixture plugins and personas, then runs the `multisite` and `without-woocommerce` groups in the running multisite environment: a network without WooCommerce, as production | | `test:integration:php74` | Starts the PHP 7.4 environment, then runs `test:integration:php74:run` | -| `test:integration:php74:run` | Builds the bootstrap persona, then runs the `Php74` suite with the host's PHP against the running PHP 7.4 environment: a single site with WordPress 6.3, as production | +| `test:integration:php74:run` | Builds consumer-a and the bootstrap persona, then runs the `Php74` suite with the host's PHP against the running PHP 7.4 environment: a single site with WordPress 6.3, as production | | `test:integration:lowest` | Starts the tests environment, then runs `test:integration:lowest:run` | | `test:integration:lowest:run` | Locks consumer-a's runtime packages at the lowest versions its constraints allow, builds it, then runs its rows in the running tests environment; its rewritten manifest and lock are restored with `git checkout` | | `test` | Unit and Integration tests | @@ -61,5 +61,5 @@ composer quality-check # Validation, PHPCS, PHPStan, Unit and Integration t - **Personas** (`tests/Fixtures/personas/`): one per stack, each requiring only the top package of its stack and built like the fixture plugins, so each production build holds exactly its scoped closure and nothing unscoped. The shared persona runs in plain PHP; the core persona boots its components and runs a migration, and the settings persona saves and rejects a value through `options.php`, both where WooCommerce is absent; the woocommerce persona saves a settings tab and product data through WooCommerce's screens and logs to WooCommerce's log; the bootstrap persona runs on PHP 7.4, which many sites still run. - **Prefer-lowest lane** (`tests/bin/prefer-lowest.sh`): consumer-a runs with its runtime packages locked at the lowest versions its constraints allow, so a minimum too low to work fails before a consumer installs it. For the core, settings and woocommerce personas, which like every persona list their stack's mirrors as `vcs` repositories, the script keeps only the tested package on the path repository and locks every other framework package from its mirror at the lowest allowed tag, so they run only where the mirrors carry release tags. - **CI** runs the Integration tests once for the newest release of every WordPress branch from the floor up, read from WordPress's version API, so a new release joins without a change here; WordPress trunk runs nightly, outside pull requests. -- **PHP 7.4 tests** (`tests/Php74/`) run with the host's PHP against a PHP 7.4 site with WordPress 6.3, through WP-CLI in its environment: the bootstrap persona passes its floors there, and a copy that needs a newer PHP shows WordPress core's message through the notice markup WordPress before 6.4 needs, both with the updater hooked. +- **PHP 7.4 tests** (`tests/Php74/`) run with the host's PHP against a PHP 7.4 site with WordPress 6.3, through WP-CLI in its environment: the bootstrap persona passes its floors there, and a copy that needs a newer PHP shows WordPress core's message through the notice markup WordPress before 6.4 needs, both with the updater hooked; consumer-a, below its floors there, keeps its safeguard's export lock. - **Type tests** (`tests/PHPStan/`) are files PHPStan analyzes during `lint:php`: `assertType()` calls pin inferred types, and each expected error carries a `@phpstan-ignore` naming its identifier, so the run fails when the error disappears. diff --git a/composer.json b/composer.json index 65a4e43..b433a41 100644 --- a/composer.json +++ b/composer.json @@ -187,6 +187,7 @@ "test:integration:php74:run": [ "Composer\\Config::disableProcessTimeout", "@putenv CI=1", + "@fixtures:build:consumer-a", "@fixtures:build:persona-bootstrap", "@php vendor/bin/phpunit --testsuite=Php74" ], diff --git a/composer.lock b/composer.lock index eeb2b61..5fe2311 100644 --- a/composer.lock +++ b/composer.lock @@ -12,7 +12,7 @@ "dist": { "type": "path", "url": "packages/bootstrap", - "reference": "549e875e34420ef69d260fc94a54772a5af31726" + "reference": "d7fbbb1be6d0467f8a25d10357daa2844d27c89c" }, "require": { "php": ">=7.4" @@ -40,7 +40,7 @@ "dist": { "type": "path", "url": "packages/core", - "reference": "c78cee38e43224154323f25fe7530225182cc140" + "reference": "5077e005c70673ef1edcb5eb3f9ef2a5815c1ac8" }, "require": { "ahegyes/wp-framework-bootstrap": "^2.0", @@ -81,7 +81,7 @@ "dist": { "type": "path", "url": "packages/settings", - "reference": "5b3c5a812ecee149477bca4f1e870960ae644f07" + "reference": "0ce4068423bab13b7c8b3a3a695cda1f1cddd341" }, "require": { "ahegyes/wp-framework-core": "^2.0", @@ -116,7 +116,7 @@ "dist": { "type": "path", "url": "packages/shared", - "reference": "d73b27fac3415878627d2fc7c1229e94cb4c59fb" + "reference": "819a2f4e24281546eb8447361e91eead55aaccdf" }, "require": { "php": ">=8.5" @@ -148,7 +148,7 @@ "dist": { "type": "path", "url": "packages/woocommerce", - "reference": "e6b08a65c6cdd3cf1cbab2e7175c905d6712d8cb" + "reference": "889aeeb7081f84560297e058a5cf5723ae7aefaa" }, "require": { "ahegyes/wp-framework-settings": "^2.0", @@ -229,7 +229,8 @@ "issues": "https://github.com/php-fig/clock/issues", "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2022-11-25T14:36:26+00:00" + "time": "2022-11-25T14:36:26+00:00", + "published-time": "2022-11-25T14:59:22+00:00" }, { "name": "psr/container", @@ -282,7 +283,8 @@ "issues": "https://github.com/php-fig/container/issues", "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2021-11-05T16:47:00+00:00", + "published-time": "2021-11-05T16:53:27+00:00" }, { "name": "psr/log", @@ -332,7 +334,8 @@ "support": { "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2024-09-11T13:17:53+00:00", + "published-time": "2024-09-11T13:21:14+00:00" }, { "name": "psr/simple-cache", @@ -383,7 +386,8 @@ "support": { "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "time": "2021-10-29T13:26:27+00:00" + "time": "2021-10-29T13:26:27+00:00", + "published-time": "2021-10-29T13:27:19+00:00" } ], "packages-dev": [ @@ -393,12 +397,12 @@ "source": { "type": "git", "url": "https://github.com/ahegyes/wordpress-configs.git", - "reference": "f420c38db593b332d58a481eb0f008f8d36d0cc4" + "reference": "8d53efd292e4abee4a9c57b0e379434f54bedbe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ahegyes/wordpress-configs/zipball/f420c38db593b332d58a481eb0f008f8d36d0cc4", - "reference": "f420c38db593b332d58a481eb0f008f8d36d0cc4", + "url": "https://api.github.com/repos/ahegyes/wordpress-configs/zipball/8d53efd292e4abee4a9c57b0e379434f54bedbe9", + "reference": "8d53efd292e4abee4a9c57b0e379434f54bedbe9", "shasum": "" }, "require": { @@ -519,7 +523,7 @@ "source": "https://github.com/ahegyes/wordpress-configs/tree/trunk", "issues": "https://github.com/ahegyes/wordpress-configs/issues" }, - "time": "2026-10-29T19:18:37+00:00" + "time": "2026-10-30T11:34:20+00:00" }, { "name": "automattic/jetpack-changelogger", @@ -586,7 +590,8 @@ "support": { "source": "https://github.com/Automattic/jetpack-changelogger/tree/v7.0.5" }, - "time": "2026-10-20T16:01:37+00:00" + "time": "2026-10-20T16:01:37+00:00", + "published-time": "2026-10-20T16:01:50+00:00" }, { "name": "composer/class-map-generator", @@ -655,7 +660,8 @@ "type": "github" } ], - "time": "2026-09-25T15:24:19+00:00" + "time": "2026-09-25T15:24:19+00:00", + "published-time": "2026-09-25T15:27:30+00:00" }, { "name": "composer/installers", @@ -801,7 +807,8 @@ "type": "tidelift" } ], - "time": "2024-06-24T20:46:46+00:00" + "time": "2024-06-24T20:46:46+00:00", + "published-time": "2024-06-24T21:01:36+00:00" }, { "name": "composer/pcre", @@ -877,7 +884,8 @@ "type": "github" } ], - "time": "2026-06-07T11:47:49+00:00" + "time": "2026-06-07T11:47:49+00:00", + "published-time": "2026-06-11T12:12:37+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -973,7 +981,8 @@ "type": "thanks_dev" } ], - "time": "2026-05-06T08:26:05+00:00" + "time": "2026-05-06T08:26:05+00:00", + "published-time": "2026-05-06T12:56:03+00:00" }, { "name": "johnbillion/wp-compat", @@ -1048,7 +1057,8 @@ "type": "github" } ], - "time": "2026-10-21T20:54:48+00:00" + "time": "2026-10-21T20:54:48+00:00", + "published-time": "2026-10-21T20:54:55+00:00" }, { "name": "johnpbloch/wordpress-core", @@ -1096,7 +1106,8 @@ "source": "https://core.trac.wordpress.org/browser", "wiki": "https://codex.wordpress.org/" }, - "time": "2026-10-28T19:49:26+00:00" + "time": "2026-10-28T19:49:26+00:00", + "published-time": "2026-10-28T19:49:51+00:00" }, { "name": "myclabs/deep-copy", @@ -1156,7 +1167,8 @@ "type": "github" } ], - "time": "2026-08-11T10:17:44+00:00" + "time": "2026-08-11T10:17:44+00:00", + "published-time": "2026-08-11T10:29:20+00:00" }, { "name": "nikic/php-parser", @@ -1213,7 +1225,8 @@ "issues": "https://github.com/nikic/PHP-Parser/issues", "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2026-09-13T18:51:52+00:00" + "time": "2026-09-13T18:51:52+00:00", + "published-time": "2026-09-13T18:53:16+00:00" }, { "name": "phar-io/manifest", @@ -1280,7 +1293,8 @@ "type": "github" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2024-03-03T12:33:53+00:00", + "published-time": "2024-03-03T12:39:44+00:00" }, { "name": "phar-io/version", @@ -1331,7 +1345,8 @@ "issues": "https://github.com/phar-io/version/issues", "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2022-02-21T01:04:05+00:00" + "time": "2022-02-21T01:04:05+00:00", + "published-time": "2022-02-21T01:04:30+00:00" }, { "name": "php-stubs/woocommerce-stubs", @@ -1375,7 +1390,8 @@ "issues": "https://github.com/php-stubs/woocommerce-stubs/issues", "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v11.2.0" }, - "time": "2026-10-07T19:02:59+00:00" + "time": "2026-10-07T19:02:59+00:00", + "published-time": "2026-10-07T19:04:59+00:00" }, { "name": "php-stubs/wordpress-stubs", @@ -1427,7 +1443,8 @@ "issues": "https://github.com/php-stubs/wordpress-stubs/issues", "source": "https://github.com/php-stubs/wordpress-stubs/tree/v7.1.2" }, - "time": "2026-10-06T06:59:21+00:00" + "time": "2026-10-06T06:59:21+00:00", + "published-time": "2026-10-06T07:32:21+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -1517,7 +1534,8 @@ "type": "thanks_dev" } ], - "time": "2025-11-28T11:36:33+00:00" + "time": "2025-11-28T11:36:33+00:00", + "published-time": "2025-11-28T11:41:33+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", @@ -1588,7 +1606,8 @@ "type": "thanks_dev" } ], - "time": "2025-11-29T13:09:49+00:00" + "time": "2025-11-29T13:09:49+00:00", + "published-time": "2025-11-29T13:22:17+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", @@ -1655,7 +1674,8 @@ "type": "thanks_dev" } ], - "time": "2025-12-16T13:35:20+00:00" + "time": "2025-12-16T13:35:20+00:00", + "published-time": "2025-12-16T13:47:29+00:00" }, { "name": "phpcsstandards/phpcsextra", @@ -1737,7 +1757,8 @@ "type": "thanks_dev" } ], - "time": "2026-07-27T11:13:17+00:00" + "time": "2026-07-27T11:13:17+00:00", + "published-time": "2026-07-27T11:19:59+00:00" }, { "name": "phpcsstandards/phpcsutils", @@ -1830,7 +1851,8 @@ "type": "thanks_dev" } ], - "time": "2026-07-27T10:28:41+00:00" + "time": "2026-07-27T10:28:41+00:00", + "published-time": "2026-07-27T10:36:31+00:00" }, { "name": "phpstan/extension-installer", @@ -1878,7 +1900,8 @@ "issues": "https://github.com/phpstan/extension-installer/issues", "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" }, - "time": "2024-09-04T20:21:43+00:00" + "time": "2024-09-04T20:21:43+00:00", + "published-time": "2024-09-04T20:22:27+00:00" }, { "name": "phpstan/phpstan", @@ -1942,7 +1965,8 @@ "type": "github" } ], - "time": "2026-10-28T22:38:53+00:00" + "time": "2026-10-28T22:38:53+00:00", + "published-time": "2026-10-28T22:39:06+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -1993,7 +2017,8 @@ "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.5" }, - "time": "2026-07-22T06:50:43+00:00" + "time": "2026-07-22T06:50:43+00:00", + "published-time": "2026-07-27T15:18:00+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -2045,7 +2070,8 @@ "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.0" }, - "time": "2026-10-01T21:30:37+00:00" + "time": "2026-10-01T21:30:37+00:00", + "published-time": "2026-10-06T07:32:29+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2134,7 +2160,8 @@ "type": "tidelift" } ], - "time": "2026-10-30T08:11:15+00:00" + "time": "2026-10-30T08:11:15+00:00", + "published-time": "2026-10-30T08:11:34+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2207,7 +2234,8 @@ "type": "tidelift" } ], - "time": "2026-08-25T14:47:43+00:00" + "time": "2026-08-25T14:47:43+00:00", + "published-time": "2026-08-25T14:48:49+00:00" }, { "name": "phpunit/php-invoker", @@ -2283,7 +2311,8 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:34:47+00:00" + "time": "2026-02-06T04:34:47+00:00", + "published-time": "2026-02-06T04:35:29+00:00" }, { "name": "phpunit/php-text-template", @@ -2355,7 +2384,8 @@ "type": "tidelift" } ], - "time": "2026-10-03T04:54:24+00:00" + "time": "2026-10-03T04:54:24+00:00", + "published-time": "2026-10-03T04:54:39+00:00" }, { "name": "phpunit/php-timer", @@ -2427,7 +2457,8 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:37:53+00:00" + "time": "2026-02-06T04:37:53+00:00", + "published-time": "2026-02-06T04:38:39+00:00" }, { "name": "phpunit/phpunit", @@ -2519,7 +2550,8 @@ "type": "other" } ], - "time": "2026-10-27T04:16:22+00:00" + "time": "2026-10-27T04:16:22+00:00", + "published-time": "2026-10-27T04:18:40+00:00" }, { "name": "roave/security-advisories", @@ -2527,12 +2559,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66" + "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66", - "reference": "7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7c964bc969c1b16050a4b85b7a55614777a3f597", + "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597", "shasum": "" }, "conflict": { @@ -2540,6 +2572,7 @@ "adaptcms/adaptcms": "<=1.3", "adawolfa/isdoc": "<1.4.3|>=1.5,<1.5.1|>=1.6,<1.6.1", "admidio/admidio": "<=5.0.11", + "adminneo-org/adminneo": "<5.5.1", "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", @@ -3688,7 +3721,8 @@ "type": "tidelift" } ], - "time": "2026-10-29T16:37:57+00:00" + "time": "2026-10-31T00:24:47+00:00", + "published-time": "2026-10-31T00:24:51+00:00" }, { "name": "sebastian/cli-parser", @@ -3757,7 +3791,8 @@ "type": "tidelift" } ], - "time": "2026-08-01T04:27:14+00:00" + "time": "2026-08-01T04:27:14+00:00", + "published-time": "2026-08-01T04:29:01+00:00" }, { "name": "sebastian/comparator", @@ -3849,7 +3884,8 @@ "type": "tidelift" } ], - "time": "2026-08-07T07:23:13+00:00" + "time": "2026-08-07T07:23:13+00:00", + "published-time": "2026-08-07T07:23:29+00:00" }, { "name": "sebastian/complexity", @@ -3919,7 +3955,8 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:41:32+00:00" + "time": "2026-02-06T04:41:32+00:00", + "published-time": "2026-02-06T04:41:49+00:00" }, { "name": "sebastian/diff", @@ -3998,7 +4035,8 @@ "type": "tidelift" } ], - "time": "2026-08-25T15:38:55+00:00" + "time": "2026-08-25T15:38:55+00:00", + "published-time": "2026-08-25T15:39:44+00:00" }, { "name": "sebastian/environment", @@ -4074,7 +4112,8 @@ "type": "tidelift" } ], - "time": "2026-10-01T12:37:56+00:00" + "time": "2026-10-01T12:37:56+00:00", + "published-time": "2026-10-01T12:38:48+00:00" }, { "name": "sebastian/exporter", @@ -4164,7 +4203,8 @@ "type": "tidelift" } ], - "time": "2026-08-07T07:22:06+00:00" + "time": "2026-08-07T07:22:06+00:00", + "published-time": "2026-08-07T07:23:47+00:00" }, { "name": "sebastian/file-filter", @@ -4233,7 +4273,8 @@ "type": "tidelift" } ], - "time": "2026-04-22T07:20:04+00:00" + "time": "2026-04-22T07:20:04+00:00", + "published-time": "2026-04-22T07:20:25+00:00" }, { "name": "sebastian/git-state", @@ -4302,7 +4343,8 @@ "type": "tidelift" } ], - "time": "2026-10-21T05:59:52+00:00" + "time": "2026-10-21T05:59:52+00:00", + "published-time": "2026-10-21T06:01:28+00:00" }, { "name": "sebastian/global-state", @@ -4376,7 +4418,8 @@ "type": "tidelift" } ], - "time": "2026-06-01T15:11:33+00:00" + "time": "2026-06-01T15:11:33+00:00", + "published-time": "2026-06-01T15:20:45+00:00" }, { "name": "sebastian/lines-of-code", @@ -4446,7 +4489,8 @@ "type": "tidelift" } ], - "time": "2026-07-09T08:42:34+00:00" + "time": "2026-07-09T08:42:34+00:00", + "published-time": "2026-07-09T08:44:06+00:00" }, { "name": "sebastian/object-enumerator", @@ -4515,7 +4559,8 @@ "type": "tidelift" } ], - "time": "2026-08-13T07:05:05+00:00" + "time": "2026-08-13T07:05:05+00:00", + "published-time": "2026-08-13T07:07:58+00:00" }, { "name": "sebastian/object-reflector", @@ -4583,7 +4628,8 @@ "type": "tidelift" } ], - "time": "2026-08-13T06:34:36+00:00" + "time": "2026-08-13T06:34:36+00:00", + "published-time": "2026-08-13T06:39:22+00:00" }, { "name": "sebastian/recursion-context", @@ -4659,7 +4705,8 @@ "type": "tidelift" } ], - "time": "2026-08-03T05:58:12+00:00" + "time": "2026-08-03T05:58:12+00:00", + "published-time": "2026-08-03T05:59:59+00:00" }, { "name": "sebastian/type", @@ -4728,7 +4775,8 @@ "type": "tidelift" } ], - "time": "2026-08-10T08:00:57+00:00" + "time": "2026-08-10T08:00:57+00:00", + "published-time": "2026-08-10T08:01:43+00:00" }, { "name": "sebastian/version", @@ -4794,7 +4842,8 @@ "type": "tidelift" } ], - "time": "2026-10-04T05:51:14+00:00" + "time": "2026-10-04T05:51:14+00:00", + "published-time": "2026-10-04T05:52:22+00:00" }, { "name": "sniccowp/php-scoper-wordpress-excludes", @@ -4841,7 +4890,8 @@ "issues": "https://github.com/snicco/php-scoper-wordpress-excludes/issues", "source": "https://github.com/snicco/php-scoper-wordpress-excludes/tree/7.1.0" }, - "time": "2026-08-28T02:50:05+00:00" + "time": "2026-08-28T02:50:05+00:00", + "published-time": "2026-09-23T19:33:26+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -4920,7 +4970,8 @@ "type": "thanks_dev" } ], - "time": "2026-08-06T00:17:32+00:00" + "time": "2026-08-06T00:17:32+00:00", + "published-time": "2026-08-06T01:05:02+00:00" }, { "name": "staabm/side-effects-detector", @@ -4972,7 +5023,8 @@ "type": "github" } ], - "time": "2024-10-20T05:08:20+00:00" + "time": "2024-10-20T05:08:20+00:00", + "published-time": "2024-10-20T05:09:39+00:00" }, { "name": "swissspidy/phpstan-no-private", @@ -5024,7 +5076,8 @@ "issues": "https://github.com/swissspidy/phpstan-no-private/issues", "source": "https://github.com/swissspidy/phpstan-no-private/tree/v1.0.0" }, - "time": "2024-11-11T11:04:45+00:00" + "time": "2024-11-11T11:04:45+00:00", + "published-time": "2024-11-11T11:08:03+00:00" }, { "name": "symfony/console", @@ -5122,7 +5175,8 @@ "type": "tidelift" } ], - "time": "2026-10-28T11:08:27+00:00" + "time": "2026-10-28T11:08:27+00:00", + "published-time": "2026-10-28T18:21:33+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5193,7 +5247,8 @@ "type": "tidelift" } ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-06-05T06:23:12+00:00", + "published-time": "2026-06-27T21:46:34+00:00" }, { "name": "symfony/filesystem", @@ -5263,7 +5318,8 @@ "type": "tidelift" } ], - "time": "2026-10-06T20:05:19+00:00" + "time": "2026-10-06T20:05:19+00:00", + "published-time": "2026-10-14T06:45:33+00:00" }, { "name": "symfony/finder", @@ -5331,7 +5387,8 @@ "type": "tidelift" } ], - "time": "2026-10-19T16:56:54+00:00" + "time": "2026-10-19T16:56:54+00:00", + "published-time": "2026-10-28T18:21:14+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5414,7 +5471,8 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-10T16:19:22+00:00", + "published-time": "2026-04-26T14:44:50+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -5496,7 +5554,8 @@ "type": "tidelift" } ], - "time": "2026-10-15T09:18:53+00:00" + "time": "2026-10-15T09:18:53+00:00", + "published-time": "2026-10-28T10:07:44+00:00" }, { "name": "symfony/polyfill-intl-normalizer", @@ -5581,7 +5640,8 @@ "type": "tidelift" } ], - "time": "2026-09-23T22:02:09+00:00" + "time": "2026-09-23T22:02:09+00:00", + "published-time": "2026-09-30T10:23:47+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -5666,7 +5726,8 @@ "type": "tidelift" } ], - "time": "2026-10-28T09:35:49+00:00" + "time": "2026-10-28T09:35:49+00:00", + "published-time": "2026-10-28T10:07:45+00:00" }, { "name": "symfony/process", @@ -5731,7 +5792,8 @@ "type": "tidelift" } ], - "time": "2026-10-25T14:40:16+00:00" + "time": "2026-10-25T14:40:16+00:00", + "published-time": "2026-10-28T18:21:18+00:00" }, { "name": "symfony/service-contracts", @@ -5818,7 +5880,8 @@ "type": "tidelift" } ], - "time": "2026-07-27T15:39:01+00:00" + "time": "2026-07-27T15:39:01+00:00", + "published-time": "2026-08-30T19:56:37+00:00" }, { "name": "symfony/string", @@ -5908,7 +5971,8 @@ "type": "tidelift" } ], - "time": "2026-10-23T17:06:53+00:00" + "time": "2026-10-23T17:06:53+00:00", + "published-time": "2026-10-28T18:24:27+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -5971,7 +6035,8 @@ "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.4" }, - "time": "2026-05-22T16:22:09+00:00" + "time": "2026-05-22T16:22:09+00:00", + "published-time": "2026-08-28T06:48:49+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -6037,7 +6102,8 @@ "type": "custom" } ], - "time": "2026-07-27T11:53:23+00:00" + "time": "2026-07-27T11:53:23+00:00", + "published-time": "2026-07-27T11:55:52+00:00" }, { "name": "wp-hooks/wordpress-core", @@ -6120,7 +6186,8 @@ "type": "github" } ], - "time": "2026-08-20T19:54:29+00:00" + "time": "2026-08-20T19:54:29+00:00", + "published-time": "2026-08-20T19:54:42+00:00" }, { "name": "wp-plugin/woocommerce", @@ -6151,7 +6218,8 @@ "issues": "https://wordpress.org/support/plugin/woocommerce", "source": "https://plugins.svn.wordpress.org/woocommerce" }, - "time": "2026-10-09T15:35:21+00:00" + "time": "2026-10-09T15:35:21+00:00", + "published-time": "2026-10-09T15:35:21+00:00" } ], "aliases": [], @@ -6165,5 +6233,5 @@ "php": ">=8.5" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.11.0" } diff --git a/phpstan.bootstrap.neon b/phpstan.bootstrap.neon index e40b263..fef9ae3 100644 --- a/phpstan.bootstrap.neon +++ b/phpstan.bootstrap.neon @@ -1,4 +1,4 @@ -# The bootstrap package runs before any floor is checked, so it is analyzed at the oldest PHP and WordPress it supports; the WordPress extensions are listed because composer.json keeps them from installing themselves. +# The bootstrap package and consumer-a's safeguard run before any floor is checked, so they are analyzed at the oldest PHP and WordPress the bootstrap package supports; the WordPress extensions are listed because composer.json keeps them from installing themselves. includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon - vendor/johnbillion/wp-compat/extension.neon @@ -8,6 +8,7 @@ includes: parameters: paths: - packages/bootstrap + - tests/Fixtures/consumer-a/includes/safeguards.php phpVersion: 70400 WPCompat: requiresAtLeast: '5.2' diff --git a/tests/Fixtures/consumer-a/acme-consumer-a.php b/tests/Fixtures/consumer-a/acme-consumer-a.php index dfce86e..0db9c20 100644 --- a/tests/Fixtures/consumer-a/acme-consumer-a.php +++ b/tests/Fixtures/consumer-a/acme-consumer-a.php @@ -26,19 +26,17 @@ define( 'ACME_CA_FILE', __FILE__ ); load_plugin_textdomain( 'acme-consumer-a', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- WordPress registers languages/ itself only for plugins it does not network-activate. +require_once __DIR__ . '/includes/safeguards.php'; // Before the floor gate, so the restrictions hold on any runtime that parses the plugin. + $acme_ca_bootstrap = __DIR__ . '/vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php'; -if ( is_readable( $acme_ca_bootstrap ) ) { - require_once $acme_ca_bootstrap; - Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. - if ( ! Requirements::check_floors( __FILE__ ) ) { - return; - } +if ( ! is_readable( $acme_ca_bootstrap ) ) { // The floor gate lives in the bootstrap package, so nothing built may load without it. + add_action( 'all_admin_notices', 'acme_ca_render_build_notice' ); + return; } -require_once __DIR__ . '/includes/safeguards.php'; - -if ( ! is_readable( $acme_ca_bootstrap ) ) { // The floor gate never ran, so nothing built may load. - add_action( 'all_admin_notices', 'acme_ca_render_build_notice' ); +require_once $acme_ca_bootstrap; +Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. +if ( ! Requirements::check_floors( __FILE__ ) ) { return; } diff --git a/tests/Integration/ConsumerA/ConsumerABuildTest.php b/tests/Integration/ConsumerA/ConsumerABuildTest.php index 5731302..d482e0e 100644 --- a/tests/Integration/ConsumerA/ConsumerABuildTest.php +++ b/tests/Integration/ConsumerA/ConsumerABuildTest.php @@ -28,8 +28,9 @@ public static function missing_build_files(): array { public function test_a_missing_build_file_shows_the_build_notice_to_plugin_managers_and_the_safeguard_holds( string $file, bool $warns ): void { $plugin = $this->install_consumer( 'consumer-a', 'acme-consumer-a', array( $file ) ); $spec = array( - 'activate' => $plugin, - 'missing' => array( $file ), + 'activate' => $plugin, + 'missing' => array( $file ), + 'relaxation' => false, ); $admin = $this->consumer_request( 'GET', 'index.php', $this->make_user( 'acme_ca_admin', 'administrator' ), $spec ); @@ -46,15 +47,24 @@ public function test_a_missing_build_file_shows_the_build_notice_to_plugin_manag self::assertStringNotContainsString( self::BUILD_NOTICE, $editor['body'] ); } - public function test_a_site_below_the_php_floor_gets_core_s_message_and_the_updater_while_nothing_else_loads(): void { + public function test_a_site_below_the_php_floor_keeps_the_safeguard_and_gets_core_s_message_and_the_updater_while_nothing_else_loads(): void { $plugin = $this->install_consumer( 'consumer-a', 'acme-consumer-a', array(), array( 'Requires PHP' => '99' ) ); - $request = $this->consumer_request( 'GET', 'index.php', $this->make_user( 'acme_ca_admin', 'administrator' ), array( 'activate' => $plugin ) ); + $request = $this->consumer_request( + 'GET', + 'index.php', + $this->make_user( 'acme_ca_admin', 'administrator' ), + array( + 'activate' => $plugin, + 'relaxation' => false, + ) + ); self::assertNull( $request['died'] ); self::assertStringContainsString( 'id="wpfooter"', $request['body'] ); self::assertStringContainsString( 'does not meet minimum requirements for Acme Consumer A. The plugin requires PHP 99.', $request['body'] ); self::assertTrue( $request['records']['updater_hooked'] ); - self::assertFalse( $request['records']['safeguards_loaded'] ); + self::assertSame( 10, $request['records']['safeguard_attached'] ); + self::assertFalse( $request['records']['can_export'] ); self::assertFalse( $request['records']['consumer_a']['functions_loaded'] ); } } diff --git a/tests/Php74/ConsumerASafeguardTest.php b/tests/Php74/ConsumerASafeguardTest.php new file mode 100644 index 0000000..b3501cd --- /dev/null +++ b/tests/Php74/ConsumerASafeguardTest.php @@ -0,0 +1,20 @@ +php74_request( array( $this->install_on_php74( 'consumer-a', 'acme-consumer-a' ) ) ); + + self::assertSame( array( 'do_not_allow' ), $record['export_caps'] ); + self::assertTrue( $record['updater_hooked'] ); + self::assertStringContainsString( 'do not meet minimum requirements for Acme Consumer A.', $record['notices'] ); + } +} diff --git a/tests/Php74/Support/php74-request.php b/tests/Php74/Support/php74-request.php index 65a5af8..70ea631 100644 --- a/tests/Php74/Support/php74-request.php +++ b/tests/Php74/Support/php74-request.php @@ -1,6 +1,6 @@ get_bloginfo( 'version' ), 'wp_admin_notice' => function_exists( 'wp_admin_notice' ), 'updater_hooked' => false !== has_filter( 'update_plugins_github.com' ), + 'export_caps' => map_meta_cap( 'acme_ca_export_orders', get_current_user_id() ), // phpcs:ignore WordPress.WP.Capabilities.Unknown -- consumer-a's safeguard maps the capability. 'notices' => $acme_notices, ) );