Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,18 @@ It has support for generating & sending documents with:
- resource collections
- to-one and to-many relationships
- errors (easily turning exceptions into jsonapi output)
- v1.1 extensions via profiles
- v1.1 extensions and profiles
- v1.1 @-members for JSON-LD and others

Also there's tools to help processing of incoming requests:

- parse request options (include paths, sparse fieldsets, sort fields, pagination, filtering)
- parse request documents for creating, updating and deleting resources and relationships

Next to custom extensions, the following [official extensions](https://jsonapi.org/extensions/) are included:
Next to custom extensions/profiles, the following [official extensions/profiles](https://jsonapi.org/extensions/) are included:

- Cursor Pagination ([example code](/examples/cursor_pagination_profile.php), [specification](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/))
- Atomic Operations extension ([example code](/examples/atomic_operations_extension.php), [specification](https://jsonapi.org/ext/atomic/))
- Cursor Pagination profile ([example code](/examples/cursor_pagination_profile.php), [specification](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/))

Plans for the future include:

Expand Down
33 changes: 33 additions & 0 deletions examples/atomic_operations_extension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

use alsvanzelf\jsonapi\objects\ResourceObject;
use alsvanzelf\jsonapi\extensions\AtomicOperationsDocument;

require 'bootstrap_examples.php';

/**
* use the atomic operations extension as extension to the document
*/

$document = new AtomicOperationsDocument();

$user1 = new ResourceObject('user', 1);
$user2 = new ResourceObject('user', 2);
$user42 = new ResourceObject('user', 42);

$user1->add('name', 'Ford');
$user2->add('name', 'Arthur');
$user42->add('name', 'Zaphod');

$document->addResults($user1);
$document->addResults($user2);
$document->addResults($user42);

/**
* get the json
*/

$options = [
'prettyPrint' => true,
];
echo '<pre>'.$document->toJson($options);
45 changes: 40 additions & 5 deletions examples/bootstrap_examples.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use alsvanzelf\jsonapi\Document;
use alsvanzelf\jsonapi\ResourceDocument;
use alsvanzelf\jsonapi\interfaces\ExtensionInterface;
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
use alsvanzelf\jsonapi\interfaces\ResourceInterface;

Expand Down Expand Up @@ -101,25 +102,59 @@ function getCurrentLocation() {
}
}

class ExampleVersionProfile implements ProfileInterface {
class ExampleVersionExtension implements ExtensionInterface {
/**
* the required method
*/

public function getOfficialLink() {
return 'https://jsonapi.org/format/1.1/#profile-keywords';
return 'https://jsonapi.org/format/1.1/#extension-rules';
}

public function getNamespace() {
return 'version';
}

/**
* optionally helpers for the specific profile
* optionally helpers for the specific extension
*/

public function setVersion(ResourceInterface $resource, $version) {
if ($resource instanceof ResourceDocument) {
$resource->addMeta('version', $version, $level=Document::LEVEL_RESOURCE);
$resource->getResource()->addExtensionMember($this, 'id', $version);
}
else {
$resource->addMeta('version', $version);
$resource->addExtensionMember($this, 'id', $version);
}
}
}

class ExampleTimestampsProfile implements ProfileInterface {
/**
* the required method
*/

public function getOfficialLink() {
return 'https://jsonapi.org/recommendations/#authoring-profiles';
}

/**
* optionally helpers for the specific profile
*/

public function setTimestamps(ResourceInterface $resource, \DateTimeInterface $created=null, \DateTimeInterface $updated=null) {
if ($resource instanceof ResourceIdentifierObject) {
throw new Exception('cannot add attributes to identifier objects');
}

$timestamps = [];
if ($created !== null) {
$timestamps['created'] = $created->format(\DateTime::ISO8601);
}
if ($updated !== null) {
$timestamps['updated'] = $updated->format(\DateTime::ISO8601);
}

$resource->add('timestamps', $timestamps);
}
}
30 changes: 0 additions & 30 deletions examples/example_profile.php

This file was deleted.

37 changes: 37 additions & 0 deletions examples/extension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

use alsvanzelf\jsonapi\Document;
use alsvanzelf\jsonapi\ResourceDocument;
use alsvanzelf\jsonapi\helpers\Converter;

require 'bootstrap_examples.php';

/**
* use an extension extend the document with new members
*/

$extension = new ExampleVersionExtension();

$document = new ResourceDocument('user', 42);
$document->applyExtension($extension);

$document->add('foo', 'bar');

/**
* you can apply the rules of the extension manually
* or use methods of the extension if provided
*/

$extension->setVersion($document, '2019');

/**
* get the json
*/

$contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, [$extension], []);
echo '<code>Content-Type: '.$contentType.'</code>'.PHP_EOL;

$options = [
'prettyPrint' => true,
];
echo '<pre>'.$document->toJson($options);
4 changes: 3 additions & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ <h3>Misc</h3>
<li><a href="null_values.php">Null values if explicitly not available</a></li>
<li><a href="meta_only.php">Meta-only use-cases</a></li>
<li><a href="status_only.php">Status-only</a></li>
<li><a href="example_profile.php">Example profile</a></li>
<li><a href="extension.php">Example extension</a></li>
<li><a href="atomic_operations_extension.php">Atomic operations extension</a></li>
<li><a href="profile.php">Example profile</a></li>
<li><a href="cursor_pagination_profile.php">Cursor pagination profile</a></li>
<li><a href="output.php">Different ways to output</a></li>
</ul>
Expand Down
39 changes: 39 additions & 0 deletions examples/profile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

use alsvanzelf\jsonapi\Document;
use alsvanzelf\jsonapi\ResourceDocument;
use alsvanzelf\jsonapi\helpers\Converter;

require 'bootstrap_examples.php';

/**
* use a profile to define rules for members
*/

$profile = new ExampleTimestampsProfile();

$document = new ResourceDocument('user', 42);
$document->applyProfile($profile);

$document->add('foo', 'bar');

/**
* you can apply the rules of the profile manually
* or use methods of the profile if provided
*/

$created = new \DateTime('-1 year');
$updated = new \DateTime('-1 month');
$profile->setTimestamps($document, $created, $updated);

/**
* get the json
*/

$contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, [], [$profile]);
echo '<code>Content-Type: '.$contentType.'</code>'.PHP_EOL;

$options = [
'prettyPrint' => true,
];
echo '<pre>'.$document->toJson($options);
56 changes: 48 additions & 8 deletions src/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

namespace alsvanzelf\jsonapi;

use alsvanzelf\jsonapi\exceptions\DuplicateException;
use alsvanzelf\jsonapi\exceptions\Exception;
use alsvanzelf\jsonapi\exceptions\InputException;
use alsvanzelf\jsonapi\helpers\AtMemberManager;
use alsvanzelf\jsonapi\helpers\Converter;
use alsvanzelf\jsonapi\helpers\ExtensionMemberManager;
use alsvanzelf\jsonapi\helpers\HttpStatusCodeManager;
use alsvanzelf\jsonapi\helpers\LinksManager;
use alsvanzelf\jsonapi\helpers\Validator;
use alsvanzelf\jsonapi\interfaces\DocumentInterface;
use alsvanzelf\jsonapi\interfaces\ExtensionInterface;
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
use alsvanzelf\jsonapi\objects\JsonapiObject;
use alsvanzelf\jsonapi\objects\LinksObject;
Expand All @@ -19,7 +22,7 @@
* @see ResourceDocument, CollectionDocument, ErrorsDocument or MetaDocument
*/
abstract class Document implements DocumentInterface, \JsonSerializable {
use AtMemberManager, HttpStatusCodeManager, LinksManager;
use AtMemberManager, ExtensionMemberManager, HttpStatusCodeManager, LinksManager;

const JSONAPI_VERSION_1_0 = '1.0';
const JSONAPI_VERSION_1_1 = '1.1';
Expand All @@ -37,6 +40,8 @@ abstract class Document implements DocumentInterface, \JsonSerializable {
protected $meta;
/** @var JsonapiObject */
protected $jsonapi;
/** @var ExtensionInterface[] */
protected $extensions = [];
/** @var ProfileInterface[] */
protected $profiles = [];
/** @var array */
Expand Down Expand Up @@ -190,25 +195,53 @@ public function unsetJsonapiObject() {
$this->jsonapi = null;
}

/**
* apply a extension which adds the link and sets a correct content-type
*
* note that the rules from the extension are not automatically enforced
* applying the rules, and applying them correctly, is manual
* however the $extension could have custom methods to help
*
* @see https://jsonapi.org/extensions/#extensions
*
* @param ExtensionInterface $extension
*
* @throws Exception if namespace uses illegal characters
* @throws DuplicateException if namespace conflicts with another applied extension
*/
public function applyExtension(ExtensionInterface $extension) {
$namespace = $extension->getNamespace();
if (strlen($namespace) < 1 || preg_match('{[^a-zA-Z0-9]}', $namespace) === 1) {
throw new Exception('invalid namespace "'.$namespace.'"');
}
if (isset($this->extensions[$namespace])) {
throw new DuplicateException('an extension with namespace "'.$namespace.'" is already applied');
}

$this->extensions[$namespace] = $extension;

if ($this->jsonapi !== null) {
$this->jsonapi->addExtension($extension);
}
}

/**
* apply a profile which adds the link and sets a correct content-type
*
* note that the rules from the profile are not automatically enforced
* applying the rules, and applying them correctly, is manual
* however the $profile could have custom methods to help
*
* @see https://jsonapi.org/format/1.1/#profiles
* @see https://jsonapi.org/extensions/#profiles
*
* @param ProfileInterface $profile
*/
public function applyProfile(ProfileInterface $profile) {
$this->profiles[] = $profile;

if ($this->links === null) {
$this->setLinksObject(new LinksObject());
if ($this->jsonapi !== null) {
$this->jsonapi->addProfile($profile);
}

$this->links->append('profile', $profile->getOfficialLink());
}

/**
Expand All @@ -219,7 +252,14 @@ public function applyProfile(ProfileInterface $profile) {
* @inheritDoc
*/
public function toArray() {
$array = $this->getAtMembers();
$array = [];

if ($this->hasAtMembers()) {
$array = array_merge($array, $this->getAtMembers());
}
if ($this->hasExtensionMembers()) {
$array = array_merge($array, $this->getExtensionMembers());
}

if ($this->jsonapi !== null && $this->jsonapi->isEmpty() === false) {
$array['jsonapi'] = $this->jsonapi->toArray();
Expand Down Expand Up @@ -273,7 +313,7 @@ public function sendResponse(array $options=[]) {

http_response_code($this->httpStatusCode);

$contentType = Converter::mergeProfilesInContentType($options['contentType'], $this->profiles);
$contentType = Converter::prepareContentType($options['contentType'], $this->extensions, $this->profiles);
header('Content-Type: '.$contentType);

echo $json;
Expand Down
Loading