run(null,function() use($operation){$key=spl_object_id($this->wpdb);self::$batch[$key]=true;try{return $operation();}finally{unset(self::$batch[$key]);}}); } public static function isActive( \wpdb $wpdb ): bool { return isset( self::$active[ spl_object_id( $wpdb ) ] ); } public function __construct( private readonly \wpdb $wpdb ) {} /** NULL locks all existing resources in ID order for global schedule and policy changes. */ public function run( ?array $ids, callable $operation ): mixed { $wpdb = $this->wpdb; $key = spl_object_id( $wpdb ); if ( isset(self::$batch[$key]) ) { return $operation(); } if ( isset( self::$active[ $key ] ) ) { throw new CatalogException( 'abece_booking_nested_transaction', __( 'A booking transaction is already in progress.', 'abece-booking' ), 500 ); } // Member waits may follow identity/schedule reads. Fresh statements must see the preceding commit. if(false===$wpdb->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'))$this->fail(); if ( false === $wpdb->query( 'START TRANSACTION' ) ) { $this->fail(); } self::$active[ $key ] = true; try { if ( null === $ids ) { $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i ORDER BY id FOR UPDATE', \ABeCe\Booking\Infrastructure\Database\Tables::name( $wpdb, 'resources' ) ) ); if ( $wpdb->last_error ) { $this->fail(); } } else { $ids = array_values( array_unique( $ids ) ); sort( $ids, SORT_NUMERIC ); foreach ( $ids as $id ) { $found = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE id = %d FOR UPDATE', \ABeCe\Booking\Infrastructure\Database\Tables::name( $wpdb, 'resources' ), $id ) ); if ( $wpdb->last_error ) { $this->fail(); } if ( null === $found ) { throw new CatalogException( 'abece_booking_not_found', __( 'The selected resource does not exist.', 'abece-booking' ), 404 ); } } } // READ COMMITTED keeps later policy reads fresh after resource or member lock waits. $result = $operation(); if ( false === $wpdb->query( 'COMMIT' ) ) { $this->fail(); } return $result; } catch ( Throwable $error ) { $wpdb->query( 'ROLLBACK' ); throw $error; } finally { unset( self::$active[ $key ] ); } } private function fail(): never { throw new CatalogException( 'abece_booking_transaction_failed', __( 'The booking transaction could not be completed. Please try again.', 'abece-booking' ), 503 ); } }