Skip to content

Commit

Permalink
Merge pull request #9 from City-of-Helsinki/UHF-162
Browse files Browse the repository at this point in the history
UHF-162: Action to mass update remote entities
  • Loading branch information
tuutti authored Dec 9, 2020
2 parents 6abe987 + fb55bee commit 7519a3b
Show file tree
Hide file tree
Showing 10 changed files with 517 additions and 105 deletions.
3 changes: 3 additions & 0 deletions config/schema/helfi_api_base.schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
action.configuration.remote_entity:migration_update:*:
type: action_configuration_default
label: 'Update given remote entity'
10 changes: 10 additions & 0 deletions src/Entity/RemoteEntityBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ abstract class RemoteEntityBase extends ContentEntityBase {
*/
protected bool $resetSyncAttempts = TRUE;

/**
* Get sthe migration name.
*
* @return string|null
* The migration name.
*/
public static function getMigration() : ? string {
return NULL;
}

/**
* {@inheritdoc}
*/
Expand Down
121 changes: 121 additions & 0 deletions src/Plugin/Action/MigrationUpdateAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types = 1);

namespace Drupal\helfi_api_base\Plugin\Action;

use Drupal\Core\Action\ActionBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\helfi_api_base\Entity\RemoteEntityBase;
use Drupal\helfi_api_base\MigrateTrait;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_tools\MigrateExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Updates the remote entity using associated migration.
*
* @Action(
* id = "remote_entity:migration_update",
* action_label = @Translation("Remote entity - Migration update"),
* deriver = "Drupal\helfi_api_base\Plugin\Derivative\MigrationUpdateActionDerivative",
* )
*/
class MigrationUpdateAction extends ActionBase implements ContainerFactoryPluginInterface {

use MigrateTrait;

/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected MigrationPluginManagerInterface $migrationPluginManager;

/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;

/**
* {@inheritdoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition
) {
$instance = new static($configuration, $plugin_definition, $plugin_definition);
$instance->migrationPluginManager = $container->get('plugin.manager.migration');
$instance->entityTypeManager = $container->get('entity_type.manager');

return $instance;
}

/**
* The migration.
*
* @param \Drupal\helfi_api_base\Entity\RemoteEntityBase $entity
* The entity.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration.
*/
protected function getMigration(RemoteEntityBase $entity) : MigrationInterface {
$definition = $this->entityTypeManager
->getDefinition($this->getPluginDefinition()['type'])
->getClass()::getMigration();

return $this->migrationPluginManager->createInstance($definition, [
'entity_ids' => [$entity->id()],
]);
}

/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
if (!$entity instanceof RemoteEntityBase) {
throw new \InvalidArgumentException('Given entity is not instanceof RemoteEntityBase.');
}
$this->setIsPartialMigrate(TRUE);
$migration = $this->getMigration($entity);
$keys = array_keys($migration->getSourcePlugin()->getIds());
$migration->getIdMap()->setUpdate(array_combine($keys, [$entity->id()]));

$executable = new MigrateExecutable($migration, new MigrateMessage());
$executable->import();
}

/**
* {@inheritdoc}
*/
public function access(
$object,
AccountInterface $account = NULL,
$return_as_object = FALSE
) {
/** @var \Drupal\helfi_api_base\Entity\RemoteEntityBase $object */
$access = $object->access('update', NULL, TRUE);

return $return_as_object ? $access : $access->isAllowed();
}

/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$module_name = $this->entityTypeManager
->getDefinition($this->getPluginDefinition()['type'])
->getProvider();
return ['module' => [$module_name]];
}

}
65 changes: 65 additions & 0 deletions src/Plugin/Derivative/MigrationUpdateActionDerivative.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Drupal\helfi_api_base\Plugin\Derivative;

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\helfi_api_base\Entity\RemoteEntityBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides an action to update singular migration.
*/
class MigrationUpdateActionDerivative extends DeriverBase implements ContainerDeriverInterface {

use StringTranslationTrait;

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
$instance = new static();
$instance->entityTypeManager = $container->get('entity_type.manager');

return $instance;
}

