post_access_checker = $post_access_checker; $this->meta_writer = $meta_writer; $this->field_renderer = $field_renderer; $this->logger = new NullLogger(); } /** * Applies the given post updates. Updates are applied independently: one failing * update does not block the others. * * @param Update_Type $type The appearance the updates target. * @param Post_Update_Collection $updates The post updates to apply. * * @return Update_Result_Collection The result per post update. */ public function update( Update_Type $type, Post_Update_Collection $updates ): Update_Result_Collection { $results = new Update_Result_Collection(); foreach ( $updates->get() as $update ) { $results->add( $this->apply( $type, $update ) ); } return $results; } /** * Applies a single post update. * * @param Update_Type $type The appearance the update targets. * @param Post_Update $update The post update to apply. * * @return Update_Result The result of the update. */ private function apply( Update_Type $type, Post_Update $update ): Update_Result { $post_id = $update->get_post_id(); if ( ! $this->post_access_checker->exists( $post_id ) ) { return Update_Result::for_failure( $post_id, Update_Error::NOT_FOUND ); } if ( ! $this->post_access_checker->is_supported_type( $post_id ) ) { return Update_Result::for_failure( $post_id, Update_Error::INVALID_POST_TYPE ); } if ( ! $this->post_access_checker->can_edit( $post_id ) ) { return Update_Result::for_failure( $post_id, Update_Error::FORBIDDEN ); } $saved_focus_keyphrase = null; try { if ( $update->has_title() ) { $this->meta_writer->write_title( $type, $post_id, $update->get_title() ); } if ( $update->has_description() ) { $this->meta_writer->write_description( $type, $post_id, $update->get_description() ); } if ( $update->has_focus_keyphrase() ) { $saved_focus_keyphrase = $this->meta_writer->write_focus_keyphrase( $post_id, $update->get_focus_keyphrase() ); } } catch ( Exception $exception ) { $this->logger->warning( 'Bulk update failed to save post {post_id}: {error}', [ 'post_id' => $post_id, 'error' => $exception->getMessage(), ], ); return Update_Result::for_failure( $post_id, Update_Error::SAVE_FAILED ); } return Update_Result::for_success( $post_id, $this->render_fields( $type, $post_id ), $this->sanitized_fields( $saved_focus_keyphrase ), ); } /** * Renders the search fields with replacement variables resolved, for re-scoring after a save. * * Only called for search updates; social updates carry no scored fields. * * @param Update_Type $type The appearance the update targets. * @param int $post_id The ID of the post. * * @return array The rendered fields, keyed by field. */ private function render_fields( Update_Type $type, int $post_id ): array { if ( ! $type->is_search() ) { return []; } return [ 'seo_title' => $this->field_renderer->render( $post_id, 'title' ), 'meta_description' => $this->field_renderer->render( $post_id, 'metadesc' ), ]; } /** * Builds the sanitized-literals payload for the response. * * The focus keyphrase is echoed back when it was part of the update, so the frontend can detect * when sanitization silently altered the submitted value (e.g. HTML stripped). * * @param string|null $saved_focus_keyphrase The sanitized focus keyphrase that was stored, or null if not updated. * * @return array The sanitized literals, keyed by field. */ private function sanitized_fields( ?string $saved_focus_keyphrase ): array { if ( $saved_focus_keyphrase === null ) { return []; } return [ 'focus_keyphrase' => $saved_focus_keyphrase ]; } }