ww_publish-8.x-1.x-dev/src/Article.php
src/Article.php
<?php
namespace Drupal\ww_publish;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Utility\Error;
use Drupal\field\FieldConfigInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\ww_publish\Entity\SnsMessageEntityInterface;
use Drupal\ww_publish\Events\ArticleFieldsEvent;
use Drupal\ww_publish\Events\PreImportEvent;
class Article {
/**
* The SNS message.
*
* @var \Drupal\ww_publish\Message
*/
protected $message;
/**
* Metadata of the article received from WoodWing Studio.
*
* @var \Drupal\ww_publish\Metadata
*/
private $articleMetadata;
/**
* Article data received from WoodWing Studio.
*
* @var Object
*/
private $articleData;
/**
* Configuration of the ww_publish module.
*
* @var \Drupal\Core\Config\Config
*/
private $config;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* Content type in which the article should be saved.
*
* @var \Drupal\node\Entity\NodeType
*/
private $contentType;
/**
* WoodWing article ID.
*
* @var int
*/
private $id;
/**
* WoodWing Studio article ID field.
*
* @var string
*/
private $articleIdField;
/**
* Paragraph field.
*
* @var string
*/
private $paragraphField;
/**
* The target node which will be created or updated.
*
* @var \Drupal\node\Entity\Node
*/
private $node;
/**
* Constructor.
*
* @param \Drupal\ww_publish\Message $message
* SNS Message.
* @param \Drupal\Core\Config\Config $config
* Configuration of the ww_publish module.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function __construct(Message $message, $config, $logger) {
$this->message = $message;
$this->articleMetadata = $message->getArticleMetadata();
$this->articleData = $message->getArticleData();
$this->config = $config;
$this->logger = $logger;
$this->contentType = $this->getContentType();
$this->paragraphField = $this->getContentTypeSetting('paragraph_field');
}
/**
* Publish a new node or update an existing node.
*
* @return bool
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function publish(): int {
if ($this->config->get('debug_mode'))
$this->logger->debug('Publish!');
$pre_import_event = $this->findRelatedNode();
if ($pre_import_event->isSkip()) {
$this->logger->notice('Import of @ww_id has been skipped.', [
'@ww_id' => $this->id,
]);
return SnsMessageEntityInterface::SKIPPED;
}
$this->node = $pre_import_event->getNode();
if (!$this->node->isNew()) {
return $this->update() ? SnsMessageEntityInterface::IMPORTED : SnsMessageEntityInterface::FAILED;
}
else {
return $this->create() ? SnsMessageEntityInterface::IMPORTED : SnsMessageEntityInterface::FAILED;
}
}
/**
* Create a new node.
*
* @return bool
* @throws \Drupal\Core\Entity\EntityStorageException|\Drupal\Core\TypedData\Exception\ReadOnlyException
*/
private function create(): bool {
try {
$this->updateFields();
// Test if during updating the fields a related node was created.
$pre_import_event = $this->findRelatedNode();
// Prevent creation of a duplicated node, when Amazon sends more request at the same time.
if (!$pre_import_event->getNode() || $pre_import_event->getNode()->isNew()) {
if ($this->config->get('debug_mode'))
$this->logger->debug('Node to be created: <pre><code>@node</code></pre>', ['@node' => print_r($this->node->toArray(), TRUE)]);
$this->node->save();
$this->logger->notice('The following article has been created: @node_title (WW ID: @ww_id, Drupal ID: @node_id)', ['@node_title' => $this->node->getTitle(), '@ww_id' => $this->id, '@node_id' => $this->node->id()]);
} else if ($pre_import_event->getNode() && !$pre_import_event->getNode()) {
$this->logger->error('The node already exists. Probably has Amazon sent more requests at the same time.');
} else {
$this->logger->error('There are more than one nodes with the same WoodWing Studio article ID.');
}
}
catch (EntityStorageException $e) {
$variables = Error::decodeException($e);
$this->logger->error('A node could not be created. @message in %function (line %line of %file).', $variables);
return FALSE;
}
return TRUE;
}
/**
* Update an existing node.
*
* @return bool
* @throws \Drupal\Core\Entity\EntityStorageException
*/
private function update(): bool {
if ($this->node instanceof NodeInterface) {
try {
$this->updateFields();
$this->node->setRevisionUserId($this->articleMetadata->getAuthor()->id());
if ($this->config->get('debug_mode'))
$this->logger->debug('Node to be updated: <pre><code>@node</code></pre>', ['@node' => print_r($this->node->toArray(), TRUE)]);
$this->node->save();
$this->logger->notice('The following article has been updated: @node_title (WW ID: @ww_id, Drupal ID: @node_id)', ['@node_title' => $this->node->getTitle(), '@ww_id' => $this->id, '@node_id' => $this->node->id()]);
}
catch (\Exception $e) {
$variables = Error::decodeException($e);
$this->logger->error('The node could not be updated. @message in %function (line %line of %file).', $variables);
return FALSE;
}
}
return TRUE;
}
/**
* Update fields.
*
* @throws \Drupal\Core\TypedData\Exception\ReadOnlyException
*/
private function updateFields() {
// Define an empty error messages field.
$messagesFieldName = $this->config->get('error_messages_field');
$messagesField = [];
// Update title and owner.
$title = $this->getTitle();
$this->node->set('title', $title);
$this->node->setOwnerId($this->articleMetadata->getAuthor());
$fields = $this->getContentTypeFields();
foreach ($fields as $fieldName => $fieldConfig) {
$contentStationIdentifier = $fieldConfig->getThirdPartySetting('ww_publish', 'content_station_identifier');
if ($contentStationIdentifier) {
$field = new Field($fieldConfig, $contentStationIdentifier, $this->message, $this->config, $this->logger);
$articleData = $field->getArticleData($messagesField);
if ($this->config->get('debug_mode'))
$this->logger->debug($fieldName . ': <pre><code>@article_data</code></pre>', ['@article_data' => print_r($articleData, TRUE)]);
$this->node->set($fieldName, $articleData);
}
if ($fieldName === $this->paragraphField) {
$paragraphField = new ParagraphField($fieldConfig, $this->message, $this->articleData->data->content, $this->config, $this->logger);
$this->node->set($fieldName, $paragraphField->getArticleData($messagesField));
}
}
$articleFieldsEvent = new ArticleFieldsEvent($this->message, $this->node);
\Drupal::service('event_dispatcher')->dispatch($articleFieldsEvent);
if ($messagesFieldName) {
\Drupal::service('account_switcher')->switchTo($this->node->getOwner());
$violations = $this->node->validate();
foreach ($violations as $violation) {
$messagesField[] = $violation->getPropertyPath() . ': ' . $violation->getMessage();
}
// Also validate all paragraphs.
if ($this->paragraphField) {
foreach ($this->node->get($this->paragraphField) as $delta => $item) {
$violations = $item->entity->validate();
foreach ($violations as $violation) {
$messagesField[] = $this->paragraphField . '.' . $delta . '.' . $violation->getPropertyPath() . ': ' . $violation->getMessage();
}
}
}
\Drupal::service('account_switcher')->switchBack();
}
if ($messagesFieldName)
$this->node->get($messagesFieldName)->setValue($messagesField);
}
/**
* Get content type fields.
*
* @return array
*/
private function getContentTypeFields(): array {
$contentType = $this->contentType->get('type');
$entityFieldManager = \Drupal::service('entity_field.manager');
$fields = [];
if(!empty($contentType)) {
$fields = array_filter(
$entityFieldManager->getFieldDefinitions('node', $contentType),
function ($field_definition) {
return
$field_definition instanceof FieldConfigInterface;
}
);
}
return $fields;
}
/**
* Get the target content type.
*
* @return mixed
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
private function getContentType() {
$contentTypeMachineName = $this->articleMetadata->getMetadataByIdentifier('ExtraMetaData', $this->config->get('content_type_field'));
if ($contentTypeMachineName) {
if ($this->config->get('debug_mode')) {
$this->logger->debug('Target content type: <pre><code>@content_type_name</code></pre>', ['@content_type_name' => print_r($contentTypeMachineName, TRUE)]);
}
// Get all content types.
try {
$types = \Drupal::entityTypeManager()
->getStorage('node_type')
->loadMultiple();
} catch (InvalidPluginDefinitionException $e) {
$this->logger->error('Could not get content types.');
return FALSE;
} catch (PluginNotFoundException $e) {
$this->logger->error('Could not get content types.');
return FALSE;
}
// Test if the target content type exists in Drupal.
if ($this->config->get('debug_mode'))
$this->logger->debug('Existing content types: <pre><code>@types</code></pre>', ['@types' => print_r($types, TRUE)]);
if ($types[$contentTypeMachineName]) {
// TODO: Test if the content type is correctly configured.
if ($this->config->get('debug_mode'))
$this->logger->debug('The target content type exists in Drupal and is correctly configured.');
return \Drupal::entityTypeManager()
->getStorage('node_type')
->load($contentTypeMachineName);
} else {
$this->logger->error('The content type does not exist in Drupal.');
return FALSE;
}
} else {
$this->logger->error('The content type field could not be found in the metadata.');
return FALSE;
}
}
/**
* Get the ID of the WoodWing Studio article.
*
* @return int|false
* Either the woodwing article ID or FALSE if no ID could be found.
*/
private function getId() {
if ($id = $this->articleMetadata->getMetadataByIdentifier('BasicMetaData', 'ID')) {
if ($this->config->get('debug_mode')) {
$this->logger->debug('WoodWing Studio article ID: <pre><code>@id</code></pre>', ['@id' => print_r($id)]);
}
return (int) $id;
} else {
$this->logger->error('No article ID found in metadata.');
return FALSE;
}
}
/**
* Get the title of the WoodWing Studio article.
*
* @return mixed
* @throws \Drupal\Core\Entity\EntityStorageException
*/
private function getTitle() {
// Get the WoodWing Studio identifier for the title from content type configuration.
$contentStationIdentifier = $this->contentType->getThirdPartySetting('ww_publish', 'content_station_identifier_title');
$contentType = $this->contentType->get('type');
$entityFieldManager = \Drupal::service('entity_field.manager');
// Get field configuration for the field title.
$fieldConfig = $entityFieldManager->getFieldDefinitions('node', $contentType)['title'];
if ($contentStationIdentifier && $fieldConfig) {
$field = new Field($fieldConfig, $contentStationIdentifier, $this->message, $this->config, $this->logger);
$title = $field->getArticleData();
if (\is_array($title) && !empty($title['value'])) {
$title = $title['value'];
}
if (!$this->config->get('html_title')) {
$title = strip_tags($title);
}
if ($this->config->get('debug_mode'))
$this->logger->debug('Title: <pre><code>@title</code></pre>', ['@title' => print_r($title, TRUE)]);
return $title;
}
$this->logger->error('No article title found.');
return FALSE;
}
/**
* Get the ww_publish setting from content type configuration.
*
* @param string $settingName
* Setting name, e.g.: 'article_id_field', 'paragraph_field'.
* @param boolean $logMissing
* Log a notice if a setting is not set.
*
* @return mixed
*/
private function getContentTypeSetting($settingName, $logMissing = TRUE) {
$setting = $this->contentType->getThirdPartySetting('ww_publish', $settingName);
if ($setting) {
return $setting;
}
elseif ($logMissing) {
$this->logger->notice('The content type setting @setting_name is not set.', ['@setting_name' => $settingName]);
}
return NULL;
}
/**
* Find the related node.
*
* @return \Drupal\ww_publish\Events\PreImportEvent
* Returns the pre import event object. If a node is set than that should
* be updated, if not then an ew one should be created and if marked as skip
* then the import should be skipped.
*/
private function findRelatedNode() {
$pre_import_event = new PreImportEvent($this->message);
try {
$storyid = $this->articleMetadata->getMetadataByIdentifier('BasicMetaData', 'StoryId');
$storyid_field = $this->getContentTypeSetting('article_storyid_field', FALSE);
$id_field = $this->getContentTypeSetting('article_id_field', FALSE);
if (!$storyid_field && !$id_field) {
throw new \Exception('Either storyid or id field needs to be configured for content type ' . $this->contentType->id());
}
$storage = \Drupal::entityTypeManager()
->getStorage('node');
// First, look up by StoryID if configured and one is available.
$nodes = [];
if ($storyid && $storyid_field) {
$this->id = $storyid;
$this->articleIdField = $storyid_field;
$nodes = $storage->loadByProperties([$storyid_field => $this->id]);
}
// Fall back to the ID field if the story ID is either not present or
// no field is configured for it.
if ($id_field) {
$id = $this->getId();
$nodes = $storage->loadByProperties([$id_field => $id]);
if (!$this->id) {
$this->id = $id;
}
}
// Test if the node exists.
if (($node = reset($nodes)) && count($nodes) === 1) {
// If a StoryID exists but the field is empty then this is
// being upgraded from an ID reference, set the story id on the existing
// node.
if ($storyid && $storyid_field && $node->get($storyid_field)->value != $storyid) {
$node->set($storyid_field, $storyid);
}
$pre_import_event->setNode($node);
}
elseif (count($nodes) > 1) {
$this->logger->error('There is more than one node with the same WoodWing Studio article ID.');
$pre_import_event->setSkip(TRUE);
}
else {
// Create a new node with the correct ID field.
$node = Node::create([
'type' => $this->contentType->id(),
]);
if ($storyid_field && $storyid) {
$node->set($storyid_field, $storyid);
}
elseif ($id_field) {
$node->set($id_field, $this->id);
}
$pre_import_event->setNode($node);
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
$pre_import_event->setSkip(TRUE);
}
\Drupal::service('event_dispatcher')->dispatch($pre_import_event);
return $pre_import_event;
}
}
