# Creating Extensions

Extensions integrate third-party functionality with Entity Builder by adding custom YAML keys, operations, and dependencies.

## Extension Architecture

```mermaid
classDiagram
    class EbExtensionInterface {
        <<interface>>
        +buildOperations(array data) array
        +getOperationDependencies(array operation, array batch) array
        +detectChanges(array operation, array context) ?string
        +checkDependencies(array operation) array
        +appliesTo(array operation) bool
        +getYamlKeys() array
        +getOperations() array
        +extractConfig(string entityType, string bundle) array
    }

    class EbExtensionBase {
        <<abstract>>
        #pluginDefinition
        #entityTypeManager
        +getYamlKeys() array
        +getOperations() array
    }

    class FieldGroupExtension {
        +buildOperations(array data) array
        +getOperationDependencies(array operation, array batch) array
        +detectChanges(array operation, array context) ?string
        +extractConfig(string entityType, string bundle) array
    }

    EbExtensionInterface <|.. EbExtensionBase
    EbExtensionBase <|-- FieldGroupExtension
```

## Creating an Extension

### Step 1: Create Module Structure

```
my_extension/
├── src/
│   └── Plugin/
│       ├── EbExtension/
│       │   └── MyExtension.php          # Extension plugin
│       └── EbOperation/
│           ├── CreateMyEntityOperation.php
│           └── UpdateMyEntityOperation.php
├── tests/
│   └── src/Unit/Plugin/EbExtension/
│       └── MyExtensionTest.php
├── my_extension.info.yml
└── README.md
```

### Step 2: Create the Info File

```yaml
# my_extension.info.yml
name: 'My Extension'
type: module
description: 'Entity Builder extension for My Feature'
package: Entity Builder
core_version_requirement: ^11
dependencies:
  - eb:eb
  - my_dependency:my_dependency  # The module this integrates
```

### Step 3: Create the Extension Plugin

