From ce8af887988cae9d18b161804d704fd2839f9e8f Mon Sep 17 00:00:00 2001 From: Akshat Date: Sat, 29 Aug 2026 20:23:25 +0530 Subject: [PATCH 1/2] feat(cli)!: static bootloader + Services locator, drop the DI container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING: the generated plugin's architecture changes. - Plugin is a singleton bootloader: Plugin::instance()->boot() news up every selected module and calls its init_hooks(); boot() is idempotent. No constructor, no provider list, no register()/boot() two-phase. - Services is a static locator — Services::cache(), Services::settings_repository(), etc. build one memoised instance per accessor, with set()/reset() test seams. Only emitted for module sets that need a shared service. - Every module class loses `implements Service_Provider` / `Conditional` and merges register()+boot() into one init_hooks(): void. Dependencies are constructor-injected from Services at the bootloader call site. - WooCommerce modules go inside one `if ( class_exists( 'WooCommerce' ) )` guard in boot() instead of each provider implementing Conditional. - Deleted: Core/Container, Core/Exceptions/Not_Found_Exception, Contracts/Service_Provider, Contracts/Conditional, tests/Unit/Container_Test. Activatable/Deactivatable keep their contract but lose the Container arg. - Activator/Deactivator run `( new X() )->method()` directly; the main file calls Plugin::instance()->boot() on plugins_loaded. - index.js: providerRegistrations -> bootLines / wooBootLines / servicesAccessors; {{PROVIDER_REGISTRATIONS}} -> {{BOOTLOADER_LINES}} + {{SERVICES_ACCESSORS}}. - Tests: Container_Test dropped; Example_Test + Plugin_Boot_Test cover the singleton + idempotent boot; new Services_Test for the locator seams. Verified: 67 generator + 13 engine tests; full all-modules scaffold — php -l clean on every file, no leftover Container/Service_Provider refs, no stray tokens. composer lint / composer test on the new shape run in CI. --- index.js | 115 +++++++++----- templates/plugin-main.php | 10 +- templates/src/Admin/Assets.php | 17 +-- templates/src/Admin/Settings_Registrar.php | 37 ++--- templates/src/Ajax/Ajax_Handler.php | 17 +-- templates/src/Blocks/Block_Registrar.php | 17 +-- templates/src/CLI/Commands.php | 23 +-- templates/src/Cache/Cache_Service.php | 27 +--- templates/src/Contracts/Activatable.php | 5 +- templates/src/Contracts/Conditional.php | 31 ---- templates/src/Contracts/Deactivatable.php | 5 +- templates/src/Contracts/Service_Provider.php | 43 ------ templates/src/Core/Activator.php | 3 +- templates/src/Core/Container.php | 122 --------------- templates/src/Core/Deactivator.php | 3 +- .../Core/Exceptions/Not_Found_Exception.php | 20 --- templates/src/Cron/Scheduler.php | 17 +-- templates/src/Database/Schema.php | 33 ++--- templates/src/Elementor/Dependency_Notice.php | 17 +-- templates/src/Elementor/Widget_Registrar.php | 17 +-- templates/src/Frontend/Interactivity.php | 17 +-- templates/src/Frontend/Shortcode.php | 17 +-- templates/src/Plugin.php | 131 ++++------------ templates/src/PostTypes/Post_Types.php | 20 +-- templates/src/Rest/Rest_Controller.php | 17 +-- templates/src/Services.php | 53 +++++++ .../Providers/Account_Endpoint_Provider.php | 47 +----- .../Providers/Action_Scheduler_Provider.php | 45 +----- .../src/Woo/Providers/Blocks_Provider.php | 26 +--- .../src/Woo/Providers/Email_Provider.php | 26 +--- .../src/Woo/Providers/Gateway_Provider.php | 26 +--- .../Woo/Providers/Order_Status_Provider.php | 45 +----- .../Woo/Providers/Product_Type_Provider.php | 26 +--- .../src/Woo/Providers/Shipping_Provider.php | 26 +--- .../src/Woo/Providers/Store_Api_Provider.php | 43 +----- .../tests/Integration/Plugin_Boot_Test.php | 62 +++----- templates/tests/Unit/Block_Registrar_Test.php | 7 +- templates/tests/Unit/Commands_Test.php | 7 +- templates/tests/Unit/Container_Test.php | 140 ------------------ templates/tests/Unit/Example_Test.php | 77 ++++------ templates/tests/Unit/Services_Test.php | 61 ++++++++ tests/generator.test.js | 93 ++++++------ 42 files changed, 409 insertions(+), 1182 deletions(-) delete mode 100644 templates/src/Contracts/Conditional.php delete mode 100644 templates/src/Contracts/Service_Provider.php delete mode 100644 templates/src/Core/Container.php delete mode 100644 templates/src/Core/Exceptions/Not_Found_Exception.php create mode 100644 templates/src/Services.php delete mode 100644 templates/tests/Unit/Container_Test.php create mode 100644 templates/tests/Unit/Services_Test.php diff --git a/index.js b/index.js index 0477829..30670ed 100644 --- a/index.js +++ b/index.js @@ -946,11 +946,8 @@ function scaffoldInto(answers, targetDir) { const templatesDir = path.join(__dirname, 'templates'); - // Copy standard templates - writeTemplateFile(path.join(templatesDir, 'src/Core/Container.php'), 'src/Core/Container.php'); - writeTemplateFile(path.join(templatesDir, 'src/Core/Exceptions/Not_Found_Exception.php'), 'src/Core/Exceptions/Not_Found_Exception.php'); - writeTemplateFile(path.join(templatesDir, 'src/Contracts/Service_Provider.php'), 'src/Contracts/Service_Provider.php'); - writeTemplateFile(path.join(templatesDir, 'src/Contracts/Conditional.php'), 'src/Contracts/Conditional.php'); + // Copy standard templates. (src/Services.php and src/Plugin.php are written + // further down, after their dynamic bodies are assembled.) writeTemplateFile(path.join(templatesDir, 'src/Contracts/Activatable.php'), 'src/Contracts/Activatable.php'); writeTemplateFile(path.join(templatesDir, 'src/Contracts/Deactivatable.php'), 'src/Contracts/Deactivatable.php'); writeTemplateFile(path.join(templatesDir, 'plugin-main.php'), `${answers.slug}.php`); @@ -959,7 +956,7 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, 'tests/bootstrap.php'), 'tests/bootstrap.php'); writeTemplateFile(path.join(templatesDir, 'phpunit.xml.dist'), 'phpunit.xml.dist'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Example_Test.php'), 'tests/Unit/Example_Test.php'); - writeTemplateFile(path.join(templatesDir, 'tests/Unit/Container_Test.php'), 'tests/Unit/Container_Test.php'); + writeTemplateFile(path.join(templatesDir, 'tests/Unit/Services_Test.php'), 'tests/Unit/Services_Test.php'); writeTemplateFile(path.join(templatesDir, 'gitignore.tpl'), '.gitignore'); writeTemplateFile(path.join(templatesDir, 'editorconfig.tpl'), '.editorconfig'); writeTemplateFile(path.join(templatesDir, 'LICENSE'), 'LICENSE'); @@ -986,15 +983,35 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, '.vscode/settings.json'), '.vscode/settings.json'); } - // Selected modules mapping: each module pushes one or more `$providers[] = new X();` - // lines, injected into Plugin::create() (see {{PROVIDER_REGISTRATIONS}} below). - const providerRegistrations = []; + // Each module contributes a `( new X() )->init_hooks();` line to Plugin::boot() + // ({{BOOTLOADER_LINES}}); WooCommerce modules go in wooBootLines and get wrapped + // in one class_exists( 'WooCommerce' ) guard. servicesAccessors holds the PHP for + // each shared-service getter on the Services locator ({{SERVICES_ACCESSORS}}). + const bootLines = []; + const wooBootLines = []; + const servicesAccessors = []; + + // One memoised getter on Services: name() -> new (). $short is the + // class name relative to the plugin root namespace (Services lives there). + function servicesAccessor(name, short) { + const type = short.split('\\').pop(); + return [ + '\t/**', + `\t * Shared ${type} instance.`, + '\t *', + `\t * @return ${short}`, + '\t */', + `\tpublic static function ${name}(): ${short} {`, + `\t\treturn self::$instances['${name}'] ??= new ${short}();`, + '\t}', + '', + ].join('\n'); + } if (selectedModules.includes('cli')) { - // Registered in Plugin::create() itself (behind a WP_CLI guard and the - // {{#if cli}} template block), not via providerRegistrations. writeTemplateFile(path.join(templatesDir, 'src/CLI/Commands.php'), 'src/CLI/Commands.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Commands_Test.php'), 'tests/Unit/Commands_Test.php'); + bootLines.push("\t\tif ( defined( 'WP_CLI' ) && WP_CLI ) {\n\t\t\t( new CLI\\Commands() )->init_hooks();\n\t\t}"); } if (selectedModules.includes('admin_settings')) { writeTemplateFile(path.join(templatesDir, 'src/Admin/Settings_Repository.php'), 'src/Admin/Settings_Repository.php'); @@ -1004,17 +1021,18 @@ function scaffoldInto(answers, targetDir) { // The React mount point is a {{#if use_react}} block inside the view now. writeTemplateFile(path.join(templatesDir, 'src/Admin/views/settings-page.php'), 'src/Admin/views/settings-page.php'); - providerRegistrations.push('\n\t\t$providers[] = new Admin\\Settings_Registrar();'); + bootLines.push('\t\t( new Admin\\Settings_Registrar( Services::settings_repository() ) )->init_hooks();'); + servicesAccessors.push(servicesAccessor('settings_repository', 'Admin\\Settings_Repository')); } if (selectedModules.includes('shortcode')) { writeTemplateFile(path.join(templatesDir, 'src/Frontend/Shortcode.php'), 'src/Frontend/Shortcode.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Shortcode_Test.php'), 'tests/Unit/Shortcode_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Frontend\\Shortcode();'); + bootLines.push('\t\t( new Frontend\\Shortcode() )->init_hooks();'); } if (selectedModules.includes('rest_api')) { writeTemplateFile(path.join(templatesDir, 'src/Rest/Rest_Controller.php'), 'src/Rest/Rest_Controller.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Rest_Controller_Test.php'), 'tests/Unit/Rest_Controller_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Rest\\Rest_Controller();'); + bootLines.push('\t\t( new Rest\\Rest_Controller() )->init_hooks();'); } if (selectedModules.includes('ajax_handler')) { writeTemplateFile(path.join(templatesDir, 'src/Ajax/Ajax_Handler.php'), 'src/Ajax/Ajax_Handler.php'); @@ -1023,29 +1041,30 @@ function scaffoldInto(answers, targetDir) { // front-end script (a nonce-guarded fetch wired to a click), so the // file rides along with the module instead of the baseline. writeTemplateFile(path.join(templatesDir, 'assets/js/main.js'), 'assets/js/main.js'); - providerRegistrations.push('\n\t\t$providers[] = new Ajax\\Ajax_Handler();'); + bootLines.push('\t\t( new Ajax\\Ajax_Handler() )->init_hooks();'); } if (selectedModules.includes('cpt_taxonomy')) { writeTemplateFile(path.join(templatesDir, 'src/PostTypes/Post_Types.php'), 'src/PostTypes/Post_Types.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Post_Types_Test.php'), 'tests/Unit/Post_Types_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new PostTypes\\Post_Types();'); + bootLines.push('\t\t( new PostTypes\\Post_Types() )->init_hooks();'); } if (selectedModules.includes('cron')) { writeTemplateFile(path.join(templatesDir, 'src/Cron/Scheduler.php'), 'src/Cron/Scheduler.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Scheduler_Test.php'), 'tests/Unit/Scheduler_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Cron\\Scheduler();'); + bootLines.push('\t\t( new Cron\\Scheduler() )->init_hooks();'); } if (selectedModules.includes('caching')) { writeTemplateFile(path.join(templatesDir, 'src/Cache/Cache_Service.php'), 'src/Cache/Cache_Service.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Cache_Service_Test.php'), 'tests/Unit/Cache_Service_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Cache\\Cache_Service();'); + servicesAccessors.push(servicesAccessor('cache', 'Cache\\Cache_Service')); } if (selectedModules.includes('custom_table')) { writeTemplateFile(path.join(templatesDir, 'src/Database/Schema.php'), 'src/Database/Schema.php'); writeTemplateFile(path.join(templatesDir, 'src/Database/Item_Repository.php'), 'src/Database/Item_Repository.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Schema_Test.php'), 'tests/Unit/Schema_Test.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Item_Repository_Test.php'), 'tests/Unit/Item_Repository_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Database\\Schema();'); + bootLines.push('\t\t( new Database\\Schema() )->init_hooks();'); + servicesAccessors.push(servicesAccessor('item_repository', 'Database\\Item_Repository')); } if (selectedModules.includes('elementor_widget')) { if (selectedModules.includes('editor_config')) { @@ -1057,8 +1076,8 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, 'assets/css/widgets/sample-widget.css'), 'assets/css/widgets/sample-widget.css'); writeTemplateFile(path.join(templatesDir, 'assets/js/widgets/sample-widget.js'), 'assets/js/widgets/sample-widget.js'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Widget_Registrar_Test.php'), 'tests/Unit/Widget_Registrar_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Elementor\\Dependency_Notice();'); - providerRegistrations.push('\n\t\t$providers[] = new Elementor\\Widget_Registrar();'); + bootLines.push('\t\t( new Elementor\\Dependency_Notice() )->init_hooks();'); + bootLines.push('\t\t( new Elementor\\Widget_Registrar() )->init_hooks();'); } if (hasWooGateway) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Gateway_Provider.php'), 'src/Woo/Providers/Gateway_Provider.php'); @@ -1066,13 +1085,13 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Gateways/Blocks_Payment_Method_Type.php'), 'src/Woo/Gateways/Blocks_Payment_Method_Type.php'); writeTemplateFile(path.join(templatesDir, 'react/assets/src/wc-gateway-block.js'), 'assets/src/wc-gateway-block.js'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Gateway_Test.php'), 'tests/Unit/Gateway_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Gateway_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Gateway_Provider() )->init_hooks();'); } if (hasWooShipping) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Shipping_Provider.php'), 'src/Woo/Providers/Shipping_Provider.php'); writeTemplateFile(path.join(templatesDir, 'src/Woo/Shipping/Shipping_Method.php'), 'src/Woo/Shipping/Shipping_Method.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Shipping_Method_Test.php'), 'tests/Unit/Shipping_Method_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Shipping_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Shipping_Provider() )->init_hooks();'); } if (hasWooEmail) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Email_Provider.php'), 'src/Woo/Providers/Email_Provider.php'); @@ -1080,13 +1099,13 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, 'woo-email-templates/emails/custom-email.php'), `templates/emails/${answers.prefix.toLowerCase()}-custom-email.php`); writeTemplateFile(path.join(templatesDir, 'woo-email-templates/emails/plain/custom-email.php'), `templates/emails/plain/${answers.prefix.toLowerCase()}-custom-email.php`); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Custom_Email_Test.php'), 'tests/Unit/Custom_Email_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Email_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Email_Provider() )->init_hooks();'); } if (hasWooProductType) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Product_Type_Provider.php'), 'src/Woo/Providers/Product_Type_Provider.php'); writeTemplateFile(path.join(templatesDir, 'src/Woo/Products/Custom_Product.php'), 'src/Woo/Products/Custom_Product.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Custom_Product_Test.php'), 'tests/Unit/Custom_Product_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Product_Type_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Product_Type_Provider() )->init_hooks();'); } if (hasWooBlocks) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Blocks_Provider.php'), 'src/Woo/Providers/Blocks_Provider.php'); @@ -1097,38 +1116,42 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, 'react/assets/src/blocks/cart-summary/index.js'), 'assets/src/blocks/cart-summary/index.js'); writeTemplateFile(path.join(templatesDir, 'react/assets/src/blocks/cart-summary/render.php'), 'assets/src/blocks/cart-summary/render.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Cart_Summary_Block_Test.php'), 'tests/Unit/Cart_Summary_Block_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Blocks_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Blocks_Provider() )->init_hooks();'); } if (hasWooOrderStatus) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Order_Status_Provider.php'), 'src/Woo/Providers/Order_Status_Provider.php'); writeTemplateFile(path.join(templatesDir, 'src/Woo/Orders/Order_Status_Service.php'), 'src/Woo/Orders/Order_Status_Service.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Order_Status_Service_Test.php'), 'tests/Unit/Order_Status_Service_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Order_Status_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Order_Status_Provider( Services::order_status_service() ) )->init_hooks();'); + servicesAccessors.push(servicesAccessor('order_status_service', 'Woo\\Orders\\Order_Status_Service')); } if (hasWooActionScheduler) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Action_Scheduler_Provider.php'), 'src/Woo/Providers/Action_Scheduler_Provider.php'); writeTemplateFile(path.join(templatesDir, 'src/Woo/Tasks/Action_Scheduler_Service.php'), 'src/Woo/Tasks/Action_Scheduler_Service.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Action_Scheduler_Service_Test.php'), 'tests/Unit/Action_Scheduler_Service_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Action_Scheduler_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Action_Scheduler_Provider( Services::action_scheduler_service() ) )->init_hooks();'); + servicesAccessors.push(servicesAccessor('action_scheduler_service', 'Woo\\Tasks\\Action_Scheduler_Service')); } if (hasWooStoreApi) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Store_Api_Provider.php'), 'src/Woo/Providers/Store_Api_Provider.php'); writeTemplateFile(path.join(templatesDir, 'src/Woo/Api/Store_Api_Extension.php'), 'src/Woo/Api/Store_Api_Extension.php'); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Store_Api_Extension_Test.php'), 'tests/Unit/Store_Api_Extension_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Store_Api_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Store_Api_Provider( Services::store_api_extension() ) )->init_hooks();'); + servicesAccessors.push(servicesAccessor('store_api_extension', 'Woo\\Api\\Store_Api_Extension')); } if (hasWooMyAccount) { writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Account_Endpoint_Provider.php'), 'src/Woo/Providers/Account_Endpoint_Provider.php'); writeTemplateFile(path.join(templatesDir, 'src/Woo/Account/Account_Endpoint_Service.php'), 'src/Woo/Account/Account_Endpoint_Service.php'); writeTemplateFile(path.join(templatesDir, 'woo-account-templates/my-account/custom-endpoint.php'), `templates/my-account/${answers.prefix.toLowerCase()}-custom.php`); writeTemplateFile(path.join(templatesDir, 'tests/Unit/Account_Endpoint_Service_Test.php'), 'tests/Unit/Account_Endpoint_Service_Test.php'); - providerRegistrations.push('\n\t\t$providers[] = new Woo\\Providers\\Account_Endpoint_Provider();'); + wooBootLines.push('\t\t\t( new Woo\\Providers\\Account_Endpoint_Provider( Services::account_endpoint_service() ) )->init_hooks();'); + servicesAccessors.push(servicesAccessor('account_endpoint_service', 'Woo\\Account\\Account_Endpoint_Service')); } if (selectedModules.includes('interactivity')) { writeTemplateFile(path.join(templatesDir, 'src/Frontend/Interactivity.php'), 'src/Frontend/Interactivity.php'); // Hand-written ESM served directly as a script module — no build step. writeTemplateFile(path.join(templatesDir, 'interactivity/view.js'), 'assets/js/view.js'); - providerRegistrations.push('\n\t\t$providers[] = new Frontend\\Interactivity();'); + bootLines.push('\t\t( new Frontend\\Interactivity() )->init_hooks();'); } if (hasBlock) { // Block_Registrar globs assets/build/blocks/*, so it's variant-agnostic; @@ -1148,7 +1171,7 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, 'blocks/example-static/edit.js'), 'assets/src/blocks/example-static/edit.js'); writeTemplateFile(path.join(templatesDir, 'blocks/example-static/save.js'), 'assets/src/blocks/example-static/save.js'); } - providerRegistrations.push('\n\t\t$providers[] = new Blocks\\Block_Registrar();'); + bootLines.push('\t\t( new Blocks\\Block_Registrar() )->init_hooks();'); } // React admin app (wp-admin only) + WooCommerce Blocks/Gateway + native @@ -1173,6 +1196,7 @@ function scaffoldInto(answers, targetDir) { writeTemplateFile(path.join(templatesDir, 'react/assets/src/index.js'), 'assets/src/index.js'); // Assets.php scopes its enqueue via a {{#if admin_settings}}/{{else}} block. writeTemplateFile(path.join(templatesDir, 'src/Admin/Assets.php'), 'src/Admin/Assets.php'); + bootLines.push('\t\t( new Admin\\Assets() )->init_hooks();'); } if (needsBuildPipeline) { @@ -1328,17 +1352,29 @@ ${entries.join('\n')} writeTemplateFile(path.join(templatesDir, 'package.json'), 'package.json'); - // Process Plugin.php template with dynamic registrations. The React - // Assets provider is a {{#if use_react}} block in the template itself; - // the per-module $providers[] lines are accumulated here because that's - // where each module's file-copy branch already lives. + // Assemble Plugin::boot()'s body: non-woo module lines, then every woo + // line inside one class_exists( 'WooCommerce' ) guard. + const allBootLines = [...bootLines]; + if (wooBootLines.length > 0) { + allBootLines.push("\t\tif ( class_exists( 'WooCommerce' ) ) {\n" + wooBootLines.join('\n') + '\n\t\t}'); + } + const bootloaderBody = allBootLines.length > 0 ? allBootLines.join('\n') + '\n' : ''; + let pluginContent = fs.readFileSync(path.join(templatesDir, 'src/Plugin.php'), 'utf8'); - pluginContent = pluginContent.replace('{{PROVIDER_REGISTRATIONS}}', () => providerRegistrations.length > 0 ? providerRegistrations.join('\n') + '\n' : ''); + pluginContent = pluginContent.replace('{{BOOTLOADER_LINES}}', () => bootloaderBody); pluginContent = processTemplateContent(pluginContent, 'src/Plugin.php'); const pluginDestPath = path.join(targetDir, 'src/Plugin.php'); fs.mkdirSync(path.dirname(pluginDestPath), { recursive: true }); fs.writeFileSync(pluginDestPath, pluginContent, 'utf8'); + // Services.php: the memoised accessors for this module set (or none). + let servicesContent = fs.readFileSync(path.join(templatesDir, 'src/Services.php'), 'utf8'); + servicesContent = servicesContent.replace('{{SERVICES_ACCESSORS}}', () => servicesAccessors.join('\n')); + servicesContent = processTemplateContent(servicesContent, 'src/Services.php'); + const servicesDestPath = path.join(targetDir, 'src/Services.php'); + fs.mkdirSync(path.dirname(servicesDestPath), { recursive: true }); + fs.writeFileSync(servicesDestPath, servicesContent, 'utf8'); + // Single supported PHP line — see MIN_PHP. The matrix also runs the next // minor so a scaffold surfaces forward-compat breakage early. const ciPhpMatrix = "['8.3', '8.4']"; @@ -1361,8 +1397,7 @@ ${entries.join('\n')} // Fully-qualified on purpose: Activator.php lives in the {{NS}}\Core namespace, // so an unqualified "PostTypes\Post_Types" reference here would resolve to the // (nonexistent) {{NS}}\Core\PostTypes\Post_Types and fatal at runtime. - activatorLines.push('\t\t$post_types = $container->get( \\{{NS}}\\PostTypes\\Post_Types::class );'); - activatorLines.push('\t\t$post_types->register_cpt_and_taxonomy();'); + activatorLines.push('\t\t( new \\{{NS}}\\PostTypes\\Post_Types() )->register_cpt_and_taxonomy();'); if (needsVip) { // WordPress VIP forbids flush_rewrite_rules() (rewrite rules there are // regenerated from deploys / a permalink re-save), so rather than @@ -1402,7 +1437,7 @@ ${entries.join('\n')} // dbDelta() must run synchronously on activation so the table exists // immediately — Schema::boot()'s plugins_loaded hook only catches // updates, which don't fire register_activation_hook(). - activatorLines.push('\t\t$container->get( \\{{NS}}\\Database\\Schema::class )->create_table();'); + activatorLines.push('\t\t( new \\{{NS}}\\Database\\Schema() )->create_table();'); uninstallLines.push('\t\t\\{{NS}}\\Database\\Schema::drop_table();'); } diff --git a/templates/plugin-main.php b/templates/plugin-main.php index 544e4b0..3e2a5f8 100644 --- a/templates/plugin-main.php +++ b/templates/plugin-main.php @@ -54,17 +54,13 @@ function ( $class_name ) { register_activation_hook( __FILE__, static function () { - $plugin = \{{NS}}\Plugin::create(); - $plugin->register_all(); - ( new \{{NS}}\Core\Activator() )->activate( $plugin->get_container() ); + ( new \{{NS}}\Core\Activator() )->activate(); } ); register_deactivation_hook( __FILE__, static function () { - $plugin = \{{NS}}\Plugin::create(); - $plugin->register_all(); - ( new \{{NS}}\Core\Deactivator() )->deactivate( $plugin->get_container() ); + ( new \{{NS}}\Core\Deactivator() )->deactivate(); } ); {{WOOCOMMERCE_HPOS}} @@ -75,7 +71,7 @@ static function () { */ function {{PREFIX}}_boot() { load_plugin_textdomain( '{{SLUG}}', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); - \{{NS}}\Plugin::create()->boot(); + \{{NS}}\Plugin::instance()->boot(); } add_action( 'plugins_loaded', '{{PREFIX}}_boot' ); diff --git a/templates/src/Admin/Assets.php b/templates/src/Admin/Assets.php index f26fe09..a58dcd6 100644 --- a/templates/src/Admin/Assets.php +++ b/templates/src/Admin/Assets.php @@ -15,9 +15,6 @@ namespace {{NS}}\Admin; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -25,24 +22,14 @@ /** * Class Assets. */ -class Assets implements Service_Provider { - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Assets { /** * Register asset hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'admin_enqueue_scripts', $this->enqueue_assets( ... ) ); } diff --git a/templates/src/Admin/Settings_Registrar.php b/templates/src/Admin/Settings_Registrar.php index 458ec5c..40e3707 100644 --- a/templates/src/Admin/Settings_Registrar.php +++ b/templates/src/Admin/Settings_Registrar.php @@ -9,9 +9,6 @@ namespace {{NS}}\Admin; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -22,37 +19,22 @@ * Registers the admin menu page and Settings API hooks. Data access is * delegated to Settings_Repository; markup lives in src/Admin/views/. */ -class Settings_Registrar implements Service_Provider { +class Settings_Registrar { /** - * Application container, kept for on-demand Settings_Repository lookups - * from inside WordPress-invoked callbacks (add_options_page() and - * add_settings_field() call these with fixed signatures, so the - * repository can't be a constructor argument here). + * Data-access layer for the plugin's option. * - * @var Container + * @param Settings_Repository $repository Settings repository. */ - private Container $container; - - /** - * Bind Settings_Repository as a singleton. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->singleton( Settings_Repository::class, static fn () => new Settings_Repository() ); + public function __construct( private readonly Settings_Repository $repository ) { } /** * Register admin menu and settings hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { - $this->container = $container; - + public function init_hooks(): void { add_action( 'admin_menu', $this->add_menu_page( ... ) ); add_action( 'admin_init', $this->register_settings( ... ) ); } @@ -78,8 +60,7 @@ public function add_menu_page() { * @return void */ public function register_settings() { - $repository = $this->container->get( Settings_Repository::class ); - $repository->register_setting(); + $this->repository->register_setting(); add_settings_section( '{{PREFIX}}_main_section', @@ -89,7 +70,7 @@ public function register_settings() { ); add_settings_field( - $repository->get_option_name(), + $this->repository->get_option_name(), __( 'Sample Setting', '{{SLUG}}' ), $this->render_sample_field( ... ), '{{SLUG}}', @@ -103,7 +84,7 @@ public function register_settings() { * @return void */ public function render_sample_field() { - $repository = $this->container->get( Settings_Repository::class ); + $repository = $this->repository; $name = $repository->get_option_name(); $value = $repository->get_value(); @@ -120,7 +101,7 @@ public function render_page() { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', '{{SLUG}}' ) ); } - $repository = $this->container->get( Settings_Repository::class ); + $repository = $this->repository; include {{PREFIX_UPPER}}_PATH . 'src/Admin/views/settings-page.php'; } diff --git a/templates/src/Ajax/Ajax_Handler.php b/templates/src/Ajax/Ajax_Handler.php index ff2cc68..84bc9ad 100644 --- a/templates/src/Ajax/Ajax_Handler.php +++ b/templates/src/Ajax/Ajax_Handler.php @@ -9,9 +9,6 @@ namespace {{NS}}\Ajax; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -19,7 +16,7 @@ /** * Class Ajax_Handler. */ -class Ajax_Handler implements Service_Provider { +class Ajax_Handler { /** * Whether to register unauthenticated (nopriv) AJAX action for logged-out visitors. @@ -30,22 +27,12 @@ class Ajax_Handler implements Service_Provider { */ protected bool $allow_nopriv = false; - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } - /** * Register AJAX actions and asset enqueueing. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'wp_ajax_{{PREFIX}}_action', $this->handle_ajax( ... ) ); if ( $this->allow_nopriv ) { add_action( 'wp_ajax_nopriv_{{PREFIX}}_action', $this->handle_ajax( ... ) ); diff --git a/templates/src/Blocks/Block_Registrar.php b/templates/src/Blocks/Block_Registrar.php index 21ca7ab..8ac7576 100644 --- a/templates/src/Blocks/Block_Registrar.php +++ b/templates/src/Blocks/Block_Registrar.php @@ -9,9 +9,6 @@ namespace {{NS}}\Blocks; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -29,29 +26,19 @@ * (add `--variant dynamic` for a server-rendered one) — then rebuild. It is * picked up automatically; nothing here changes. */ -class Block_Registrar implements Service_Provider { +class Block_Registrar { /** * Directory (relative to the plugin root) holding compiled block metadata. */ private const BUILD_DIR = 'assets/build/blocks'; - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } - /** * Register block hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'init', $this->register_blocks( ... ) ); } diff --git a/templates/src/CLI/Commands.php b/templates/src/CLI/Commands.php index 9c532d9..a3e98c4 100644 --- a/templates/src/CLI/Commands.php +++ b/templates/src/CLI/Commands.php @@ -9,9 +9,6 @@ namespace {{NS}}\CLI; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -19,27 +16,17 @@ /** * WP-CLI Commands for {{PLUGIN_NAME}}. */ -class Commands implements Service_Provider { - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Commands { /** * Register WP-CLI commands. * - * @param Container $container Application container. + * The bootloader only instantiates this class behind a WP_CLI guard; the + * repeat check here keeps the class safe to call directly (e.g. in tests). + * * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - // Guard here rather than with a top-level `return` in this file — that - // would stop the class from ever being declared and break PSR-4 - // autoloading (and unit tests) outside a WP-CLI context. + public function init_hooks(): void { if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { return; } diff --git a/templates/src/Cache/Cache_Service.php b/templates/src/Cache/Cache_Service.php index 3a4f672..6f2c3b2 100644 --- a/templates/src/Cache/Cache_Service.php +++ b/templates/src/Cache/Cache_Service.php @@ -9,9 +9,6 @@ namespace {{NS}}\Cache; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -26,10 +23,9 @@ * * `set_transient()` already routes to the object cache when one is present, * so writing to both would just store every value twice on a Redis site — - * this picks one. Resolve from the container: - * $container->get( Cache_Service::class ). + * this picks one. Reach the shared instance via Services::cache(). */ -class Cache_Service implements Service_Provider { +class Cache_Service { /** * Object cache group / transient key prefix. @@ -38,25 +34,6 @@ class Cache_Service implements Service_Provider { */ private const GROUP = '{{PREFIX}}'; - /** - * Bind this instance so other services can resolve it from the container. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->instance( self::class, $this ); - } - - /** - * No hooks to register — this is a plain utility service, not a hook registrar. - * - * @param Container $container Application container. - * @return void - */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } - /** * Get a cached value, or $fallback if it isn't cached (or has expired). * diff --git a/templates/src/Contracts/Activatable.php b/templates/src/Contracts/Activatable.php index 86f3a9b..b2322bb 100644 --- a/templates/src/Contracts/Activatable.php +++ b/templates/src/Contracts/Activatable.php @@ -9,8 +9,6 @@ namespace {{NS}}\Contracts; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -25,8 +23,7 @@ interface Activatable { /** * Run activation tasks. * - * @param Container $container Application container. * @return void */ - public function activate( Container $container ): void; + public function activate(): void; } diff --git a/templates/src/Contracts/Conditional.php b/templates/src/Contracts/Conditional.php deleted file mode 100644 index 1a2f3fa..0000000 --- a/templates/src/Contracts/Conditional.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ - private array $bindings = array(); - - /** - * Already-resolved singleton instances and explicit instance() values, keyed by id. - * - * @var array - */ - private array $instances = array(); - - /** - * Bind a factory that runs on every get() call. - * - * @param string $id Binding identifier (typically a fully-qualified class name). - * @param callable $factory Factory receiving this Container, returning the resolved value. - * @return void - */ - public function bind( string $id, callable $factory ): void { - $this->bindings[ $id ] = array( - 'factory' => $factory, - 'singleton' => false, - ); - unset( $this->instances[ $id ] ); - } - - /** - * Bind a factory whose result is cached after the first get() call. - * - * @param string $id Binding identifier (typically a fully-qualified class name). - * @param callable $factory Factory receiving this Container, returning the resolved value. - * @return void - */ - public function singleton( string $id, callable $factory ): void { - $this->bindings[ $id ] = array( - 'factory' => $factory, - 'singleton' => true, - ); - unset( $this->instances[ $id ] ); - } - - /** - * Register an already-constructed value directly (no factory involved). - * - * @param string $id Binding identifier. - * @param object $value The value to return for every subsequent get() call. - * @return void - */ - public function instance( string $id, object $value ): void { - $this->instances[ $id ] = $value; - } - - /** - * Whether an id has an instance or a factory binding registered. - * - * @param string $id Binding identifier. - * @return bool - */ - public function has( string $id ): bool { - // array_key_exists, not isset: a singleton factory may legitimately - // resolve to null, and isset() would report it as absent. - return array_key_exists( $id, $this->instances ) || array_key_exists( $id, $this->bindings ); - } - - /** - * Resolve a binding. - * - * @param string $id Binding identifier. - * @return mixed - * - * @throws Not_Found_Exception When no instance or binding is registered for $id. - */ - public function get( string $id ): mixed { - // array_key_exists, not isset: a resolved singleton may be null, and - // isset() would re-run its factory on every call. - if ( array_key_exists( $id, $this->instances ) ) { - return $this->instances[ $id ]; - } - - if ( ! array_key_exists( $id, $this->bindings ) ) { - throw new Not_Found_Exception( sprintf( 'No binding registered for "%s".', $id ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- $id is a developer-supplied binding identifier, not user input or output. - } - - $binding = $this->bindings[ $id ]; - $value = ( $binding['factory'] )( $this ); - - if ( $binding['singleton'] ) { - $this->instances[ $id ] = $value; - } - - return $value; - } -} diff --git a/templates/src/Core/Deactivator.php b/templates/src/Core/Deactivator.php index 631f392..a62f63e 100644 --- a/templates/src/Core/Deactivator.php +++ b/templates/src/Core/Deactivator.php @@ -25,9 +25,8 @@ class Deactivator implements Deactivatable { /** * Execute deactivation tasks. * - * @param Container $container Application container (already registered — register_all() has run). * @return void */ - public function deactivate( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- required by the Deactivatable contract; not every generated deactivator body uses it. + public function deactivate(): void { {{DEACTIVATOR_BODY}} } } diff --git a/templates/src/Core/Exceptions/Not_Found_Exception.php b/templates/src/Core/Exceptions/Not_Found_Exception.php deleted file mode 100644 index 9a2a825..0000000 --- a/templates/src/Core/Exceptions/Not_Found_Exception.php +++ /dev/null @@ -1,20 +0,0 @@ -execute_cron_job( ... ) ); } diff --git a/templates/src/Database/Schema.php b/templates/src/Database/Schema.php index 714d49e..232e50e 100644 --- a/templates/src/Database/Schema.php +++ b/templates/src/Database/Schema.php @@ -9,9 +9,6 @@ namespace {{NS}}\Database; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -20,14 +17,14 @@ * Class Schema. * * Owns the plugin's custom table. dbDelta() creates or upgrades it — called - * synchronously on activation (Activator resolves this from the container), - * and again on every request via maybe_upgrade() so a plugin *update* - * (which doesn't fire register_activation_hook()) still gets migrated. - * dbDelta() is idempotent: re-running it against an up-to-date table is a - * cheap no-op, it only ever adds/alters, and get_option() short-circuits - * maybe_upgrade() once VERSION_OPTION already matches VERSION. + * synchronously on activation (Activator news one up), and again via + * maybe_upgrade() so a plugin *update* (which doesn't fire + * register_activation_hook()) still gets migrated. dbDelta() is idempotent: + * re-running it against an up-to-date table is a cheap no-op, it only ever + * adds/alters, and get_option() short-circuits maybe_upgrade() once + * VERSION_OPTION already matches VERSION. */ -class Schema implements Service_Provider { +class Schema { /** * Bump this whenever create_table()'s SQL changes — dbDelta() diffs @@ -56,23 +53,11 @@ public static function table_name(): string { } /** - * Bind this instance so Activator can resolve it to run create_table() - * once, synchronously, on activation. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->instance( self::class, $this ); - } - - /** - * Check for pending migrations on every request. + * Check for pending migrations. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'plugins_loaded', $this->maybe_upgrade( ... ) ); } diff --git a/templates/src/Elementor/Dependency_Notice.php b/templates/src/Elementor/Dependency_Notice.php index a29f57f..da9f846 100644 --- a/templates/src/Elementor/Dependency_Notice.php +++ b/templates/src/Elementor/Dependency_Notice.php @@ -9,9 +9,6 @@ namespace {{NS}}\Elementor; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -19,24 +16,14 @@ /** * Class Dependency_Notice. */ -class Dependency_Notice implements Service_Provider { - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Dependency_Notice { /** * Register admin notice hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'admin_notices', $this->render_notice( ... ) ); } diff --git a/templates/src/Elementor/Widget_Registrar.php b/templates/src/Elementor/Widget_Registrar.php index 067bdcf..bba95f2 100644 --- a/templates/src/Elementor/Widget_Registrar.php +++ b/templates/src/Elementor/Widget_Registrar.php @@ -9,9 +9,6 @@ namespace {{NS}}\Elementor; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -23,24 +20,14 @@ * glob() + reflection, and registers each one's on-demand assets plus the * widget itself. */ -class Widget_Registrar implements Service_Provider { - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Widget_Registrar { /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_filter( '{{PREFIX}}_cache_keys', $this->register_cache_keys( ... ) ); add_action( 'wp_enqueue_scripts', $this->register_widget_assets( ... ) ); add_action( 'elementor/editor/after_enqueue_styles', $this->register_widget_assets( ... ) ); diff --git a/templates/src/Frontend/Interactivity.php b/templates/src/Frontend/Interactivity.php index 692b7a1..6e37a1b 100644 --- a/templates/src/Frontend/Interactivity.php +++ b/templates/src/Frontend/Interactivity.php @@ -14,9 +14,6 @@ namespace {{NS}}\Frontend; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -24,7 +21,7 @@ /** * Class Interactivity. */ -class Interactivity implements Service_Provider { +class Interactivity { /** * Interactivity API namespace, shared between data-wp-interactive and the JS store(). @@ -33,22 +30,12 @@ class Interactivity implements Service_Provider { */ const NAMESPACE_KEY = '{{SLUG}}'; - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } - /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'init', $this->register_script_module( ... ) ); add_shortcode( '{{PREFIX}}_interactivity_demo', $this->render_demo( ... ) ); } diff --git a/templates/src/Frontend/Shortcode.php b/templates/src/Frontend/Shortcode.php index 19ca815..56e64db 100644 --- a/templates/src/Frontend/Shortcode.php +++ b/templates/src/Frontend/Shortcode.php @@ -9,9 +9,6 @@ namespace {{NS}}\Frontend; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -19,24 +16,14 @@ /** * Class Shortcode. */ -class Shortcode implements Service_Provider { - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Shortcode { /** * Register shortcode. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_shortcode( '{{PREFIX}}_display', $this->render_shortcode( ... ) ); } diff --git a/templates/src/Plugin.php b/templates/src/Plugin.php index 887cda1..22909d6 100644 --- a/templates/src/Plugin.php +++ b/templates/src/Plugin.php @@ -1,6 +1,8 @@ container; + private function __construct() { } /** - * Get the registered providers (unfiltered, unconditioned). + * The shared Plugin instance. * - * @return array + * @return self */ - public function get_providers(): array { - return $this->providers; + public static function instance(): self { + return self::$instance ??= new self(); } /** - * Run only the register() pass on every active provider. - * - * Used by the activation/deactivation bridge in the main plugin file, - * which needs bindings available (e.g. so Activator can resolve a - * service from the container) without booting WordPress hooks that - * make no sense to fire during activation, and without running the - * '{{PREFIX}}_providers' filter (third-party filter callbacks aren't - * reliably available that early). + * Replace (or, with null, clear) the shared instance. Test seam. * + * @param self|null $plugin Replacement instance, or null to reset. * @return void */ - public function register_all(): void { - foreach ( $this->active_providers( $this->providers ) as $provider ) { - $provider->register( $this->container ); - } + public static function set_instance( ?self $plugin ): void { + self::$instance = $plugin; } /** - * Register and boot every active provider for a normal request. + * Instantiate every selected module and register its WordPress hooks. + * + * Idempotent: safe to call more than once, only the first call wires + * anything up. * * @return void */ public function boot(): void { - /** - * Filter the providers to be registered and booted. - * - * @param array $providers Array of Service_Provider instances. - */ - $providers = apply_filters( '{{PREFIX}}_providers', $this->providers ); - - foreach ( $this->active_providers( is_array( $providers ) ? $providers : $this->providers ) as $provider ) { - $provider->register( $this->container ); - $provider->boot( $this->container ); + if ( $this->booted ) { + return; } - } - - /** - * Filter a provider list down to the ones that should actually run: - * must implement Service_Provider, and if it also implements - * Conditional, is_needed() must return true. - * - * @param array $providers Candidate provider list (e.g. straight from - * the constructor, or from the '{{PREFIX}}_providers' filter). - * @return array - */ - private function active_providers( array $providers ): array { - $active = array(); - foreach ( $providers as $provider ) { - if ( ! $provider instanceof Contracts\Service_Provider ) { - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'Every entry filtered into the providers list must implement Service_Provider.', '{{SLUG}}' ), - '{{VERSION}}' - ); - } - continue; - } - - if ( $provider instanceof Contracts\Conditional && ! $provider->is_needed() ) { - continue; - } - - $active[] = $provider; - } - - return $active; - } + $this->booted = true; +{{BOOTLOADER_LINES}} } } diff --git a/templates/src/PostTypes/Post_Types.php b/templates/src/PostTypes/Post_Types.php index 1ec12ce..c431813 100644 --- a/templates/src/PostTypes/Post_Types.php +++ b/templates/src/PostTypes/Post_Types.php @@ -9,9 +9,6 @@ namespace {{NS}}\PostTypes; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -19,27 +16,14 @@ /** * Class Post_Types. */ -class Post_Types implements Service_Provider { - - /** - * Bind this instance so Activator can resolve it to run - * register_cpt_and_taxonomy() once, synchronously, on activation - * (before the 'init' hook it's normally registered against would fire). - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->instance( self::class, $this ); - } +class Post_Types { /** * Register post types and taxonomies. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'init', $this->register_cpt_and_taxonomy( ... ) ); } diff --git a/templates/src/Rest/Rest_Controller.php b/templates/src/Rest/Rest_Controller.php index afb8f25..806f566 100644 --- a/templates/src/Rest/Rest_Controller.php +++ b/templates/src/Rest/Rest_Controller.php @@ -9,9 +9,6 @@ namespace {{NS}}\Rest; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -19,7 +16,7 @@ /** * Class Rest_Controller. */ -class Rest_Controller extends \WP_REST_Controller implements Service_Provider { +class Rest_Controller extends \WP_REST_Controller { /** * Constructor. @@ -32,22 +29,12 @@ public function __construct() { $this->rest_base = 'data'; } - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } - /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'rest_api_init', $this->register_routes( ... ) ); } diff --git a/templates/src/Services.php b/templates/src/Services.php new file mode 100644 index 0000000..d5e17e0 --- /dev/null +++ b/templates/src/Services.php @@ -0,0 +1,53 @@ + + */ + private static array $instances = array(); + +{{SERVICES_ACCESSORS}} + /** + * Replace a memoised service (test seam). + * + * @param string $name Accessor name (e.g. 'cache'). + * @param object $service Replacement instance. + * @return void + */ + public static function set( string $name, object $service ): void { + self::$instances[ $name ] = $service; + } + + /** + * Forget every memoised service (test seam — call in tearDown()). + * + * @return void + */ + public static function reset(): void { + self::$instances = array(); + } +} diff --git a/templates/src/Woo/Providers/Account_Endpoint_Provider.php b/templates/src/Woo/Providers/Account_Endpoint_Provider.php index aa3caa8..907bdc4 100644 --- a/templates/src/Woo/Providers/Account_Endpoint_Provider.php +++ b/templates/src/Woo/Providers/Account_Endpoint_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Account\Account_Endpoint_Service; if ( ! defined( 'ABSPATH' ) ) { @@ -21,52 +18,24 @@ /** * Class Account_Endpoint_Provider. */ -class Account_Endpoint_Provider implements Service_Provider, Conditional { +class Account_Endpoint_Provider { /** - * Accept an optional service override; the container builds a default - * lazily when one isn't injected. + * My Account endpoint service. * - * @param Account_Endpoint_Service|null $service Service instance. + * @param Account_Endpoint_Service $service Endpoint service. */ - public function __construct( private readonly ?Account_Endpoint_Service $service = null ) { - } - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * Register service in container. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->singleton( - Account_Endpoint_Service::class, - function () { - return $this->service ?? new Account_Endpoint_Service(); - } - ); + public function __construct( private readonly Account_Endpoint_Service $service ) { } /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { - $service = $container->get( Account_Endpoint_Service::class ); - - add_action( 'init', array( $service, 'register_endpoint' ) ); - add_filter( 'woocommerce_account_menu_items', array( $service, 'add_menu_item' ) ); - add_action( 'woocommerce_account_' . Account_Endpoint_Service::ENDPOINT . '_endpoint', array( $service, 'render_endpoint' ) ); + public function init_hooks(): void { + add_action( 'init', $this->service->register_endpoint( ... ) ); + add_filter( 'woocommerce_account_menu_items', $this->service->add_menu_item( ... ) ); + add_action( 'woocommerce_account_' . Account_Endpoint_Service::ENDPOINT . '_endpoint', $this->service->render_endpoint( ... ) ); } } diff --git a/templates/src/Woo/Providers/Action_Scheduler_Provider.php b/templates/src/Woo/Providers/Action_Scheduler_Provider.php index 142976e..ab8dc95 100644 --- a/templates/src/Woo/Providers/Action_Scheduler_Provider.php +++ b/templates/src/Woo/Providers/Action_Scheduler_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Tasks\Action_Scheduler_Service; if ( ! defined( 'ABSPATH' ) ) { @@ -21,51 +18,23 @@ /** * Class Action_Scheduler_Provider. */ -class Action_Scheduler_Provider implements Service_Provider, Conditional { +class Action_Scheduler_Provider { /** - * Accept an optional service override; the container builds a default - * lazily when one isn't injected. + * Recurring-task service. * - * @param Action_Scheduler_Service|null $service Service instance. + * @param Action_Scheduler_Service $service Task service. */ - public function __construct( private readonly ?Action_Scheduler_Service $service = null ) { - } - - /** - * Needed when WooCommerce or Action Scheduler is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ) || function_exists( 'as_schedule_recurring_action' ); - } - - /** - * Register service in container. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->singleton( - Action_Scheduler_Service::class, - function () { - return $this->service ?? new Action_Scheduler_Service(); - } - ); + public function __construct( private readonly Action_Scheduler_Service $service ) { } /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { - $service = $container->get( Action_Scheduler_Service::class ); - - add_action( 'init', array( $service, 'schedule_tasks' ) ); - add_action( Action_Scheduler_Service::HOOK, array( $service, 'handle_task' ) ); + public function init_hooks(): void { + add_action( 'init', $this->service->schedule_tasks( ... ) ); + add_action( Action_Scheduler_Service::HOOK, $this->service->handle_task( ... ) ); } } diff --git a/templates/src/Woo/Providers/Blocks_Provider.php b/templates/src/Woo/Providers/Blocks_Provider.php index 346602c..e3511de 100644 --- a/templates/src/Woo/Providers/Blocks_Provider.php +++ b/templates/src/Woo/Providers/Blocks_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Blocks\Cart_Summary_Block; use {{NS}}\Woo\Blocks\Integration; @@ -22,33 +19,14 @@ /** * Class Blocks_Provider. */ -class Blocks_Provider implements Service_Provider, Conditional { - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Blocks_Provider { /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_action( 'init', Cart_Summary_Block::register( ... ) ); add_action( 'woocommerce_blocks_loaded', $this->register_blocks_integration( ... ) ); } diff --git a/templates/src/Woo/Providers/Email_Provider.php b/templates/src/Woo/Providers/Email_Provider.php index 6ff5b1d..b6248b5 100644 --- a/templates/src/Woo/Providers/Email_Provider.php +++ b/templates/src/Woo/Providers/Email_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Emails\Custom_Email; if ( ! defined( 'ABSPATH' ) ) { @@ -21,33 +18,14 @@ /** * Class Email_Provider. */ -class Email_Provider implements Service_Provider, Conditional { - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Email_Provider { /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_filter( 'woocommerce_email_classes', $this->register_email( ... ) ); } diff --git a/templates/src/Woo/Providers/Gateway_Provider.php b/templates/src/Woo/Providers/Gateway_Provider.php index 3e3c1fe..c101bfe 100644 --- a/templates/src/Woo/Providers/Gateway_Provider.php +++ b/templates/src/Woo/Providers/Gateway_Provider.php @@ -14,9 +14,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Gateways\Blocks_Payment_Method_Type; use {{NS}}\Woo\Gateways\Gateway; @@ -27,33 +24,14 @@ /** * Class Gateway_Provider. */ -class Gateway_Provider implements Service_Provider, Conditional { - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Gateway_Provider { /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_filter( 'woocommerce_payment_gateways', $this->register_gateway( ... ) ); add_action( 'woocommerce_blocks_loaded', $this->register_blocks_payment_method( ... ) ); } diff --git a/templates/src/Woo/Providers/Order_Status_Provider.php b/templates/src/Woo/Providers/Order_Status_Provider.php index aff8bec..4882bf8 100644 --- a/templates/src/Woo/Providers/Order_Status_Provider.php +++ b/templates/src/Woo/Providers/Order_Status_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Orders\Order_Status_Service; if ( ! defined( 'ABSPATH' ) ) { @@ -21,51 +18,23 @@ /** * Class Order_Status_Provider. */ -class Order_Status_Provider implements Service_Provider, Conditional { +class Order_Status_Provider { /** - * Accept an optional service override; the container builds a default - * lazily when one isn't injected. + * Custom order status service. * - * @param Order_Status_Service|null $service Order status service. + * @param Order_Status_Service $service Order status service. */ - public function __construct( private readonly ?Order_Status_Service $service = null ) { - } - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * Bind service to container. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->singleton( - Order_Status_Service::class, - function () { - return $this->service ?? new Order_Status_Service(); - } - ); + public function __construct( private readonly Order_Status_Service $service ) { } /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { - $service = $container->get( Order_Status_Service::class ); - - add_action( 'init', array( $service, 'register_status' ) ); - add_filter( 'wc_order_statuses', array( $service, 'add_to_order_statuses' ) ); + public function init_hooks(): void { + add_action( 'init', $this->service->register_status( ... ) ); + add_filter( 'wc_order_statuses', $this->service->add_to_order_statuses( ... ) ); } } diff --git a/templates/src/Woo/Providers/Product_Type_Provider.php b/templates/src/Woo/Providers/Product_Type_Provider.php index 50abe44..09bd211 100644 --- a/templates/src/Woo/Providers/Product_Type_Provider.php +++ b/templates/src/Woo/Providers/Product_Type_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Products\Custom_Product; if ( ! defined( 'ABSPATH' ) ) { @@ -21,33 +18,14 @@ /** * Class Product_Type_Provider. */ -class Product_Type_Provider implements Service_Provider, Conditional { - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Product_Type_Provider { /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_filter( 'woocommerce_product_class', Custom_Product::filter_product_class( ... ), 10, 2 ); add_filter( 'product_type_selector', Custom_Product::filter_product_type_selector( ... ) ); add_action( 'woocommerce_single_product_summary', $this->custom_product_summary_note( ... ), 25 ); diff --git a/templates/src/Woo/Providers/Shipping_Provider.php b/templates/src/Woo/Providers/Shipping_Provider.php index d4329b4..5a831f7 100644 --- a/templates/src/Woo/Providers/Shipping_Provider.php +++ b/templates/src/Woo/Providers/Shipping_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Shipping\Shipping_Method; if ( ! defined( 'ABSPATH' ) ) { @@ -21,33 +18,14 @@ /** * Class Shipping_Provider. */ -class Shipping_Provider implements Service_Provider, Conditional { - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } +class Shipping_Provider { /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + public function init_hooks(): void { add_filter( 'woocommerce_shipping_methods', $this->register_shipping_method( ... ) ); } diff --git a/templates/src/Woo/Providers/Store_Api_Provider.php b/templates/src/Woo/Providers/Store_Api_Provider.php index a0e4b11..4991d0d 100644 --- a/templates/src/Woo/Providers/Store_Api_Provider.php +++ b/templates/src/Woo/Providers/Store_Api_Provider.php @@ -9,9 +9,6 @@ namespace {{NS}}\Woo\Providers; -use {{NS}}\Contracts\Conditional; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Woo\Api\Store_Api_Extension; if ( ! defined( 'ABSPATH' ) ) { @@ -21,50 +18,22 @@ /** * Class Store_Api_Provider. */ -class Store_Api_Provider implements Service_Provider, Conditional { +class Store_Api_Provider { /** - * Accept an optional service override; the container builds a default - * lazily when one isn't injected. + * Store API cart extension. * - * @param Store_Api_Extension|null $service Extension instance. + * @param Store_Api_Extension $service Extension instance. */ - public function __construct( private readonly ?Store_Api_Extension $service = null ) { - } - - /** - * Only needed when WooCommerce is active. - * - * @return bool - */ - public function is_needed(): bool { - return class_exists( 'WooCommerce' ); - } - - /** - * Register service in container. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { - $container->singleton( - Store_Api_Extension::class, - function () { - return $this->service ?? new Store_Api_Extension(); - } - ); + public function __construct( private readonly Store_Api_Extension $service ) { } /** * Register hooks. * - * @param Container $container Application container. * @return void */ - public function boot( Container $container ): void { - $service = $container->get( Store_Api_Extension::class ); - - add_action( 'woocommerce_blocks_loaded', array( $service, 'register_store_api_extension' ) ); + public function init_hooks(): void { + add_action( 'woocommerce_blocks_loaded', $this->service->register_store_api_extension( ... ) ); } } diff --git a/templates/tests/Integration/Plugin_Boot_Test.php b/templates/tests/Integration/Plugin_Boot_Test.php index 6987053..33964f5 100644 --- a/templates/tests/Integration/Plugin_Boot_Test.php +++ b/templates/tests/Integration/Plugin_Boot_Test.php @@ -10,8 +10,6 @@ namespace {{NS}}\Tests\Integration; use {{NS}}\Plugin; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; use {{NS}}\Core\Activator; use WP_UnitTestCase; @@ -29,60 +27,36 @@ class Plugin_Boot_Test extends WP_UnitTestCase { /** - * A provider added via the '{{PREFIX}}_providers' filter should be - * booted exactly like one built into Plugin::create(). + * Reset the singleton between tests. * * @return void */ - public function test_plugin_boots_providers_added_via_the_providers_filter(): void { - $probe = new class() implements Service_Provider { - /** - * Whether boot() ran. - * - * @var bool - */ - public bool $booted = false; - - /** - * No bindings needed. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - } - - /** - * Record that boot() ran. - * - * @param Container $container Application container. - * @return void - */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - $this->booted = true; - } - }; - - add_filter( - '{{PREFIX}}_providers', - function ( $providers ) use ( $probe ) { - $providers[] = $probe; - return $providers; - } - ); + public function tear_down(): void { + Plugin::set_instance( null ); + parent::tear_down(); + } - Plugin::create()->boot(); + /** + * Booting the plugin against real WordPress registers hooks without error + * and is safe to call twice. + * + * @return void + */ + public function test_plugin_boots_without_error(): void { + Plugin::instance()->boot(); + Plugin::instance()->boot(); - $this->assertTrue( $probe->booted ); + $this->assertTrue( did_action( 'plugins_loaded' ) > 0 ); } /** - * Activation should persist the current version through a real get_option()/update_option() round trip. + * Activation persists the current version through a real + * get_option()/update_option() round trip. * * @return void */ public function test_activation_persists_the_version_option(): void { - ( new Activator() )->activate( new Container() ); + ( new Activator() )->activate(); $this->assertSame( {{PREFIX_UPPER}}_VERSION, get_option( '{{PREFIX}}_version' ) ); } diff --git a/templates/tests/Unit/Block_Registrar_Test.php b/templates/tests/Unit/Block_Registrar_Test.php index b8f0095..76250b8 100644 --- a/templates/tests/Unit/Block_Registrar_Test.php +++ b/templates/tests/Unit/Block_Registrar_Test.php @@ -14,7 +14,6 @@ use Brain\Monkey\Actions; use Brain\Monkey\Functions; use {{NS}}\Blocks\Block_Registrar; -use {{NS}}\Core\Container; /** * Class Block_Registrar_Test. @@ -38,12 +37,12 @@ protected function tearDown(): void { } /** - * Block registration is deferred to the `init` hook by boot(). + * Block registration is deferred to the `init` hook by init_hooks(). */ - public function test_boot_hooks_init(): void { + public function test_init_hooks_hooks_init(): void { Actions\expectAdded( 'init' )->once(); - ( new Block_Registrar() )->boot( new Container() ); + ( new Block_Registrar() )->init_hooks(); $this->assertTrue( true ); } diff --git a/templates/tests/Unit/Commands_Test.php b/templates/tests/Unit/Commands_Test.php index e7de4bb..5a77ee8 100644 --- a/templates/tests/Unit/Commands_Test.php +++ b/templates/tests/Unit/Commands_Test.php @@ -13,7 +13,6 @@ use Brain\Monkey; use Brain\Monkey\Functions; use {{NS}}\CLI\Commands; -use {{NS}}\Core\Container; /** * Class Commands_Test. @@ -38,14 +37,14 @@ protected function tearDown(): void { } /** - * Both WP-CLI commands are registered by boot() when WP_CLI is defined. + * Both WP-CLI commands are registered by init_hooks() when WP_CLI is defined. */ - public function test_boot_registers_commands_under_wp_cli(): void { + public function test_init_hooks_registers_commands_under_wp_cli(): void { if ( ! defined( 'WP_CLI' ) ) { define( 'WP_CLI', true ); } - ( new Commands() )->boot( new Container() ); + ( new Commands() )->init_hooks(); $this->assertArrayHasKey( '{{PREFIX}} status', \WP_CLI::$commands ); $this->assertArrayHasKey( '{{PREFIX}} cache clear', \WP_CLI::$commands ); diff --git a/templates/tests/Unit/Container_Test.php b/templates/tests/Unit/Container_Test.php deleted file mode 100644 index 784e2ec..0000000 --- a/templates/tests/Unit/Container_Test.php +++ /dev/null @@ -1,140 +0,0 @@ -bind( - 'thing', - static function () use ( &$calls ) { - ++$calls; - return new \stdClass(); - } - ); - - $first = $container->get( 'thing' ); - $second = $container->get( 'thing' ); - - $this->assertSame( 2, $calls ); - $this->assertNotSame( $first, $second ); - } - - /** - * A singleton factory runs once and caches the result. - */ - public function test_singleton_resolves_once(): void { - $container = new Container(); - $calls = 0; - - $container->singleton( - 'thing', - static function () use ( &$calls ) { - ++$calls; - return new \stdClass(); - } - ); - - $this->assertSame( $container->get( 'thing' ), $container->get( 'thing' ) ); - $this->assertSame( 1, $calls ); - } - - /** - * A singleton factory that resolves to null is still cached — array_key_exists, - * not isset (this is the regression the audit flagged). - */ - public function test_singleton_caches_a_null_result(): void { - $container = new Container(); - $calls = 0; - - $container->singleton( - 'maybe', - static function () use ( &$calls ) { - ++$calls; - return null; - } - ); - - $this->assertNull( $container->get( 'maybe' ) ); - $this->assertNull( $container->get( 'maybe' ) ); - $this->assertSame( 1, $calls, 'null must not re-trigger the factory' ); - $this->assertTrue( $container->has( 'maybe' ) ); - } - - /** - * An instance is returned exactly as it was given. - */ - public function test_instance_returns_the_same_object(): void { - $container = new Container(); - $object = new \stdClass(); - - $container->instance( 'obj', $object ); - - $this->assertSame( $object, $container->get( 'obj' ) ); - $this->assertTrue( $container->has( 'obj' ) ); - } - - /** - * The factory receives the container, so bindings can depend on bindings. - */ - public function test_factory_receives_the_container(): void { - $container = new Container(); - $container->instance( 'dep', new \stdClass() ); - $container->bind( - 'consumer', - static function ( Container $c ) { - $wrapper = new \stdClass(); - $wrapper->dep = $c->get( 'dep' ); - return $wrapper; - } - ); - - $this->assertSame( $container->get( 'dep' ), $container->get( 'consumer' )->dep ); - } - - /** - * Re-binding an id drops any cached singleton instance. - */ - public function test_rebinding_clears_the_cached_instance(): void { - $container = new Container(); - $container->singleton( 'thing', static fn () => 'first' ); - $this->assertSame( 'first', $container->get( 'thing' ) ); - - $container->bind( 'thing', static fn () => 'second' ); - $this->assertSame( 'second', $container->get( 'thing' ) ); - } - - /** - * An unknown id throws Not_Found_Exception from get(); has() reports false. - */ - public function test_unknown_id_throws_and_is_absent(): void { - $container = new Container(); - - $this->assertFalse( $container->has( 'nope' ) ); - $this->expectException( Not_Found_Exception::class ); - $container->get( 'nope' ); - } -} diff --git a/templates/tests/Unit/Example_Test.php b/templates/tests/Unit/Example_Test.php index 09e7107..58d7019 100644 --- a/templates/tests/Unit/Example_Test.php +++ b/templates/tests/Unit/Example_Test.php @@ -11,10 +11,7 @@ use PHPUnit\Framework\TestCase; use Brain\Monkey; -use Brain\Monkey\Functions; use {{NS}}\Plugin; -use {{NS}}\Contracts\Service_Provider; -use {{NS}}\Core\Container; /** * Class Example_Test. @@ -33,6 +30,7 @@ protected function setUp(): void { * Tear down test environment after each test. */ protected function tearDown(): void { + Plugin::set_instance( null ); Monkey\tearDown(); parent::tearDown(); } @@ -45,59 +43,40 @@ public function test_plugin_version_constant() { } /** - * Test that Plugin::boot() registers and boots every active provider, - * using a fake Service_Provider injected directly into the constructor - * rather than going through create()'s real module discovery. + * instance() hands back the same object every time. */ - public function test_plugin_boot() { - Functions\stubs( - array( - 'apply_filters' => function ( $tag, $value ) { - return $value; - }, - ) - ); - - $provider = new class() implements Service_Provider { - /** - * Whether register() ran. - * - * @var bool - */ - public bool $registered = false; + public function test_instance_is_shared() { + $this->assertSame( Plugin::instance(), Plugin::instance() ); + } - /** - * Whether boot() ran. - * - * @var bool - */ - public bool $booted = false; + /** + * set_instance() swaps the shared instance; null clears it. + */ + public function test_set_instance_controls_the_singleton() { + $first = Plugin::instance(); + Plugin::set_instance( null ); - /** - * Record that register() ran. - * - * @param Container $container Application container. - * @return void - */ - public function register( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - $this->registered = true; - } + $this->assertNotSame( $first, Plugin::instance() ); + } - /** - * Record that boot() ran. - * - * @param Container $container Application container. - * @return void - */ - public function boot( Container $container ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - $this->booted = true; + /** + * boot() runs once; a second call is a no-op (no double hook registration). + */ + public function test_boot_is_idempotent() { + $calls = 0; + Monkey\Functions\when( 'add_action' )->alias( + static function () use ( &$calls ) { + ++$calls; } - }; + ); + Monkey\Functions\when( 'add_filter' )->justReturn( true ); + Monkey\Functions\when( 'add_shortcode' )->justReturn( true ); - $plugin = new Plugin( new Container(), array( $provider ) ); + $plugin = Plugin::instance(); + $plugin->boot(); + $after_first = $calls; $plugin->boot(); - $this->assertTrue( $provider->registered ); - $this->assertTrue( $provider->booted ); + $this->assertSame( $after_first, $calls, 'boot() must not re-register hooks' ); } } diff --git a/templates/tests/Unit/Services_Test.php b/templates/tests/Unit/Services_Test.php new file mode 100644 index 0000000..d39d8d6 --- /dev/null +++ b/templates/tests/Unit/Services_Test.php @@ -0,0 +1,61 @@ +assertTrue( $ref->isStatic() ); + + Services::reset(); + + // After reset() the override is gone; a real accessor would rebuild. + $prop = new \ReflectionProperty( Services::class, 'instances' ); + $prop->setAccessible( true ); + $this->assertSame( array(), $prop->getValue() ); + } + + /** + * set() keeps the exact instance it was handed. + */ + public function test_set_stores_the_given_instance(): void { + $double = new \stdClass(); + Services::set( 'thing', $double ); + + $prop = new \ReflectionProperty( Services::class, 'instances' ); + $prop->setAccessible( true ); + + $this->assertSame( $double, $prop->getValue()['thing'] ); + } +} diff --git a/tests/generator.test.js b/tests/generator.test.js index c250782..04d76d4 100644 --- a/tests/generator.test.js +++ b/tests/generator.test.js @@ -220,20 +220,20 @@ test('every scaffold pins PHP 8.3 and emits modern PHP (promotion, readonly, fir assert.match(ci, /php-version:\s*\['8\.3', '8\.4'\]/); const plugin = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); - assert.match(plugin, /private readonly Container \$container/, 'constructor property promotion'); - assert.doesNotMatch(plugin, /\$this->container = \$container;/, 'no hand-written assignment'); + assert.match(plugin, /public static function instance\(\): self/, 'singleton accessor'); + assert.match(plugin, /self::\$instance \?\?= new self\(\)/); - const container = fs.readFileSync(path.join(outDir, 'src/Core/Container.php'), 'utf8'); - assert.match(container, /public function get\( string \$id \): mixed \{/, '`: mixed` is unconditional'); + const services = fs.readFileSync(path.join(outDir, 'src/Services.php'), 'utf8'); + assert.match(services, /public static function store_api_extension\(\): Woo\\Api\\Store_Api_Extension/); const settings = fs.readFileSync(path.join(outDir, 'src/Admin/Settings_Registrar.php'), 'utf8'); assert.match(settings, /add_action\( '[^']+', \$this->[a-z_]+\( \.\.\. \) \)/, 'first-class callable hook'); assert.doesNotMatch(settings, /array\( \$this, '/, 'no array-style callbacks'); const storeApiProvider = fs.readFileSync(path.join(outDir, 'src/Woo/Providers/Store_Api_Provider.php'), 'utf8'); - assert.match(storeApiProvider, /public function __construct\( private readonly \?Store_Api_Extension \$service = null \)/); + assert.match(storeApiProvider, /public function __construct\( private readonly Store_Api_Extension \$service \)/); - assert.doesNotMatch(plugin + container, /\{\{[#/]?if/, 'no leftover conditional tags'); + assert.doesNotMatch(plugin + services, /\{\{[#/]?if/, 'no leftover conditional tags'); fs.rmSync(outDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); @@ -451,7 +451,7 @@ test('generated plugin version defaults to 1.0.0', () => { fs.rmSync(outDir, { recursive: true, force: true }); }); -test('foundational contracts and container are always scaffolded with no leftover tokens', () => { +test('foundational classes (Plugin bootloader + Services locator) always scaffold with no leftover tokens', () => { const outDir = path.join(__dirname, '../tmp-test-foundation'); runGenerator({ name: 'Foundation Plugin', @@ -464,10 +464,8 @@ test('foundational contracts and container are always scaffolded with no leftove }); const files = [ - 'src/Core/Container.php', - 'src/Core/Exceptions/Not_Found_Exception.php', - 'src/Contracts/Service_Provider.php', - 'src/Contracts/Conditional.php', + 'src/Plugin.php', + 'src/Services.php', 'src/Contracts/Activatable.php', 'src/Contracts/Deactivatable.php' ]; @@ -476,7 +474,8 @@ test('foundational contracts and container are always scaffolded with no leftove const content = fs.readFileSync(path.join(outDir, f), 'utf8'); assert.ok(!/\{\{[A-Z_]+\}\}/.test(content), `no unreplaced template tokens should remain in ${f}`); } - assert.ok(!fs.existsSync(path.join(outDir, 'src/Contracts/Registrable.php')), 'Registrable was replaced by Service_Provider'); + assert.ok(!fs.existsSync(path.join(outDir, 'src/Core/Container.php')), 'the DI container is gone: static bootloader + Services locator'); + assert.ok(!fs.existsSync(path.join(outDir, 'src/Contracts/Service_Provider.php')), 'no Service_Provider contract'); assert.ok(!fs.existsSync(path.join(outDir, 'src/Core/Uninstaller.php')), 'no Uninstaller without a module that persists cleanup-worthy state (0.7)'); assert.ok(!fs.existsSync(path.join(outDir, 'uninstall.php')), 'no uninstall.php in a zero-module scaffold (0.7)'); @@ -488,7 +487,7 @@ test('foundational contracts and container are always scaffolded with no leftove fs.rmSync(outDir, { recursive: true, force: true }); }); -test('Plugin.php is a pure composition root (no hooks registered directly), and Widget_Registrar owns Elementor\'s hooks in its own boot()', () => { +test('Plugin.php is a pure bootloader (no hooks registered directly), and Widget_Registrar owns Elementor\'s hooks in its own init_hooks()', () => { const outDir = path.join(__dirname, '../tmp-test-elementor-boot'); runGenerator({ name: 'Elementor Boot Plugin', @@ -502,15 +501,13 @@ test('Plugin.php is a pure composition root (no hooks registered directly), and const pluginPhp = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); assert.ok(!pluginPhp.includes('add_action'), 'Plugin.php itself should never register WordPress hooks directly'); - assert.ok(pluginPhp.includes('private readonly Container $container')); - assert.ok(pluginPhp.includes('private readonly array $providers')); - assert.ok(pluginPhp.includes('public static function create(): self')); - assert.ok(pluginPhp.includes('new Elementor\\Widget_Registrar();')); - assert.ok(pluginPhp.includes('new Elementor\\Dependency_Notice();')); + assert.ok(pluginPhp.includes('public static function instance(): self')); + assert.ok(pluginPhp.includes('( new Elementor\\Widget_Registrar() )->init_hooks();')); + assert.ok(pluginPhp.includes('( new Elementor\\Dependency_Notice() )->init_hooks();')); const widgetRegistrar = fs.readFileSync(path.join(outDir, 'src/Elementor/Widget_Registrar.php'), 'utf8'); assert.ok(!/\{\{[A-Z_]+\}\}/.test(widgetRegistrar), 'no unreplaced template tokens should remain'); - const registrarBootBody = widgetRegistrar.slice(widgetRegistrar.indexOf('public function boot(')); + const registrarBootBody = widgetRegistrar.slice(widgetRegistrar.indexOf('public function init_hooks(')); assert.ok(registrarBootBody.includes("add_action( 'elementor/widgets/register'")); assert.ok(registrarBootBody.includes("add_action( 'wp_enqueue_scripts'")); @@ -544,8 +541,8 @@ test('React admin app + admin_settings: root div mounted, Assets.php scoped to t assert.ok(!/\{\{[A-Z_]+\}\}/.test(assetsPhp), 'no unreplaced template tokens should remain'); const pluginPhp = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); - assert.ok(pluginPhp.includes("new Admin\\Assets()")); - assert.ok(pluginPhp.includes("new Admin\\Settings_Registrar()")); + assert.ok(pluginPhp.includes("( new Admin\\Assets() )->init_hooks();")); + assert.ok(pluginPhp.includes("( new Admin\\Settings_Registrar( Services::settings_repository() ) )->init_hooks();")); const mainPhp = fs.readFileSync(path.join(outDir, 'react-admin-plugin.php'), 'utf8'); assert.ok(mainPhp.includes('Requires at least: 6.0'), 'React alone must not bump the minimum WP version'); @@ -663,14 +660,15 @@ test('block module: native block.json + edit + server render, wired via Block_Re assert.ok(fs.readFileSync(path.join(outDir, 'assets/src/blocks/example-static/save.js'), 'utf8').includes('RichText.Content'), 'static save() serializes markup'); const registrar = fs.readFileSync(path.join(outDir, 'src/Blocks/Block_Registrar.php'), 'utf8'); - assert.ok(registrar.includes('implements Service_Provider')); + assert.ok(!registrar.includes('implements'), 'plain class, no Service_Provider contract'); + assert.ok(registrar.includes('public function init_hooks(): void')); assert.ok(registrar.includes("add_action( 'init', $this->register_blocks( ... ) )")); assert.ok(registrar.includes("glob( $build_dir . '/*', GLOB_ONLYDIR )"), 'discovers every built block dir, so new blocks need no PHP change'); assert.ok(registrar.includes('register_block_type( $block_dir )')); assert.ok(!/\{\{[A-Z_]+\}\}/.test(registrar), 'no unreplaced tokens'); const pluginPhp = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); - assert.ok(pluginPhp.includes('new Blocks\\Block_Registrar();')); + assert.ok(pluginPhp.includes('( new Blocks\\Block_Registrar() )->init_hooks();')); // block flips the build pipeline on, but a block-only build needs no // webpack.config.js override — wp-scripts finds block.json on its own. @@ -774,7 +772,8 @@ test('WooCommerce module: gateway, shipping, email, product type, blocks payment const gatewayProvider = fs.readFileSync(path.join(outDir, 'src/Woo/Providers/Gateway_Provider.php'), 'utf8'); assert.ok(gatewayProvider.includes("add_filter( 'woocommerce_payment_gateways'")); assert.ok(gatewayProvider.includes('woocommerce_blocks_payment_method_type_registration')); - assert.ok(gatewayProvider.includes('function is_needed(): bool')); + assert.ok(gatewayProvider.includes('public function init_hooks(): void')); + assert.ok(!gatewayProvider.includes('is_needed'), 'the class_exists guard lives in Plugin::boot() now'); const shippingProvider = fs.readFileSync(path.join(outDir, 'src/Woo/Providers/Shipping_Provider.php'), 'utf8'); assert.ok(shippingProvider.includes("add_filter( 'woocommerce_shipping_methods'")); @@ -787,11 +786,12 @@ test('WooCommerce module: gateway, shipping, email, product type, blocks payment assert.ok(productTypeProvider.includes("add_filter( 'product_type_selector'")); const pluginPhpWoo = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); - assert.ok(pluginPhpWoo.includes('new Woo\\Providers\\Gateway_Provider();')); - assert.ok(pluginPhpWoo.includes('new Woo\\Providers\\Shipping_Provider();')); - assert.ok(pluginPhpWoo.includes('new Woo\\Providers\\Email_Provider();')); - assert.ok(pluginPhpWoo.includes('new Woo\\Providers\\Product_Type_Provider();')); - assert.ok(pluginPhpWoo.includes('new Woo\\Providers\\Blocks_Provider();')); + assert.ok(pluginPhpWoo.includes("if ( class_exists( 'WooCommerce' ) ) {"), 'woo providers wrapped in one guard'); + assert.ok(pluginPhpWoo.includes('( new Woo\\Providers\\Gateway_Provider() )->init_hooks();')); + assert.ok(pluginPhpWoo.includes('( new Woo\\Providers\\Shipping_Provider() )->init_hooks();')); + assert.ok(pluginPhpWoo.includes('( new Woo\\Providers\\Email_Provider() )->init_hooks();')); + assert.ok(pluginPhpWoo.includes('( new Woo\\Providers\\Product_Type_Provider() )->init_hooks();')); + assert.ok(pluginPhpWoo.includes('( new Woo\\Providers\\Blocks_Provider() )->init_hooks();')); const blocksType = fs.readFileSync(path.join(outDir, 'src/Woo/Gateways/Blocks_Payment_Method_Type.php'), 'utf8'); assert.ok(blocksType.includes("protected $name = 'wfp_gateway';")); @@ -885,7 +885,7 @@ test('composer.json package name derives from the author, not a literal "vendor/ fs.rmSync(outDir, { recursive: true, force: true }); }); -test('cpt_taxonomy Activator resolves Post_Types through the container with a fully-qualified class reference', () => { +test('cpt_taxonomy Activator news up Post_Types with a fully-qualified class reference', () => { const outDir = path.join(__dirname, '../tmp-test-cpt-activator'); runGenerator({ name: 'Cpt Activator Plugin', @@ -900,12 +900,12 @@ test('cpt_taxonomy Activator resolves Post_Types through the container with a fu const activatorPhp = fs.readFileSync(path.join(outDir, 'src/Core/Activator.php'), 'utf8'); assert.ok(!/\{\{[A-Z_]+\}\}/.test(activatorPhp), 'no unreplaced template tokens should remain'); assert.ok(activatorPhp.includes('implements Activatable')); - assert.ok(activatorPhp.includes('public function activate( Container $container )')); + assert.ok(activatorPhp.includes('public function activate(): void')); // Must be fully-qualified (leading backslash): Activator.php lives in the // {{NS}}\Core namespace, so an unqualified "PostTypes\Post_Types" reference // would resolve to the nonexistent {{NS}}\Core\PostTypes\Post_Types and // fatal at runtime the moment the plugin is activated. - assert.ok(activatorPhp.includes('$container->get( \\CptActivatorPlugin\\PostTypes\\Post_Types::class )')); + assert.ok(activatorPhp.includes('( new \\CptActivatorPlugin\\PostTypes\\Post_Types() )->register_cpt_and_taxonomy();')); // B6.19: soft flush, and no phpcs:ignore papering over the VIP sniff. assert.ok(activatorPhp.includes('flush_rewrite_rules( false );')); assert.ok(!activatorPhp.includes('phpcs:ignore'), 'wp-org target needs no suppression for flush_rewrite_rules'); @@ -1050,7 +1050,7 @@ test('Jest unit tests + admin_settings-aware E2E spec ship with React admin app' fs.rmSync(outDir, { recursive: true, force: true }); }); -test('caching module scaffolds Cache_Service as a container-resolvable provider', () => { +test('caching module scaffolds Cache_Service, reachable via Services::cache()', () => { const outDir = path.join(__dirname, '../tmp-test-caching'); runGenerator({ name: 'Caching Plugin', @@ -1064,12 +1064,12 @@ test('caching module scaffolds Cache_Service as a container-resolvable provider' const cacheService = fs.readFileSync(path.join(outDir, 'src/Cache/Cache_Service.php'), 'utf8'); assert.ok(!/\{\{[A-Z_]+\}\}/.test(cacheService), 'no unreplaced template tokens should remain'); - assert.ok(cacheService.includes('implements Service_Provider')); + assert.ok(!cacheService.includes('implements'), 'plain class now'); assert.ok(cacheService.includes("wp_cache_get")); assert.ok(cacheService.includes('get_transient')); - const pluginPhp = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); - assert.ok(pluginPhp.includes('new Cache\\Cache_Service();')); + const services = fs.readFileSync(path.join(outDir, 'src/Services.php'), 'utf8'); + assert.ok(services.includes('public static function cache(): Cache\\Cache_Service')); fs.rmSync(outDir, { recursive: true, force: true }); }); @@ -1088,7 +1088,8 @@ test('custom_table module scaffolds a dbDelta Schema + Item_Repository, wired in const schema = fs.readFileSync(path.join(outDir, 'src/Database/Schema.php'), 'utf8'); assert.ok(!/\{\{[A-Z_]+\}\}/.test(schema), 'no unreplaced template tokens should remain'); - assert.ok(schema.includes('implements Service_Provider')); + assert.ok(!schema.includes('implements'), 'plain class now'); + assert.ok(schema.includes('public function init_hooks(): void')); assert.ok(schema.includes('dbDelta(')); assert.ok(schema.includes("PRIMARY KEY")); assert.ok(schema.includes('KEY status')); @@ -1097,11 +1098,11 @@ test('custom_table module scaffolds a dbDelta Schema + Item_Repository, wired in assert.ok(!/\{\{[A-Z_]+\}\}/.test(repository)); const pluginPhp = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); - assert.ok(pluginPhp.includes('new Database\\Schema();')); + assert.ok(pluginPhp.includes('( new Database\\Schema() )->init_hooks();')); const activatorPhp = fs.readFileSync(path.join(outDir, 'src/Core/Activator.php'), 'utf8'); assert.ok(!/\{\{[A-Z_]+\}\}/.test(activatorPhp)); - assert.ok(activatorPhp.includes('$container->get( \\CustomTablePlugin\\Database\\Schema::class )->create_table();')); + assert.ok(activatorPhp.includes('( new \\CustomTablePlugin\\Database\\Schema() )->create_table();')); const uninstallerPhp = fs.readFileSync(path.join(outDir, 'src/Core/Uninstaller.php'), 'utf8'); assert.ok(!/\{\{[A-Z_]+\}\}/.test(uninstallerPhp)); @@ -1167,7 +1168,7 @@ test('0.6 cli module owns src/CLI/Commands.php and its Plugin.php wiring', () => assert.ok(!fs.existsSync(path.join(off, 'src/CLI/Commands.php')), 'no Commands.php without the cli module'); assert.ok(!fs.existsSync(path.join(off, 'tests/Unit/Commands_Test.php'))); const pluginOff = fs.readFileSync(path.join(off, 'src/Plugin.php'), 'utf8'); - assert.ok(!pluginOff.includes('WP_CLI'), 'Plugin::create() must not reference WP_CLI without the module'); + assert.ok(!pluginOff.includes('WP_CLI'), 'Plugin::boot() must not reference WP_CLI without the module'); assert.ok(!pluginOff.includes('new CLI\\Commands()')); assert.ok(!/\{\{[#/]?[A-Za-z_]/.test(pluginOff), 'no leftover template tags'); fs.rmSync(off, { recursive: true, force: true }); @@ -1179,11 +1180,11 @@ test('0.6 cli module owns src/CLI/Commands.php and its Plugin.php wiring', () => }); const commands = fs.readFileSync(path.join(on, 'src/CLI/Commands.php'), 'utf8'); assert.ok(!/^\s*if \( ! defined\( 'WP_CLI' \) \|\| ! WP_CLI \) \{\s*$/m.test(commands.split('class Commands')[0]), 'no top-level return guard before the class (B6.17)'); - assert.ok(commands.includes('class Commands implements Service_Provider')); + assert.ok(/class Commands \{/.test(commands), 'plain class, no Service_Provider contract'); assert.ok(fs.existsSync(path.join(on, 'tests/Unit/Commands_Test.php'))); const pluginOn = fs.readFileSync(path.join(on, 'src/Plugin.php'), 'utf8'); assert.ok(pluginOn.includes("if ( defined( 'WP_CLI' ) && WP_CLI ) {")); - assert.ok(pluginOn.includes('new CLI\\Commands();')); + assert.ok(pluginOn.includes('( new CLI\\Commands() )->init_hooks();')); fs.rmSync(on, { recursive: true, force: true }); }); @@ -1393,10 +1394,10 @@ test('WooCommerce granular sub-modules: order-status, action-scheduler, store-ap assert.ok(fs.existsSync(path.join(outDir, 'tests/Unit/Account_Endpoint_Service_Test.php'))); const pluginPhp = fs.readFileSync(path.join(outDir, 'src/Plugin.php'), 'utf8'); - assert.ok(pluginPhp.includes('new Woo\\Providers\\Order_Status_Provider();')); - assert.ok(pluginPhp.includes('new Woo\\Providers\\Action_Scheduler_Provider();')); - assert.ok(pluginPhp.includes('new Woo\\Providers\\Store_Api_Provider();')); - assert.ok(pluginPhp.includes('new Woo\\Providers\\Account_Endpoint_Provider();')); + assert.ok(pluginPhp.includes('( new Woo\\Providers\\Order_Status_Provider( Services::order_status_service() ) )->init_hooks();')); + assert.ok(pluginPhp.includes('( new Woo\\Providers\\Action_Scheduler_Provider( Services::action_scheduler_service() ) )->init_hooks();')); + assert.ok(pluginPhp.includes('( new Woo\\Providers\\Store_Api_Provider( Services::store_api_extension() ) )->init_hooks();')); + assert.ok(pluginPhp.includes('( new Woo\\Providers\\Account_Endpoint_Provider( Services::account_endpoint_service() ) )->init_hooks();')); fs.rmSync(outDir, { recursive: true, force: true }); }); From 3584fcb9bc098f1371bfd17839b463dad2588813 Mon Sep 17 00:00:00 2001 From: Akshat Date: Sat, 29 Aug 2026 20:26:31 +0530 Subject: [PATCH 2/2] fix(templates): capitalise docblock short descriptions in Example_Test/Services_Test --- templates/tests/Unit/Example_Test.php | 6 +++--- templates/tests/Unit/Services_Test.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/tests/Unit/Example_Test.php b/templates/tests/Unit/Example_Test.php index 58d7019..a90d7c5 100644 --- a/templates/tests/Unit/Example_Test.php +++ b/templates/tests/Unit/Example_Test.php @@ -43,14 +43,14 @@ public function test_plugin_version_constant() { } /** - * instance() hands back the same object every time. + * The shared instance is handed back on every instance() call. */ public function test_instance_is_shared() { $this->assertSame( Plugin::instance(), Plugin::instance() ); } /** - * set_instance() swaps the shared instance; null clears it. + * A set_instance() call swaps the shared instance; null clears it. */ public function test_set_instance_controls_the_singleton() { $first = Plugin::instance(); @@ -60,7 +60,7 @@ public function test_set_instance_controls_the_singleton() { } /** - * boot() runs once; a second call is a no-op (no double hook registration). + * The first boot() wires hooks; a second call is a no-op (no double registration). */ public function test_boot_is_idempotent() { $calls = 0; diff --git a/templates/tests/Unit/Services_Test.php b/templates/tests/Unit/Services_Test.php index d39d8d6..ca31267 100644 --- a/templates/tests/Unit/Services_Test.php +++ b/templates/tests/Unit/Services_Test.php @@ -29,7 +29,7 @@ protected function tearDown(): void { } /** - * set() then reset() controls what an accessor hands back. + * A set() then reset() controls what an accessor hands back. */ public function test_set_overrides_and_reset_clears(): void { $double = new \stdClass(); @@ -47,7 +47,7 @@ public function test_set_overrides_and_reset_clears(): void { } /** - * set() keeps the exact instance it was handed. + * A set() keeps the exact instance it was handed. */ public function test_set_stores_the_given_instance(): void { $double = new \stdClass();