prefix . 'glovario_rules'; } public function register() { add_action( 'glovario_variable_deleted', array( $this, 'delete_for_variable' ) ); } public function find( $id ) { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $this->table(), absint( $id ) ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); admin lookup by ID before edits (resolution reads the cached for_variable()). return $this->hydrate( $row ); } /** * Return a variable's rules in evaluation order. * * Enabled rules are read on every resolution, so they are cached per * variable in the object cache (shared by every repository instance and * persistent when a persistent object cache is installed). The cache also * stores empty results, which is the common case for variables without rules. * * @param int $variable_id Variable ID. * @param bool $enabled_only Whether to return only enabled rules. * @return array[] */ public function for_variable( $variable_id, $enabled_only = false ) { global $wpdb; $variable_id = absint( $variable_id ); if ( $enabled_only ) { $cached = wp_cache_get( $variable_id, self::CACHE_GROUP, false, $found ); if ( $found && is_array( $cached ) ) { return $cached; } } if ( $enabled_only ) { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE variable_id = %d AND enabled = 1 ORDER BY priority DESC, id ASC', $this->table(), $variable_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Glovario's own table (no WordPress API for custom tables); cached above and below. } else { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE variable_id = %d ORDER BY priority DESC, id ASC', $this->table(), $variable_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); every rule of one variable, for its edit screen and exports. } $rules = array_map( array( $this, 'hydrate' ), $rows ); if ( $enabled_only ) { wp_cache_set( $variable_id, $rules, self::CACHE_GROUP ); } return $rules; } public function invalidate( $variable_id ) { wp_cache_delete( absint( $variable_id ), self::CACHE_GROUP ); wp_cache_delete( self::USED_LOCALES_KEY, self::CACHE_GROUP ); /** * Fires after a variable's rules were added, changed, or removed. * * @param int $variable_id Variable ID. */ do_action( 'glovario_rules_changed', absint( $variable_id ) ); } public function all( $enabled_only = false ) { global $wpdb; if ( $enabled_only ) { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE enabled = 1 ORDER BY variable_id ASC, priority DESC, id ASC', $this->table() ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); read when a rule change reschedules the boundary event. } else { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY variable_id ASC, priority DESC, id ASC', $this->table() ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); read by the Usage Inspector scan an administrator starts. } return array_map( array( $this, 'hydrate' ), $rows ); } /** * Locales named by any rule. * * Read whenever a multilingual plugin's locale is normalized (every * resolution in Automatic mode) and for the admin toolbar's preview list, * so the result is cached; invalidate() clears it on every rule write. * * @return string[] */ public function used_locales() { global $wpdb; $cached = wp_cache_get( self::USED_LOCALES_KEY, self::CACHE_GROUP, false, $found ); if ( $found && is_array( $cached ) ) { return $cached; } $rows = $wpdb->get_col( $wpdb->prepare( 'SELECT conditions FROM %i', $this->table() ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Glovario's own table (no WordPress API for custom tables); cached above. $locales = array(); foreach ( $rows as $conditions_json ) { $conditions = json_decode( $conditions_json, true ); foreach ( (array) ( $conditions['locales'] ?? array() ) as $locale ) { $locale = sanitize_locale_name( $locale ); if ( $locale ) { $locales[ $locale ] = true; } } } $locales = array_keys( $locales ); wp_cache_set( self::USED_LOCALES_KEY, $locales, self::CACHE_GROUP ); return $locales; } public function save( $data, $id = 0 ) { global $wpdb; $now = current_time( 'mysql', true ); $row = array( 'variable_id' => absint( $data['variable_id'] ), 'name' => sanitize_text_field( $data['name'] ), 'alternative_value' => (string) $data['alternative_value'], 'enabled' => empty( $data['enabled'] ) ? 0 : 1, 'priority' => (int) $data['priority'], 'conditions' => wp_json_encode( $data['conditions'] ), 'updated_at' => $now, ); $formats = array( '%d', '%s', '%s', '%d', '%d', '%s', '%s' ); if ( $id ) { $previous = $this->find( $id ); if ( $previous ) { $this->invalidate( $previous['variable_id'] ); } $ok = $wpdb->update( $this->table(), $row, array( 'id' => absint( $id ) ), $formats, array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); invalidate() clears the rule caches after the write. } else { $row['created_at'] = $now; $formats[] = '%s'; $ok = $wpdb->insert( $this->table(), $row, $formats ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Glovario's own table (no WordPress API for custom tables); invalidate() clears the rule caches after the write. $id = $wpdb->insert_id; } $this->invalidate( $row['variable_id'] ); return false === $ok ? new \WP_Error( 'rule_database_error', __( 'The rule could not be saved.', 'glovario' ) ) : $this->find( $id ); } public function delete( $id ) { global $wpdb; $rule = $this->find( $id ); $deleted = (bool) $wpdb->delete( $this->table(), array( 'id' => absint( $id ) ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); invalidate() clears the rule caches after the write. if ( $rule ) { $this->invalidate( $rule['variable_id'] ); } return $deleted; } public function delete_for_variable( $variable ) { if ( is_array( $variable ) && isset( $variable['id'] ) ) { global $wpdb; $wpdb->delete( $this->table(), array( 'variable_id' => absint( $variable['id'] ) ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); invalidate() clears the rule caches after the write. $this->invalidate( $variable['id'] ); } } public function replace_for_variable( $variable_id, $rules ) { return Transaction::run( function () use ( $variable_id, $rules ) { global $wpdb; $deleted = $wpdb->delete( $this->table(), array( 'variable_id' => absint( $variable_id ) ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Glovario's own table (no WordPress API for custom tables); invalidate() clears the rule caches after the write. $this->invalidate( $variable_id ); if ( false === $deleted ) { return new \WP_Error( 'rule_database_error', __( 'The existing rules could not be prepared for replacement.', 'glovario' ) ); } foreach ( $rules as $rule ) { $rule['variable_id'] = absint( $variable_id ); $result = $this->save( $rule ); if ( is_wp_error( $result ) ) { return $result; } } return true; } ); } private function hydrate( $row ) { if ( ! $row ) { return null; } $row['conditions'] = json_decode( $row['conditions'], true ); $row['conditions'] = is_array( $row['conditions'] ) ? $row['conditions'] : array( 'version' => 1 ); return $row; } }