strategy = in_array( $strategy, self::strategies(), true ) ? $strategy : self::SKIP; } /** * The strategies a caller may pass. * * @return string[] */ public static function strategies(): array { return array( self::SKIP, self::UPDATE, self::CREATE_NEW ); } /** * The strategy in force. * * @return string */ public function strategy(): string { return $this->strategy; } /** * Decide what happens to one incoming post record. * * @param array $record One post record from the package. * @return array{action:string,existing_id:int} Action is create, update or skip. */ public function resolve( array $record ): array { $existing = $this->find_existing( $record ); if ( 0 === $existing ) { return array( 'action' => 'create', 'existing_id' => 0, ); } if ( self::CREATE_NEW === $this->strategy ) { return array( 'action' => 'create', 'existing_id' => 0, ); } if ( self::UPDATE === $this->strategy ) { return array( 'action' => 'update', 'existing_id' => $existing, ); } return array( 'action' => 'skip', 'existing_id' => $existing, ); } /** * Find the local post matching an incoming record. * * Slug plus post type, because ids do not survive a move between sites and * a title is not unique. * * A draft has no slug — WordPress assigns one on publish — so a record with * an empty slug falls back to an exact title match inside the post type. * Without the fallback every re-import creates another copy of every draft. * * @param array $record One post record. * @return int Local post id, or 0. */ private function find_existing( array $record ): int { $post_type = isset( $record['post_type'] ) ? (string) $record['post_type'] : 'post'; $slug = isset( $record['post_name'] ) ? (string) $record['post_name'] : ''; if ( '' === $slug ) { return $this->find_by_title( $record, $post_type ); } return $this->find_by( 'post_name', $slug, $post_type ); } /** * Find a slugless record by its exact title. * * Only reached for records WordPress never gave a slug — drafts, and the * auto-draft and pending states beside them. * * @param array $record One post record. * @param string $post_type The record's post type. * @return int Local post id, or 0. */ private function find_by_title( array $record, string $post_type ): int { $title = isset( $record['post_title'] ) ? (string) $record['post_title'] : ''; if ( '' === $title ) { return 0; } return $this->find_by( 'post_title', $title, $post_type ); } /** * The newest local post of a type whose column equals a value. * * Read from the posts table, not get_posts(): plugins narrow every query on * pre_get_posts (WP Travel Engine's Fixed Departure Dates hides expired * trips everywhere, admin and CLI included), and a post the resolver cannot * see is a post it duplicates — then a rollback deleting that duplicate * takes the original's packages with it. Same reasoning as Object_Query. * * Trash and auto-drafts are not matches, as with get_posts( 'any' ). * * @param string $column post_name or post_title. * @param string $value Value to match exactly. * @param string $post_type The record's post type. * @return int Local post id, or 0. */ private function find_by( string $column, string $value, string $post_type ): int { global $wpdb; // A query filter must not hide an existing post, so this reads the table. if ( 'post_title' === $column ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- a query WordPress has no API for; nothing to cache. $found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = %s AND post_status NOT IN ( 'trash', 'auto-draft' ) ORDER BY post_date DESC, ID DESC LIMIT 1", $value, $post_type ) ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- a query WordPress has no API for; nothing to cache. $found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s AND post_status NOT IN ( 'trash', 'auto-draft' ) ORDER BY post_date DESC, ID DESC LIMIT 1", $value, $post_type ) ); } return (int) $found; } }