external_entity-1.0.x-dev/src/Plugin/ExternalEntity/ConnectionType/ExternalEntityServer.php
src/Plugin/ExternalEntity/ConnectionType/ExternalEntityServer.php
<?php
declare(strict_types=1);
namespace Drupal\external_entity\Plugin\ExternalEntity\ConnectionType;
use GuzzleHttp\TransferStats;
use Drupal\Core\Utility\Error;
use Drupal\Core\Http\ClientFactory;
use Drupal\Core\Form\FormStateInterface;
use GuzzleHttp\Exception\GuzzleException;
use Drupal\external_entity\AjaxFormTrait;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Component\Utility\DeprecationHelper;
use Drupal\external_entity\ExternalEntityResource;
use Drupal\external_entity\ExternalEntityDefinition;
use Drupal\external_entity\Entity\Query\SearchQuery;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\external_entity\Annotation\ExternalEntityConnectionType;
use Drupal\external_entity\Definition\ExternalEntitySearchDefinition;
use Drupal\external_entity\Definition\ExternalEntityDefaultDefinition;
use Drupal\external_entity\Definition\ExternalEntityResourceDefinition;
use Drupal\external_entity\Contracts\ExternalEntityConnectionAwareInterface;
use Drupal\external_entity\Contracts\ExternalEntityAuthenticationTypeInterface;
use Drupal\external_entity\Plugin\ExternalEntity\ExternalEntityConnectionTypeBase;
use Drupal\external_entity\Plugin\ExternalEntity\ExternalEntityConnectionAwareTrait;
/**
* @ExternalEntityConnectionType(
* id = "external_entity_server",
* label = @Translation("External Entity Server")
* )
*/
class ExternalEntityServer extends ExternalEntityConnectionTypeBase implements ExternalEntityConnectionAwareInterface {
use AjaxFormTrait;
use ExternalEntityConnectionAwareTrait;
/**
* Define the external server's successful status.
*/
public const string EXTERNAL_ENTITY_SERVER_OK = 'connected';
/**
* @var \Drupal\Core\Http\ClientFactory
*/
protected ClientFactory $httpClientFactory;
/**
* The external entity server constructor.
*
* @param array $configuration
* An array of plugin configurations.
* @param string $plugin_id
* The plugin identifier.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Http\ClientFactory $http_client_factory
* The HTTP client factory.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
ClientFactory $http_client_factory,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->httpClientFactory = $http_client_factory;
}
/**
* {@inheritDoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('http_client_factory')
);
}
/**
* {@inheritDoc}
*/
public function defaultConfiguration(): array {
return [
'resources' => [],
'server_domain' => NULL,
];
}
/**
* {@inheritDoc}
*/
public function buildConfigurationForm(
array $form,
FormStateInterface $form_state,
): array {
$form['#prefix'] = '<div id="external-entity-server-connection-type">';
$form['#suffix'] = '</div>';
$server_domain = $this->getFormStateValue(
'server_domain',
$form_state,
$this->getServerDomain()
);
$form['server_domain'] = [
'#type' => 'url',
'#title' => $this->t('Server Domain'),
'#required' => TRUE,
'#description' => $this->t('Input the external entity server domain.'),
'#default_value' => $server_domain,
'#depth' => 1,
'#ajax' => [
'method' => 'replaceWith',
'event' => 'change',
'wrapper' => 'external-entity-server-connection-type',
'callback' => [$this, 'ajaxFormDepthCallback'],
],
];
if (!empty($server_domain)) {
$this->setConfiguration(['server_domain' => $server_domain]);
if ($this->isConnectionValid()) {
$form['resources'] = [
'#type' => 'select',
'#title' => $this->t('Server Resources'),
'#required' => TRUE,
'#description' => $this->t(
'Select all resources that can be used when looking up definitions.'
),
'#multiple' => TRUE,
'#options' => $this->getResourceOptions(),
'#default_value' => $this->getResources(),
];
}
else {
$this->messenger()->addError($this->t('Unable to connect to @domain.', [
'@domain' => $server_domain,
]));
}
}
return $form;
}
/**
* {@inheritDoc}
*/
public function validateConfigurationForm(
array &$form,
FormStateInterface $form_state,
): void {
if ($form_state->hasValue('server_domain') && !$this->isConnectionValid()) {
$form_state->setError(
$form['server_domain'],
'Unable to connect to the external entity server.'
);
}
}
/**
* {@inheritDoc}
*/
public function fetchResourceDefinitions(): array {
$resources = [];
$results = array_intersect_key(
$this->fetchAllResources(),
$this->getResources()
);
foreach ($results as $name => $value) {
$resources[$name] = new ExternalEntityResourceDefinition(
$value
);
}
return $resources;
}
/**
* {@inheritDoc}
*/
public function lookupDefinitions(
array $ids = [],
array $options = [],
): array {
$entities = [];
$results = $this->lookupDefinitionsFromResources($ids);
foreach ($results as $uuid => $values) {
$entities[$uuid] = new ExternalEntityDefaultDefinition($values);
}
return $entities;
}
/**
* {@inheritDoc}
*/
public function searchDefinitions(
string $resource,
SearchQuery $query,
): ?ExternalEntitySearchDefinition {
if (!isset($resource)) {
return NULL;
}
$response = $this->makeHttpRequest(
'GET',
"definition/{$resource}/search",
$query->format()
);
if (!is_array($response)) {
return NULL;
}
return new ExternalEntitySearchDefinition($response);
}
/**
* Fetch all external entity resources.
*
* @return array
* An array of external entity resources.
*/
protected function fetchAllResources(): array {
return $this->makeHttpRequest(
'GET',
'resource'
) ?? [];
}
/**
* Get the external entity server status info.
*
* @return array
* An array of server status.
*/
protected function getServerInfo(): array {
return $this->makeHttpRequest(
'GET',
'status'
) ?? ['status' => 'error'];
}
/**
* Get selected external entity resources.
*
* @return array
* An array of external entity resources.
*/
protected function getResources(): array {
return $this->getConfiguration()['resources'] ?: [];
}
/**
* Get the external entity server domain URL.
*
* @return string|null
*/
protected function getServerDomain(): ?string {
return $this->getConfiguration()['server_domain'] ?? NULL;
}
/**
* Check is external entity server connection is valid.
*
* @return bool
* Return TRUE if the server connection is valid; otherwise FALSE.
*/
protected function isConnectionValid(): bool {
return ($this->getServerInfo()['status'] ?? NULL) === static::EXTERNAL_ENTITY_SERVER_OK;
}
/**
* Get external entity resource options.
*
* @return array
* An array of resource options.
*/
protected function getResourceOptions(): array {
return array_column(
$this->fetchAllResources(),
'type',
'type'
);
}
/**
* Lookup definition IDs within all resources.
*
* @param array $ids
* An array definition ids to look up.
*
* @return array
* An array of the HTTP request contents.
*/
protected function lookupDefinitionsFromResources(
array $ids = [],
): array {
$resources = [];
$lookup_id_count = count($ids);
foreach ($this->getResources() as $resource) {
$contents = $this->makeHttpRequest(
'GET',
"definition/{$resource}/lookup",
['entity_uuids' => $ids]
);
if (!empty($contents)) {
$resources += $contents;
// No need to make additional HTTP requests if we have retrieved all our
// resources based on the given ids.
if ($lookup_id_count === count($resources)) {
break;
}
}
}
return $resources;
}
/**
* Make an HTTP request to a URI.
*
* @param string $method
* The request method.
* @param string $uri
* The request URI.
* @param array $query
* The request query parameters.
*
* @return mixed
* Return the response contents, if the content type is application/json,
* then an array is provided.
*/
protected function makeHttpRequest(
string $method,
string $uri,
array $query = []
): mixed {
try {
$client = $this->httpClientFactory->fromOptions([
'base_uri' => "{$this->getServerDomain()}/external-entity/",
]);
$response = $client->request($method, $uri, [
'query' => $query,
] + $this->httpRequestOptions());
if ($response->getStatusCode() === Response::HTTP_OK) {
$contents = $response->getBody()->getContents();
if (
is_string($contents)
&& 'application/json' === $response->getHeaderLine('Content-Type')
) {
return json_decode($contents, TRUE, 512, JSON_THROW_ON_ERROR);
}
return $contents;
}
} catch (\Exception|GuzzleException $exception) {
DeprecationHelper::backwardsCompatibleCall(
\Drupal::VERSION,
'10.1.0',
static fn() => Error::logException(\Drupal::logger('external_entity'), $exception),
static fn() => watchdog_exception('external_entity', $exception)
);
}
return NULL;
}
/**
* Get the HTTP request options.
*
* @return array
* An array of HTTP request options.
*/
protected function httpRequestOptions(): array {
$options = [
'on_stats' => function (TransferStats $stats) {
$stats->getEffectiveUri();
},
];
if ($authentication = $this->getConnectionAuthenticationType()) {
$authentication->alterRequestOptions(
$options
);
}
return $options;
}
/**
* Get the connection authentication type instance.
*
* @return \Drupal\external_entity\Contracts\ExternalEntityAuthenticationTypeInterface|null
* Return the connection authentication type; otherwise NULL.
*/
protected function getConnectionAuthenticationType(): ?ExternalEntityAuthenticationTypeInterface {
if ($connection = $this->getConnection()) {
return $connection->createAuthenticationTypeInstance();
}
return NULL;
}
}
