Cybo Advanced Field EngineCyboweb Technologies · Documentation · v1.0.2

Custom PHP migrations

When a migration needs logic the screen cannot express, write it in PHP with cafe_import_object(). You never touch CAFE's storage yourself: CAFE's API does the saving, validation and caching.

Full example: properties, agents and team members

Old tables:

wp_old_agents        (id, name, email)
wp_old_properties    (id, title, description, price, agent_id, featured_image, status)
wp_old_property_team (row_id, property_id, name, role, photo)

Target: post types agent and property; CAFE fields price (Number, decimal), agent (Post Object), hero_image (Image), status (Select: available, sold) and team_members (Collection: name, role, photo).

Save as a small plugin (for example wp-content/plugins/my-migration/my-migration.php) and run it with wp my-migration run:

<?php
/**
 * Plugin Name: My migration
 */

if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
	return;
}

WP_CLI::add_command( 'my-migration run', function ( $args, $assoc_args ) {
	global $wpdb;

	$dry_run = ! empty( $assoc_args['dry-run'] );
	$options = [ 'dry_run' => $dry_run, 'default_status' => 'publish' ];

	// 1. Agents first: properties link to them.
	foreach ( $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}old_agents ORDER BY id" ) as $agent ) {
		cafe_import_object(
			[
				'post_type' => 'agent',
				'source'    => 'legacy-agents',
				'source_id' => $agent->id,
				'wp'        => [ 'post_title' => $agent->name ],
			],
			$options
		);
	}

	// 2. Properties, 200 at a time: memory stays flat.
	$batch_size = 200;
	$offset     = 0;

	do {
		$properties = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}old_properties ORDER BY id LIMIT %d OFFSET %d", $batch_size, $offset )
		);

		if ( ! $properties ) {
			break;
		}

		// All team rows of this batch in one query.
		$ids          = wp_list_pluck( $properties, 'id' );
		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
		$team_rows    = $wpdb->get_results(
			$wpdb->prepare( "SELECT property_id, name, role, photo FROM {$wpdb->prefix}old_property_team WHERE property_id IN ($placeholders) ORDER BY property_id, row_id", ...$ids )
		);

		$teams = [];

		foreach ( $team_rows as $row ) {
			$teams[ $row->property_id ][] = [
				'name'  => $row->name,
				'role'  => $row->role,
				'photo' => (int) $row->photo ?: null, // attachment IDs in this example
			];
		}

		foreach ( $properties as $property ) {
			$result = cafe_import_object(
				[
					'post_type' => 'property',
					'source'    => 'legacy-properties',
					'source_id' => $property->id,
					'wp'        => [
						'post_title'   => $property->title,
						'post_content' => $property->description,
					],
					'fields'    => [
						'price'        => (float) str_replace( ',', '.', $property->price ),
						'agent'        => cafe_imported_object_id( 'legacy-agents', $property->agent_id ) ?: null,
						'hero_image'   => (int) $property->featured_image ?: null,
						'status'       => '1' === $property->status ? 'available' : 'sold',
						'team_members' => $teams[ $property->id ] ?? [],
					],
				],
				$options
			);

			if ( 'failed' === $result['status'] ) {
				foreach ( $result['problems'] as $problem ) {
					WP_CLI::warning( "Property {$property->id}, {$problem['field']}: {$problem['message']}" );
				}
			}
		}

		$offset += $batch_size;
		cafe()->flush_runtime_cache(); // Free what this batch loaded.
		WP_CLI::log( "{$offset} properties done" );
	} while ( count( $properties ) === $batch_size );

	WP_CLI::success( $dry_run ? 'Dry run finished. Nothing was written.' : 'Migration finished.' );
} );
wp my-migration run --dry-run
wp my-migration run

Running it twice updates the same posts (source + source_id). Each property with its values and team is written in one transaction: a property with an invalid value is reported and not written at all.

Smaller building blocks

Need Use
Save several values of an existing object cafe_save_values()
Check a value first cafe_check_value()
Old ID → new object cafe_imported_object_id()
Settings Page values cafe_import_object( [ 'object_type' => 'option', 'fields' => [ … ] ] ) or cafe_update_option()
Read many objects afterwards without N+1 cafe_prime_fields()

Or: a source for the screen

When non-developers should run the migration (with preview, dry run, history and rollback), write a custom source instead: your code only reads records, and CAFE → Migration does the rest.