PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
← All changes | includes/Init.php +139 -11 1.10.7 → 1.10.17 View file →
@@ -66,12 +66,12 @@
66 66 * | 7 | `woocommerce_pos_sync_proxy_response` | `Sync\Revision::stamp_proxy_revisions` (via `Augmentation_Pipeline::install()`) | **9** | **ORDER-CRITICAL** | Between `Meta_Normalizer` (5) and the uuid/digest stampers (10). Revision must hash the normalized-but-not-yet-augmented payload. |
67 67 * | 8 | `woocommerce_pos_sync_proxy_response`, `..._serialized_product` | `Proxy_Uuid_Stamper`, `Integrity_Digest` digest stampers, pipeline projections | 10 | order-critical (by number) | Preserved verbatim from the hand-wiring the pipeline replaced, so third-party code hooking either public filter still runs where it always did. |
68 68 * | 9 | `woocommerce_before_product_object_save`, `woocommerce_before_product_variation_object_save` | `Sync\Pos_Uuid::stamp_on_save` | 10 | irrelevant | Default. The HOOK is the design (before the data store writes, so the uuid lands in the same save); the number is not. Registered unconditionally — identity is core, not an observer. |
69 69 * | 9a | `untrashed_post`, `woocommerce_untrash_order` | `Sync\Pos_Uuid::recheck_ownership_after_untrash`, `::recheck_order_ownership_after_untrash` | 10 | irrelevant | Default. Re-proves uuid ownership when a record leaves the trash — the one seam a native restore passes through (#1805, ADR 0038). Unconditional for the same reason as row 9; the journal and digest observers (rows 10, 12) share both hooks at the same priority once the latch is set, and nothing depends on the order. |
70 - * | 10 | 32 catalogue/customer/order hooks | `Sync\Sync_Journal` (33 callbacks) | 10 | irrelevant | Default throughout. |
70 + * | 10 | 32 catalogue/customer/order hooks, plus `shutdown` | `Sync\Sync_Journal` (34 callbacks) | 10 (`shutdown` at `PHP_INT_MAX`) | `shutdown`: order-critical (by number) | Default throughout. `woocommerce_update_order` only MARKS the order dirty; the `hook:update` row lands on `flush_pending_order_updates()` — at `shutdown`, before any other-origin row for that order, or when a different order is saved — so one online checkout writes one update row, not eleven. The shutdown flush runs LAST because WooCommerce saves the customer at 10 and the session at 20; a save those trigger after the flush is written immediately. |
71 71 * | 10b | `delete_option` plus `pre_update_option_*`, `update_option_*`, `add_option_*`, `delete_option_*` for the two `Pos_Visibility::source_options()` | `Sync\Visibility_Observer` (9 callbacks) | 10 | irrelevant | Default. Appends the journal row for a record entering or leaving the POS servable set — the transition the sequence-log stream relies on, since it drops a hidden record's update rows. `delete_option` is the generic PRE-delete action (the per-option form fires after) and is gated on the option name inside the callback. Registered after `Sync_Journal` only because it writes through it; the constructor also runs the observer's one-time tombstone seed. |
72 72 * | 11 | `wcpos_sync_journal_purge` | `Sync\Sync_Journal_Purge::run_purge` | 10 | irrelevant | Cron callback; sole listener. This call also SCHEDULES the daily event. |
73 - * | 12 | 21 catalogue/customer/order hooks (a subset of row 10's) | `Sync\Integrity_Digest` | 10 | unknown | Default. Shares every one of its hooks with `Sync_Journal` at the same priority, so the journal always runs first — no code found that depends on that, but nothing pins it either. |
73 + * | 12 | 21 catalogue/customer/order hooks (a subset of row 10's), plus `shutdown` | `Sync\Integrity_Digest` | 10 (`shutdown` at `PHP_INT_MAX`) | unknown | Default. Shares every one of its hooks with `Sync_Journal` at the same priority, so the journal always runs first — no code found that depends on that, but nothing pins it either. Every save — product, variation, customer, order — only MARKS the digest dirty; the upsert lands on `flush_pending_digests()` at `shutdown`, before any `Digest_Index::read_digests()`, or when the queue holds 50 records. |
74 74 * | 13 | `init` | `Init::init` | 10 | **ORDER-CRITICAL, CROSS-PLUGIN** | Default. **Pro registers its own `init` at 20** (`woocommerce-pos-pro/includes/Init.php:32`) so free's services exist first. Raising free's number silently breaks Pro; nothing on either side tests it. |
75 75 * | 14 | `rest_api_init` | `Init::init_rest_api` | **20** | **ORDER-CRITICAL, CROSS-PLUGIN** | Free's own reason: unknown — the number dates to the initial commit (8f2b9eac, 2021-03-16). It is load-bearing anyway: **Pro registers `rest_api_init` at 9**, commented "Before the free version" (`woocommerce-pos-pro/includes/Init.php:33`). Untested on both sides. |
76 76 * | 15 | `query_vars` | `Init::query_vars` | 10 | irrelevant | Default; appends one var. |
77 77 * | 16 | `pre_update_option_woocommerce_pos_pro_settings_license` | `Init::remove_license_transient` | 10 | irrelevant | Default. The reentrancy guard, not the priority, is what makes it safe (f33b8d655). |
@@ -443,41 +443,159 @@
443 443 }
444 444 }
445 445
446 446 /**
447 - * Common initializations.
447 + * Groups constructed so far in this request (test seam; see constructed_groups()).
448 + *
449 + * @var array<string, bool>
448 450 */
451 + private static array $constructed = array();
452 +
453 + /**
454 + * Common initializations, by request lane.
455 + *
456 + * Every request gets the services whose hooks WooCommerce consults on a
457 + * plain shopper page BEFORE any order exists: translations, the product
458 + * visibility filters, the order statuses and the read-side order filters
459 + * (My Account renders POS orders), the gateway registration (WooCommerce
460 + * builds its gateway list on cart pages) and the reserved-stock filter
461 + * (POS drafts must reduce online availability at add-to-cart time).
462 + *
463 + * Everything else is constructed only on the lanes that use it, and the
464 + * order-event services additionally on the first order write of ANY request
465 + * ({@see ensure_order_services()}), so the lane classifier is an
466 + * optimisation rather than a correctness gate. Measured 2026-09-03: a
467 + * storefront page loaded ~80 plugin files and 22 objects for hooks that
468 + * never fire there (see .claude/research/2026-09-03-lazy-service-construction-spec.md).
469 + */
449 470 private function init_common(): void {
471 + self::$constructed['always'] = true;
472 +
450 473 // init the Services.
451 474 SettingsService::instance();
452 475 AuthService::instance();
453 - Extensions::instance();
454 - Receipt_Snapshot_Store::instance();
455 476
456 - // init other functionality needed by both frontend and admin.
477 + // Needed on every lane, including a plain storefront page.
457 478 new i18n();
458 479 new Gateways();
459 480 new Products();
460 481 new Orders();
461 - new Emails();
462 - new Templates();
463 482 Services\Stock_Validator::instance();
483 +
484 + if ( Services\Request_Lane::is_storefront() ) {
485 + // Order-event services arrive on the first order write, if any.
486 + self::arm_order_services();
487 + return;
488 + }
489 +
490 + self::ensure_order_services();
491 + self::construct_pos_services();
492 + }
493 +
494 + /**
495 + * Services only POS, admin, REST, cron and CLI requests use.
496 + */
497 + private static function construct_pos_services(): void {
498 + if ( isset( self::$constructed['pos'] ) ) {
499 + return;
500 + }
501 + self::$constructed['pos'] = true;
502 + Extensions::instance();
464 503 new Services\Decimal_Quantities();
465 504 new Services\Customer_Meta_Parity();
505 + }
506 +
507 + /**
508 + * Hook the order-event services to the first order write of the request.
509 + *
510 + * Every WooCommerce order write — create, update, status transition,
511 + * `payment_complete()`, refund — goes through `WC_Abstract_Order::save()`,
512 + * which fires `woocommerce_before_order_object_save` before the data store
513 + * writes and before `woocommerce_new_order` / `woocommerce_order_status_changed`
514 + * / `woocommerce_payment_complete` fire. Priority 0 there means every
515 + * observer exists before any order is written — on a webhook, a cron
516 + * spawned from a page view, a third-party plugin creating an order on
517 + * `template_redirect`, or a lane the classifier got wrong. Nothing in the
518 + * order group listens to trash or delete, so those need no arming.
519 + */
520 + private static function arm_order_services(): void {
521 + add_action( 'woocommerce_before_order_object_save', array( self::class, 'ensure_order_services' ), 0, 0 );
522 + }
523 +
524 + /**
525 + * Construct the order-event services exactly once per request.
526 + *
527 + * Idempotent and safe to call after `init`; each service handles its own
528 + * late registration. Fires `woocommerce_pos_order_services_ready` once so
529 + * Pro and extensions can construct their own order-event services at the
530 + * same moment on every lane.
531 + */
532 + public static function ensure_order_services(): void {
533 + if ( isset( self::$constructed['order'] ) ) {
534 + return;
535 + }
536 + self::$constructed['order'] = true;
537 +
538 + Receipt_Snapshot_Store::instance();
539 + new Emails();
540 + new Templates();
466 541 new Services\Print_Job_Service();
467 542 new Services\Cloud_Print_Trigger_Service();
468 543 new Services\Cloud_Print_Submit_Service();
469 544 new Services\Cloud_Print_Relay_Service();
545 +
546 + /**
547 + * Fires once per request when the POS order-event services exist:
548 + * eagerly on POS, admin, REST, cron and CLI requests (from this
549 + * plugin's `init` callback at priority 10), and on a storefront
550 + * request the moment the first order is about to be written.
551 + *
552 + * Because the eager firing happens at `init` priority 10, a listener
553 + * added later than that (for example from another plugin's `init`
554 + * callback at priority 20) must check `did_action()` first and
555 + * construct immediately when the action has already fired.
556 + *
557 + * @since 1.10.8
558 + */
559 + do_action( 'woocommerce_pos_order_services_ready' );
470 560 }
471 561
472 562 /**
563 + * Which service groups this request has constructed: 'always', 'order', 'pos'.
564 + *
565 + * @internal Test seam.
566 + *
567 + * @return string[]
568 + */
569 + public static function constructed_groups(): array {
570 + return array_keys( self::$constructed );
571 + }
572 +
573 + /**
574 + * Forget which groups were constructed. Tests only.
575 + *
576 + * @internal
577 + */
578 + public static function reset_request_state(): void {
579 + self::$constructed = array();
580 + Services\Request_Lane::reset();
581 + }
582 +
583 + /**
473 584 * Frontend specific initializations.
474 585 */
475 586 private function init_frontend(): void {
476 - if ( ! is_admin() ) {
587 + if ( is_admin() ) {
588 + return;
589 + }
590 + // The public receipt shortcode and the My Account receipt action are
591 + // storefront features; they construct the template services when used.
592 + new Storefront_Receipts();
593 + if ( ! Services\Request_Lane::is_storefront() ) {
594 + // The POS routes (rewrite rules, checkout context, order-pay and
595 + // coupon forms) only matter on requests the classifier saw as POS.
477 596 new Template_Router();
478 597 new Form_Handler();
479 - new Storefront_Receipts();
480 598 }
481 599 }
482 600
483 601 /**
@@ -512,7 +630,17 @@
512 630 }
513 631
514 632 // wePOS alters the WooCommerce REST API, breaking the expected schema
515 633 // It's very bad form on their part, but we need to work around it.
516 - new Integrations\WePOS();
634 + // Its only hook is admin_init (a conflict notice), so admin lane only.
635 + if ( is_admin() ) {
636 + new Integrations\WePOS();
637 + }
638 +
639 + // WooCommerce Tax - https://wordpress.org/plugins/woocommerce-services/
640 + // Its class exists whenever the plugin is active, but its callbacks are
641 + // only hooked when automated taxes are on and the store country is
642 + // supported, so the integration looks them up on the hooks at
643 + // recalculation time instead of gating on the class here.
644 + new Integrations\WooCommerce_Tax();
517 645 }
518 646 }