/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
if (empty($this->derivatives)) {
$definitions = [];

$entity_types = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entityType) {
if (!$entityType->entityClassImplements(RemoteEntityBase::class)) {
return FALSE;
}
return $entityType->getClass()::getMigration() ?? FALSE;
});

foreach ($entity_types as $entity_type_id => $entity_type) {
$definition = $base_plugin_definition;
$definition['type'] = $entity_type_id;
$definition['label'] = sprintf('%s %s', $base_plugin_definition['action_label'], $entity_type->getSingularLabel());
$definitions[$entity_type_id] = $definition;
}
$this->derivatives = $definitions;
}

return parent::getDerivativeDefinitions($base_plugin_definition);
}

}
141 changes: 141 additions & 0 deletions src/Plugin/migrate/source/HttpSourcePluginBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types = 1);

namespace Drupal\helfi_api_base\Plugin\migrate\source;

use Drupal\Component\Utility\UrlHelper;
use Drupal\helfi_api_base\MigrateTrait;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Plugin\MigrationInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides a HTTP source plugin.
*/
abstract class HttpSourcePluginBase extends SourcePluginBase {

use MigrateTrait;

/**
* The number of ignored rows until we stop the migrate.
*
* This assumes that your API can be sorted in a way that the newest
* changes are listed first.
*
* For this to have any effect 'track_changes' source setting must be set to
* true and you must run the migrate with PARTIAL_MIGRATE=1 setting.
*
* @var int
*/
protected const NUM_IGNORED_ROWS_BEFORE_STOPPING = 20;

/**
* The http client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected ClientInterface $httpClient;

/**
* An array of entity ids to import.
*
* @var array
*/
protected array $entityIds = [];

/**
* Sends a HTTP request and returns response data as array.
*
* @param string $url
* The url.
*
* @return array
* The JSON returned by Ahjo service.
*/
protected function getContent(string $url) : array {
try {
$content = (string) $this->httpClient->request('GET', $url)->getBody();
return \GuzzleHttp\json_decode($content, TRUE);
}
catch (GuzzleException $e) {
}
return [];
}

/**
* Initializes the list iterator.
*
* @return \Iterator
* The iterator.
*/
abstract protected function initializeListIterator() : \Iterator;

/**
* Initializes iterator with set of entity IDs.
*
* @return \Iterator
* The iterator.
*/
protected function initializeSingleImportIterator() : \Iterator {
foreach ($this->entityIds as $entityId) {
yield $this->getContent($this->buildCanonicalUrl($entityId));
}
}

/**
* {@inheritdoc}
*/
protected function initializeIterator() {
if ($this->entityIds) {
return $this->initializeSingleImportIterator();
}
return $this->initializeListIterator();
}

/**
* Builds a canonical url to individual entity.
*
* @param string $id
* The entity ID.
*
* @return string
* The url to canonical page of given entity.
*/
protected function buildCanonicalUrl(string $id) : string {
$urlParts = UrlHelper::parse($this->configuration['url']);

return vsprintf('%s/%s/?%s', [
rtrim($urlParts['path'], '/'),
$id,
UrlHelper::buildQuery($urlParts['query']),
]);
}

/**
* {@inheritdoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
MigrationInterface $migration = NULL
) {
$instance = new static($configuration, $plugin_id, $plugin_definition, $migration);
$instance->httpClient = $container->get('http_client');

if (!isset($configuration['url'])) {
throw new \InvalidArgumentException('The "url" configuration missing.');
}

// Allow certain entity IDs to be updated.
if (isset($migration->entity_ids)) {
$instance->entityIds = $migration->entity_ids;
}
return $instance;
}

}
40 changes: 40 additions & 0 deletions tests/src/Functional/MigrationTestBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types = 1);

namespace Drupal\Tests\helfi_api_base\Functional;

use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\helfi_api_base\Traits\MigrationTestTrait;

/**
* Base class for multilingual migration tests.
*/
abstract class MigrationTestBase extends BrowserTestBase implements MigrateMessageInterface {

use MigrationTestTrait;

/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_plus',
'language',
'content_translation',
];

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

foreach (['fi', 'sv'] as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
}

}
Loading

0 comments on commit 7519a3b

Please sign in to comment.