Commit 0ac67959 authored by APolitsin's avatar APolitsin
Browse files

Content install services

parent 80ee3c13
services:
synhelper.export_command:
class: Drupal\synhelper\Command\ExportCommand
arguments: ['@synhelper.content_exporter', '@entity_type.manager', '@entity_type.bundle.info']
tags:
- { name: drupal.command }
<?php
namespace Drupal\synhelper\Command;
// @codingStandardsIgnoreStart
use Drupal\commerce\EntityHelper;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Serialization\Yaml;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use Drupal\Console\Core\Command\Shared\CommandTrait;
use Drupal\Console\Core\Style\DrupalStyle;
use Drupal\Console\Annotations\DrupalCommand;
use Drupal\catalog_synhelper\Services\ContentExporter;
use Symfony\Component\Console\Question\ChoiceQuestion;
// @codingStandardsIgnoreEnd
/**
* Class ExportCommand.
*
* @package Drupal\synhelper
*
* @DrupalCommand (
* extension="synhelper",
* extensionType="module"
* )
*/
class ExportCommand extends Command {
use CommandTrait;
/**
* The content exporter.
*
* @var \Drupal\synhelper\ContentExporter
*/
protected $contentExporter;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* Constructs a new ExportCommand object.
*
* @param \Drupal\synhelper\ContentExporter $content_exporter
* The content exporter.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
*/
public function __construct(ContentExporter $content_exporter, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
parent::__construct();
$this->contentExporter = $content_exporter;
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* {@inheritdoc}
*/
protected function configure() {
$this
->setName('synhelper:export')
->setDescription($this->trans('commands.synhelper.export.description'))
->addOption('directory', '', InputOption::VALUE_OPTIONAL, $this->trans('commands.synhelper.export.options.directory'))
->addArgument('entity_type', InputArgument::REQUIRED, $this->trans('commands.synhelper.export.arguments.entity_type'))
->addArgument('bundle', InputArgument::REQUIRED, $this->trans('commands.synhelper.export.arguments.bundle'));
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$entity_type_id = $input->getArgument('entity_type');
$bundle = $input->getArgument('bundle');
$directory = $input->getOption('directory');
if ($directory && strpos($directory, -1, 1) != '/') {
$directory .= '/';
}
// Add the bundle to the filename only if the entity type has one.
$filename = $entity_type_id;
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if ($entity_type->getKey('bundle')) {
$filename .= '.' . $bundle;
}
$destination = $directory . $filename . '.yml';
$export = $this->contentExporter->exportAll($entity_type_id, $bundle);
file_put_contents($destination, Yaml::encode($export));
$io = new DrupalStyle($input, $output);
$io->writeln(sprintf($this->trans('commands.synhelper.export.messages.success'), $destination));
}
/**
* {@inheritdoc}
*/
protected function interact(InputInterface $input, OutputInterface $output) {
$helper = $this->getHelper('question');
$entity_types = $this->entityTypeManager->getDefinitions();
$entity_types = array_filter($entity_types, function (EntityType $entity_type) {
return $entity_type->entityClassImplements(ContentEntityInterface::class);
});
$entity_types = array_map(function (EntityType $entity_type) {
return $entity_type->getLabel();
}, $entity_types);
// --entity_type argument.
$entity_type_id = $input->getArgument('entity_type');
if (!$entity_type_id) {
$question = new ChoiceQuestion(
$this->trans('commands.synhelper.export.questions.entity_type'),
$entity_types
);
$entity_type_id = $helper->ask($input, $output, $question);
}
$input->setArgument('entity_type', $entity_type_id);
// --bundle argument.
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$bundles = array_map(function ($bundle) {
return $bundle['label'];
}, $bundles);
if (count($bundles) === 1) {
$bundle_keys = array_keys($bundles);
$bundle = reset($bundle_keys);
}
else {
$bundle = $input->getArgument('bundle');
if (!$bundle) {
$question = new ChoiceQuestion(
$this->trans('commands.synhelper.export.questions.bundle'),
$bundles
);
$bundle = $helper->ask($input, $output, $question);
}
}
$input->setArgument('bundle', $bundle);
}
}
<?php
namespace Drupal\synhelper\Service;
use Drupal\commerce_product\Entity\ProductAttributeValueInterface;
use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\taxonomy\TermInterface;
/**
* Defines the content exporter.
*
* @internal
* For internal usage by the Commerce synhelper module.
*/
class ContentExporter {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new ContentExporter object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* Exports all entities of the given type, restricted by bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
*
* @return array
* The exported entities, keyed by UUID.
*/
public function exportAll($entity_type_id, $bundle = '') {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) {
throw new \InvalidArgumentException(sprintf('The %s entity type is not a content entity type.', $entity_type_id));
}
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$query = $storage->getQuery();
if ($bundle_key = $entity_type->getKey('bundle')) {
$query->condition($bundle_key, $bundle);
}
// Root terms need to be imported first.
if ($entity_type_id == 'taxonomy_term') {
$query->sort('depth_level', 'ASC');
$query->sort('name', 'ASC');
}
$ids = $query->execute();
if (!$ids) {
return [];
}
$export = [];
$entities = $storage->loadMultiple($ids);
foreach ($entities as $entity) {
$export[$entity->uuid()] = $this->export($entity);
// The array is keyed by UUID, no need to have it in the export too.
unset($export[$entity->uuid()]['uuid']);
}
return $export;
}
/**
* Exports the given entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
*
* @return array
* The export array.
*/
public function export(ContentEntityInterface $entity) {
$id_key = $entity->getEntityType()->getKey('id');
$skip_fields = [
$id_key, 'langcode', 'default_langcode',
'uid', 'created', 'changed',
];
$export = [];
foreach ($entity->getFields() as $field_name => $items) {
if (in_array($field_name, $skip_fields)) {
continue;
}
$items->filterEmptyItems();
if ($items->isEmpty()) {
continue;
}
$storage_definition = $items->getFieldDefinition()->getFieldStorageDefinition();;
$list = $items->getValue();
foreach ($list as $delta => $item) {
if ($storage_definition->getType() == 'entity_reference') {
$target_entity_type_id = $storage_definition->getSetting('target_type');
$target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
if ($target_entity_type->entityClassImplements(ContentEntityInterface::class)) {
// Map entity_reference IDs to UUIDs.
$item['target_id'] = $this->mapToUuid($target_entity_type_id, $item['target_id']);
}
}
elseif ($storage_definition->getType() == 'image') {
// Replace the 'target_id' with the filename.
/** @var \Drupal\file\FileInterface $file */
$file = $this->entityTypeManager->getStorage('file')->load($item['target_id']);
$item['filename'] = $file->getFilename();
unset($item['target_id']);
// Remove calculated values.
unset($item['height']);
unset($item['width']);
// Remove empty keys.
$item = array_filter($item);
}
elseif ($storage_definition->getType() == 'path') {
// Remove calculated values.
$item = array_intersect_key($item, ['alias' => 'alias']);
}
// Simplify items with a single key (such as "value").
$main_property_name = $storage_definition->getMainPropertyName();
if ($main_property_name && isset($item[$main_property_name]) && count($item) === 1) {
$item = $item[$main_property_name];
}
$list[$delta] = $item;
}
// Remove the wrapping array if the field is single-valued.
if ($storage_definition->getCardinality() === 1) {
$list = reset($list);
}
if (!empty($list)) {
$export[$field_name] = $list;
}
}
$entity_type_id = $entity->getEntityTypeId();
// Perform generic processing.
if (substr($entity_type_id, 0, 9) == 'commerce_') {
$export = $this->processCommerce($export, $entity);
}
// Process by entity type ID.
if ($entity_type_id == 'commerce_product') {
$export = $this->processProduct($export, $entity);
}
elseif ($entity_type_id == 'commerce_product_variation') {
$export = $this->processVariation($export, $entity);
}
elseif ($entity_type_id == 'commerce_product_attribute_value') {
$export = $this->processAttributeValue($export, $entity);
}
elseif ($entity_type_id == 'taxonomy_term') {
$export = $this->processTerm($export, $entity);
}
return $export;
}
/**
* Processes the exported Commerce entity.
*
* @param array $export
* The export array.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The Commerce entity.
*
* @return array
* The processed export array.
*/
protected function processCommerce(array $export, ContentEntityInterface $entity) {
// Imported entities are always assigned to the default store.
unset($export['stores']);
return $export;
}
/**
* Processes the exported product.
*
* @param array $export
* The export array.
* @param \Drupal\commerce_product\Entity\ProductInterface $product
* The product.
*
* @return array
* The processed export array.
*/
protected function processProduct(array $export, ProductInterface $product) {
// Export the variations as well.
$variations = [];
foreach ($product->getVariations() as $variation) {
$variations[$variation->uuid()] = $this->export($variation);
// The array is keyed by UUID, no need to have it in the export too.
unset($variations[$variation->uuid()]['uuid']);
}
$export['variations'] = $variations;
return $export;
}
/**
* Processes the exported product variation.
*
* @param array $export
* The export array.
* @param \Drupal\commerce_product\Entity\ProductVariationInterface $variation
* The product variation.
*
* @return array
* The processed export array.
*/
protected function processVariation(array $export, ProductVariationInterface $variation) {
// Don't export the product_id backreference, it's automatically populated.
unset($export['product_id']);
return $export;
}
/**
* Processes the exported attribute value.
*
* @param array $export
* The export array.
* @param \Drupal\commerce_product\Entity\ProductAttributeValueInterface $attribute_value
* The attribute value.
*
* @return array
* The processed export array.
*/
protected function processAttributeValue(array $export, ProductAttributeValueInterface $attribute_value) {
// Don't export the weight for now.
unset($export['weight']);
return $export;
}
/**
* Processes the exported taxonomy term.
*
* @param array $export
* The export array.
* @param \Drupal\taxonomy\TermInterface $term
* The taxonomy term.
*
* @return array
* The processed export array.
*/
protected function processTerm(array $export, TermInterface $term) {
/** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
$term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
if ($parents = $term_storage->loadParents($term->id())) {
// The 'parent' doesn't export properly before Drupal 8.6.0. See #2543726.
$parent_ids = array_keys($parents);
$parent_ids = array_map(function ($parent_id) {
return $this->mapToUuid('taxonomy_term', $parent_id);
}, $parent_ids);
$export = [
'parent' => $parent_ids,
] + $export;
}
return $export;
}
/**
* Maps an entity ID to a UUID.
*
* @param string $entity_type_id
* The entity type ID.
* @param int $entity_id
* The entity ID.
*
* @return string|null
* The entity UUID, or NULL if the entity no longer exists.
*/
protected function mapToUuid($entity_type_id, $entity_id) {
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$entity = $storage->load($entity_id);
return $entity ? $entity->uuid() : NULL;
}
}
<?php
namespace Drupal\synhelper\Service;
use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\taxonomy\TermInterface;
/**
* Defines the content importer.
*
* @internal
* For internal usage by the Commerce synhelper module.
*/
class ContentImporter {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The full path to the content directory.
*
* @var string
*/
protected $contentPath;
/**
* The current store.
*
* @var \Drupal\commerce_store\Entity\StoreInterface
*/
protected $store;
/**
* Constructs a new ContentImporter object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* Reacts on the module being installed, imports all content.
*/
public function onInstall($path, $content) {
// It is necessary to hardcode the available entity types/bundles to ensure
// the right import order, because there is no dependency tracking.
$this->contentPath = $path;
foreach ($content as $keys) {
$this->importAll($keys[0], $keys[1]);
}
}
/**
* Imports all content for the given entity type and bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
*/
public function importAll($entity_type_id, $bundle = '') {
$filepath = $this->buildFilepath($entity_type_id, $bundle);
if (!is_readable($filepath)) {
throw new \InvalidArgumentException(sprintf('The %s file could not be found/read.', $filepath));
}
$data = Yaml::decode(file_get_contents($filepath));
foreach ($data as $uuid => $values) {
$values['uuid'] = $uuid;
$this->importEntity($entity_type_id, $values);
}
}
/**
* Imports a given entity.
*
* If an entity with the given UUID already exists, it will be updated.
*
* @param string $entity_type_id
* The entity type ID.
* @param array $values
* The entity values.
*
* @return \Drupal\Core\Entity\EntityInterface
* The created or updated entity.
*/
public function importEntity($entity_type_id, array $values) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$wanted_keys = ['bundle', 'langcode', 'uuid'];
$wanted_keys = array_combine($wanted_keys, $wanted_keys);
$entity_keys = array_intersect_key($entity_type->getKeys(), $wanted_keys);
$storage = $this->entityTypeManager->getStorage($entity_type_id);
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->loadEntityByUuid($entity_type_id, $values['uuid']);
if (!$entity) {
// No existing entity found, create a new one.
$initial_values = array_intersect_key($values, array_flip($entity_keys));
$entity = $storage->create($initial_values);
}
// Process values.
$values = array_diff_key($values, array_flip($entity_keys));
foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
if (!isset($values[$field_name])) {
continue;
}
$storage_definition = $definition->getFieldStorageDefinition();
$items = $values[$field_name];
// Re-add the wrapper array stripped by ContentExporter.
if ($storage_definition->getCardinality() === 1) {
$items = [$items];
}
foreach ($items as $delta => $item) {
if ($definition->getType() == 'entity_reference' && is_string($item)) {
$target_entity_type_id = $storage_definition->getSetting('target_type');
$target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
if ($target_entity_type->entityClassImplements(ContentEntityInterface::class)) {
$target_entity = $this->loadEntityByUuid($target_entity_type_id, $item);
if ($target_entity) {
$items[$delta] = $target_entity->id();
}
else {
unset($items[$delta]);
}
}
}
elseif ($definition->getType() == 'image') {
$file = $this->ensureFile($item['filename']);
$items[$delta] = [
'target_id' => $file->id(),
] + $item;
}
$values[$field_name] = $items;
}
}
// Perform generic processing.
if (substr($entity_type_id, 0, 9) == 'commerce_') {
$values = $this->processCommerce($values, $entity);
}
// Process by entity type ID.
if ($entity_type_id == 'commerce_product') {
$values = $this->processProduct($values, $entity);
}
elseif ($entity_type_id == 'taxonomy_term') {
$values = $this->processTerm($values, $entity);
}