```php
<?php

namespace Drupal\my_extension\Plugin\EbExtension;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\eb\Attribute\EbExtension;
use Drupal\eb\PluginBase\EbExtensionBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Extension for My Feature integration.
 */
#[EbExtension(
    id: 'my_extension',
    label: new TranslatableMarkup('My Extension'),
    description: new TranslatableMarkup('Adds My Feature support to Entity Builder'),
    yaml_keys: ['my_entity_definitions'],
    operations: ['create_my_entity', 'update_my_entity', 'delete_my_entity'],
    module_dependencies: ['my_dependency'],
)]
class MyExtension extends EbExtensionBase implements ContainerFactoryPluginInterface {

    /**
     * The entity type manager.
     */
    protected EntityTypeManagerInterface $entityTypeManager;

    /**
     * {@inheritdoc}
     */
    public function __construct(
        array $configuration,
        $plugin_id,
        $plugin_definition,
        EntityTypeManagerInterface $entityTypeManager,
    ) {
        parent::__construct($configuration, $plugin_id, $plugin_definition);
        $this->entityTypeManager = $entityTypeManager;
    }

    /**
     * {@inheritdoc}
     */
    public static function create(
        ContainerInterface $container,
        array $configuration,
        $plugin_id,
        $plugin_definition
    ): static {
        return new static(
            $configuration,
            $plugin_id,
            $plugin_definition,
            $container->get('entity_type.manager'),
        );
    }

    /**
     * {@inheritdoc}
     *
     * This is the PRIMARY method that extensions must implement.
     * Converts definition data to operation arrays.
     */
    public function buildOperations(array $data): array {
        if (empty($data['my_entity_definitions'])) {
            return [];
        }

        $operations = [];
        foreach ($data['my_entity_definitions'] as $item) {
            if (!is_array($item) || empty($item['entity_id'])) {
                continue;
            }

            $operations[] = [
                'operation' => 'create_my_entity',
                'entity_type' => $item['entity_type'] ?? '',
                'bundle' => $item['bundle'] ?? '',
                'entity_id' => $item['entity_id'],
                'label' => $item['label'] ?? '',
                'settings' => $item['settings'] ?? [],
            ];
        }

        return $operations;
    }

    /**
     * {@inheritdoc}
     *
     * Declares dependencies for dependency resolution.
     */
    public function getOperationDependencies(array $operation, array $batch): array {
        $dependencies = [];

        if (!$this->appliesTo($operation)) {
            return [];
        }

        // Depend on the bundle
        $entityType = $operation['entity_type'] ?? '';
        $bundle = $operation['bundle'] ?? '';

        if ($entityType && $bundle) {
            $dependencies[] = "bundle:{$entityType}:{$bundle}";
        }

        return $dependencies;
    }

    /**
     * {@inheritdoc}
     *
     * Custom change detection for sync mode.
     */
    public function detectChanges(array $operation, array $context): ?string {
        if (!$this->appliesTo($operation)) {
            return NULL;
        }

        $entityId = $operation['entity_id'] ?? '';
        if (empty($entityId)) {
            return NULL;
        }

        // Check if entity exists
        $existing = $this->loadExisting($entityId);

        if (!$existing) {
            return 'create_my_entity';
        }

        // Compare settings
        if ($this->hasChanges($operation, $existing)) {
            return 'update_my_entity';
        }

        // No changes - skip
        return NULL;
    }

    /**
     * {@inheritdoc}
     *
     * Check if this extension handles the given operation.
     */
    public function appliesTo(array $operation): bool {
        $operationType = $operation['operation'] ?? '';
        return in_array($operationType, $this->getOperations(), TRUE);
    }

    /**
     * {@inheritdoc}
     *
     * Extract existing configuration for "Import from Drupal" feature.
     */
    public function extractConfig(string $entityType, string $bundle): array {
        $entities = $this->loadEntitiesForBundle($entityType, $bundle);

        if (empty($entities)) {
            return [];
        }

        $definitions = [];
        foreach ($entities as $entity) {
            $definitions[] = [
                'entity_type' => $entityType,
                'bundle' => $bundle,
                'entity_id' => $entity->id(),
                'label' => $entity->label(),
                'settings' => $entity->getSettings(),
            ];
        }

        return ['my_entity_definitions' => $definitions];
    }

    /**
     * Load existing entity.
     */
    protected function loadExisting(string $entityId): ?object {
        return $this->entityTypeManager
            ->getStorage('my_entity')
            ->load($entityId);
    }

    /**
     * Check if operation has changes compared to existing.
     */
    protected function hasChanges(array $operation, object $existing): bool {
        // Compare relevant fields
        if ($operation['label'] !== $existing->label()) {
            return TRUE;
        }

        // Compare settings
        $newSettings = $operation['settings'] ?? [];
        $existingSettings = $existing->getSettings();

        return $newSettings !== $existingSettings;
    }

    /**
     * Load entities for a bundle.
     */
    protected function loadEntitiesForBundle(string $entityType, string $bundle): array {
        return $this->entityTypeManager
            ->getStorage('my_entity')
            ->loadByProperties([
                'entity_type' => $entityType,
                'bundle' => $bundle,
            ]);
    }

}
```

### Step 4: Create Operation Plugins

