You are here

class SpecFetcher in Apigee API Catalog 8

Same name and namespace in other branches
  1. 8.2 src/SpecFetcher.php \Drupal\apigee_api_catalog\SpecFetcher

Class SpecFetcher.

Hierarchy

Expanded class hierarchy of SpecFetcher

1 string reference to 'SpecFetcher'
apigee_api_catalog.services.yml in ./apigee_api_catalog.services.yml
apigee_api_catalog.services.yml
1 service uses SpecFetcher
apigee_api_catalog.spec_fetcher in ./apigee_api_catalog.services.yml
Drupal\apigee_api_catalog\SpecFetcher

File

src/SpecFetcher.php, line 41

Namespace

Drupal\apigee_api_catalog
View source
class SpecFetcher implements SpecFetcherInterface {
  use StringTranslationTrait;
  use MessengerTrait;

  /**
   * Drupal\Core\File\FileSystemInterface definition.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * GuzzleHttp\ClientInterface definition.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private $logger;

  /**
   * Constructs a new SpecFetcher.
   *
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file_system service.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The http_client service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
   *   The string translation.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   */
  public function __construct(FileSystemInterface $file_system, ClientInterface $http_client, EntityTypeManagerInterface $entityTypeManager, TranslationInterface $translation, MessengerInterface $messenger, LoggerInterface $logger) {
    $this->fileSystem = $file_system;
    $this->httpClient = $http_client;
    $this->entityTypeManager = $entityTypeManager;
    $this->stringTranslation = $translation;
    $this->messenger = $messenger;
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public function fetchSpec(ApiDocInterface $apidoc) : string {
    $spec_value = $apidoc
      ->get('spec')
      ->isEmpty() ? [] : $apidoc
      ->get('spec')
      ->getValue()[0];

    // If "spec_file_source" uses URL, grab file from "file_link" and save it
    // into the "spec" file field. The file_link field should already have
    // validated that a valid file exists at that URL.
    if ($apidoc
      ->get('spec_file_source')->value === ApiDocInterface::SPEC_AS_URL) {

      // If the file_link field is empty, return without changes.
      // TODO: The file link shouldn't be empty. Consider throwing an error.
      if ($apidoc
        ->get('file_link')
        ->isEmpty()) {
        return FALSE;
      }
      $source_uri = $apidoc
        ->get('file_link')
        ->getValue()[0]['uri'];
      $source_uri = Url::fromUri($source_uri, [
        'absolute' => TRUE,
      ])
        ->toString();
      $request = new Request('GET', $source_uri);
      $options = [
        'exceptions' => TRUE,
        'allow_redirects' => [
          'strict' => TRUE,
        ],
      ];

      // Generate conditional GET header.
      if (!$apidoc
        ->get('fetched_timestamp')
        ->isEmpty()) {
        $request = $request
          ->withAddedHeader('If-Modified-Since', gmdate(DateTimePlus::RFC7231, $apidoc
          ->get('fetched_timestamp')->value));
      }
      try {
        $response = $this->httpClient
          ->send($request, $options);
      } catch (GuzzleException $e) {
        $this
          ->log(LogLevel::ERROR, 'API Doc %label: Could not retrieve OpenAPI specification file located at %url.', [
          '%url' => $source_uri,
          '%label' => $apidoc
            ->label(),
        ]);
        return self::STATUS_ERROR;
      }

      // In case of a 304 Not Modified there are no changes, but update
      // last fetched timestamp.
      if ($response
        ->getStatusCode() === 304) {
        $apidoc
          ->set('fetched_timestamp', time());
        return self::STATUS_UNCHANGED;
      }
      $data = (string) $response
        ->getBody();
      if (($file_size = $response
        ->getBody()
        ->getSize()) && $file_size < 1) {
        $this
          ->log(LogLevel::ERROR, 'API Doc %label: OpenAPI specification file located at %url is empty.', [
          '%url' => $source_uri,
          '%label' => $apidoc
            ->label(),
        ]);
        return self::STATUS_ERROR;
      }

      // Only save file if it hasn't been fetched previously.
      $data_md5 = md5($data);
      $prev_md5 = $apidoc
        ->get('spec_md5')
        ->isEmpty() ? NULL : $apidoc
        ->get('spec_md5')->value;
      if ($prev_md5 != $data_md5) {
        $filename = $this->fileSystem
          ->basename($source_uri);
        $specs_definition = $apidoc
          ->getFieldDefinition('spec')
          ->getItemDefinition();
        $target_dir = $specs_definition
          ->getSetting('file_directory');
        $uri_scheme = $specs_definition
          ->getSetting('uri_scheme');
        $destination = "{$uri_scheme}://{$target_dir}/";
        try {
          $this
            ->checkRequirements($destination);
          $file = file_save_data($data, $destination . $filename, FileSystemInterface::EXISTS_RENAME);
          if (empty($file)) {
            throw new \Exception('Could not save API Doc specification file.');
          }
        } catch (\Exception $e) {
          $this
            ->log(LogLevel::ERROR, 'Error while saving API Doc spec file from URL on API Doc ID: %id. Error: %error', [
            '%id' => $apidoc
              ->id(),
            '%error' => $e
              ->getMessage(),
          ]);
          return self::STATUS_ERROR;
        }
        $spec_value = [
          'target_id' => $file
            ->id(),
        ] + $spec_value;
        $apidoc
          ->set('spec', $spec_value);
        $apidoc
          ->set('spec_md5', $data_md5);
        $apidoc
          ->set('fetched_timestamp', time());
        return self::STATUS_UPDATED;
      }
    }
    return self::STATUS_ERROR;
  }

  /**
   * Log a message, and optionally display it on the UI.
   *
   * @param string $level
   *   The Error level.
   * @param string $message
   *   The message.
   * @param array $params
   *   Optional parameters array.
   */
  private function log(string $level, string $message, array $params = []) {
    $this->logger
      ->log($level, $message, $params);

    // Show the message.
    $this
      ->messenger()
      ->addMessage($this
      ->t($message, $params), static::LOG_LEVEL_MAP[$level] ?? MessengerInterface::TYPE_ERROR);
  }

  /**
   * Checks requirements for saving of a file spec.
   *
   * If a requirement is not fulfilled it throws an exception.
   *
   * @param string $destination
   *   The specification file destination directory, including scheme.
   *
   * @throws \Exception
   */
  private function checkRequirements(string $destination) : void {

    // If using private filesystem, check that it's been configured.
    if (strpos($destination, 'private://') === 0 && !$this
      ->isPrivateFileSystemConfigured()) {
      throw new \Exception('Private filesystem has not been configured.');
    }
    if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
      throw new \Exception('Could not prepare API Doc specification file destination directory.');
    }
  }

