Skip to content

Commit 5fdddb0

Browse files
committed
Merge branch 'profile-links' into 2.1.0
2 parents 6902f16 + 991f3b7 commit 5fdddb0

17 files changed

Lines changed: 636 additions & 1 deletion

examples/bootstrap_examples.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<?php
22

3+
use alsvanzelf\jsonapi\Document;
4+
use alsvanzelf\jsonapi\ResourceDocument;
5+
use alsvanzelf\jsonapi\helpers\ProfileAliasManager;
6+
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
7+
use alsvanzelf\jsonapi\interfaces\ResourceInterface;
8+
39
ini_set('display_errors', 1);
410
error_reporting(-1);
511

@@ -95,3 +101,30 @@ function getCurrentLocation() {
95101
return 'Earth';
96102
}
97103
}
104+
105+
class ExampleVersionProfile extends ProfileAliasManager implements ProfileInterface {
106+
/**
107+
* the required methods (next to extending ProfileAliasManager)
108+
*/
109+
110+
public function getOfficialLink() {
111+
return 'https://jsonapi.org/format/1.1/#profile-keywords-and-aliases';
112+
}
113+
114+
public function getOfficialKeywords() {
115+
return ['version'];
116+
}
117+
118+
/**
119+
* optionally helpers for the specific profile
120+
*/
121+
122+
public function setVersion(ResourceInterface $resource, $version) {
123+
if ($resource instanceof ResourceDocument) {
124+
$resource->addMeta($this->getKeyword('version'), $version, $level=Document::LEVEL_RESOURCE);
125+
}
126+
else {
127+
$resource->addMeta($this->getKeyword('version'), $version);
128+
}
129+
}
130+
}

examples/example_profile.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use alsvanzelf\jsonapi\ResourceDocument;
4+
5+
require 'bootstrap_examples.php';
6+
7+
/**
8+
* use a profile as extension to the document
9+
*
10+
* allowing to define aliases for the keywords to solve conflicts between different profiles
11+
*/
12+
13+
$profile = new ExampleVersionProfile(['version' => 'ref']);
14+
15+
$document = new ResourceDocument('user', 42);
16+
$document->applyProfile($profile);
17+
18+
/**
19+
* you can apply the rules of the profile manually
20+
* or use methods of the profile if provided
21+
*/
22+
23+
$profile->setVersion($document, '2019');
24+
25+
/**
26+
* get the json
27+
*/
28+
29+
$options = [
30+
'prettyPrint' => true,
31+
];
32+
echo '<pre>'.$document->toJson($options);

examples/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ <h3>Misc</h3>
4848
<li><a href="null_values.php">Null values if explicitly not available</a></li>
4949
<li><a href="meta_only.php">Meta-only use-cases</a></li>
5050
<li><a href="status_only.php">Status-only</a></li>
51+
<li><a href="example_profile.php">Example profile</a></li>
5152
<li><a href="output.php">Different ways to output</a></li>
5253
</ul>
5354

src/Document.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
use alsvanzelf\jsonapi\exceptions\Exception;
66
use alsvanzelf\jsonapi\exceptions\InputException;
77
use alsvanzelf\jsonapi\helpers\AtMemberManager;
8+
use alsvanzelf\jsonapi\helpers\Converter;
89
use alsvanzelf\jsonapi\helpers\HttpStatusCodeManager;
910
use alsvanzelf\jsonapi\helpers\LinksManager;
1011
use alsvanzelf\jsonapi\helpers\Validator;
1112
use alsvanzelf\jsonapi\interfaces\DocumentInterface;
13+
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
1214
use alsvanzelf\jsonapi\objects\JsonapiObject;
15+
use alsvanzelf\jsonapi\objects\LinkObject;
1316
use alsvanzelf\jsonapi\objects\LinksObject;
1417
use alsvanzelf\jsonapi\objects\MetaObject;
18+
use alsvanzelf\jsonapi\objects\ProfileLinkObject;
1519

