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_Errorwith a message for people; do not include SQL or response bodies.