  /**
   * Checks whether the private filesystem is configured.
   *
   * @return bool
   *   True if configured, FALSE otherwise.
   */
  private function isPrivateFileSystemConfigured() : bool {
    return (bool) $this->fileSystem
      ->realpath('private://');
  }

}

Members

Namesort descending Modifiers Type Description Overrides
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
SpecFetcher::$entityTypeManager protected property The entity type manager.
SpecFetcher::$fileSystem protected property Drupal\Core\File\FileSystemInterface definition.
SpecFetcher::$httpClient protected property GuzzleHttp\ClientInterface definition.
SpecFetcher::$logger private property The logger.
SpecFetcher::checkRequirements private function Checks requirements for saving of a file spec.
SpecFetcher::fetchSpec public function Fetch OpenAPI specification file from URL. Overrides SpecFetcherInterface::fetchSpec
SpecFetcher::isPrivateFileSystemConfigured private function Checks whether the private filesystem is configured.
SpecFetcher::log private function Log a message, and optionally display it on the UI.
SpecFetcher::__construct public function Constructs a new SpecFetcher.
SpecFetcherInterface::LOG_LEVEL_MAP public constant
SpecFetcherInterface::STATUS_ERROR public constant The status when an error happened during the fetch operation.
SpecFetcherInterface::STATUS_UNCHANGED public constant The status when a spec update finds the remote file unchanged.
SpecFetcherInterface::STATUS_UPDATED public constant The status when a spec update completed successfully.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.