| @@ -12,11 +12,9 @@ | ||
| 12 | 12 | |
| 13 | 13 | use WCPOS\WooCommercePOS\Admin\Consent; |
| 14 | 14 | use WCPOS\WooCommercePOS\Admin\Menu; |
| 15 | 15 | use WCPOS\WooCommercePOS\Services\Auth as AuthService; |
| 16 | -use WCPOS\WooCommercePOS\Services\Extensions; | |
| 17 | -use WCPOS\WooCommercePOS\Services\Receipt_Snapshot_Store; | |
| 18 | -use WCPOS\WooCommercePOS\Services\Settings as SettingsService; | |
| 16 | +use WCPOS\WooCommercePOS\Services\Service_Groups; | |
| 19 | 17 | |
| 20 | 18 | /** |
| 21 | 19 | * Init class. |
| 22 | 20 | */ |
| @@ -272,8 +270,20 @@ | ||
| 272 | 270 | 'args' => 0, |
| 273 | 271 | 'reason' => 'Append after lifecycle; REST priority 999 reports the final response status, gated by consent (#1811).', |
| 274 | 272 | 'phase' => 'post-latch', |
| 275 | 273 | ), |
| 274 | + array( | |
| 275 | + 'hook' => null, | |
| 276 | + 'callback' => static function (): void { | |
| 277 | + add_action( 'wp_logout', array( Services\Auth::class, 'revoke_web_session_on_logout' ), 10, 1 ); | |
| 278 | + add_action( 'password_reset', array( Services\Auth::class, 'revoke_sessions_on_password_reset' ), 10, 1 ); | |
| 279 | + add_action( 'profile_update', array( Services\Auth::class, 'revoke_sessions_on_password_change' ), 10, 2 ); | |
| 280 | + }, | |
| 281 | + 'priority' => 10, | |
| 282 | + 'args' => 0, | |
| 283 | + 'reason' => 'Default 10; logout ends this browser\'s web POS session, a password reset or change ends every POS session. Auth is built only when one fires.', | |
| 284 | + 'phase' => 'post-latch', | |
| 285 | + ), | |
| 276 | 286 | ); |
| 277 | 287 | |
| 278 | 288 | return array_values( |
| 279 | 289 | array_filter( |
| @@ -508,142 +518,52 @@ | ||
| 508 | 518 | } |
| 509 | 519 | } |
| 510 | 520 | |
| 511 | 521 | /** |
| 512 | - * Groups constructed so far in this request (test seam; see constructed_groups()). | |
| 513 | - * | |
| 514 | - * @var array<string, bool> | |
| 515 | - */ | |
| 516 | - private static array $constructed = array(); | |
| 517 | - | |
| 518 | - /** | |
| 519 | 522 | * Common initializations, by request lane. |
| 520 | 523 | * |
| 521 | - * Every request gets the services whose hooks WooCommerce consults on a | |
| 522 | - * plain shopper page BEFORE any order exists: translations, the product | |
| 523 | - * visibility filters, the order statuses and the read-side order filters | |
| 524 | - * (My Account renders POS orders), the gateway registration (WooCommerce | |
| 525 | - * builds its gateway list on cart pages) and the reserved-stock filter | |
| 526 | - * (POS drafts must reduce online availability at add-to-cart time). | |
| 524 | + * Every request gets the always group; everything else is constructed only | |
| 525 | + * on the lanes that use it, and the order group additionally on the first | |
| 526 | + * order write of ANY request, so the lane classifier is an optimisation | |
| 527 | + * rather than a correctness gate. {@see Service_Groups} holds the membership | |
| 528 | + * of each group and the reasons behind it. | |
| 527 | 529 | * |
| 528 | - * Everything else is constructed only on the lanes that use it, and the | |
| 529 | - * order-event services additionally on the first order write of ANY request | |
| 530 | - * ({@see ensure_order_services()}), so the lane classifier is an | |
| 531 | - * optimisation rather than a correctness gate. Measured 2026-09-03: a | |
| 532 | - * storefront page loaded ~80 plugin files and 22 objects for hooks that | |
| 533 | - * never fire there (see .claude/research/2026-09-03-lazy-service-construction-spec.md). | |
| 530 | + * Measured 2026-09-03: a storefront page loaded ~80 plugin files and 22 | |
| 531 | + * objects for hooks that never fire there (see | |
| 532 | + * .claude/research/2026-09-03-lazy-service-construction-spec.md). | |
| 534 | 533 | */ |
| 535 | 534 | private function init_common(): void { |
| 536 | - self::$constructed['always'] = true; | |
| 535 | + Service_Groups::ensure( Service_Groups::ALWAYS ); | |
| 537 | 536 | |
| 538 | - // init the Services. | |
| 539 | - SettingsService::instance(); | |
| 540 | - AuthService::instance(); | |
| 541 | - | |
| 542 | - // Needed on every lane, including a plain storefront page. | |
| 543 | - new i18n(); | |
| 544 | - new Gateways(); | |
| 545 | - new Products(); | |
| 546 | - new Orders(); | |
| 547 | - Services\Stock_Validator::instance(); | |
| 548 | - Services\Order_Write_Intent::register(); | |
| 549 | - | |
| 550 | 537 | if ( Services\Request_Lane::is_storefront() ) { |
| 551 | 538 | // Order-event services arrive on the first order write, if any. |
| 552 | - self::arm_order_services(); | |
| 539 | + Service_Groups::arm_order_group(); | |
| 553 | 540 | return; |
| 554 | 541 | } |
| 555 | 542 | |
| 556 | - self::ensure_order_services(); | |
| 557 | - self::construct_pos_services(); | |
| 543 | + Service_Groups::ensure( Service_Groups::ORDER ); | |
| 544 | + Service_Groups::ensure( Service_Groups::POS ); | |
| 558 | 545 | } |
| 559 | 546 | |
| 560 | 547 | /** |
| 561 | - * Services only POS, admin, REST, cron and CLI requests use. | |
| 562 | - */ | |
| 563 | - private static function construct_pos_services(): void { | |
| 564 | - if ( isset( self::$constructed['pos'] ) ) { | |
| 565 | - return; | |
| 566 | - } | |
| 567 | - self::$constructed['pos'] = true; | |
| 568 | - Extensions::instance(); | |
| 569 | - new Services\Decimal_Quantities(); | |
| 570 | - new Services\Customer_Meta_Parity(); | |
| 571 | - } | |
| 572 | - | |
| 573 | - /** | |
| 574 | - * Hook the order-event services to the first order write of the request. | |
| 575 | - * | |
| 576 | - * Every WooCommerce order write — create, update, status transition, | |
| 577 | - * `payment_complete()`, refund — goes through `WC_Abstract_Order::save()`, | |
| 578 | - * which fires `woocommerce_before_order_object_save` before the data store | |
| 579 | - * writes and before `woocommerce_new_order` / `woocommerce_order_status_changed` | |
| 580 | - * / `woocommerce_payment_complete` fire. Priority 0 there means every | |
| 581 | - * observer exists before any order is written — on a webhook, a cron | |
| 582 | - * spawned from a page view, a third-party plugin creating an order on | |
| 583 | - * `template_redirect`, or a lane the classifier got wrong. Nothing in the | |
| 584 | - * order group listens to trash or delete, so those need no arming. | |
| 585 | - */ | |
| 586 | - private static function arm_order_services(): void { | |
| 587 | - add_action( 'woocommerce_before_order_object_save', array( self::class, 'ensure_order_services' ), 0, 0 ); | |
| 588 | - } | |
| 589 | - | |
| 590 | - /** | |
| 591 | 548 | * Construct the order-event services exactly once per request. |
| 592 | 549 | * |
| 593 | 550 | * Idempotent and safe to call after `init`; each service handles its own |
| 594 | 551 | * late registration. Fires `woocommerce_pos_order_services_ready` once so |
| 595 | 552 | * Pro and extensions can construct their own order-event services at the |
| 596 | - * same moment on every lane. | |
| 553 | + * same moment on every lane. Implementation now lives in Service_Groups. | |
| 597 | 554 | */ |
| 598 | 555 | public static function ensure_order_services(): void { |
| 599 | - if ( isset( self::$constructed['order'] ) ) { | |
| 600 | - return; | |
| 601 | - } | |
| 602 | - self::$constructed['order'] = true; | |
| 603 | - | |
| 604 | - Receipt_Snapshot_Store::instance(); | |
| 605 | - new Emails(); | |
| 606 | - new Templates(); | |
| 607 | - new Services\Print_Job_Service(); | |
| 608 | - new Services\Cloud_Print_Trigger_Service(); | |
| 609 | - new Services\Cloud_Print_Submit_Service(); | |
| 610 | - new Services\Cloud_Print_Relay_Service(); | |
| 611 | - | |
| 612 | - /** | |
| 613 | - * Fires once per request when the POS order-event services exist: | |
| 614 | - * eagerly on POS, admin, REST, cron and CLI requests (from this | |
| 615 | - * plugin's `init` callback at priority 10), and on a storefront | |
| 616 | - * request the moment the first order is about to be written. | |
| 617 | - * | |
| 618 | - * Because the eager firing happens at `init` priority 10, a listener | |
| 619 | - * added later than that (for example from another plugin's `init` | |
| 620 | - * callback at priority 20) must check `did_action()` first and | |
| 621 | - * construct immediately when the action has already fired. | |
| 622 | - * | |
| 623 | - * @since 1.10.8 | |
| 624 | - */ | |
| 625 | - do_action( 'woocommerce_pos_order_services_ready' ); | |
| 556 | + Service_Groups::ensure( Service_Groups::ORDER ); | |
| 626 | 557 | } |
| 627 | 558 | |
| 628 | 559 | /** |
| 629 | - * Which service groups this request has constructed: 'always', 'order', 'pos'. | |
| 630 | - * | |
| 631 | - * @internal Test seam. | |
| 632 | - * | |
| 633 | - * @return string[] | |
| 634 | - */ | |
| 635 | - public static function constructed_groups(): array { | |
| 636 | - return array_keys( self::$constructed ); | |
| 637 | - } | |
| 638 | - | |
| 639 | - /** | |
| 640 | 560 | * Forget which groups were constructed. Tests only. |
| 641 | 561 | * |
| 642 | 562 | * @internal |
| 643 | 563 | */ |
| 644 | 564 | public static function reset_request_state(): void { |
| 645 | - self::$constructed = array(); | |
| 565 | + Service_Groups::reset(); | |
| 646 | 566 | Services\Request_Lane::reset(); |
| 647 | 567 | } |
| 648 | 568 | |
| 649 | 569 | /** |