Cybo Advanced Field EngineCyboweb Technologies · Documentation · v1.0.2

Custom sources

A custom source connects CAFE → Migration to data CAFE cannot read by itself: an old CRM, an API export, a database with an unusual layout. It only reads; mapping, transformations, preview, dry run, batches, history and rollback come for free.

The contract

Implement Cybo\AdvancedFieldEngine\Connector\Source\SourceAdapterInterface:

Method Returns
get_name() Unique name: lowercase letters, digits, underscores
get_label(), get_description() Shown on the source card (translate them)
is_available() Whether it can be used now (unavailable sources are hidden)
get_option_controls() Options the administrator fills in: [ [ 'name', 'label', 'type' => text|textarea|select|file, 'choices', 'required', 'help' ] ]
validate_options( $options ) Normalized options, or a WP_Error
get_fields( $options ) SourceField[]: what a record holds
count_records( $options ) Number of records (for the progress bar)
get_records( $options, $offset, $limit ) SourceRecord[]: one batch, always in the same order

A SourceField has a name, label, kind (text, number, date, boolean, list, rows, id), group heading, children (columns of a rows field), a suggested target and a sensitive flag (masked in previews). A SourceRecord has a stable ID (imported objects remember it), its values, and (when the record is a WordPress object) its object ID.

A source that also knows field group definitions can implement Cybo\AdvancedFieldEngine\Connector\Source\DefinitionSourceInterface (as the ACF source does).

Example: an old CRM's JSON API

use Cybo\AdvancedFieldEngine\Connector\Source\SourceAdapterInterface;
use Cybo\AdvancedFieldEngine\Connector\Source\SourceField;
use Cybo\AdvancedFieldEngine\Connector\Source\SourceRecord;

final class My_Crm_Source implements SourceAdapterInterface {

	public function get_name(): string {
		return 'my_crm';
	}

	public function get_label(): string {
		return __( 'Old CRM', 'my-plugin' );
	}

	public function get_description(): string {
		return __( 'Contacts from the old CRM (read-only API).', 'my-plugin' );
	}

	public function is_available(): bool {
		return defined( 'MY_CRM_TOKEN' );
	}

	public function get_option_controls(): array {
		return [];
	}

	public function validate_options( array $options ): array|\WP_Error {
		return [];
	}

	public function get_fields( array $options ): array|\WP_Error {
		return [
			new SourceField( 'name', 'Name', 'text', 'Contact', [], 'post_title' ),
			new SourceField( 'email', 'E-mail', 'text', 'Contact', [], 'contact_email' ),
			new SourceField(
				'phones',
				'Phone numbers',
				'rows',
				'Contact',
				[ new SourceField( 'label' ), new SourceField( 'number' ) ]
			),
		];
	}

	public function count_records( array $options ): int|\WP_Error {
		$response = $this->get( 'contacts/count' );

		return is_wp_error( $response ) ? $response : (int) $response['count'];
	}

	public function get_records( array $options, int $offset, int $limit ): array|\WP_Error {
		// One request per batch, never one per record.
		$response = $this->get( "contacts?offset={$offset}&limit={$limit}&sort=id" );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		return array_map(
			static fn ( array $contact ): SourceRecord => new SourceRecord( (string) $contact['id'], $contact ),
			$response['items']
		);
	}

	private function get( string $path ): array|\WP_Error {
		$response = wp_remote_get(
			'https://crm.example.com/api/' . $path,
			[ 'headers' => [ 'Authorization' => 'Bearer ' . MY_CRM_TOKEN ], 'timeout' => 20 ]
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$data = json_decode( wp_remote_retrieve_body( $response ), true );

		return is_array( $data ) ? $data : new \WP_Error( 'my_crm', __( 'The CRM answered with invalid data.', 'my-plugin' ) );
	}
}

add_action( 'cafe_register_migration_sources', function () {
	cafe_register_migration_source( new My_Crm_Source() );
} );

Rules

  • Stable order and IDs. Runs continue from an offset; record IDs must not change between runs.
  • Batch queries. Load a whole batch with as few queries or requests as possible.
  • Read only. Never write to the source from a source adapter, and never to CAFE tables: the Connector writes through CAFE's API.
  • Secrets. Keep tokens in constants or options, never in option controls; mark sensitive fields with sensitive: true.
  • Errors. Return a WP_Error with a message for people; do not include SQL or response bodies.