```php
<?php

namespace Drupal\my_extension\Plugin\EbOperation;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\eb\Attribute\EbOperation;
use Drupal\eb\Exception\ExecutionException;
use Drupal\eb\PluginBase\OperationBase;
use Drupal\eb\Result\ExecutionResult;
use Drupal\eb\Result\PreviewResult;
use Drupal\eb\Result\RollbackResult;
use Drupal\eb\Result\ValidationResult;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Operation for creating My Entity.
 */
#[EbOperation(
    id: 'create_my_entity',
    label: new TranslatableMarkup('Create My Entity'),
    description: new TranslatableMarkup('Creates a My Entity configuration'),
    operationType: 'create',
)]
class CreateMyEntityOperation extends OperationBase implements ContainerFactoryPluginInterface {

    /**
     * My custom service.
     */
    protected $myService;

    /**
     * {@inheritdoc}
     */
    public function __construct(
        array $configuration,
        string $plugin_id,
        mixed $plugin_definition,
        EntityTypeManagerInterface $entityTypeManager,
        LoggerInterface $logger,
        $myService,
    ) {
        parent::__construct($configuration, $plugin_id, $plugin_definition, $entityTypeManager, $logger);
        $this->myService = $myService;
    }

    /**
     * {@inheritdoc}
     */
    public static function create(
        ContainerInterface $container,
        array $configuration,
        $plugin_id,
        $plugin_definition
    ): static {
        return new static(
            $configuration,
            $plugin_id,
            $plugin_definition,
            $container->get('entity_type.manager'),
            $container->get('logger.channel.eb'),
            $container->get('my_service'),
        );
    }

    /**
     * {@inheritdoc}
     */
    public function validate(): ValidationResult {
        $result = new ValidationResult();

        $this->validateRequiredFields(['entity_type', 'bundle', 'entity_id'], $result);

        if (!$result->isValid()) {
            return $result;
        }

        // Custom validation
        $entityId = $this->getDataValue('entity_id');
        if (!preg_match('/^[a-z_]+$/', $entityId)) {
            $result->addError(
                'Entity ID must be lowercase with underscores only.',
                'entity_id',
                'invalid_entity_id'
            );
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function preview(): PreviewResult {
        $preview = new PreviewResult();
        $entityId = $this->getDataValue('entity_id');
        $label = $this->getDataValue('label', $entityId);

        $preview->addOperation(
            'create',
            'my_entity',
            $entityId,
            $this->t('Create My Entity "@label"', ['@label' => $label])
        );

        $preview->addDetails([
            'Entity ID' => $entityId,
            'Label' => $label,
            'Entity Type' => $this->getDataValue('entity_type'),
            'Bundle' => $this->getDataValue('bundle'),
        ]);

        return $preview;
    }

    /**
     * {@inheritdoc}
     */
    public function execute(): ExecutionResult {
        try {
            $entityId = $this->getDataValue('entity_id');
            $label = $this->getDataValue('label', $entityId);
            $entityType = $this->getDataValue('entity_type');
            $bundle = $this->getDataValue('bundle');
            $settings = $this->getDataValue('settings', []);

            // Create the entity using your service
            $entity = $this->myService->create([
                'id' => $entityId,
                'label' => $label,
                'entity_type' => $entityType,
                'bundle' => $bundle,
                'settings' => $settings,
            ]);
            $entity->save();

            $result = new ExecutionResult(TRUE);
            $result->addMessage($this->t('My Entity "@label" created successfully.', [
                '@label' => $label,
            ]));

            $result->addAffectedEntity([
                'type' => 'my_entity',
                'id' => $entityId,
                'label' => $label,
            ]);

            // Store rollback data
            $result->setRollbackData([
                'entity_id' => $entityId,
                'was_new' => TRUE,
            ]);

            $this->logger->info('Created My Entity: @id', ['@id' => $entityId]);

            return $result;
        }
        catch (\Exception $e) {
            $this->logger->error('My Entity creation failed: @message', [
                '@message' => $e->getMessage(),
            ]);
            throw new ExecutionException($e->getMessage(), [], 0, $e);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function rollback(): RollbackResult {
        $rollbackData = $this->getDataValue('_rollback_data', []);
        $entityId = $rollbackData['entity_id'] ?? $this->getDataValue('entity_id');
        $wasNew = $rollbackData['was_new'] ?? FALSE;

        if ($wasNew) {
            // Delete the newly created entity
            $entity = $this->myService->load($entityId);
            if ($entity) {
                $entity->delete();
            }

            $result = new RollbackResult(TRUE);
            $result->addMessage($this->t('My Entity "@id" deleted.', ['@id' => $entityId]));
        }
        else {
            $result = new RollbackResult(FALSE);
            $result->addMessage($this->t('No rollback data available for "@id".', ['@id' => $entityId]));
        }

        $result->addRestoredEntity([
            'type' => 'my_entity',
            'id' => $entityId,
        ]);

        return $result;
    }

}
```