1620
/**
1721
* @see ResourceDocument, CollectionDocument, ErrorsDocument or MetaDocument
@@ -35,6 +39,8 @@ abstract class Document implements DocumentInterface, \JsonSerializable {
3539
protected $meta;
3640
/** @var JsonapiObject */
3741
protected $jsonapi;
42+
/** @var ProfileInterface[] */
43+
protected $profiles = [];
3844
/** @var array */
3945
protected static $defaults = [
4046
/**
@@ -172,6 +178,33 @@ public function unsetJsonapiObject() {
172178
$this->jsonapi = null;
173179
}
174180

181+
/**
182+
* apply a profile which adds the link and sets a correct content-type
183+
*
184+
* note that the rules from the profile are not automatically enforced
185+
* applying the rules, and applying them correctly, is manual
186+
* however the $profile could have custom methods to help
187+
*
188+
* @see https://jsonapi.org/format/1.1/#profiles
189+
*
190+
* @param ProfileInterface $profile
191+
*/
192+
public function applyProfile(ProfileInterface $profile) {
193+
$this->profiles[] = $profile;
194+
195+
if ($this->links === null) {
196+
$this->setLinksObject(new LinksObject());
197+
}
198+
199+
$link = $profile->getAliasedLink();
200+
if ($link instanceof LinkObject) {
201+
$this->links->appendLinkObject('profile', $link);
202+
}
203+
else {
204+
$this->links->append('profile', $link);
205+
}
206+
}
207+
175208
/**
176209
* DocumentInterface
177210
*/
@@ -233,7 +266,9 @@ public function sendResponse(array $options=[]) {
233266
$json = ($options['json'] !== null) ? $options['json'] : $this->toJson($options);
234267

235268
http_response_code($this->httpStatusCode);
236-
header('Content-Type: '.$options['contentType']);
269+
270+
$contentType = Converter::mergeProfilesInContentType($options['contentType'], $this->profiles);
271+
header('Content-Type: '.$contentType);
237272

238273
echo $json;
239274
}

src/helpers/Converter.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace alsvanzelf\jsonapi\helpers;
44

55
use alsvanzelf\jsonapi\interfaces\ObjectInterface;
6+
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
7+
use alsvanzelf\jsonapi\objects\LinkObject;
68

79
/**
810
* @internal
@@ -31,4 +33,26 @@ public static function camelCaseToWords($camelCase) {
3133

3234
return implode(' ', $parts);
3335
}
36+
37+
/**
38+
* generates the value for a content type header, with profiles merged in if available
39+
*
40+
* @param string $contentType
41+
* @param ProfileInterface[] $profiles
42+
* @return string
43+
*/
44+
public static function mergeProfilesInContentType($contentType, array $profiles) {
45+
if ($profiles === []) {
46+
return $contentType;
47+
}
48+
49+
$profileLinks = [];
50+
foreach ($profiles as $profile) {
51+
$link = $profile->getAliasedLink();
52+
$profileLinks[] = ($link instanceof LinkObject) ? $link->toArray()['href'] : $link;
53+
}
54+
$profileLinks = implode(' ', $profileLinks);
55+
56+
return $contentType.';profile="'.$profileLinks.'", '.$contentType;
57+
}
3458
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace alsvanzelf\jsonapi\helpers;
4+
5+
use alsvanzelf\jsonapi\exceptions\InputException;
6+
use alsvanzelf\jsonapi\helpers\Validator;
7+
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
8+
use alsvanzelf\jsonapi\objects\ProfileLinkObject;
9+
10+
abstract class ProfileAliasManager {
11+
/** @var array */
12+
private $aliasMapping = [];
13+
/** @var array */
14+
private $keywordMapping = [];
15+
16+
/**
17+
* ProfileInterface
18+
*/
19+
20+
/**
21+
* @inheritDoc
22+
*/
23+
public function __construct(array $aliases=[]) {
24+
$officialKeywords = $this->getOfficialKeywords();
25+
if ($officialKeywords === []) {
26+
return;
27+
}
28+
29+
$this->keywordMapping = array_combine($officialKeywords, $officialKeywords);
30+
if ($aliases === []) {
31+
return;
32+
}
33+
34+
foreach ($aliases as $keyword => $alias) {
35+
if ($alias === $keyword) {
36+
throw new InputException('an alias should be different from its keyword');
37+
}
38+
if (in_array($keyword, $officialKeywords, $strict=true) === false) {
39+
throw new InputException('unknown keyword "'.$keyword.'" to alias');
40+
}
41+
Validator::checkMemberName($alias);
42+
43+
$this->keywordMapping[$keyword] = $alias;
44+
}
45+
46+
$this->aliasMapping = $aliases;
47+
}
48+
49+
/**
50+
* @inheritDoc
51+
*/
52+
public function getKeyword($keyword) {
53+
if (isset($this->keywordMapping[$keyword]) === false) {
54+
throw new InputException('unknown keyword "'.$keyword.'"');
55+
}
56+
57+
return $this->keywordMapping[$keyword];
58+
}
59+
60+
/**
61+
* @inheritDoc
62+
*/
63+
public function getAliasedLink() {
64+
if ($this->aliasMapping === []) {
65+
return $this->getOfficialLink();
66+
}
67+
68+
return new ProfileLinkObject($this->getOfficialLink(), $this->aliasMapping);
69+
}
70+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace alsvanzelf\jsonapi\interfaces;
4+
5+
use alsvanzelf\jsonapi\exceptions\InputException;
6+
use alsvanzelf\jsonapi\objects\LinkObject;
7+
8+
/**
9+
* @see ProfileAliasManager which implement most of the methods
10+
*/
11+
interface ProfileInterface {
12+
/**
13+
* get a profile with its aliases to keywords of the profile
14+
*
15+
* having this in the constructor makes sure the aliases are used from the start
16+
*
17+
* @param array $aliases optional mapping keywords to aliases
18+
*
19+
* @throws InputException if the alias is not different from the keyword
20+
* @throws InputException if the keyword is not known to the profile
21+
* @throws InputException if the alias is not a valid member name
22+
*/
23+
public function __construct(array $aliases=[]);
24+
25+
/**
26+
* get the keyword or current alias based on the official keyword from the profile
27+
*
28+
* e.g. for a profile defining an official keyword 'version', this would return 'version'
29+
* or if ->alias('version', 'v') was called before, this would return 'v'
30+
*
31+
* @param string $keyword
32+
* @return string
33+
*
34+
* @throws InputException if the keyword is not known to the profile
35+
*/
36+
public function getKeyword($keyword);
37+
38+
/**
39+
* returns an array of official keywords this profile defines
40+
*
41+
* @internal
42+
*
43+
* @return string[]
44+
*/
45+
public function getOfficialKeywords();
46+
47+
/**
48+
* the unique link identifying and describing the profile
49+
*
50+
* @internal
51+
*
52+
* @return string
53+
*/
54+
public function getOfficialLink();
55+
56+
/**
57+
* get the official link, or a LinkObject with the link and its aliases
58+
*
59+
* optionally also contains the aliases applied
60+
*
61+
* @internal
62+
*
63+
* @return LinkObject|string
64+
*/
65+
public function getAliasedLink();
66+
}

src/objects/ProfileLinkObject.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace alsvanzelf\jsonapi\objects;
4+
5+
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
6+
use alsvanzelf\jsonapi\objects\LinkObject;
7+
8+
class ProfileLinkObject extends LinkObject {
9+
/** @var array */
10+
protected $aliases = [];
11+
12+
/**
13+
* @param string $href
14+
* @param array $aliases optional
15+
* @param array $meta optional
16+
*/
17+
public function __construct($href, array $aliases=[], array $meta=[]) {
18+
parent::__construct($href, $meta);
19+
20+
$this->aliases = $aliases;
21+
}
22+
23+
/**
24+
* human api
25+
*/
26+
27+
/**
28+
* spec api
29+
*/
30+
31+
/**
32+
* ObjectInterface
33+
*/
34+
35+
/**
36+
* @inheritDoc
37+
*/
38+
public function toArray() {
39+
$array = parent::toArray();
40+
41+
if ($this->aliases !== []) {
42+
$array['aliases'] = $this->aliases;
43+
}
44+
45+
return $array;
46+
}
47+
}

0 commit comments

Comments
 (0)