LinkCollection.php

Same filename in other branches
  1. 9 core/modules/jsonapi/src/JsonApiResource/LinkCollection.php
  2. 8.9.x core/modules/jsonapi/src/JsonApiResource/LinkCollection.php
  3. 11.x core/modules/jsonapi/src/JsonApiResource/LinkCollection.php

Namespace

Drupal\jsonapi\JsonApiResource

File

core/modules/jsonapi/src/JsonApiResource/LinkCollection.php

View source
<?php

namespace Drupal\jsonapi\JsonApiResource;

use Drupal\Component\Assertion\Inspector;

/**
 * Contains a set of JSON:API Link objects.
 *
 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
 *   may change at any time and could break any dependencies on it.
 *
 * @see https://www.drupal.org/project/drupal/issues/3032787
 * @see jsonapi.api.php
 */
final class LinkCollection implements \IteratorAggregate {
    
    /**
     * The links in the collection, keyed by unique strings.
     *
     * @var \Drupal\jsonapi\JsonApiResource\Link[]
     */
    protected $links;
    
    /**
     * The link context.
     *
     * All links objects exist within a context object. Links form a relationship
     * between a source IRI and target IRI. A context is the link's source.
     *
     * @var \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship
     *
     * @see https://tools.ietf.org/html/rfc8288#section-3.2
     */
    protected $context;
    
    /**
     * LinkCollection constructor.
     *
     * @param \Drupal\jsonapi\JsonApiResource\Link[] $links
     *   An associated array of key names and JSON:API Link objects.
     * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship $context
     *   (internal use only) The context object. Use the self::withContext()
     *   method to establish a context. This should be done automatically when
     *   a LinkCollection is passed into a context object.
     */
    public function __construct(array $links, $context = NULL) {
        assert(Inspector::assertAll(function ($key) {
            return static::validKey($key);
        }, array_keys($links)));
        assert(Inspector::assertAll(function ($link) {
            return $link instanceof Link || is_array($link) && Inspector::assertAllObjects($link, Link::class);
        }, $links));
        assert(is_null($context) || Inspector::assertAllObjects([
            $context,
        ], JsonApiDocumentTopLevel::class, ResourceObject::class, Relationship::class));
        ksort($links);
        $this->links = array_map(function ($link) {
            return is_array($link) ? $link : [
                $link,
            ];
        }, $links);
        $this->context = $context;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getIterator() {
        assert(!is_null($this->context), 'A LinkCollection is invalid unless a context has been established.');
        return new \ArrayIterator($this->links);
    }
    
    /**
     * Gets a new LinkCollection with the given link inserted.
     *
     * @param string $key
     *   A key for the link. If the key already exists and the link shares an
     *   href, link relation type and attributes with an existing link with that
     *   key, those links will be merged together.
     * @param \Drupal\jsonapi\JsonApiResource\Link $new_link
     *   The link to insert.
     *
     * @return static
     *   A new LinkCollection with the given link inserted or merged with the
     *   current set of links.
     */
    public function withLink($key, Link $new_link) {
        assert(static::validKey($key));
        $merged = $this->links;
        if (isset($merged[$key])) {
            foreach ($merged[$key] as $index => $existing_link) {
                if (Link::compare($existing_link, $new_link) === 0) {
                    $merged[$key][$index] = Link::merge($existing_link, $new_link);
                    return new static($merged, $this->context);
                }
            }
        }
        $merged[$key][] = $new_link;
        return new static($merged, $this->context);
    }
    
    /**
     * Whether a link with the given key exists.
     *
     * @param string $key
     *   The key.
     *
     * @return bool
     *   TRUE if a link with the given key exist, FALSE otherwise.
     */
    public function hasLinkWithKey($key) {
        return array_key_exists($key, $this->links);
    }
    
    /**
     * Establishes a new context for a LinkCollection.
     *
     * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship $context
     *   The new context object.
     *
     * @return static
     *   A new LinkCollection with the given context.
     */
    public function withContext($context) {
        return new static($this->links, $context);
    }
    
    /**
     * Gets the LinkCollection's context object.
     *
     * @return \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship
     *   The LinkCollection's context.
     */
    public function getContext() {
        assert(!is_null($this->context), 'A LinkCollection is invalid unless a context has been established.');
        return $this->context;
    }
    
    /**
     * Filters a LinkCollection using the provided callback.
     *
     * @param callable $f
     *   The filter callback. The callback has the signature below.
     *
     * @code
     *   boolean callback(string $key, \Drupal\jsonapi\JsonApiResource\Link $link, mixed $context))
     * @endcode
     *
     * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
     *   A new, filtered LinkCollection.
     */
    public function filter(callable $f) {
        $links = iterator_to_array($this);
        $filtered = array_reduce(array_keys($links), function ($filtered, $key) use ($links, $f) {
            if ($f($key, $links[$key], $this->context)) {
                $filtered[$key] = $links[$key];
            }
            return $filtered;
        }, []);
        return new LinkCollection($filtered, $this->context);
    }
    
    /**
     * Merges two LinkCollections.
     *
     * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $a
     *   The first link collection.
     * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $b
     *   The second link collection.
     *
     * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
     *   A new LinkCollection with the links of both inputs.
     */
    public static function merge(LinkCollection $a, LinkCollection $b) {
        assert($a->getContext() === $b->getContext());
        $merged = new LinkCollection([], $a->getContext());
        foreach ($a as $key => $links) {
            $merged = array_reduce($links, function (self $merged, Link $link) use ($key) {
                return $merged->withLink($key, $link);
            }, $merged);
        }
        foreach ($b as $key => $links) {
            $merged = array_reduce($links, function (self $merged, Link $link) use ($key) {
                return $merged->withLink($key, $link);
            }, $merged);
        }
        return $merged;
    }
    
    /**
     * Ensures that a link key is valid.
     *
     * @param string $key
     *   A key name.
     *
     * @return bool
     *   TRUE if the key is valid, FALSE otherwise.
     */
    protected static function validKey($key) {
        return is_string($key) && !is_numeric($key) && !str_contains($key, ':');
    }

}

Classes

Title Deprecated Summary
LinkCollection Contains a set of JSON:API Link objects.

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