Chris@18
|
1 <?php
|
Chris@18
|
2
|
Chris@18
|
3 namespace Drupal\jsonapi\JsonApiResource;
|
Chris@18
|
4
|
Chris@18
|
5 use Drupal\Component\Assertion\Inspector;
|
Chris@18
|
6
|
Chris@18
|
7 /**
|
Chris@18
|
8 * Contains a set of JSON:API Link objects.
|
Chris@18
|
9 *
|
Chris@18
|
10 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
|
Chris@18
|
11 * may change at any time and could break any dependencies on it.
|
Chris@18
|
12 *
|
Chris@18
|
13 * @see https://www.drupal.org/project/jsonapi/issues/3032787
|
Chris@18
|
14 * @see jsonapi.api.php
|
Chris@18
|
15 */
|
Chris@18
|
16 final class LinkCollection implements \IteratorAggregate {
|
Chris@18
|
17
|
Chris@18
|
18 /**
|
Chris@18
|
19 * The links in the collection, keyed by unique strings.
|
Chris@18
|
20 *
|
Chris@18
|
21 * @var \Drupal\jsonapi\JsonApiResource\Link[]
|
Chris@18
|
22 */
|
Chris@18
|
23 protected $links;
|
Chris@18
|
24
|
Chris@18
|
25 /**
|
Chris@18
|
26 * The link context.
|
Chris@18
|
27 *
|
Chris@18
|
28 * All links objects exist within a context object. Links form a relationship
|
Chris@18
|
29 * between a source IRI and target IRI. A context is the link's source.
|
Chris@18
|
30 *
|
Chris@18
|
31 * @var \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject
|
Chris@18
|
32 *
|
Chris@18
|
33 * @see https://tools.ietf.org/html/rfc8288#section-3.2
|
Chris@18
|
34 */
|
Chris@18
|
35 protected $context;
|
Chris@18
|
36
|
Chris@18
|
37 /**
|
Chris@18
|
38 * LinkCollection constructor.
|
Chris@18
|
39 *
|
Chris@18
|
40 * @param \Drupal\jsonapi\JsonApiResource\Link[] $links
|
Chris@18
|
41 * An associated array of key names and JSON:API Link objects.
|
Chris@18
|
42 * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject $context
|
Chris@18
|
43 * (internal use only) The context object. Use the self::withContext()
|
Chris@18
|
44 * method to establish a context. This should be done automatically when
|
Chris@18
|
45 * a LinkCollection is passed into a context object.
|
Chris@18
|
46 */
|
Chris@18
|
47 public function __construct(array $links, $context = NULL) {
|
Chris@18
|
48 assert(Inspector::assertAll(function ($key) {
|
Chris@18
|
49 return static::validKey($key);
|
Chris@18
|
50 }, array_keys($links)));
|
Chris@18
|
51 assert(Inspector::assertAll(function ($link) {
|
Chris@18
|
52 return $link instanceof Link || is_array($link) && Inspector::assertAllObjects($link, Link::class);
|
Chris@18
|
53 }, $links));
|
Chris@18
|
54 assert(is_null($context) || Inspector::assertAllObjects([$context], JsonApiDocumentTopLevel::class, ResourceObject::class));
|
Chris@18
|
55 ksort($links);
|
Chris@18
|
56 $this->links = array_map(function ($link) {
|
Chris@18
|
57 return is_array($link) ? $link : [$link];
|
Chris@18
|
58 }, $links);
|
Chris@18
|
59 $this->context = $context;
|
Chris@18
|
60 }
|
Chris@18
|
61
|
Chris@18
|
62 /**
|
Chris@18
|
63 * {@inheritdoc}
|
Chris@18
|
64 */
|
Chris@18
|
65 public function getIterator() {
|
Chris@18
|
66 assert(!is_null($this->context), 'A LinkCollection is invalid unless a context has been established.');
|
Chris@18
|
67 return new \ArrayIterator($this->links);
|
Chris@18
|
68 }
|
Chris@18
|
69
|
Chris@18
|
70 /**
|
Chris@18
|
71 * Gets a new LinkCollection with the given link inserted.
|
Chris@18
|
72 *
|
Chris@18
|
73 * @param string $key
|
Chris@18
|
74 * A key for the link. If the key already exists and the link shares an href
|
Chris@18
|
75 * with an existing link with that key, those links will be merged together.
|
Chris@18
|
76 * @param \Drupal\jsonapi\JsonApiResource\Link $new_link
|
Chris@18
|
77 * The link to insert.
|
Chris@18
|
78 *
|
Chris@18
|
79 * @return static
|
Chris@18
|
80 * A new LinkCollection with the given link inserted or merged with the
|
Chris@18
|
81 * current set of links.
|
Chris@18
|
82 */
|
Chris@18
|
83 public function withLink($key, Link $new_link) {
|
Chris@18
|
84 assert(static::validKey($key));
|
Chris@18
|
85 $merged = $this->links;
|
Chris@18
|
86 if (isset($merged[$key])) {
|
Chris@18
|
87 foreach ($merged[$key] as $index => $existing_link) {
|
Chris@18
|
88 if (Link::compare($existing_link, $new_link) === 0) {
|
Chris@18
|
89 $merged[$key][$index] = Link::merge($existing_link, $new_link);
|
Chris@18
|
90 return new static($merged, $this->context);
|
Chris@18
|
91 }
|
Chris@18
|
92 }
|
Chris@18
|
93 }
|
Chris@18
|
94 $merged[$key][] = $new_link;
|
Chris@18
|
95 return new static($merged, $this->context);
|
Chris@18
|
96 }
|
Chris@18
|
97
|
Chris@18
|
98 /**
|
Chris@18
|
99 * Whether a link with the given key exists.
|
Chris@18
|
100 *
|
Chris@18
|
101 * @param string $key
|
Chris@18
|
102 * The key.
|
Chris@18
|
103 *
|
Chris@18
|
104 * @return bool
|
Chris@18
|
105 * TRUE if a link with the given key exist, FALSE otherwise.
|
Chris@18
|
106 */
|
Chris@18
|
107 public function hasLinkWithKey($key) {
|
Chris@18
|
108 return array_key_exists($key, $this->links);
|
Chris@18
|
109 }
|
Chris@18
|
110
|
Chris@18
|
111 /**
|
Chris@18
|
112 * Establishes a new context for a LinkCollection.
|
Chris@18
|
113 *
|
Chris@18
|
114 * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject $context
|
Chris@18
|
115 * The new context object.
|
Chris@18
|
116 *
|
Chris@18
|
117 * @return static
|
Chris@18
|
118 * A new LinkCollection with the given context.
|
Chris@18
|
119 */
|
Chris@18
|
120 public function withContext($context) {
|
Chris@18
|
121 return new static($this->links, $context);
|
Chris@18
|
122 }
|
Chris@18
|
123
|
Chris@18
|
124 /**
|
Chris@18
|
125 * Gets the LinkCollection's context object.
|
Chris@18
|
126 *
|
Chris@18
|
127 * @return \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject
|
Chris@18
|
128 * The LinkCollection's context.
|
Chris@18
|
129 */
|
Chris@18
|
130 public function getContext() {
|
Chris@18
|
131 assert(!is_null($this->context), 'A LinkCollection is invalid unless a context has been established.');
|
Chris@18
|
132 return $this->context;
|
Chris@18
|
133 }
|
Chris@18
|
134
|
Chris@18
|
135 /**
|
Chris@18
|
136 * Filters a LinkCollection using the provided callback.
|
Chris@18
|
137 *
|
Chris@18
|
138 * @param callable $f
|
Chris@18
|
139 * The filter callback. The callback has the signature below.
|
Chris@18
|
140 *
|
Chris@18
|
141 * @code
|
Chris@18
|
142 * boolean callback(string $key, \Drupal\jsonapi\JsonApiResource\Link $link, mixed $context))
|
Chris@18
|
143 * @endcode
|
Chris@18
|
144 *
|
Chris@18
|
145 * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
|
Chris@18
|
146 * A new, filtered LinkCollection.
|
Chris@18
|
147 */
|
Chris@18
|
148 public function filter(callable $f) {
|
Chris@18
|
149 $links = iterator_to_array($this);
|
Chris@18
|
150 $filtered = array_reduce(array_keys($links), function ($filtered, $key) use ($links, $f) {
|
Chris@18
|
151 if ($f($key, $links[$key], $this->context)) {
|
Chris@18
|
152 $filtered[$key] = $links[$key];
|
Chris@18
|
153 }
|
Chris@18
|
154 return $filtered;
|
Chris@18
|
155 }, []);
|
Chris@18
|
156 return new LinkCollection($filtered, $this->context);
|
Chris@18
|
157 }
|
Chris@18
|
158
|
Chris@18
|
159 /**
|
Chris@18
|
160 * Merges two LinkCollections.
|
Chris@18
|
161 *
|
Chris@18
|
162 * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $a
|
Chris@18
|
163 * The first link collection.
|
Chris@18
|
164 * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $b
|
Chris@18
|
165 * The second link collection.
|
Chris@18
|
166 *
|
Chris@18
|
167 * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
|
Chris@18
|
168 * A new LinkCollection with the links of both inputs.
|
Chris@18
|
169 */
|
Chris@18
|
170 public static function merge(LinkCollection $a, LinkCollection $b) {
|
Chris@18
|
171 assert($a->getContext() === $b->getContext());
|
Chris@18
|
172 $merged = new LinkCollection([], $a->getContext());
|
Chris@18
|
173 foreach ($a as $key => $links) {
|
Chris@18
|
174 $merged = array_reduce($links, function (self $merged, Link $link) use ($key) {
|
Chris@18
|
175 return $merged->withLink($key, $link);
|
Chris@18
|
176 }, $merged);
|
Chris@18
|
177 }
|
Chris@18
|
178 foreach ($b as $key => $links) {
|
Chris@18
|
179 $merged = array_reduce($links, function (self $merged, Link $link) use ($key) {
|
Chris@18
|
180 return $merged->withLink($key, $link);
|
Chris@18
|
181 }, $merged);
|
Chris@18
|
182 }
|
Chris@18
|
183 return $merged;
|
Chris@18
|
184 }
|
Chris@18
|
185
|
Chris@18
|
186 /**
|
Chris@18
|
187 * Ensures that a link key is valid.
|
Chris@18
|
188 *
|
Chris@18
|
189 * @param string $key
|
Chris@18
|
190 * A key name.
|
Chris@18
|
191 *
|
Chris@18
|
192 * @return bool
|
Chris@18
|
193 * TRUE if the key is valid, FALSE otherwise.
|
Chris@18
|
194 */
|
Chris@18
|
195 protected static function validKey($key) {
|
Chris@18
|
196 return is_string($key) && !is_numeric($key) && strpos($key, ':') === FALSE;
|
Chris@18
|
197 }
|
Chris@18
|
198
|
Chris@18
|
199 }
|