diff --git a/web/modules/custom/bnf/src/Services/BnfImporter.php b/web/modules/custom/bnf/src/Services/BnfImporter.php index 138db8519..53325dc0f 100644 --- a/web/modules/custom/bnf/src/Services/BnfImporter.php +++ b/web/modules/custom/bnf/src/Services/BnfImporter.php @@ -4,12 +4,18 @@ use Drupal\bnf\BnfStateEnum; use Drupal\bnf\Exception\AlreadyExistsException; +use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\node\NodeInterface; +use Drupal\paragraphs\ParagraphInterface; use GuzzleHttp\ClientInterface; use Psr\Log\LoggerInterface; use function Safe\json_decode; use function Safe\parse_url; +use function Safe\preg_replace; /** * Service related to importing content from an external source. @@ -20,46 +26,235 @@ */ class BnfImporter { + const ALLOWED_PARAGRAPHS = [ + 'ParagraphTextBody' => 'text_body', + 'ParagraphAccordion' => 'accordion', + ]; + /** * Constructor. */ public function __construct( protected ClientInterface $httpClient, + protected EntityFieldManagerInterface $entityFieldManager, protected EntityTypeManagerInterface $entityTypeManager, protected TranslationInterface $translation, protected LoggerInterface $logger, ) {} /** - * Importing a node from a GraphQL source endpoint. + * Loading the columns of a field, that we use to ask GraphQL for data. + * + * E.g. a WYSIWYG field will have both a "value" and a "format" that we want + * to pull out. + * + * @return mixed[] + * Return an array of fields, along with their column keys. */ - public function importNode(string $uuid, string $endpointUrl, string $nodeType = 'article'): void { - $nodeStorage = $this->entityTypeManager->getStorage('node'); + protected function getFieldColumns(string $entityType, string $bundle): array { + $values = []; + $fields = []; + $fieldDefinitions = $this->entityFieldManager->getFieldDefinitions($entityType, $bundle); - $existingNodes = - $nodeStorage->loadByProperties(['uuid' => $uuid]); + foreach ($fieldDefinitions as $fieldKey => $fieldDefinition) { + if ($fieldDefinition instanceof FieldConfig) { + $fields[] = $fieldKey; + } + } - if (!empty($existingNodes)) { - $this->logger->error( - 'Cannot import @type @uuid from @url - Node already exists.', - ['@type' => $nodeType, '@uuid' => $uuid, '@url' => $endpointUrl] - ); + foreach ($fields as $fieldKey) { + $field = $this->entityTypeManager->getStorage('field_storage_config')->load("$entityType.$fieldKey"); - throw new AlreadyExistsException('Cannot import node - already exists.'); + if ($field instanceof FieldStorageConfig) { + $values[$fieldKey] = array_keys($field->getColumns()); + } } - // Example of GraphQL query: "nodeArticle". - $queryName = 'node' . ucfirst($nodeType); + return $values; + } - // For now, we only support the title of the nodes. + /** + * Builds the query used to get data from the source. + */ + public function getQuery(string $uuid, string $queryName): string { + // Start building the GraphQL query. $query = << $drupalBundle) { + $query .= <<getFieldColumns('paragraph', $drupalBundle); + foreach ($fieldColumns as $fieldKey => $columns) { + $fieldKey = $this->drupalFieldToGraphField($fieldKey); + + $columnsString = implode("\r\n ", $columns); + $query .= << $bundleName]; + + // Map fields dynamically. + foreach ($paragraphData as $key => $value) { + if ($key === '__typename') { + continue; + } + + // Assume Drupal uses field names like "field_{key}". + $drupalFieldName = $this->graphFieldToDrupalField($key); + $paragraph[$drupalFieldName] = $value; + } + + $parsedParagraphs[] = $paragraph; + } + + return $parsedParagraphs; + } + + /** + * Creating the paragraphs, that we will add to the nodes. + * + * @param mixed[] $nodeData + * The GraphQL node data containing paragraphs. + * + * @return \Drupal\paragraphs\ParagraphInterface[] + * The paragraph entities. + */ + protected function getParagraphs(array $nodeData): array { + $parsedParagraphs = $this->parseGraphParagraphs($nodeData); + $storage = $this->entityTypeManager->getStorage('paragraph'); + $paragraphs = []; + foreach ($parsedParagraphs as $paragraphData) { + $paragraph = $storage->create($paragraphData); + + if ($paragraph instanceof ParagraphInterface) { + $paragraph->save(); + $paragraphs[] = $paragraph; + } + } + + return $paragraphs; + } + + /** + * Loading the node data from a GraphQL endpoint. + * + * @return mixed[] + * Array of node values, that we can use to create node entities. + */ + public function loadNodeData(string $uuid, string $endpointUrl, string $nodeType = 'article'): array { + $queryName = 'node' . ucfirst($nodeType); + + $nodeStorage = $this->entityTypeManager->getStorage('node'); + + $existingNodes = + $nodeStorage->loadByProperties(['uuid' => $uuid]); + + if (!empty($existingNodes)) { + $this->logger->error( + 'Cannot import @type @uuid from @url - Node already exists.', + ['@type' => $nodeType, '@uuid' => $uuid, '@url' => $endpointUrl] + ); + + throw new AlreadyExistsException('Cannot import node - already exists.'); + } + if (!filter_var($endpointUrl, FILTER_VALIDATE_URL)) { throw new \InvalidArgumentException('The provided callback URL is not valid.'); } @@ -71,6 +266,8 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType = throw new \InvalidArgumentException('The provided callback URL must use HTTPS.'); } + $query = $this->getQuery($uuid, $queryName); + $response = $this->httpClient->request('post', $endpointUrl, [ 'headers' => [ 'Content-Type' => 'application/json', @@ -87,15 +284,30 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType = $nodeData = $data['data'][$queryName] ?? NULL; if (empty($nodeData)) { - $this->logger->error('Could not find any node data in GraphQL response.'); + $this->logger->error( + 'Could not find any node data in GraphQL response. @query', + ['@query' => $query] + ); throw new \Exception('Could not retrieve content values.'); } + return $nodeData; + } + + /** + * Importing a node from a GraphQL source endpoint. + */ + public function importNode(string $uuid, string $endpointUrl, string $nodeType = 'article'): NodeInterface { + $nodeStorage = $this->entityTypeManager->getStorage('node'); try { + $nodeData = $this->loadNodeData($uuid, $endpointUrl, $nodeType); $nodeData['type'] = $nodeType; $nodeData['uuid'] = $uuid; + $nodeData['field_paragraphs'] = $this->getParagraphs($nodeData); + $nodeData['status'] = NodeInterface::NOT_PUBLISHED; + /** @var \Drupal\node\NodeInterface $node */ $node = $nodeStorage->create($nodeData); $node->save(); @@ -116,6 +328,7 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType = '@type' => $nodeType, ]); + return $node; } }