mspc copy --to= * * Copies a source post (any enabled CPT, auto-detected) to the target subsite * and prints (a) a recursive side-by-side dump of every ACF value for visual * verification, then (b) the structured warning report read straight from * _mspc_clone_warnings — the same JSON the editor panel renders, so the CLI * and editor never disagree. * * Exit code is 0 even when warnings are present (warnings ≠ copy failure). * Non-zero only when Post_Cloner::copy() returns WP_Error. * * @package MultiSitePostCloner * @license GPL-2.0-or-later */ if ( ! defined( 'ABSPATH' ) ) exit; if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { return; } class MSPC_CLI_Command { /** * Copy a post to a target subsite. * * ## OPTIONS * * * : Source post ID (must exist on the --url blog and belong to an enabled CPT). * * --to= * : Target blog ID. * * ## EXAMPLES * * wp --url=https://chicago.example.com mspc copy 123 --to=4 * * @when after_wp_load */ public function copy( $args, $assoc_args ) { list( $source_id ) = $args; $source_id = (int) $source_id; $target_blog_id = isset( $assoc_args['to'] ) ? (int) $assoc_args['to'] : 0; if ( ! $source_id || ! $target_blog_id ) { \WP_CLI::error( 'Usage: wp mspc copy --to=' ); } $source_post = get_post( $source_id ); if ( ! $source_post ) { \WP_CLI::error( sprintf( 'Source post %d not found on this blog.', $source_id ) ); } $source_cpt = $source_post->post_type; $walker = new \MSPC\ACF_Walker(); $fields_by_name = $walker->load_fields_for_post( $source_id, $source_cpt ); $source_values = function_exists( 'get_fields' ) ? get_fields( $source_id, false ) : array(); if ( ! is_array( $source_values ) ) { $source_values = array(); } $source_thumb = (int) get_post_thumbnail_id( $source_id ); $cloner = new \MSPC\Post_Cloner( $walker ); $result = $cloner->copy( $source_id, $target_blog_id ); if ( is_wp_error( $result ) ) { \WP_CLI::error( $result->get_error_message() ); } $new_id = (int) $result['new_id']; $stats = isset( $result['stats'] ) ? (array) $result['stats'] : array(); $report = isset( $result['report'] ) ? (array) $result['report'] : array(); $id_map = isset( $stats['id_map'] ) ? (array) $stats['id_map'] : array(); switch_to_blog( $target_blog_id ); $target_values = function_exists( 'get_fields' ) ? get_fields( $new_id, false ) : array(); if ( ! is_array( $target_values ) ) { $target_values = array(); } $target_thumb = (int) get_post_thumbnail_id( $new_id ); // Re-read the persisted JSON so we exercise the same source the editor uses. $persisted = \MSPC\Warning_Report::read( $new_id ); restore_current_blog(); \WP_CLI::log( '' ); \WP_CLI::log( sprintf( '== Copied [%s] %d → %d (blog %d) ==', $source_cpt, $source_id, $new_id, $target_blog_id ) ); \WP_CLI::log( '' ); if ( ! empty( $fields_by_name ) ) { \WP_CLI::log( '-- ACF values (source → target) --' ); $this->print_diff( array_values( $fields_by_name ), $source_values, $target_values, $id_map, 0 ); \WP_CLI::log( '' ); } else { \WP_CLI::log( '-- No ACF field groups attached to this CPT. ACF diff skipped. --' ); \WP_CLI::log( '' ); } \WP_CLI::log( '-- Featured image --' ); \WP_CLI::log( sprintf( ' source id=%d → target id=%d %s', $source_thumb, $target_thumb, ( $source_thumb && ! $target_thumb ) ? '[MISSING ON TARGET]' : '' ) ); \WP_CLI::log( '' ); \WP_CLI::log( sprintf( '-- Sideload summary -- uploaded=%d already_on_target=%d failures=%d reused=%d total=%d', (int) ( $stats['successes'] ?? 0 ), (int) ( $stats['existing'] ?? 0 ), (int) ( $stats['failures'] ?? 0 ), (int) ( $stats['reused'] ?? 0 ), (int) ( $stats['total'] ?? 0 ) ) ); \WP_CLI::log( '' ); $this->print_report( $persisted ?: $report ); \WP_CLI::log( '' ); \WP_CLI::log( sprintf( 'Edit on target: %s', $result['edit_url'] ) ); } /** * @param array> $fields * @param array $src * @param array $tgt * @param array $id_map * @param int $indent */ private function print_diff( array $fields, array $src, array $tgt, array $id_map, $indent ) { $pad = str_repeat( ' ', $indent ); foreach ( $fields as $field ) { $name = isset( $field['name'] ) ? $field['name'] : ''; $type = isset( $field['type'] ) ? $field['type'] : ''; $key = isset( $field['key'] ) ? $field['key'] : ''; $s = array_key_exists( $name, $src ) ? $src[ $name ] : null; $t = array_key_exists( $name, $tgt ) ? $tgt[ $name ] : null; switch ( $type ) { case 'image': case 'file': \WP_CLI::log( sprintf( '%s%s [%s] (%s): src=%s → tgt=%s%s', $pad, $name, $type, $key, $this->scalar_str( $s ), $this->scalar_str( $t ), $this->flag_attachment_diff( $s, $t, $id_map ) ) ); break; case 'gallery': $s_arr = is_array( $s ) ? array_map( array( $this, 'scalar_str' ), $s ) : array(); $t_arr = is_array( $t ) ? array_map( array( $this, 'scalar_str' ), $t ) : array(); \WP_CLI::log( sprintf( '%s%s [gallery] (%s):', $pad, $name, $key ) ); \WP_CLI::log( sprintf( '%s src=[%s] (count=%d)', $pad, implode( ',', $s_arr ), count( $s_arr ) ) ); \WP_CLI::log( sprintf( '%s tgt=[%s] (count=%d)', $pad, implode( ',', $t_arr ), count( $t_arr ) ) ); if ( count( $s_arr ) !== count( $t_arr ) ) { \WP_CLI::log( sprintf( '%s ⚠ gallery row count mismatch', $pad ) ); } break; case 'post_object': case 'page_link': case 'relationship': $s_arr = is_array( $s ) ? array_map( array( $this, 'scalar_str' ), $s ) : array( $this->scalar_str( $s ) ); $t_arr = is_array( $t ) ? array_map( array( $this, 'scalar_str' ), $t ) : array( $this->scalar_str( $t ) ); \WP_CLI::log( sprintf( '%s%s [%s] (%s): src=[%s] → tgt=[%s]', $pad, $name, $type, $key, implode( ',', $s_arr ), implode( ',', $t_arr ) ) ); break; case 'repeater': $s_rows = is_array( $s ) ? count( $s ) : 0; $t_rows = is_array( $t ) ? count( $t ) : 0; \WP_CLI::log( sprintf( '%s%s [repeater] (%s) — src rows=%d / tgt rows=%d%s', $pad, $name, $key, $s_rows, $t_rows, $s_rows !== $t_rows ? ' ⚠ row count mismatch' : '' ) ); $rows = max( $s_rows, $t_rows ); for ( $i = 0; $i < $rows; $i++ ) { \WP_CLI::log( sprintf( '%s row[%d]:', $pad, $i ) ); $this->print_diff( isset( $field['sub_fields'] ) ? (array) $field['sub_fields'] : array(), is_array( $s ) && isset( $s[ $i ] ) && is_array( $s[ $i ] ) ? $s[ $i ] : array(), is_array( $t ) && isset( $t[ $i ] ) && is_array( $t[ $i ] ) ? $t[ $i ] : array(), $id_map, $indent + 2 ); } break; case 'group': \WP_CLI::log( sprintf( '%s%s [group] (%s):', $pad, $name, $key ) ); $this->print_diff( isset( $field['sub_fields'] ) ? (array) $field['sub_fields'] : array(), is_array( $s ) ? $s : array(), is_array( $t ) ? $t : array(), $id_map, $indent + 1 ); break; default: $sv = $this->truncate( $this->scalar_str( $s ) ); $tv = $this->truncate( $this->scalar_str( $t ) ); $flag = ( $sv !== $tv ) ? ' ⚠ differs' : ''; \WP_CLI::log( sprintf( '%s%s [%s] (%s): src=%s | tgt=%s%s', $pad, $name, $type, $key, $sv, $tv, $flag ) ); } } } /** * @param array|null $report */ private function print_report( $report ) { \WP_CLI::log( '-- Warning report --' ); if ( empty( $report ) || empty( $report['categories'] ) ) { \WP_CLI::log( ' (clean — no warnings)' ); return; } $cats = $report['categories']; if ( ! empty( $cats['attachments'] ) ) { \WP_CLI::log( sprintf( ' attachments failed: %d', count( $cats['attachments'] ) ) ); foreach ( $cats['attachments'] as $e ) { \WP_CLI::log( sprintf( ' - %s (src id %d%s)%s', $e['field_label'] ?: '(unnamed)', (int) ( $e['source_id'] ?? 0 ), ! empty( $e['source_title'] ) ? ': ' . $e['source_title'] : '', ( 'gallery_item' === ( $e['kind'] ?? '' ) ) ? ' [gallery]' : '' ) ); } \WP_CLI::warning( sprintf( '%d attachment(s) failed to sideload. Check error_log for [MSPC] entries.', count( $cats['attachments'] ) ) ); } if ( ! empty( $cats['references'] ) ) { \WP_CLI::log( sprintf( ' references unmatched: %d', count( $cats['references'] ) ) ); foreach ( $cats['references'] as $e ) { \WP_CLI::log( sprintf( ' - %s [%s]: %s (%s)', $e['field_label'] ?: '(unnamed)', $e['status'] ?? '', $e['source_title'] ?? '', $e['source_post_type'] ?? '' ) ); } } if ( ! empty( $cats['locations'] ) ) { foreach ( $cats['locations'] as $e ) { \WP_CLI::log( sprintf( ' field group "%s" (%s rule): %s', $e['group_title'] ?? '', $e['param'] ?? '', 'added' === ( $e['status'] ?? '' ) ? 'new post added to location rules' : 'registered in JSON/PHP — add this post to its location rules manually' ) ); } } if ( ! empty( $cats['post_parent'] ) ) { foreach ( $cats['post_parent'] as $e ) { \WP_CLI::log( sprintf( ' post_parent dropped: "%s" (%s)', $e['source_title'] ?? '', $e['source_post_type'] ?? '' ) ); } } if ( ! empty( $cats['taxonomies'] ) ) { \WP_CLI::log( sprintf( ' taxonomy terms created: %d', count( $cats['taxonomies'] ) ) ); foreach ( $cats['taxonomies'] as $e ) { \WP_CLI::log( sprintf( ' + %s "%s" (slug: %s)', $e['taxonomy_label'] ?? '', $e['term_name'] ?? '', $e['term_slug'] ?? '' ) ); } } if ( ! empty( $cats['drift'] ) ) { \WP_CLI::log( sprintf( ' field-group drift: %d', count( $cats['drift'] ) ) ); foreach ( $cats['drift'] as $e ) { \WP_CLI::log( sprintf( ' - %s (key %s)', $e['field_label'] ?: ( $e['field_name'] ?? '' ), $e['field_key'] ?? '' ) ); } } } private function scalar_str( $value ) { if ( is_null( $value ) ) { return '∅'; } if ( is_bool( $value ) ) { return $value ? 'true' : 'false'; } if ( is_scalar( $value ) ) { return (string) $value; } return wp_json_encode( $value ); } private function truncate( $s ) { $s = (string) $s; return ( strlen( $s ) > 80 ) ? substr( $s, 0, 77 ) . '...' : $s; } private function flag_attachment_diff( $src, $tgt, array $id_map ) { $sid = (int) $src; $tid = (int) $tgt; if ( $sid && ! $tid ) { return ' ⚠ FAILED (no target ID)'; } if ( $sid && isset( $id_map[ $sid ] ) && (int) $id_map[ $sid ] === $tid ) { return ' ✓'; } return ''; } } WP_CLI::add_command( 'mspc', 'MSPC_CLI_Command' );