### Step 5: Write Tests

```php
<?php

namespace Drupal\Tests\my_extension\Unit\Plugin\EbExtension;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\my_extension\Plugin\EbExtension\MyExtension;
use Drupal\Tests\UnitTestCase;

/**
 * Tests for MyExtension.
 *
 * @group my_extension
 * @coversDefaultClass \Drupal\my_extension\Plugin\EbExtension\MyExtension
 */
class MyExtensionTest extends UnitTestCase {

    /**
     * The extension under test.
     */
    protected MyExtension $extension;

    /**
     * {@inheritdoc}
     */
    protected function setUp(): void {
        parent::setUp();

        $entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);

        $this->extension = new MyExtension(
            [],
            'my_extension',
            [
                'id' => 'my_extension',
                'yaml_keys' => ['my_entity_definitions'],
                'operations' => ['create_my_entity', 'update_my_entity'],
            ],
            $entityTypeManager,
        );
    }

    /**
     * @covers ::buildOperations
     */
    public function testBuildOperationsEmpty(): void {
        $result = $this->extension->buildOperations([]);
        $this->assertEmpty($result);
    }

    /**
     * @covers ::buildOperations
     */
    public function testBuildOperationsWithData(): void {
        $data = [
            'my_entity_definitions' => [
                [
                    'entity_type' => 'node',
                    'bundle' => 'article',
                    'entity_id' => 'test_entity',
                    'label' => 'Test Entity',
                ],
            ],
        ];

        $result = $this->extension->buildOperations($data);

        $this->assertCount(1, $result);
        $this->assertEquals('create_my_entity', $result[0]['operation']);
        $this->assertEquals('node', $result[0]['entity_type']);
        $this->assertEquals('article', $result[0]['bundle']);
        $this->assertEquals('test_entity', $result[0]['entity_id']);
    }

    /**
     * @covers ::appliesTo
     */
    public function testAppliesTo(): void {
        $this->assertTrue($this->extension->appliesTo(['operation' => 'create_my_entity']));
        $this->assertTrue($this->extension->appliesTo(['operation' => 'update_my_entity']));
        $this->assertFalse($this->extension->appliesTo(['operation' => 'create_field']));
    }

    /**
     * @covers ::getYamlKeys
     */
    public function testGetYamlKeys(): void {
        $keys = $this->extension->getYamlKeys();
        $this->assertContains('my_entity_definitions', $keys);
    }

}
```

## Extension Interface Reference

| Method | Required | Purpose |
|--------|----------|---------|
| `buildOperations()` | **Yes** | Convert definition data to operation arrays |
| `getOperationDependencies()` | No | Declare dependencies for topological sort |
| `detectChanges()` | No | Custom change detection for sync mode |
| `checkDependencies()` | No | Verify custom dependencies exist |
| `appliesTo()` | No | Check if extension handles an operation |
| `getYamlKeys()` | No | Get YAML keys from plugin definition |
| `getOperations()` | No | Get operation types from plugin definition |
| `extractConfig()` | No | Extract existing config for export |

## Integration Points

Extensions integrate at these points in the processing pipeline:

1. **OperationDataBuilder::build()** - Calls `extension->buildOperations()`
2. **DependencyResolver::findDependencies()** - Calls `extension->getOperationDependencies()`
3. **ChangeDetector::determineOperation()** - Calls `extension->detectChanges()`
4. **DefinitionGenerator::generate()** - Calls `extension->extractConfig()`
