# Extension contracts

<!-- example: contract Capell\VoyagerImporter\Contracts\VoyagerImportContributor -->

```php
<?php
declare(strict_types=1);
final class ExampleVoyagerImportContributor implements \Capell\VoyagerImporter\Contracts\VoyagerImportContributor
{
    public function columns(): array
    {
        return ['posts.subtitle' => 'subtitle'];
    }

    public function tables(): array
    {
        return ['legacy_events' => 'events'];
    }

    public function settings(): array
    {
        return ['site.title' => 'general.site_name'];
    }

    public function templates(): array
    {
        return ['landing' => 'landing-page'];
    }
}

app()->tag([ExampleVoyagerImportContributor::class], \Capell\VoyagerImporter\Support\VoyagerContributorRegistry::TAG);
```

<!-- example: contract Capell\VoyagerImporter\Contracts\VoyagerTableImportContributor -->

```php
<?php
declare(strict_types=1);
final class ExampleVoyagerTableImportContributor implements \Capell\VoyagerImporter\Contracts\VoyagerTableImportContributor
{
    public function tableImports(): array
    {
        return [
            new \Capell\VoyagerImporter\Data\VoyagerTableImportDefinition(
                sourceTable: 'legacy_events',
                identityColumns: ['id'],
                targetClass: \App\Models\Event::class,
                fields: ['title' => 'title', 'page_id' => 'page_id'],
                siteScoped: true,
                converters: [
                    'page_id' => static fn (mixed $value, \Capell\VoyagerImporter\Support\VoyagerSourceKeyResolver $resolver): int|string|null => $resolver->resolve('page', $value),
                ],
            ),
        ];
    }
}

app()->tag([ExampleVoyagerTableImportContributor::class], \Capell\VoyagerImporter\Support\VoyagerContributorRegistry::TAG);
```

Implement `VoyagerImportContributor` and tag its container binding with `VoyagerContributorRegistry::TAG`.

`columns()` maps `source_table.column` to an existing, unprotected destination column for pages or posts. `settings()` maps a source key to a destination `group.name` or to a name within the source group. `templates()` maps a source template to a layout key on the destination site. Duplicate mappings and unavailable destinations fail the import. Mapped JSON column values can rewrite imported media paths.

Existing `tables()` declarations continue to describe inventory mappings. To import rows, also implement `VoyagerTableImportContributor::tableImports()` and return `VoyagerTableImportDefinition` objects. Each definition supplies the source table, one or more source identity columns, the destination Eloquent model, a source-column-to-attribute map, whether the rows belong to a site, and optional converters keyed by source column. Converters receive the source value and a `VoyagerSourceKeyResolver`, and must return a scalar or null. A converter for a foreign key column calls `$resolver->resolve('page', $value)` (entity types `page`, `post`, `user`, `role`, `permission` and `setting`) to get the id of the Capell record imported from that source id, or `null` when it has not been imported. Import the referenced entities in the same or an earlier run; `--only` runs entities before contributed tables, so `--only=pages,tables` resolves page ids. Identity columns must contain strings or integers and uniquely identify source rows.

The destination model must be registered in Migration Assistant's `rollback_targets` and its mapped attributes must be fillable, present in its table and outside the protected-attribute list. For site-scoped definitions the importer supplies `site_id`; contributors cannot map it themselves. Select `--only=tables`, with `--site-id` for site-scoped definitions. A session imports only the contributed tables whose scope matches it: site-scoped tables with `--site-id`, global tables without. Tables of the other scope are skipped with a notice and need their own run. Source-key provenance tracks creates and updates and supports reruns and rollback. Unmapped source columns are reported by name, without values.

Category assignment is also pending. Capell's `Taggable` model represents a composite-key pivot without an ID or timestamps; `SourceKeyImporter` requires a scalar model key, and database rollback provenance requires an allowlisted model with timestamps. Pages and articles can be soft-deleted during rollback, so relying on a database cascade would leave their pivot assignments behind. A reversible assignment representation and rollback path are needed before categories can be imported without losing or silently retaining relationships.