|null $publication Publication the import selected. * @param bool $global_export Whether the request came through a global export route. * * @return MappedStory|null The mapping, or null when the story cannot be imported. */ public function map_for_create( StoryPayload $payload, ?array $publication, bool $global_export = false ): ?MappedStory { $title = $this->title_for_create( $payload, $global_export ); if ( '' === $title ) { return null; } $post_data = array( 'post_title' => $title ); if ( $payload->has_slug() ) { $post_data = SlugMapper::apply( $post_data, $payload->slug() ); } $status = $this->status_for_create( $publication ); if ( '' !== $status ) { $post_data['post_status'] = $status; } $dates = $this->dates_for( $publication ); if ( false === $dates ) { return null; } return new MappedStory( array_merge( $post_data, $dates ), $payload->description( $global_export ) ); } /** * Mapping a story onto an existing post. * * @param StoryPayload $payload Story payload. * @param array|null $publication Publication the import selected. * @param bool $global_export Whether the request came through a global export route. * * @return MappedStory|null The mapping, or null when the story cannot be imported. */ public function map_for_update( StoryPayload $payload, ?array $publication, bool $global_export = false ): ?MappedStory { $post_id = $payload->post_id(); $description = $payload->description( $global_export ); if ( '' === $description ) { return null; } $post_data = array(); if ( $post_id > 0 ) { $post_data['ID'] = $post_id; } $title = $this->title_for_update( $payload, $post_id ); if ( '' !== $title ) { $post_data['post_title'] = $title; } if ( $payload->has_slug() && FieldSyncPolicy::may_sync_slug( $post_id ) ) { $post_data = SlugMapper::apply( $post_data, $payload->slug() ); } $status = $this->status_for_update( $publication, $post_id ); if ( '' !== $status ) { $post_data['post_status'] = $status; } $dates = $this->dates_for( $publication ); if ( false === $dates ) { return null; } return new MappedStory( array_merge( $post_data, $dates ), $description ); } /** * Deciding a new post's title. * * Preference: first task headline, slug (when mapped to title), then description. * * @param StoryPayload $payload Story payload. * @param bool $global_export Whether the request came through a global export route. * * @return string The title, or an empty string when the story carries none. */ private function title_for_create( StoryPayload $payload, bool $global_export ): string { $headline = $payload->headline(); if ( '' !== $headline ) { return $headline; } if ( $payload->has_slug() && SlugMapper::targets_title() ) { return $payload->slug(); } return Str::truncate( $payload->description( $global_export ), Config::MAX_DESCRIPTION_TITLE_LENGTH ); } /** * Deciding whether an existing post's title should change. * * No description fallback: it would overwrite a title the editors rewrote. * * @param StoryPayload $payload Story payload. * @param int $post_id Post ID. * * @return string The new title, or an empty string to leave it alone. */ private function title_for_update( StoryPayload $payload, int $post_id ): string { $headline = $payload->headline(); if ( '' !== $headline ) { return $headline; } if ( $payload->has_slug() && SlugMapper::targets_title() && FieldSyncPolicy::may_sync_slug( $post_id ) ) { return $payload->slug(); } return ''; } /** * Deciding a new post's status. * * @param array|null $publication Publication the import selected. * * @return string The status, or an empty string to let WordPress decide. */ private function status_for_create( ?array $publication ): string { $status_id = PublicationResolver::status_id( $publication ); if ( 0 === $status_id || StatusMapper::is_status_sync_disabled( $status_id ) ) { return ''; } return StatusMapper::wp_status_for( $status_id ); } /** * Deciding whether an existing post's status should change. * * Only when the Kordiam status moved, so an editor's change between syncs survives. * * @param array|null $publication Publication the import selected. * @param int $post_id Post ID. * * @return string The new status, or an empty string to leave it alone. */ private function status_for_update( ?array $publication, int $post_id ): string { $status_id = PublicationResolver::status_id( $publication ); if ( 0 === $status_id || StatusMapper::is_status_sync_disabled( $status_id ) ) { return ''; } if ( ! FieldSyncPolicy::may_sync_status( $post_id ) ) { return ''; } if ( $this->recorded_status_id( $post_id ) === $status_id ) { return ''; } return StatusMapper::wp_status_for( $status_id ); } /** * Reading the Kordiam status recorded against a post at its last sync. * * @param int $post_id Post ID. * * @return int Status ID, or 0 when none is recorded. */ private function recorded_status_id( int $post_id ): int { if ( $post_id <= 0 ) { return 0; } $recorded = get_post_meta( $post_id, MetaKeys::STATUS_ID, true ); if ( is_numeric( $recorded ) ) { return (int) $recorded; } $legacy = get_post_meta( $post_id, MetaKeys::legacy_post_meta()[ MetaKeys::STATUS_ID ], true ); return is_numeric( $legacy ) ? (int) $legacy : 0; } /** * Building the date fields from a publication's schedule. * * No schedule means no dates. An invalid schedule abandons the import rather than * risk publishing at the wrong time. * * @param array|null $publication Publication the import selected. * * @return array|false Date fields, or false when the schedule is unusable. */ private function dates_for( ?array $publication ) { $schedule = PublicationResolver::schedule( $publication ); if ( null === $schedule ) { return array(); } $scheduled_utc = Dates::from_kordiam( $schedule['date'], $schedule['time'] ); if ( null === $scheduled_utc ) { return false; } return array( 'post_date_gmt' => $scheduled_utc, 'post_date' => Dates::utc_to_local( $scheduled_utc ), 'post_modified' => Dates::now_local(), 'post_modified_gmt' => Dates::now_utc(), ); } }