*/ private static $record_augmenters = array(); /** * Projection callbacks added to the public filters, kept for uninstall. * * @var array */ private static $projections = array(); /** * Declare a per-record augmenter. * * @param callable $callback callable( array $payload, mixed $object, mixed $request ): array. * @param int $priority Filter priority, matching the lane's public filter. * @param string[] $lanes Lanes this augmenter serves. Defaults to both. * @param string $resource Batch-lane resource slug this augmenter serves. */ public static function add_record_augmenter( callable $callback, int $priority = 10, array $lanes = array( self::LANE_PROXY, self::LANE_SERIALIZED ), string $resource = 'products' ): void { self::$record_augmenters[] = array( 'callback' => $callback, 'priority' => $priority, 'lanes' => $lanes, 'resource' => $resource, ); } /** * Wire the default WCPOS augmentations onto both public filters. * * Called once from {@see \WCPOS\WooCommercePOS\Init} when the sync feature is * enabled and the schema latch is healthy. */ public static function install(): void { self::reset(); // Batch-lane-only augmenters. Each bulk-reads its meta store once per page, // so it MUST see the whole list; each keeps its own registrar because the // registrars are also the seams the tests and the ops endpoints drive. // Revision runs at 9 — BEFORE uuid/digest augmentation — so the stamped // bytes equal what the write path recomputes from a bare wc/v3 re-read. // The digest registrar also owns the ORDER pull lane's stamper, so every // served-record augmentation is wired from here and nowhere else. Revision::register_proxy_stamps(); Proxy_Uuid_Stamper::register_proxy_stampers(); Integrity_Digest::register_proxy_digest_stampers(); // Per-object-lane-only identity stamp: the batch lane's identity twin is // Proxy_Uuid_Stamper above, which bulk-reads instead of loading per record. self::add_record_augmenter( array( Pos_Uuid::class, 'stamp_serialized_record' ), 10, array( self::LANE_SERIALIZED ) ); // Declared ONCE, projected onto both lanes. self::add_record_augmenter( array( Variable_Prices::class, 'augment_record' ), 10 ); // After the revision stamper at 9, so narrowing the child list cannot perturb the parent's // `_rxdb_revision` — the write path recomputes that from a bare wc/v3 re-read. self::add_record_augmenter( array( Variable_Children::class, 'augment_record' ), 10 ); self::add_record_augmenter( array( Product_Images::class, 'augment_record' ), 10 ); self::wire(); } /** * Project the current registry onto the two public filters. * * Adds ONE projection callback per (lane, priority) group, replacing any * projections a previous call installed. Call this after declaring extra * augmenters with {@see add_record_augmenter}. */ public static function wire(): void { self::unwire(); foreach ( self::grouped( self::LANE_PROXY ) as $priority => $entries ) { $callback = static function ( $data, $resource = '', $request = null ) use ( $entries ) { return self::apply_to_list( $data, $resource, $request, $entries ); }; add_filter( self::PROXY_FILTER, $callback, $priority, 3 ); self::$projections[] = array( self::PROXY_FILTER, $callback, $priority ); } foreach ( self::grouped( self::LANE_SERIALIZED ) as $priority => $entries ) { $callback = static function ( $payload, $object = null, $request = null ) use ( $entries ) { return self::apply_to_record( $payload, $object, $request, $entries ); }; add_filter( self::SERIALIZED_FILTER, $callback, $priority, 3 ); self::$projections[] = array( self::SERIALIZED_FILTER, $callback, $priority ); } } /** * Remove every projection this pipeline installed and forget the registry. * * The batch-lane registrars own their own unregister seams, so callers that * need a full teardown call those too. */ public static function reset(): void { self::unwire(); self::$record_augmenters = array(); } /** * Apply the per-object lane's record augmenters to ONE serialized payload. * * @param mixed $payload Serialized product payload. * @param mixed $object Product or variation backing the payload. * @param mixed $request Serialization context. * @param array $entries Record augmenters to apply. * * @return mixed */ private static function apply_to_record( $payload, $object, $request, array $entries ) { if ( ! \is_array( $payload ) ) { return $payload; } foreach ( $entries as $entry ) { $result = ( $entry['callback'] )( $payload, $object, $request ); $payload = \is_array( $result ) ? $result : $payload; } return $payload; } /** * Apply the batch lane's record augmenters across a proxied list. * * Entries are applied OUTERMOST — the whole list passes through one augmenter * before the next — preserving the order the hand-wired twins ran in. * * @param mixed $data Proxied response list. * @param mixed $resource Proxy resource slug. * @param mixed $request Request context. * @param array $entries Record augmenters to apply. * * @return mixed */ private static function apply_to_list( $data, $resource, $request, array $entries ) { if ( ! \is_array( $data ) ) { return $data; } foreach ( $entries as $entry ) { if ( $entry['resource'] !== $resource ) { continue; } foreach ( $data as $index => $record ) { if ( ! \is_array( $record ) ) { continue; } // The batch lane has no loaded object: augmenters resolve it lazily // by id, exactly as they do on the per-object lane. $result = ( $entry['callback'] )( $record, null, $request ); if ( \is_array( $result ) ) { $data[ $index ] = $result; } } } return $data; } /** * Detach every projection callback this pipeline added, keeping the registry. */ private static function unwire(): void { foreach ( self::$projections as $projection ) { remove_filter( $projection[0], $projection[1], $projection[2] ); } self::$projections = array(); } /** * Group one lane's record augmenters by priority, ascending, registration-stable. * * @param string $lane Lane id. * * @return array> */ private static function grouped( string $lane ): array { $groups = array(); foreach ( self::$record_augmenters as $entry ) { if ( ! \in_array( $lane, $entry['lanes'], true ) ) { continue; } $groups[ $entry['priority'] ][] = $entry; } ksort( $groups ); return $groups; } }