Skip to content

Commit

Permalink
Update BnfImporter to import text and accordion paragraphs.
Browse files Browse the repository at this point in the history
Making some assumptions on how graphql_compose builds the links
(as there is no way of doing it reverse, as far as I can see..),
we build a query that looks up available fields, on paragraphs
we support.

This feels a bit dirty, but for now, until we've cleared how to
deal with GraphQL schemas/codegen, it'll do.
Only text_body and accordion paragraphs are imported.
  • Loading branch information
rasben committed Dec 23, 2024
1 parent becd376 commit ff928f9
Showing 1 changed file with 216 additions and 13 deletions.
229 changes: 216 additions & 13 deletions web/modules/custom/bnf/src/Services/BnfImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

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\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.
Expand All @@ -20,20 +25,224 @@
*/
class BnfImporter {

const ALLOWED_PARAGRAPHS = [
'ParagraphTextBody' => 'text_body',
'ParagraphAccordion' => 'accordion',
];

/**
* The BNF UUID of the content that we import.
*/
protected string $uuid;

/**
* Constructor.
*/
public function __construct(
protected ClientInterface $httpClient,
protected EntityFieldManagerInterface $entityFieldManager,
protected EntityTypeManagerInterface $entityTypeManager,
protected TranslationInterface $translation,
protected LoggerInterface $logger,
) {}

/**
* 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.
*/
protected function getFieldColumns(string $entityType, string $bundle): array {
$values = [];
$fields = [];
$fieldDefinitions = $this->entityFieldManager->getFieldDefinitions($entityType, $bundle);

foreach ($fieldDefinitions as $fieldKey => $fieldDefinition) {
if ($fieldDefinition instanceof FieldConfig) {
$fields[] = $fieldKey;
}
}

foreach ($fields as $fieldKey) {
$field = $this->entityTypeManager->getStorage('field_storage_config')->load("$entityType.$fieldKey");

if ($field instanceof FieldStorageConfig) {
$values[$fieldKey] = array_keys($field->getColumns());
}
}

return $values;
}

/**
* Builds the query used to get data from the source.
*/
public function getQuery(string $queryName): string {
// Start building the GraphQL query.
$query = <<<GRAPHQL
query {
$queryName(id: "$this->uuid") {
title
paragraphs {
GRAPHQL;

// Loop through allowed paragraphs and add their structures.
foreach (self::ALLOWED_PARAGRAPHS as $graphBundle => $drupalBundle) {
$query .= <<<GRAPHQL
... on $graphBundle {
__typename
GRAPHQL;

// Add field columns for the current paragraph type.
$fieldColumns = $this->getFieldColumns('paragraph', $drupalBundle);
foreach ($fieldColumns as $fieldKey => $columns) {
$fieldKey = $this->drupalFieldToGraphField($fieldKey);

$columnsString = implode("\r\n ", $columns);
$query .= <<<GRAPHQL
$fieldKey {
$columnsString
}
GRAPHQL;
}

// Close the paragraph type block.
$query .= <<<GRAPHQL
}
GRAPHQL;
}

// Close the paragraphs and main query block.
$query .= <<<GRAPHQL
}
}
}
GRAPHQL;

return $query;
}

/**
* Turn GraphQL field format (camelCase) to Drupal format (snake_case).
*/
protected function graphFieldToDrupalField(string $fieldKey): string {
// Prefix all capitalized letters with an underscore.
$fieldKey = preg_replace('/(?<!^)[A-Z]/', '_$0', $fieldKey);

// Lowercase everything.
$fieldKey = strtolower($fieldKey);

// Prefix with "field_".
return "field_$fieldKey";
}

/**
* Turn Drupal format (snake_case) to GraphQL field format (camelCase).
*/
protected function drupalFieldToGraphField(string $fieldKey): string {
// Remove 'field_' prefix, if it exists.
if (str_starts_with($fieldKey, 'field_')) {
$fieldKey = substr($fieldKey, strlen('field_'));
}

// Replace underscores with spaces.
$fieldKey = str_replace('_', ' ', $fieldKey);

// Convert the first character of each word to uppercase.
$fieldKey = ucwords($fieldKey);

// Remove all spaces.
$fieldKey = str_replace(' ', '', $fieldKey);

// Make first letter lowercase, to match camelCase.
return lcfirst($fieldKey);
}

/**
* Parses paragraphs from GraphQL node data into Drupal-compatible structures.
*
* @param mixed[] $nodeData
* The GraphQL node data containing paragraphs.
*
* @return mixed[]
* Array of paragraph values, that we can use to create paragraph entities.
*/
protected function parseGraphParagraphs(array $nodeData) {
$parsedParagraphs = [];

// Ensure paragraphs exist in the GraphQL response.
if (empty($nodeData['paragraphs'])) {
return $parsedParagraphs;
}

foreach ($nodeData['paragraphs'] as $paragraphData) {
$type = $paragraphData['__typename'] ?? '';

// Convert typename to Drupal paragraph bundle name.
$bundleName = self::ALLOWED_PARAGRAPHS[$type] ?? NULL;

if (empty($bundleName)) {
continue;
}

$paragraph = ['type' => $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;
}

/**
* Importing a node from a GraphQL source endpoint.
*/
public function importNode(string $uuid, string $endpointUrl, string $nodeType = 'article'): void {
$this->uuid = $uuid;
$queryName = 'node' . ucfirst($nodeType);

$nodeStorage = $this->entityTypeManager->getStorage('node');

$existingNodes =
Expand All @@ -48,18 +257,6 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType =
throw new AlreadyExistsException('Cannot import node - already exists.');
}

// Example of GraphQL query: "nodeArticle".
$queryName = 'node' . ucfirst($nodeType);

// For now, we only support the title of the nodes.
$query = <<<GRAPHQL
query {
$queryName(id: "$uuid") {
title
}
}
GRAPHQL;

if (!filter_var($endpointUrl, FILTER_VALIDATE_URL)) {
throw new \InvalidArgumentException('The provided callback URL is not valid.');
}
Expand All @@ -71,6 +268,8 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType =
throw new \InvalidArgumentException('The provided callback URL must use HTTPS.');
}

$query = $this->getQuery($queryName);

$response = $this->httpClient->request('post', $endpointUrl, [
'headers' => [
'Content-Type' => 'application/json',
Expand All @@ -87,14 +286,18 @@ 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' => $queryName]
);

throw new \Exception('Could not retrieve content values.');
}

try {
$nodeData['type'] = $nodeType;
$nodeData['uuid'] = $uuid;
$nodeData['field_paragraphs'] = $this->getParagraphs($nodeData);

$node = $nodeStorage->create($nodeData);
$node->save();
Expand Down

0 comments on commit ff928f9

Please sign in to comment.