class AnnounceFetcher

Same name in other branches
  1. 11.x core/modules/announcements_feed/src/AnnounceFetcher.php \Drupal\announcements_feed\AnnounceFetcher

Service to fetch announcements from the external feed.

@internal

Hierarchy

Expanded class hierarchy of AnnounceFetcher

1 file declares its use of AnnounceFetcher
AnnounceFetcherUnitTest.php in core/modules/announcements_feed/tests/src/Unit/AnnounceFetcherUnitTest.php
1 string reference to 'AnnounceFetcher'
announcements_feed.services.yml in core/modules/announcements_feed/announcements_feed.services.yml
core/modules/announcements_feed/announcements_feed.services.yml
1 service uses AnnounceFetcher
announcements_feed.fetcher in core/modules/announcements_feed/announcements_feed.services.yml
Drupal\announcements_feed\AnnounceFetcher

File

core/modules/announcements_feed/src/AnnounceFetcher.php, line 22

Namespace

Drupal\announcements_feed
View source
final class AnnounceFetcher {
    
    /**
     * The configuration settings of this module.
     *
     * @var \Drupal\Core\Config\ImmutableConfig
     */
    protected ImmutableConfig $config;
    
    /**
     * The tempstore service.
     *
     * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactory
     */
    protected KeyValueStoreInterface $tempStore;
    
    /**
     * Construct an AnnounceFetcher service.
     *
     * @param \GuzzleHttp\ClientInterface $httpClient
     *   The http client.
     * @param \Drupal\Core\Config\ConfigFactoryInterface $config
     *   The config factory service.
     * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $temp_store
     *   The tempstore factory service.
     * @param \Psr\Log\LoggerInterface $logger
     *   The logger service.
     * @param string $feedUrl
     *   The feed url path.
     */
    public function __construct(ClientInterface $httpClient, ConfigFactoryInterface $config, KeyValueExpirableFactoryInterface $temp_store, LoggerInterface $logger, string $feedUrl) {
        $this->config = $config->get('announcements_feed.settings');
        $this->tempStore = $temp_store->get('announcements_feed');
    }
    
    /**
     * Fetch ids of announcements.
     *
     * @return array
     *   An array with ids of all announcements in the feed.
     */
    public function fetchIds() : array {
        return array_column($this->fetch(), 'id');
    }
    
    /**
     * Check whether the version given is relevant to the Drupal version used.
     *
     * @param string $version
     *   Version to check.
     *
     * @return bool
     *   Return True if the version matches Drupal version.
     */
    protected static function isRelevantItem(string $version) : bool {
        return !empty($version) && Semver::satisfies(\Drupal::VERSION, $version);
    }
    
    /**
     * Check whether a link is controlled by D.O.
     *
     * @param string $url
     *   URL to check.
     *
     * @return bool
     *   Return True if the URL is controlled by the D.O.
     */
    public static function validateUrl(string $url) : bool {
        if (empty($url)) {
            return FALSE;
        }
        $host = parse_url($url, PHP_URL_HOST);
        // First character can only be a letter or a digit.
        // @see https://www.rfc-editor.org/rfc/rfc1123#page-13
        return $host && preg_match('/^([a-zA-Z0-9][a-zA-Z0-9\\-_]*\\.)?drupal\\.org$/', $host);
    }
    
    /**
     * Fetches the feed either from a local cache or fresh remotely.
     *
     * The feed follows the "JSON Feed" format:
     * - https://www.jsonfeed.org/version/1.1/
     *
     * The structure of an announcement item in the feed is:
     *   - id: Id.
     *   - title: Title of the announcement.
     *   - content_html: Announcement teaser.
     *   - url: URL
     *   - date_modified: Last updated timestamp.
     *   - date_published: Created timestamp.
     *   - _drupalorg.featured: 1 if featured, 0 if not featured.
     *   - _drupalorg.version: Target version of Drupal, as a Composer version.
     *
     * @param bool $force
     *   (optional) Whether to always fetch new items or not. Defaults to FALSE.
     *
     * @return \Drupal\announcements_feed\Announcement[]
     *   An array of announcements from the feed relevant to the Drupal version.
     *   The array is empty if there were no matching announcements. If an error
     *   occurred while fetching/decoding the feed, it is thrown as an exception.
     *
     * @throws \Exception
     */
    public function fetch(bool $force = FALSE) : array {
        $announcements = $this->tempStore
            ->get('announcements');
        if ($force || $announcements === NULL) {
            try {
                $feed_content = (string) $this->httpClient
                    ->get($this->feedUrl)
                    ->getBody();
            } catch (\Exception $e) {
                $this->logger
                    ->error(Error::DEFAULT_ERROR_MESSAGE, Error::decodeException($e));
                throw $e;
            }
            $announcements = Json::decode($feed_content);
            if (!isset($announcements['items'])) {
                $this->logger
                    ->error('The feed format is not valid.');
                throw new \Exception('Invalid format');
            }
            $announcements = $announcements['items'] ?? [];
            // Ensure that announcements reference drupal.org and are applicable to
            // the current Drupal version.
            $announcements = array_filter($announcements, function (array $announcement) {
                return static::validateUrl($announcement['url'] ?? '') && static::isRelevantItem($announcement['_drupalorg']['version'] ?? '');
            });
            // Save the raw decoded and filtered array to temp store.
            $this->tempStore
                ->setWithExpire('announcements', $announcements, $this->config
                ->get('max_age'));
        }
        // The drupal.org endpoint is sorted by created date in descending order.
        // We will limit the announcements based on the configuration limit.
        $announcements = array_slice($announcements, 0, $this->config
            ->get('limit') ?? 10);
        // For the remaining announcements, put all the featured announcements
        // before the rest.
        uasort($announcements, function ($a, $b) {
            $a_value = (int) $a['_drupalorg']['featured'];
            $b_value = (int) $b['_drupalorg']['featured'];
            if ($a_value == $b_value) {
                return 0;
            }
            return $a_value < $b_value ? -1 : 1;
        });
        // Map the multidimensional array into an array of Announcement objects.
        $announcements = array_map(function ($announcement) {
            return new Announcement($announcement['id'], $announcement['title'], $announcement['url'], $announcement['date_modified'], $announcement['date_published'], $announcement['content_html'], $announcement['_drupalorg']['version'], (bool) $announcement['_drupalorg']['featured']);
        }, $announcements);
        return $announcements;
    }

}

Members

Title Sort descending Modifiers Object type Summary
AnnounceFetcher::$config protected property The configuration settings of this module.
AnnounceFetcher::$tempStore protected property The tempstore service.
AnnounceFetcher::fetch public function Fetches the feed either from a local cache or fresh remotely.
AnnounceFetcher::fetchIds public function Fetch ids of announcements.
AnnounceFetcher::isRelevantItem protected static function Check whether the version given is relevant to the Drupal version used.
AnnounceFetcher::validateUrl public static function Check whether a link is controlled by D.O.
AnnounceFetcher::__construct public function Construct an AnnounceFetcher service.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.