commit
bf39286614
11 changed files with 764 additions and 0 deletions
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
name: Islandora ROR |
||||
type: module |
||||
description: 'Provides a ROR (Research Organization Registry) field type and widgets.' |
||||
core_version_requirement: ^10 || ^11 |
||||
package: Islandora |
||||
dependencies: |
||||
- drupal:field |
||||
- drupal:system |
||||
@ -0,0 +1,6 @@
|
||||
islandora_ror.autocomplete: |
||||
path: '/islandora-ror/autocomplete' |
||||
defaults: |
||||
_controller: '\Drupal\islandora_ror\Controller\RorAutocompleteController::autocomplete' |
||||
requirements: |
||||
_permission: 'access content' |
||||
@ -0,0 +1,12 @@
|
||||
services: |
||||
|
||||
logger.channel.islandora_ror: |
||||
class: Drupal\Core\Logger\LoggerChannel |
||||
arguments: ['islandora_ror'] |
||||
|
||||
islandora_ror.ror_client: |
||||
class: Drupal\islandora_ror\Service\RorClient |
||||
arguments: |
||||
- '@http_client' |
||||
- '@logger.channel.islandora_ror' |
||||
- '@cache.default' |
||||
Binary file not shown.
@ -0,0 +1,72 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_ror\Controller; |
||||
|
||||
use Drupal\Core\Controller\ControllerBase; |
||||
use Drupal\islandora_ror\Service\RorClient; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
use Symfony\Component\HttpFoundation\JsonResponse; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
/** |
||||
* Returns autocomplete responses for ROR organizations. |
||||
*/ |
||||
class RorAutocompleteController extends ControllerBase { |
||||
|
||||
/** |
||||
* ROR client service. |
||||
* |
||||
* @var \Drupal\islandora_ror\Service\RorClient |
||||
*/ |
||||
protected RorClient $rorClient; |
||||
|
||||
/** |
||||
* Constructs a RorAutocompleteController object. |
||||
*/ |
||||
public function __construct(RorClient $ror_client) { |
||||
$this->rorClient = $ror_client; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function create(ContainerInterface $container): self { |
||||
return new static( |
||||
$container->get('islandora_ror.ror_client') |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Autocomplete callback. |
||||
* |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* The request object. |
||||
* |
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse |
||||
* The JSON response. |
||||
*/ |
||||
public function autocomplete(Request $request): JsonResponse { |
||||
$search_string = (string) $request->query->get('q', ''); |
||||
|
||||
$matches = []; |
||||
if ($search_string !== '') { |
||||
$items = $this->rorClient->search($search_string, 20); |
||||
|
||||
foreach ($items as $item) { |
||||
$id = $item['id'] ?? ''; |
||||
$name = $this->rorClient->getName($item['names']); |
||||
if (!$id || !$name) { |
||||
continue; |
||||
} |
||||
|
||||
$matches[] = [ |
||||
'value' => $id, |
||||
'label' => sprintf('%s (%s)', $name, $id), |
||||
]; |
||||
} |
||||
} |
||||
|
||||
return new JsonResponse($matches); |
||||
} |
||||
|
||||
} |
||||
Binary file not shown.
@ -0,0 +1,172 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_ror\Plugin\Field\FieldFormatter; |
||||
|
||||
use Drupal\Core\Field\FieldItemListInterface; |
||||
use Drupal\Core\Field\FormatterBase; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
use Drupal\Core\Link; |
||||
use Drupal\Core\Url; |
||||
|
||||
/** |
||||
* Display formatter for ROR organization fields. |
||||
* |
||||
* @FieldFormatter( |
||||
* id = "ror_formatter", |
||||
* label = @Translation("ROR organization formatter"), |
||||
* field_types = { |
||||
* "ror_field" |
||||
* } |
||||
* ) |
||||
*/ |
||||
class RorFormatter extends FormatterBase { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function defaultSettings() { |
||||
return [ |
||||
'show_ror_link' => TRUE, |
||||
'show_website' => TRUE, |
||||
'show_wikipedia' => TRUE, |
||||
'style' => 'inline', // inline or stacked |
||||
'separator' => ' • ', |
||||
] + parent::defaultSettings(); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function settingsSummary() { |
||||
$summary = []; |
||||
|
||||
$summary[] = $this->t('Style: @style', [ |
||||
'@style' => $this->getSetting('style'), |
||||
]); |
||||
$summary[] = $this->t('Show links: ROR (@ror), Website (@web), Wikipedia (@wiki)', [ |
||||
'@ror' => $this->getSetting('show_ror_link') ? 'yes' : 'no', |
||||
'@web' => $this->getSetting('show_website') ? 'yes' : 'no', |
||||
'@wiki' => $this->getSetting('show_wikipedia') ? 'yes' : 'no', |
||||
]); |
||||
|
||||
return $summary; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function settingsForm(array $form, FormStateInterface $form_state) { |
||||
$form['style'] = [ |
||||
'#type' => 'select', |
||||
'#title' => $this->t('Display style'), |
||||
'#options' => [ |
||||
'inline' => $this->t('Inline'), |
||||
'stacked' => $this->t('Stacked'), |
||||
], |
||||
'#default_value' => $this->getSetting('style'), |
||||
]; |
||||
|
||||
$form['separator'] = [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Inline separator'), |
||||
'#default_value' => $this->getSetting('separator'), |
||||
'#states' => [ |
||||
'visible' => [ |
||||
':input[name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][style]"]' => ['value' => 'inline'], |
||||
], |
||||
], |
||||
]; |
||||
|
||||
$form['show_ror_link'] = [ |
||||
'#type' => 'checkbox', |
||||
'#title' => $this->t('Show ROR link'), |
||||
'#default_value' => $this->getSetting('show_ror_link'), |
||||
]; |
||||
|
||||
$form['show_website'] = [ |
||||
'#type' => 'checkbox', |
||||
'#title' => $this->t('Show website'), |
||||
'#default_value' => $this->getSetting('show_website'), |
||||
]; |
||||
|
||||
$form['show_wikipedia'] = [ |
||||
'#type' => 'checkbox', |
||||
'#title' => $this->t('Show Wikipedia'), |
||||
'#default_value' => $this->getSetting('show_wikipedia'), |
||||
]; |
||||
|
||||
return $form; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function viewElements(FieldItemListInterface $items, $langcode): array { |
||||
$elements = []; |
||||
$settings = $this->getSettings(); |
||||
$separator = $settings['separator']; |
||||
|
||||
foreach ($items as $delta => $item) { |
||||
if (empty($item->id) || empty($item->name)) { |
||||
// Do not render empty values. |
||||
continue; |
||||
} |
||||
|
||||
$links = []; |
||||
$name = $item->name; |
||||
|
||||
// ROR link. |
||||
if ($settings['show_ror_link'] && !empty($item->id)) { |
||||
$id = $item->id; |
||||
|
||||
if (str_starts_with($id, 'http://') || str_starts_with($id, 'https://')) { |
||||
$ror_uri = $id; |
||||
} |
||||
else { |
||||
$ror_uri = 'https://ror.org/' . ltrim($id, '/'); |
||||
} |
||||
|
||||
$ror_url = Url::fromUri($ror_uri); |
||||
$links[] = Link::fromTextAndUrl($this->t('ROR'), $ror_url) |
||||
->toRenderable(); |
||||
} |
||||
|
||||
if ($settings['show_website'] && !empty($item->website)) { |
||||
$website_url = Url::fromUri($item->website); |
||||
$links[] = Link::fromTextAndUrl($this->t('Website'), $website_url) |
||||
->toRenderable(); |
||||
} |
||||
|
||||
// Wikipedia. |
||||
if ($settings['show_wikipedia'] && !empty($item->wikipedia)) { |
||||
$wiki_url = Url::fromUri($item->wikipedia); |
||||
$links[] = Link::fromTextAndUrl($this->t('Wikipedia'), $wiki_url) |
||||
->toRenderable(); |
||||
} |
||||
|
||||
// Build output depending on style. |
||||
if ($settings['style'] === 'inline') { |
||||
$render_links = []; |
||||
foreach ($links as $link) { |
||||
$render_links[] = \Drupal::service('renderer')->renderPlain($link); |
||||
} |
||||
|
||||
$elements[$delta] = [ |
||||
'#markup' => $name . ' ' . $separator . implode($separator, $render_links), |
||||
'#allowed_tags' => ['a', 'span', 'div'], |
||||
]; |
||||
} |
||||
else { |
||||
// Stacked version. |
||||
$elements[$delta] = [ |
||||
'#theme' => 'item_list', |
||||
'#title' => $name, |
||||
'#items' => $links, |
||||
]; |
||||
} |
||||
} |
||||
|
||||
return $elements; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,79 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_ror\Plugin\Field\FieldType; |
||||
|
||||
use Drupal\Core\Field\FieldItemBase; |
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface; |
||||
use Drupal\Core\TypedData\DataDefinition; |
||||
|
||||
/** |
||||
* Plugin implementation of the 'ror_field' field type. |
||||
* |
||||
* @FieldType( |
||||
* id = "ror_field", |
||||
* label = @Translation("ROR organization"), |
||||
* description = @Translation("Stores an organization identifier and metadata from the Research Organization Registry (ROR)."), |
||||
* default_widget = "ror_widget", |
||||
* default_formatter = "string" |
||||
* ) |
||||
*/ |
||||
class RorFieldItem extends FieldItemBase { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array { |
||||
$properties['id'] = DataDefinition::create('string') |
||||
->setLabel(t('ROR ID')) |
||||
->setRequired(TRUE); |
||||
|
||||
$properties['name'] = DataDefinition::create('string') |
||||
->setLabel(t('Organization name')); |
||||
|
||||
$properties['website'] = DataDefinition::create('string') |
||||
->setLabel(t('Website URL')); |
||||
|
||||
$properties['wikipedia'] = DataDefinition::create('string') |
||||
->setLabel(t('Wikipedia URL')); |
||||
|
||||
return $properties; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function schema(FieldStorageDefinitionInterface $field_definition): array { |
||||
return [ |
||||
'columns' => [ |
||||
'id' => [ |
||||
'type' => 'varchar', |
||||
'length' => 255, |
||||
], |
||||
'name' => [ |
||||
'type' => 'varchar', |
||||
'length' => 1024, |
||||
'not null' => FALSE, |
||||
], |
||||
'website' => [ |
||||
'type' => 'varchar', |
||||
'length' => 1024, |
||||
'not null' => FALSE, |
||||
], |
||||
'wikipedia' => [ |
||||
'type' => 'varchar', |
||||
'length' => 1024, |
||||
'not null' => FALSE, |
||||
], |
||||
], |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function isEmpty(): bool { |
||||
$value = $this->get('id')->getValue(); |
||||
return $value === NULL || $value === ''; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,184 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_ror\Plugin\Field\FieldWidget; |
||||
|
||||
use Drupal\Core\Field\FieldItemListInterface; |
||||
use Drupal\Core\Field\WidgetBase; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
use Drupal\islandora_ror\Service\RorClient; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
|
||||
/** |
||||
* Plugin implementation of the 'ror_widget' widget. |
||||
* |
||||
* @FieldWidget( |
||||
* id = "ror_widget", |
||||
* label = @Translation("ROR organization widget"), |
||||
* field_types = { |
||||
* "ror_field" |
||||
* } |
||||
* ) |
||||
*/ |
||||
class RorWidget extends WidgetBase { |
||||
|
||||
/** |
||||
* The ROR client service. |
||||
* |
||||
* @var \Drupal\islandora_ror\Service\RorClient |
||||
*/ |
||||
protected RorClient $rorClient; |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
* |
||||
* Widgets MUST implement ::create() manually to support DI. |
||||
*/ |
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); |
||||
$instance->rorClient = $container->get('islandora_ror.ror_client'); |
||||
return $instance; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array { |
||||
$id = $items[$delta]->id ?? ''; |
||||
$name = $items[$delta]->name ?? ''; |
||||
$website = $items[$delta]->website ?? ''; |
||||
$wikipedia = $items[$delta]->wikipedia ?? ''; |
||||
|
||||
$wrapper_id = 'islandora-ror-details-' . $this->fieldDefinition->getName() . '-' . $delta; |
||||
|
||||
$element['search'] = [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('ROR identifier (search by name)'), |
||||
'#description' => $this->t('Start typing to search the Research Organization Registry (ROR). Selecting a result will populate the fields below.'), |
||||
'#default_value' => $id, |
||||
'#autocomplete_route_name' => 'islandora_ror.autocomplete', |
||||
'#ajax' => [ |
||||
'callback' => [$this, 'updateDetails'], |
||||
'event' => 'autocompleteclose', |
||||
'wrapper' => $wrapper_id, |
||||
], |
||||
'#attributes' => [ |
||||
'onclick' => 'this.value = "";', |
||||
], |
||||
]; |
||||
|
||||
$element['details'] = [ |
||||
'#type' => 'container', |
||||
'#attributes' => ['id' => $wrapper_id], |
||||
]; |
||||
|
||||
$element['details']['id'] = [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('ROR ID'), |
||||
'#default_value' => $id, |
||||
'#attributes' => ['readonly' => 'readonly'], |
||||
]; |
||||
|
||||
$element['details']['name'] = [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Name'), |
||||
'#default_value' => $name, |
||||
'#attributes' => ['readonly' => 'readonly'], |
||||
]; |
||||
|
||||
$element['details']['website'] = [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Website'), |
||||
'#default_value' => $website, |
||||
'#attributes' => ['readonly' => 'readonly'], |
||||
]; |
||||
|
||||
$element['details']['wikipedia'] = [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Wikipedia'), |
||||
'#default_value' => $wikipedia, |
||||
'#attributes' => ['readonly' => 'readonly'], |
||||
]; |
||||
|
||||
return $element; |
||||
} |
||||
|
||||
/** |
||||
* AJAX callback: update the details based on selected ROR ID. |
||||
*/ |
||||
public function updateDetails(array &$form, FormStateInterface $form_state): array { |
||||
$trigger = $form_state->getTriggeringElement(); |
||||
$id_value = $trigger['#value'] ?? ''; |
||||
|
||||
if (is_string($id_value) && $id_value !== '') { |
||||
try { |
||||
$record = $this->rorClient->get($id_value); |
||||
|
||||
if ($record) { |
||||
[$website, $wikipedia] = $this->rorClient->extractLinks($record); |
||||
|
||||
// Find details container. |
||||
$container_parents = $trigger['#array_parents']; |
||||
array_pop($container_parents); |
||||
$container_parents[] = 'details'; |
||||
|
||||
$details =& $form; |
||||
foreach ($container_parents as $parent) { |
||||
if (!isset($details[$parent])) { |
||||
break; |
||||
} |
||||
$details =& $details[$parent]; |
||||
} |
||||
|
||||
if (is_array($details)) { |
||||
$details['id']['#value'] = $record['id'] ?? $id_value; |
||||
$details['name']['#value'] = $this->rorClient->getName($record['names']) ?? ''; |
||||
$details['website']['#value'] = $website; |
||||
$details['wikipedia']['#value'] = $wikipedia; |
||||
} |
||||
} |
||||
} |
||||
catch (\Throwable $e) { |
||||
\Drupal::logger('islandora_ror')->error('ROR widget update failed: @message', [ |
||||
'@message' => $e->getMessage(), |
||||
]); |
||||
} |
||||
} |
||||
|
||||
return $this->getDetailsSubform($form, $form_state); |
||||
} |
||||
|
||||
/** |
||||
* Extract the container that needs updating. |
||||
*/ |
||||
protected function getDetailsSubform(array &$form, FormStateInterface $form_state): array { |
||||
$trigger = $form_state->getTriggeringElement(); |
||||
$container_parents = $trigger['#array_parents']; |
||||
array_pop($container_parents); |
||||
$container_parents[] = 'details'; |
||||
|
||||
$element =& $form; |
||||
foreach ($container_parents as $parent) { |
||||
if (!isset($element[$parent])) { |
||||
return []; |
||||
} |
||||
$element =& $element[$parent]; |
||||
} |
||||
return $element; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function massageFormValues(array $values, array $form, FormStateInterface $form_state): array { |
||||
foreach ($values as &$value) { |
||||
if (isset($value['details']) && is_array($value['details'])) { |
||||
$value['id'] = $value['details']['id'] ?? ''; |
||||
$value['name'] = $value['details']['name'] ?? ''; |
||||
$value['website'] = $value['details']['website'] ?? ''; |
||||
$value['wikipedia'] = $value['details']['wikipedia'] ?? ''; |
||||
} |
||||
} |
||||
return $values; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,231 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_ror\Service; |
||||
|
||||
use GuzzleHttp\ClientInterface; |
||||
use GuzzleHttp\Exception\GuzzleException; |
||||
use Psr\Log\LoggerInterface; |
||||
use Drupal\Core\Cache\CacheBackendInterface; |
||||
|
||||
/** |
||||
* Client for interacting with the Research Organization Registry (ROR) API. |
||||
* |
||||
* Provides caching, ID normalization, name extraction, and lookup utilities. |
||||
*/ |
||||
class RorClient { |
||||
|
||||
/** |
||||
* HTTP client used for API requests. |
||||
* |
||||
* @var \GuzzleHttp\ClientInterface |
||||
*/ |
||||
protected ClientInterface $httpClient; |
||||
|
||||
/** |
||||
* Logger service for reporting API or cache failures. |
||||
* |
||||
* @var \Psr\Log\LoggerInterface |
||||
*/ |
||||
protected LoggerInterface $logger; |
||||
|
||||
/** |
||||
* Cache backend to store API results. |
||||
* |
||||
* @var \Drupal\Core\Cache\CacheBackendInterface |
||||
*/ |
||||
protected CacheBackendInterface $cache; |
||||
|
||||
/** |
||||
* Base URL for the ROR API. |
||||
* |
||||
* @var string |
||||
*/ |
||||
protected string $baseUrl = 'https://api.ror.org/v2'; |
||||
|
||||
/** |
||||
* Constructs the ROR client. |
||||
* |
||||
* @param \GuzzleHttp\ClientInterface $http_client |
||||
* The HTTP client service. |
||||
* @param \Psr\Log\LoggerInterface $logger |
||||
* The logger for this service. |
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache |
||||
* The cache backend for storing API responses. |
||||
*/ |
||||
public function __construct( |
||||
ClientInterface $http_client, |
||||
LoggerInterface $logger, |
||||
CacheBackendInterface $cache |
||||
) { |
||||
$this->httpClient = $http_client; |
||||
$this->logger = $logger; |
||||
$this->cache = $cache; |
||||
} |
||||
|
||||
/** |
||||
* Searches the ROR API for organizations matching a query string. |
||||
* |
||||
* Results are cached for the specified TTL. |
||||
* |
||||
* @param string $query |
||||
* The search term (organization name). |
||||
* @param int $ttl |
||||
* Time-to-live in seconds for cached results. |
||||
* |
||||
* @return array |
||||
* A list of organization entries returned by the ROR API. |
||||
*/ |
||||
public function search(string $query, int $ttl = 21600): array { |
||||
if ($query === '') { |
||||
return []; |
||||
} |
||||
|
||||
$cid = 'ror:search:' . md5($query); |
||||
|
||||
if ($cache = $this->cache->get($cid)) { |
||||
return $cache->data; |
||||
} |
||||
|
||||
try { |
||||
$response = $this->httpClient->get($this->baseUrl . '/organizations', [ |
||||
'query' => ['query' => $query], |
||||
'timeout' => 5, |
||||
]); |
||||
|
||||
$data = json_decode((string) $response->getBody(), TRUE); |
||||
$items = $data['items'] ?? []; |
||||
|
||||
$this->cache->set($cid, $items, time() + $ttl); |
||||
|
||||
return $items; |
||||
} |
||||
catch (GuzzleException $e) { |
||||
$this->logger->error('ROR search failed: @msg', ['@msg' => $e->getMessage()]); |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Retrieves a single organization record by ROR ID. |
||||
* |
||||
* Accepts full URLs, domain-based IDs, or bare IDs. |
||||
* Uses caching to reduce API calls. |
||||
* |
||||
* @param string $id |
||||
* The ROR identifier (URL or slug). |
||||
* @param int $ttl |
||||
* Time-to-live for caching in seconds. |
||||
* |
||||
* @return array|null |
||||
* A ROR organization record or NULL if lookup fails. |
||||
*/ |
||||
public function get(string $id, int $ttl = 86400): ?array { |
||||
if ($id === '') { |
||||
return NULL; |
||||
} |
||||
|
||||
$id_part = $this->normalizeId($id); |
||||
$cid = 'ror:get:' . $id_part; |
||||
|
||||
if ($cache = $this->cache->get($cid)) { |
||||
return $cache->data; |
||||
} |
||||
|
||||
try { |
||||
$response = $this->httpClient->get($this->baseUrl . '/organizations/' . rawurlencode($id_part), [ |
||||
'timeout' => 5, |
||||
]); |
||||
|
||||
$data = json_decode((string) $response->getBody(), TRUE); |
||||
|
||||
if (is_array($data)) { |
||||
$this->cache->set($cid, $data, time() + $ttl); |
||||
return $data; |
||||
} |
||||
} |
||||
catch (GuzzleException $e) { |
||||
$this->logger->error('ROR get failed for @id: @msg', [ |
||||
'@id' => $id, |
||||
'@msg' => $e->getMessage(), |
||||
]); |
||||
} |
||||
|
||||
return NULL; |
||||
} |
||||
|
||||
/** |
||||
* Extracts website and Wikipedia URLs from a ROR record. |
||||
* |
||||
* @param array $record |
||||
* A single ROR organization record. |
||||
* |
||||
* @return array |
||||
* An array containing: |
||||
* - string $website |
||||
* - string $wikipedia |
||||
*/ |
||||
public function extractLinks(array $record): array { |
||||
$website = ''; |
||||
$wikipedia = ''; |
||||
|
||||
if (!empty($record['links'])) { |
||||
foreach ($record['links'] as $link) { |
||||
if ($link['type'] === 'website' && !$website) { |
||||
$website = $link['value']; |
||||
} |
||||
elseif ($link['type'] === 'wikipedia' && !$wikipedia) { |
||||
$wikipedia = $link['value']; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return [$website, $wikipedia]; |
||||
} |
||||
|
||||
/** |
||||
* Normalizes a ROR ID into its canonical slug form. |
||||
* |
||||
* Examples: |
||||
* - https://ror.org/02xh9x144 → 02xh9x144 |
||||
* - ror.org/02xh9x144 → 02xh9x144 |
||||
* |
||||
* @param string $id |
||||
* A ROR ID in any known format. |
||||
* |
||||
* @return string |
||||
* The normalized ROR ID. |
||||
*/ |
||||
protected function normalizeId(string $id): string { |
||||
$id = trim($id); |
||||
|
||||
if (str_starts_with($id, 'http://') || str_starts_with($id, 'https://')) { |
||||
$parts = parse_url($id); |
||||
return ltrim($parts['path'] ?? '', '/'); |
||||
} |
||||
|
||||
if (str_starts_with($id, 'ror.org/')) { |
||||
return substr($id, strlen('ror.org/')); |
||||
} |
||||
|
||||
return $id; |
||||
} |
||||
|
||||
/** |
||||
* Extracts the display name from a ROR “names” array. |
||||
* |
||||
* @param array $names |
||||
* The `names` array from a ROR record. |
||||
* |
||||
* @return string|null |
||||
* The preferred organization name if found, or NULL otherwise. |
||||
*/ |
||||
public function getName(array $names): ?string { |
||||
foreach ($names as $name) { |
||||
if (in_array('ror_display', $name['types'] ?? [])) { |
||||
return $name['value'] ?? NULL; |
||||
} |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue