From 3d61e4789b89cb25df68233d92dfc488a91a9a54 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 9 Mar 2017 16:03:12 +1030 Subject: [PATCH 01/37] New resource-based API Simpler, 2-3x faster, and doesn't violate the LSP like the old serializer-based API. --- ...actSerializer.php => AbstractResource.php} | 41 +- src/Collection.php | 126 ------ src/Document.php | 364 ++++++++++++---- src/ElementInterface.php | 54 --- src/Relationship.php | 39 +- src/Resource.php | 406 ------------------ ...zerInterface.php => ResourceInterface.php} | 34 +- src/Util.php | 50 --- 8 files changed, 321 insertions(+), 793 deletions(-) rename src/{AbstractSerializer.php => AbstractResource.php} (68%) delete mode 100644 src/Collection.php delete mode 100644 src/ElementInterface.php delete mode 100644 src/Resource.php rename src/{SerializerInterface.php => ResourceInterface.php} (52%) delete mode 100644 src/Util.php diff --git a/src/AbstractSerializer.php b/src/AbstractResource.php similarity index 68% rename from src/AbstractSerializer.php rename to src/AbstractResource.php index 6e343cc..4dcfd34 100644 --- a/src/AbstractSerializer.php +++ b/src/AbstractResource.php @@ -13,10 +13,13 @@ use LogicException; -abstract class AbstractSerializer implements SerializerInterface +abstract class AbstractResource implements ResourceInterface { + use LinksTrait; + use MetaTrait; + /** - * The type. + * The resource type. * * @var string */ @@ -25,7 +28,7 @@ abstract class AbstractSerializer implements SerializerInterface /** * {@inheritdoc} */ - public function getType($model) + public function getType() { return $this->type; } @@ -33,31 +36,7 @@ public function getType($model) /** * {@inheritdoc} */ - public function getId($model) - { - return $model->id; - } - - /** - * {@inheritdoc} - */ - public function getAttributes($model, array $fields = null) - { - return []; - } - - /** - * {@inheritdoc} - */ - public function getLinks($model) - { - return []; - } - - /** - * {@inheritdoc} - */ - public function getMeta($model) + public function getAttributes(array $fields = null) { return []; } @@ -67,12 +46,12 @@ public function getMeta($model) * * @throws \LogicException */ - public function getRelationship($model, $name) + public function getRelationship($name) { $method = $this->getRelationshipMethodName($name); if (method_exists($this, $method)) { - $relationship = $this->$method($model); + $relationship = $this->$method(); if ($relationship !== null && ! ($relationship instanceof Relationship)) { throw new LogicException('Relationship method must return null or an instance of Tobscure\JsonApi\Relationship'); @@ -83,7 +62,7 @@ public function getRelationship($model, $name) } /** - * Get the serializer method name for the given relationship. + * Get the method name for the given relationship. * * snake_case and kebab-case are converted into camelCase. * diff --git a/src/Collection.php b/src/Collection.php deleted file mode 100644 index 4d6707b..0000000 --- a/src/Collection.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi; - -class Collection implements ElementInterface -{ - /** - * @var array - */ - protected $resources = []; - - /** - * Create a new collection instance. - * - * @param mixed $data - * @param \Tobscure\JsonApi\SerializerInterface $serializer - */ - public function __construct($data, SerializerInterface $serializer) - { - $this->resources = $this->buildResources($data, $serializer); - } - - /** - * Convert an array of raw data to Resource objects. - * - * @param mixed $data - * @param SerializerInterface $serializer - * - * @return \Tobscure\JsonApi\Resource[] - */ - protected function buildResources($data, SerializerInterface $serializer) - { - $resources = []; - - foreach ($data as $resource) { - if (! ($resource instanceof Resource)) { - $resource = new Resource($resource, $serializer); - } - - $resources[] = $resource; - } - - return $resources; - } - - /** - * {@inheritdoc} - */ - public function getResources() - { - return $this->resources; - } - - /** - * Set the resources array. - * - * @param array $resources - * - * @return void - */ - public function setResources($resources) - { - $this->resources = $resources; - } - - /** - * Request a relationship to be included for all resources. - * - * @param string|array $relationships - * - * @return $this - */ - public function with($relationships) - { - foreach ($this->resources as $resource) { - $resource->with($relationships); - } - - return $this; - } - - /** - * Request a restricted set of fields. - * - * @param array|null $fields - * - * @return $this - */ - public function fields($fields) - { - foreach ($this->resources as $resource) { - $resource->fields($fields); - } - - return $this; - } - - /** - * {@inheritdoc} - */ - public function toArray() - { - return array_map(function (Resource $resource) { - return $resource->toArray(); - }, $this->resources); - } - - /** - * {@inheritdoc} - */ - public function toIdentifier() - { - return array_map(function (Resource $resource) { - return $resource->toIdentifier(); - }, $this->resources); - } -} diff --git a/src/Document.php b/src/Document.php index cc524d3..7e4331e 100644 --- a/src/Document.php +++ b/src/Document.php @@ -12,6 +12,7 @@ namespace Tobscure\JsonApi; use JsonSerializable; +use LogicException; class Document implements JsonSerializable { @@ -19,158 +20,170 @@ class Document implements JsonSerializable use MetaTrait; /** - * The included array. + * The data object. * - * @var array + * @var ResourceInterface|ResourceInterface[]|null */ - protected $included = []; + protected $data; /** * The errors array. * - * @var array + * @var array|null */ protected $errors; /** * The jsonapi array. * - * @var array + * @var array|null */ protected $jsonapi; /** - * The data object. - * - * @var ElementInterface + * Relationships to include. + * + * @var array */ - protected $data; + protected $include = []; /** - * @param ElementInterface $data + * Sparse fieldsets. + * + * @var array */ - public function __construct(ElementInterface $data = null) + protected $fields = []; + + /** + * @param ResourceInterface|ResourceInterface[] $data + */ + public function __construct($data = null) { $this->data = $data; } /** - * Get included resources. + * Get the data object. + * + * @return ResourceInterface|ResourceInterface[]|null $data + */ + public function getData() + { + return $this->data; + } + + /** + * Set the data object. * - * @param \Tobscure\JsonApi\ElementInterface $element - * @param bool $includeParent + * @param ResourceInterface|ResourceInterface[]|null $data * - * @return \Tobscure\JsonApi\Resource[] + * @return $this */ - protected function getIncluded(ElementInterface $element, $includeParent = false) + public function setData($data) { - $included = []; - - foreach ($element->getResources() as $resource) { - if ($resource->isIdentifier()) { - continue; - } - - if ($includeParent) { - $included = $this->mergeResource($included, $resource); - } else { - $type = $resource->getType(); - $id = $resource->getId(); - } - - foreach ($resource->getUnfilteredRelationships() as $relationship) { - $includedElement = $relationship->getData(); - - if (! $includedElement instanceof ElementInterface) { - continue; - } - - foreach ($this->getIncluded($includedElement, true) as $child) { - // If this resource is the same as the top-level "data" - // resource, then we don't want it to show up again in the - // "included" array. - if (! $includeParent && $child->getType() === $type && $child->getId() === $id) { - continue; - } - - $included = $this->mergeResource($included, $child); - } - } - } - - $flattened = []; + $this->data = $data; - array_walk_recursive($included, function ($a) use (&$flattened) { - $flattened[] = $a; - }); + return $this; + } - return $flattened; + /** + * Get the errors array. + * + * @return array|null $errors + */ + public function getErrors() + { + return $this->errors; } /** - * @param \Tobscure\JsonApi\Resource[] $resources - * @param \Tobscure\JsonApi\Resource $newResource + * Set the errors array. + * + * @param array|null $errors * - * @return \Tobscure\JsonApi\Resource[] + * @return $this */ - protected function mergeResource(array $resources, Resource $newResource) + public function setErrors(array $errors = null) { - $type = $newResource->getType(); - $id = $newResource->getId(); + $this->errors = $errors; - if (isset($resources[$type][$id])) { - $resources[$type][$id]->merge($newResource); - } else { - $resources[$type][$id] = $newResource; - } + return $this; + } - return $resources; + /** + * Get the jsonapi array. + * + * @return array|null $jsonapi + */ + public function getJsonapi() + { + return $this->jsonapi; } /** - * Set the data object. + * Set the jsonapi array. * - * @param \Tobscure\JsonApi\ElementInterface $element + * @param array|null $jsonapi * * @return $this */ - public function setData(ElementInterface $element) + public function setJsonapi(array $jsonapi = null) { - $this->data = $element; + $this->jsonapi = $jsonapi; return $this; } /** - * Set the errors array. - * - * @param array $errors + * Get the relationships to include. + * + * @return array $include + */ + public function getInclude() + { + return $this->include; + } + + /** + * Set the relationships to include. + * + * @param array $include * * @return $this */ - public function setErrors($errors) + public function setInclude(array $include) { - $this->errors = $errors; + $this->include = $include; return $this; } /** - * Set the jsonapi array. - * - * @param array $jsonapi + * Get the sparse fieldsets. + * + * @return array $fields + */ + public function getFields() + { + return $this->fields; + } + + /** + * Set the sparse fieldsets. + * + * @param array $fields * * @return $this */ - public function setJsonapi($jsonapi) + public function setFields(array $fields) { - $this->jsonapi = $jsonapi; + $this->fields = $fields; return $this; } /** - * Map everything to arrays. + * Build the JSON-API document as an array. * * @return array */ @@ -178,31 +191,52 @@ public function toArray() { $document = []; - if (! empty($this->links)) { + if ($this->links) { $document['links'] = $this->links; } - if (! empty($this->data)) { - $document['data'] = $this->data->toArray(); + if ($this->data) { + $isCollection = is_array($this->data); + + // Build a multi-dimensional map of all of the distinct resources + // that are present in the document, indexed by type and ID. This is + // done by recursively looping through each of the resources and + // their included relationships. We do this so that any resources + // that are duplicated may be merged back into a single instance. + $map = []; + $resources = $isCollection ? $this->data : [$this->data]; - $resources = $this->getIncluded($this->data); + $this->addResourcesToMap($map, $resources, $this->include); - if (count($resources)) { - $document['included'] = array_map(function (Resource $resource) { - return $resource->toArray(); - }, $resources); + // Now extract the document's primary resource(s) from the resource + // map, and flatten the map's remaining resources to be included in + // the document's "included" array. + foreach ($resources as $resource) { + $type = $resource->getType(); + $id = $resource->getId(); + + $primary[] = $map[$type][$id]; + unset($map[$type][$id]); + } + + $included = call_user_func_array('array_merge', $map); + + $document['data'] = $isCollection ? $primary : $primary[0]; + + if ($included) { + $document['included'] = $included; } } - if (! empty($this->meta)) { + if ($this->meta) { $document['meta'] = $this->meta; } - if (! empty($this->errors)) { + if ($this->errors) { $document['errors'] = $this->errors; } - if (! empty($this->jsonapi)) { + if ($this->jsonapi) { $document['jsonapi'] = $this->jsonapi; } @@ -210,7 +244,7 @@ public function toArray() } /** - * Map to string. + * Build the JSON-API document and encode it as a JSON string. * * @return string */ @@ -228,4 +262,146 @@ public function jsonSerialize() { return $this->toArray(); } + + /** + * Recursively add the given resources and their relationships to a map. + * + * @param array &$map The map to merge resources into. + * @param ResourceInterface[] $resources + * @param array $include An array of relationship paths to include. + */ + private function addResourcesToMap(array &$map, array $resources, array $include) + { + // Index relationship paths so that we have a list of the direct + // relationships that will be included on these resources, and arrays + // of their respective nested relationships. + $include = $this->indexRelationshipPaths($include); + + foreach ($resources as $resource) { + $relationships = []; + + // Get each of the relationships we're including on this resource, + // and add their resources (and their relationships, and so on) to + // the map. + foreach ($include as $name => $nested) { + if (! ($relationship = $resource->getRelationship($name))) { + continue; + } + + $relationships[$name] = $relationship; + + if ($data = $relationship->getData()) { + $children = is_array($data) ? $data : [$data]; + + $this->addResourcesToMap($map, $children, $nested); + } + } + + // Serialize the resource into an array and add it to the map. If + // it is already present, its properties will be merged into the + // existing resource. + $this->addResourceToMap($map, $resource, $relationships); + } + } + + /** + * Serialize the given resource as an array and add it to the given map. + * + * If it is already present in the map, its properties will be merged into + * the existing array. + * + * @param array &$map + * @param ResourceInterface $resource + * @param Relationship[] $resource + */ + private function addResourceToMap(array &$map, ResourceInterface $resource, array $relationships) + { + $type = $resource->getType(); + $id = $resource->getId(); + + if (empty($map[$type][$id])) { + $map[$type][$id] = [ + 'type' => $type, + 'id' => $id + ]; + } + + $array = &$map[$type][$id]; + $fields = $this->getFieldsForType($type); + + if ($meta = $resource->getMeta()) { + $array['meta'] = array_replace_recursive(isset($array['meta']) ? $array['meta'] : [], $meta); + } + + if ($links = $resource->getLinks()) { + $array['links'] = array_replace_recursive(isset($array['links']) ? $array['links'] : [], $links); + } + + if ($attributes = $resource->getAttributes($fields)) { + if ($fields) { + $attributes = array_intersect_key($attributes, array_flip($fields)); + } + if ($attributes) { + $array['attributes'] = array_replace_recursive(isset($array['attributes']) ? $array['attributes'] : [], $attributes); + } + } + + if ($relationships && $fields) { + $relationships = array_intersect_key($relationships, array_flip($fields)); + } + if ($relationships) { + $relationships = array_map(function ($relationship) { + return $relationship->toArray(); + }, $relationships); + + $array['relationships'] = array_replace_recursive(isset($array['relationships']) ? $array['relationships'] : [], $relationships); + } + } + + /** + * Index relationship paths by top-level relationships. + * + * Given an array of relationship paths such as: + * + * ['user', 'user.employer', 'user.employer.country', 'comments'] + * + * Returns an array with key-value pairs of top-level relationships and + * their nested relationships: + * + * ['user' => ['employer', 'employer.country'], 'comments' => []] + * + * @param array $paths + * + * @return array + */ + private function indexRelationshipPaths(array $paths) + { + $tree = []; + + foreach ($paths as $path) { + list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null); + + if (! isset($tree[$primary])) { + $tree[$primary] = []; + } + + if ($nested) { + $tree[$primary][] = $nested; + } + } + + return $tree; + } + + /** + * Get the fields that should be included for resources of the given type. + * + * @param string $type + * + * @return array|null + */ + private function getFieldsForType($type) + { + return isset($this->fields[$type]) ? $this->fields[$type] : null; + } } diff --git a/src/ElementInterface.php b/src/ElementInterface.php deleted file mode 100644 index 81b74ba..0000000 --- a/src/ElementInterface.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi; - -interface ElementInterface -{ - /** - * Get the resources array. - * - * @return array - */ - public function getResources(); - - /** - * Map to a "resource object" array. - * - * @return array - */ - public function toArray(); - - /** - * Map to a "resource object identifier" array. - * - * @return array - */ - public function toIdentifier(); - - /** - * Request a relationship to be included. - * - * @param string|array $relationships - * - * @return $this - */ - public function with($relationships); - - /** - * Request a restricted set of fields. - * - * @param array|null $fields - * - * @return $this - */ - public function fields($fields); -} diff --git a/src/Relationship.php b/src/Relationship.php index 29f7fc6..8667452 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -19,16 +19,16 @@ class Relationship /** * The data object. * - * @var \Tobscure\JsonApi\ElementInterface|null + * @var \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null */ protected $data; /** * Create a new relationship. * - * @param \Tobscure\JsonApi\ElementInterface|null $data + * @param \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null $data */ - public function __construct(ElementInterface $data = null) + public function __construct($data = null) { $this->data = $data; } @@ -36,7 +36,7 @@ public function __construct(ElementInterface $data = null) /** * Get the data object. * - * @return \Tobscure\JsonApi\ElementInterface|null + * @return \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null */ public function getData() { @@ -46,7 +46,7 @@ public function getData() /** * Set the data object. * - * @param \Tobscure\JsonApi\ElementInterface|null $data + * @param \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null $data * * @return $this */ @@ -58,7 +58,7 @@ public function setData($data) } /** - * Map everything to an array. + * Build the relationship as an array. * * @return array */ @@ -66,18 +66,37 @@ public function toArray() { $array = []; - if (! empty($this->data)) { - $array['data'] = $this->data->toIdentifier(); + if ($this->data) { + if (is_array($this->data)) { + $array['data'] = array_map([$this, 'buildIdentifier'], $this->data); + } else { + $array['data'] = $this->buildIdentifier($this->data); + } } - if (! empty($this->meta)) { + if ($this->meta) { $array['meta'] = $this->meta; } - if (! empty($this->links)) { + if ($this->links) { $array['links'] = $this->links; } return $array; } + + /** + * Build an idenitfier array for the given resource. + * + * @param ResourceInterface $resource + * + * @return array + */ + private function buildIdentifier(ResourceInterface $resource) + { + return [ + 'type' => $resource->getType(), + 'id' => $resource->getId() + ]; + } } diff --git a/src/Resource.php b/src/Resource.php deleted file mode 100644 index b52230b..0000000 --- a/src/Resource.php +++ /dev/null @@ -1,406 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi; - -class Resource implements ElementInterface -{ - use LinksTrait; - use MetaTrait; - - /** - * @var mixed - */ - protected $data; - - /** - * @var \Tobscure\JsonApi\SerializerInterface - */ - protected $serializer; - - /** - * A list of relationships to include. - * - * @var array - */ - protected $includes = []; - - /** - * A list of fields to restrict to. - * - * @var array|null - */ - protected $fields; - - /** - * An array of Resources that should be merged into this one. - * - * @var \Tobscure\JsonApi\Resource[] - */ - protected $merged = []; - - /** - * @var \Tobscure\JsonApi\Relationship[] - */ - private $relationships; - - /** - * @param mixed $data - * @param \Tobscure\JsonApi\SerializerInterface $serializer - */ - public function __construct($data, SerializerInterface $serializer) - { - $this->data = $data; - $this->serializer = $serializer; - } - - /** - * {@inheritdoc} - */ - public function getResources() - { - return [$this]; - } - - /** - * {@inheritdoc} - */ - public function toArray() - { - $array = $this->toIdentifier(); - - if (! $this->isIdentifier()) { - $attributes = $this->getAttributes(); - if ($attributes) { - $array['attributes'] = $attributes; - } - } - - $relationships = $this->getRelationshipsAsArray(); - - if (count($relationships)) { - $array['relationships'] = $relationships; - } - - $links = []; - if (! empty($this->links)) { - $links = $this->links; - } - $serializerLinks = $this->serializer->getLinks($this->data); - if (! empty($serializerLinks)) { - $links = array_merge($serializerLinks, $links); - } - if (! empty($links)) { - $array['links'] = $links; - } - - $meta = []; - if (! empty($this->meta)) { - $meta = $this->meta; - } - $serializerMeta = $this->serializer->getMeta($this->data); - if (! empty($serializerMeta)) { - $meta = array_merge($serializerMeta, $meta); - } - if (! empty($meta)) { - $array['meta'] = $meta; - } - - return $array; - } - - /** - * Check whether or not this resource is an identifier (i.e. does it have - * any data attached?). - * - * @return bool - */ - public function isIdentifier() - { - return ! is_object($this->data) && ! is_array($this->data); - } - - /** - * {@inheritdoc} - */ - public function toIdentifier() - { - if (! $this->data) { - return; - } - - $array = [ - 'type' => $this->getType(), - 'id' => $this->getId() - ]; - - if (! empty($this->meta)) { - $array['meta'] = $this->meta; - } - - return $array; - } - - /** - * Get the resource type. - * - * @return string - */ - public function getType() - { - return $this->serializer->getType($this->data); - } - - /** - * Get the resource ID. - * - * @return string - */ - public function getId() - { - if (! is_object($this->data) && ! is_array($this->data)) { - return (string) $this->data; - } - - return (string) $this->serializer->getId($this->data); - } - - /** - * Get the resource attributes. - * - * @return array - */ - public function getAttributes() - { - $attributes = (array) $this->serializer->getAttributes($this->data, $this->getOwnFields()); - - $attributes = $this->filterFields($attributes); - - $attributes = $this->mergeAttributes($attributes); - - return $attributes; - } - - /** - * Get the requested fields for this resource type. - * - * @return array|null - */ - protected function getOwnFields() - { - $type = $this->getType(); - - if (isset($this->fields[$type])) { - return $this->fields[$type]; - } - } - - /** - * Filter the given fields array (attributes or relationships) according - * to the requested fieldset. - * - * @param array $fields - * - * @return array - */ - protected function filterFields(array $fields) - { - if ($requested = $this->getOwnFields()) { - $fields = array_intersect_key($fields, array_flip($requested)); - } - - return $fields; - } - - /** - * Merge the attributes of merged resources into an array of attributes. - * - * @param array $attributes - * - * @return array - */ - protected function mergeAttributes(array $attributes) - { - foreach ($this->merged as $resource) { - $attributes = array_replace_recursive($attributes, $resource->getAttributes()); - } - - return $attributes; - } - - /** - * Get the resource relationships. - * - * @return \Tobscure\JsonApi\Relationship[] - */ - public function getRelationships() - { - $relationships = $this->buildRelationships(); - - return $this->filterFields($relationships); - } - - /** - * Get the resource relationships without considering requested ones. - * - * @return \Tobscure\JsonApi\Relationship[] - */ - public function getUnfilteredRelationships() - { - return $this->buildRelationships(); - } - - /** - * Get the resource relationships as an array. - * - * @return array - */ - public function getRelationshipsAsArray() - { - $relationships = $this->getRelationships(); - - $relationships = $this->convertRelationshipsToArray($relationships); - - return $this->mergeRelationships($relationships); - } - - /** - * Get an array of built relationships. - * - * @return \Tobscure\JsonApi\Relationship[] - */ - protected function buildRelationships() - { - if (isset($this->relationships)) { - return $this->relationships; - } - - $paths = Util::parseRelationshipPaths($this->includes); - - $relationships = []; - - foreach ($paths as $name => $nested) { - $relationship = $this->serializer->getRelationship($this->data, $name); - - if ($relationship) { - $relationshipData = $relationship->getData(); - if ($relationshipData instanceof ElementInterface) { - $relationshipData->with($nested)->fields($this->fields); - } - - $relationships[$name] = $relationship; - } - } - - return $this->relationships = $relationships; - } - - /** - * Merge the relationships of merged resources into an array of - * relationships. - * - * @param array $relationships - * - * @return array - */ - protected function mergeRelationships(array $relationships) - { - foreach ($this->merged as $resource) { - $relationships = array_replace_recursive($relationships, $resource->getRelationshipsAsArray()); - } - - return $relationships; - } - - /** - * Convert the given array of Relationship objects into an array. - * - * @param \Tobscure\JsonApi\Relationship[] $relationships - * - * @return array - */ - protected function convertRelationshipsToArray(array $relationships) - { - return array_map(function (Relationship $relationship) { - return $relationship->toArray(); - }, $relationships); - } - - /** - * Merge a resource into this one. - * - * @param \Tobscure\JsonApi\Resource $resource - * - * @return void - */ - public function merge(Resource $resource) - { - $this->merged[] = $resource; - } - - /** - * {@inheritdoc} - */ - public function with($relationships) - { - $this->includes = array_unique(array_merge($this->includes, (array) $relationships)); - - $this->relationships = null; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function fields($fields) - { - $this->fields = $fields; - - return $this; - } - - /** - * @return mixed - */ - public function getData() - { - return $this->data; - } - - /** - * @param mixed $data - * - * @return void - */ - public function setData($data) - { - $this->data = $data; - } - - /** - * @return \Tobscure\JsonApi\SerializerInterface - */ - public function getSerializer() - { - return $this->serializer; - } - - /** - * @param \Tobscure\JsonApi\SerializerInterface $serializer - * - * @return void - */ - public function setSerializer(SerializerInterface $serializer) - { - $this->serializer = $serializer; - } -} diff --git a/src/SerializerInterface.php b/src/ResourceInterface.php similarity index 52% rename from src/SerializerInterface.php rename to src/ResourceInterface.php index 96730fa..351f7b0 100644 --- a/src/SerializerInterface.php +++ b/src/ResourceInterface.php @@ -11,61 +11,51 @@ namespace Tobscure\JsonApi; -interface SerializerInterface +interface ResourceInterface { /** - * Get the type. - * - * @param mixed $model + * Get the resource type. * * @return string */ - public function getType($model); + public function getType(); /** - * Get the id. - * - * @param mixed $model + * Get the resource ID. * * @return string */ - public function getId($model); + public function getId(); /** - * Get the attributes array. + * Get the resource attributes. * - * @param mixed $model * @param array|null $fields * * @return array */ - public function getAttributes($model, array $fields = null); + public function getAttributes(array $fields = null); /** - * Get the links array. - * - * @param mixed $model + * Get the resource links. * * @return array */ - public function getLinks($model); + public function getLinks(); /** - * Get the meta. - * - * @param mixed $model + * Get the resource meta. * * @return array */ - public function getMeta($model); + public function getMeta(); /** * Get a relationship. * - * @param mixed $model * @param string $name * * @return \Tobscure\JsonApi\Relationship|null */ - public function getRelationship($model, $name); + public function getRelationship($name); } diff --git a/src/Util.php b/src/Util.php deleted file mode 100644 index cac77a3..0000000 --- a/src/Util.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi; - -class Util -{ - /** - * Parse relationship paths. - * - * Given a flat array of relationship paths like: - * - * ['user', 'user.employer', 'user.employer.country', 'comments'] - * - * create a nested array of relationship paths one-level deep that can - * be passed on to other serializers: - * - * ['user' => ['employer', 'employer.country'], 'comments' => []] - * - * @param array $paths - * - * @return array - */ - public static function parseRelationshipPaths(array $paths) - { - $tree = []; - - foreach ($paths as $path) { - list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null); - - if (! isset($tree[$primary])) { - $tree[$primary] = []; - } - - if ($nested) { - $tree[$primary][] = $nested; - } - } - - return $tree; - } -} From f395b0110cac6df2508b0c1759a7dc6ab041b44e Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 9 Mar 2017 05:40:14 +0000 Subject: [PATCH 02/37] Apply fixes from StyleCI --- src/Document.php | 27 +++++++++++++-------------- src/Relationship.php | 4 ++-- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Document.php b/src/Document.php index 85f82d9..10ce4f2 100644 --- a/src/Document.php +++ b/src/Document.php @@ -12,7 +12,6 @@ namespace Tobscure\JsonApi; use JsonSerializable; -use LogicException; class Document implements JsonSerializable { @@ -44,14 +43,14 @@ class Document implements JsonSerializable /** * Relationships to include. - * + * * @var array */ protected $include = []; /** * Sparse fieldsets. - * + * * @var array */ protected $fields = []; @@ -66,7 +65,7 @@ public function __construct($data = null) /** * Get the data object. - * + * * @return ResourceInterface|ResourceInterface[]|null $data */ public function getData() @@ -90,7 +89,7 @@ public function setData($data) /** * Get the errors array. - * + * * @return array|null $errors */ public function getErrors() @@ -114,7 +113,7 @@ public function setErrors(array $errors = null) /** * Get the jsonapi array. - * + * * @return array|null $jsonapi */ public function getJsonapi() @@ -138,7 +137,7 @@ public function setJsonapi(array $jsonapi = null) /** * Get the relationships to include. - * + * * @return array $include */ public function getInclude() @@ -148,7 +147,7 @@ public function getInclude() /** * Set the relationships to include. - * + * * @param array $include * * @return $this @@ -162,7 +161,7 @@ public function setInclude(array $include) /** * Get the sparse fieldsets. - * + * * @return array $fields */ public function getFields() @@ -172,7 +171,7 @@ public function getFields() /** * Set the sparse fieldsets. - * + * * @param array $fields * * @return $this @@ -267,7 +266,7 @@ public function jsonSerialize() /** * Recursively add the given resources and their relationships to a map. - * + * * @param array &$map The map to merge resources into. * @param ResourceInterface[] $resources * @param array $include An array of relationship paths to include. @@ -311,7 +310,7 @@ private function addResourcesToMap(array &$map, array $resources, array $include * * If it is already present in the map, its properties will be merged into * the existing array. - * + * * @param array &$map * @param ResourceInterface $resource * @param Relationship[] $resource @@ -397,9 +396,9 @@ private function indexRelationshipPaths(array $paths) /** * Get the fields that should be included for resources of the given type. - * + * * @param string $type - * + * * @return array|null */ private function getFieldsForType($type) diff --git a/src/Relationship.php b/src/Relationship.php index 8667452..12ff618 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -87,9 +87,9 @@ public function toArray() /** * Build an idenitfier array for the given resource. - * + * * @param ResourceInterface $resource - * + * * @return array */ private function buildIdentifier(ResourceInterface $resource) From ba8a379ac957de44c38ad1c95339d086343473cd Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 26 Mar 2017 17:25:50 +1030 Subject: [PATCH 03/37] Only pull a resource out of the map if it exists --- src/Document.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Document.php b/src/Document.php index 10ce4f2..5d1c2ca 100644 --- a/src/Document.php +++ b/src/Document.php @@ -216,8 +216,10 @@ public function toArray() $type = $resource->getType(); $id = $resource->getId(); - $primary[] = $map[$type][$id]; - unset($map[$type][$id]); + if (isset($map[$type][$id])) { + $primary[] = $map[$type][$id]; + unset($map[$type][$id]); + } } $included = call_user_func_array('array_merge', $map); From d25a3f4875c1edf0add42c8cd5f4a8a895b197f3 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 26 Mar 2017 17:26:48 +1030 Subject: [PATCH 04/37] Remove some methods from public API --- src/LinksTrait.php | 2 +- src/Parameters.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 1f22171..0048d97 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -102,7 +102,7 @@ public function addPaginationLinks($url, array $queryParams, $offset, $limit, $t * * @return void */ - protected function addPaginationLink($name, $url, array $queryParams, $offset, $limit) + private function addPaginationLink($name, $url, array $queryParams, $offset, $limit) { if (! isset($queryParams['page']) || ! is_array($queryParams['page'])) { $queryParams['page'] = []; diff --git a/src/Parameters.php b/src/Parameters.php index 0280e47..2f044b2 100644 --- a/src/Parameters.php +++ b/src/Parameters.php @@ -199,7 +199,7 @@ public function getFilter() * * @return mixed */ - protected function getInput($key, $default = null) + private function getInput($key, $default = null) { return isset($this->input[$key]) ? $this->input[$key] : $default; } @@ -211,7 +211,7 @@ protected function getInput($key, $default = null) * * @return string */ - protected function getPage($key) + private function getPage($key) { $page = $this->getInput('page'); From 4e80ea80683af895f42ae8210519d951033d173f Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 26 Mar 2017 17:27:34 +1030 Subject: [PATCH 05/37] Decode pagination link query string (no reason for it to be encoded) --- src/LinksTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 0048d97..5d9437c 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -128,7 +128,7 @@ private function addPaginationLink($name, $url, array $queryParams, $offset, $li $page['limit'] = $limit; } - $queryString = http_build_query($queryParams); + $queryString = urldecode(http_build_query($queryParams)); $this->addLink($name, $url.($queryString ? '?'.$queryString : '')); } From 5430fba4c0a18f9211e7ede29324b8a06f10a980 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 26 Mar 2017 17:28:32 +1030 Subject: [PATCH 06/37] Update ResourceInterface docblocks --- src/ResourceInterface.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ResourceInterface.php b/src/ResourceInterface.php index 351f7b0..df6ba8c 100644 --- a/src/ResourceInterface.php +++ b/src/ResourceInterface.php @@ -32,21 +32,21 @@ public function getId(); * * @param array|null $fields * - * @return array + * @return array|null */ public function getAttributes(array $fields = null); /** * Get the resource links. * - * @return array + * @return array|null */ public function getLinks(); /** * Get the resource meta. * - * @return array + * @return array|null */ public function getMeta(); From 2ab0ac2d439f121a66888a21cff4bbb79413a3a0 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 26 Mar 2017 17:29:00 +1030 Subject: [PATCH 07/37] Rewrite tests --- tests/AbstractResourceTest.php | 77 +++++++++ tests/AbstractSerializerTest.php | 101 ------------ tests/AbstractTestCase.php | 6 - tests/CollectionTest.php | 65 -------- tests/DocumentTest.php | 180 +++++++++++++++------ tests/ErrorHandlerTest.php | 2 +- tests/LinksTraitTest.php | 29 ++-- tests/ParametersTest.php | 5 - tests/RelationshipTest.php | 52 ++++++ tests/ResourceTest.php | 262 ------------------------------- tests/UtilTest.php | 30 ---- 11 files changed, 271 insertions(+), 538 deletions(-) create mode 100644 tests/AbstractResourceTest.php delete mode 100644 tests/AbstractSerializerTest.php delete mode 100644 tests/CollectionTest.php create mode 100644 tests/RelationshipTest.php delete mode 100644 tests/ResourceTest.php delete mode 100644 tests/UtilTest.php diff --git a/tests/AbstractResourceTest.php b/tests/AbstractResourceTest.php new file mode 100644 index 0000000..4f35796 --- /dev/null +++ b/tests/AbstractResourceTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\Tests\JsonApi; + +use Tobscure\JsonApi\AbstractResource; +use Tobscure\JsonApi\Collection; +use Tobscure\JsonApi\Relationship; +use Tobscure\JsonApi\Resource; + +class AbstractResourceTest extends AbstractTestCase +{ + public function testGetTypeReturnsTheType() + { + $resource = new AbstractResourceStub; + + $this->assertEquals('stub', $resource->getType()); + } + + public function testGetAttributesReturnsEmptyArray() + { + $resource = new AbstractResourceStub; + + $this->assertEquals([], $resource->getAttributes()); + } + + public function testGetRelationshipReturnsRelationshipFromMethod() + { + $resource = new AbstractResourceStub; + + $relationship = $resource->getRelationship('valid'); + $this->assertTrue($relationship instanceof Relationship); + + $relationship = $resource->getRelationship('va-lid'); + $this->assertTrue($relationship instanceof Relationship); + + $relationship = $resource->getRelationship('va_lid'); + $this->assertTrue($relationship instanceof Relationship); + } + + /** + * @expectedException \LogicException + */ + public function testGetRelationshipValidatesRelationship() + { + $resource = new AbstractResourceStub; + + $resource->getRelationship('invalid'); + } +} + +class AbstractResourceStub extends AbstractResource +{ + protected $type = 'stub'; + + public function getId() + { + } + + public function valid() + { + return new Relationship(); + } + + public function invalid() + { + return 'invalid'; + } +} diff --git a/tests/AbstractSerializerTest.php b/tests/AbstractSerializerTest.php deleted file mode 100644 index 0ddc86e..0000000 --- a/tests/AbstractSerializerTest.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\Tests\JsonApi; - -use Tobscure\JsonApi\AbstractSerializer; -use Tobscure\JsonApi\Collection; -use Tobscure\JsonApi\Relationship; -use Tobscure\JsonApi\Resource; - -class AbstractSerializerTest extends AbstractTestCase -{ - public function testGetTypeReturnsTheType() - { - $serializer = new PostSerializer1; - - $this->assertEquals('posts', $serializer->getType(null)); - } - - public function testGetAttributesReturnsTheAttributes() - { - $serializer = new PostSerializer1; - $post = (object) ['foo' => 'bar']; - - $this->assertEquals(['foo' => 'bar'], $serializer->getAttributes($post)); - } - - public function testGetRelationshipReturnsRelationshipFromMethod() - { - $serializer = new PostSerializer1; - - $relationship = $serializer->getRelationship(null, 'comments'); - - $this->assertTrue($relationship instanceof Relationship); - } - - public function testGetRelationshipReturnsRelationshipFromMethodUnderscored() - { - $serializer = new PostSerializer1; - - $relationship = $serializer->getRelationship(null, 'parent_post'); - - $this->assertTrue($relationship instanceof Relationship); - } - - public function testGetRelationshipReturnsRelationshipFromMethodKebabCase() - { - $serializer = new PostSerializer1; - - $relationship = $serializer->getRelationship(null, 'parent-post'); - - $this->assertTrue($relationship instanceof Relationship); - } - - /** - * @expectedException \LogicException - */ - public function testGetRelationshipValidatesRelationship() - { - $serializer = new PostSerializer1; - - $serializer->getRelationship(null, 'invalid'); - } -} - -class PostSerializer1 extends AbstractSerializer -{ - protected $type = 'posts'; - - public function getAttributes($post, array $fields = null) - { - return ['foo' => $post->foo]; - } - - public function comments($post) - { - $element = new Collection([], new self); - - return new Relationship($element); - } - - public function parentPost($post) - { - $element = new Resource([], new self); - - return new Relationship($element); - } - - public function invalid($post) - { - return 'invalid'; - } -} diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index 4c8ac63..8a046e0 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -13,12 +13,6 @@ use PHPUnit_Framework_TestCase; -/** - * This is the abstract test case class. - * - * @author Vincent Klaiber - */ abstract class AbstractTestCase extends PHPUnit_Framework_TestCase { - // } diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php deleted file mode 100644 index ab4a866..0000000 --- a/tests/CollectionTest.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\Tests\JsonApi\Element; - -use Tobscure\JsonApi\AbstractSerializer; -use Tobscure\JsonApi\Collection; -use Tobscure\JsonApi\Resource; -use Tobscure\Tests\JsonApi\AbstractTestCase; - -/** - * This is the collection test class. - * - * @author Toby Zerner - */ -class CollectionTest extends AbstractTestCase -{ - public function testToArrayReturnsArrayOfResources() - { - $serializer = new PostSerializer3; - - $post1 = (object) ['id' => 1, 'foo' => 'bar']; - $post2 = new Resource((object) ['id' => 2, 'foo' => 'baz'], $serializer); - - $collection = new Collection([$post1, $post2], $serializer); - - $resource1 = new Resource($post1, $serializer); - $resource2 = $post2; - - $this->assertEquals([$resource1->toArray(), $resource2->toArray()], $collection->toArray()); - } - - public function testToIdentifierReturnsArrayOfResourceIdentifiers() - { - $serializer = new PostSerializer3; - - $post1 = (object) ['id' => 1]; - $post2 = (object) ['id' => 2]; - - $collection = new Collection([$post1, $post2], $serializer); - - $resource1 = new Resource($post1, $serializer); - $resource2 = new Resource($post2, $serializer); - - $this->assertEquals([$resource1->toIdentifier(), $resource2->toIdentifier()], $collection->toIdentifier()); - } -} - -class PostSerializer3 extends AbstractSerializer -{ - protected $type = 'posts'; - - public function getAttributes($post, array $fields = null) - { - return ['foo' => $post->foo]; - } -} diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index e1c6f51..ac27ff5 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -11,99 +11,177 @@ namespace Tobscure\Tests\JsonApi; -use Tobscure\JsonApi\AbstractSerializer; -use Tobscure\JsonApi\Collection; +use Tobscure\JsonApi\ResourceInterface; use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Relationship; -use Tobscure\JsonApi\Resource; -/** - * This is the document test class. - * - * @author Toby Zerner - */ class DocumentTest extends AbstractTestCase { - public function testToArrayIncludesTheResourcesRepresentation() + public function testItCanBeSerializedToJson() { - $post = (object) [ - 'id' => 1, - 'foo' => 'bar' - ]; + $this->assertEquals('[]', (string) new Document()); + } - $resource = new Resource($post, new PostSerializer2); + public function testResource() + { + $resource = $this->mockResource('a', '1'); $document = new Document($resource); - $this->assertEquals(['data' => $resource->toArray()], $document->toArray()); + $this->assertEquals([ + 'data' => ['type' => 'a', 'id' => '1'] + ], $document->toArray()); } - public function testItCanBeSerializedToJson() + public function testCollection() { - $this->assertEquals('[]', (string) new Document()); + $resource1 = $this->mockResource('a', '1'); + $resource2 = $this->mockResource('a', '2'); + + $document = new Document([$resource1, $resource2]); + + $this->assertEquals([ + 'data' => [ + ['type' => 'a', 'id' => '1'], + ['type' => 'a', 'id' => '2'] + ] + ], $document->toArray()); } - public function testToArrayIncludesIncludedResources() + public function testMergeResource() { - $comment = (object) ['id' => 1, 'foo' => 'bar']; - $post = (object) ['id' => 1, 'foo' => 'bar', 'comments' => [$comment]]; + $array1 = ['a' => 1, 'b' => 1]; + $array2 = ['a' => 2, 'c' => 2]; - $resource = new Resource($post, new PostSerializer2); - $includedResource = new Resource($comment, new CommentSerializer2); + $resource1 = $this->mockResource('a', '1', $array1, $array1, $array1); + $resource2 = $this->mockResource('a', '1', $array2, $array2, $array2); - $document = new Document($resource->with('comments')); + $document = new Document([$resource1, $resource2]); $this->assertEquals([ - 'data' => $resource->toArray(), - 'included' => [ - $includedResource->toArray() + 'data' => [ + [ + 'type' => 'a', + 'id' => '1', + 'attributes' => $merged = array_merge($array1, $array2), + 'meta' => $merged, + 'links' => $merged + ] ] ], $document->toArray()); } - public function testNoEmptyAttributes() + public function testSparseFieldsets() { - $post = (object) [ - 'id' => 1, - ]; + $resource = $this->mockResource('a', '1', ['present' => 1, 'absent' => 1]); - $resource = new Resource($post, new PostSerializerEmptyAttributes2); + $resource->expects($this->once())->method('getAttributes')->with($this->equalTo(['present'])); $document = new Document($resource); + $document->setFields(['a' => ['present']]); - $this->assertEquals('{"data":{"type":"posts","id":"1"}}', (string) $document, 'Attributes should be omitted'); + $this->assertEquals([ + 'data' => [ + 'type' => 'a', + 'id' => '1', + 'attributes' => ['present' => 1] + ] + ], $document->toArray()); } -} -class PostSerializer2 extends AbstractSerializer -{ - protected $type = 'posts'; + public function testIncludeRelationships() + { + $resource1 = $this->mockResource('a', '1'); + $resource2 = $this->mockResource('a', '2'); + $resource3 = $this->mockResource('b', '1'); + + $relationshipArray = ['data' => 'stub']; - public function getAttributes($post, array $fields = null) + $relationshipA = $this->getMock(Relationship::class); + $relationshipA->method('getData')->willReturn($resource2); + $relationshipA->method('toArray')->willReturn($relationshipArray); + + $relationshipB = $this->getMock(Relationship::class); + $relationshipB->method('getData')->willReturn($resource3); + $relationshipB->method('toArray')->willReturn($relationshipArray); + + $resource1 + ->expects($this->once()) + ->method('getRelationship') + ->with($this->equalTo('a')) + ->willReturn($relationshipA); + + $resource2 + ->expects($this->once()) + ->method('getRelationship') + ->with($this->equalTo('b')) + ->willReturn($relationshipB); + + $document = new Document($resource1); + $document->setInclude(['a', 'a.b']); + + $this->assertEquals([ + 'data' => [ + 'type' => 'a', + 'id' => '1', + 'relationships' => ['a' => $relationshipArray] + ], + 'included' => [ + [ + 'type' => 'b', + 'id' => '1' + ], + [ + 'type' => 'a', + 'id' => '2', + 'relationships' => ['b' => $relationshipArray] + ] + ] + ], $document->toArray()); + } + + public function testErrors() { - return ['foo' => $post->foo]; + $document = new Document(); + $document->setErrors(['a']); + + $this->assertEquals(['errors' => ['a']], $document->toArray()); } - public function comments($post) + public function testJsonapi() { - return new Relationship(new Collection($post->comments, new CommentSerializer2)); + $document = new Document(); + $document->setJsonapi(['a']); + + $this->assertEquals(['jsonapi' => ['a']], $document->toArray()); } -} -class PostSerializerEmptyAttributes2 extends PostSerializer2 -{ - public function getAttributes($post, array $fields = null) + public function testLinks() { - return []; + $document = new Document(); + $document->setLinks(['a']); + + $this->assertEquals(['links' => ['a']], $document->toArray()); } -} -class CommentSerializer2 extends AbstractSerializer -{ - protected $type = 'comments'; + public function testMeta() + { + $document = new Document(); + $document->setMeta(['a']); + + $this->assertEquals(['meta' => ['a']], $document->toArray()); + } - public function getAttributes($comment, array $fields = null) + private function mockResource($type, $id, $attributes = [], $meta = [], $links = []) { - return ['foo' => $comment->foo]; + $mock = $this->getMock(ResourceInterface::class); + + $mock->method('getType')->willReturn($type); + $mock->method('getId')->willReturn($id); + $mock->method('getAttributes')->willReturn($attributes); + $mock->method('getMeta')->willReturn($meta); + $mock->method('getLinks')->willReturn($links); + + return $mock; } } diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php index b2b9583..4d49a98 100644 --- a/tests/ErrorHandlerTest.php +++ b/tests/ErrorHandlerTest.php @@ -16,7 +16,7 @@ class ErrorHandlerTest extends AbstractTestCase { - public function test_it_should_throw_an_exception_when_no_handlers_are_present() + public function testThrowExceptionWhenNoHandlersPresent() { $this->setExpectedException('RuntimeException'); diff --git a/tests/LinksTraitTest.php b/tests/LinksTraitTest.php index 5d80bb3..fef56a3 100644 --- a/tests/LinksTraitTest.php +++ b/tests/LinksTraitTest.php @@ -13,46 +13,41 @@ use Tobscure\JsonApi\LinksTrait; -/** - * This is the document test class. - * - * @author Toby Zerner - */ class LinksTraitTest extends AbstractTestCase { public function testAddPaginationLinks() { - $document = new Document; + $document = new LinksTraitStub; $document->addPaginationLinks('http://example.org', [], 0, 20); $this->assertEquals([ 'first' => 'http://example.org', - 'next' => 'http://example.org?page%5Boffset%5D=20' + 'next' => 'http://example.org?page[offset]=20' ], $document->getLinks()); - $document = new Document; + $document = new LinksTraitStub; $document->addPaginationLinks('http://example.org', ['foo' => 'bar', 'page' => ['limit' => 20]], 30, 20, 100); $this->assertEquals([ - 'first' => 'http://example.org?foo=bar&page%5Blimit%5D=20', - 'prev' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=10', - 'next' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=50', - 'last' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=80' + 'first' => 'http://example.org?foo=bar&page[limit]=20', + 'prev' => 'http://example.org?foo=bar&page[limit]=20&page[offset]=10', + 'next' => 'http://example.org?foo=bar&page[limit]=20&page[offset]=50', + 'last' => 'http://example.org?foo=bar&page[limit]=20&page[offset]=80' ], $document->getLinks()); - $document = new Document; + $document = new LinksTraitStub; $document->addPaginationLinks('http://example.org', ['page' => ['number' => 2]], 50, 20, 100); $this->assertEquals([ 'first' => 'http://example.org', - 'prev' => 'http://example.org?page%5Bnumber%5D=2', - 'next' => 'http://example.org?page%5Bnumber%5D=4', - 'last' => 'http://example.org?page%5Bnumber%5D=5' + 'prev' => 'http://example.org?page[number]=2', + 'next' => 'http://example.org?page[number]=4', + 'last' => 'http://example.org?page[number]=5' ], $document->getLinks()); } } -class Document +class LinksTraitStub { use LinksTrait; } diff --git a/tests/ParametersTest.php b/tests/ParametersTest.php index e38362b..b365e47 100644 --- a/tests/ParametersTest.php +++ b/tests/ParametersTest.php @@ -13,11 +13,6 @@ use Tobscure\JsonApi\Parameters; -/** - * This is the parameters test class. - * - * @author Toby Zerner - */ class ParametersTest extends AbstractTestCase { public function testGetIncludeReturnsArrayOfIncludes() diff --git a/tests/RelationshipTest.php b/tests/RelationshipTest.php new file mode 100644 index 0000000..0cf22cb --- /dev/null +++ b/tests/RelationshipTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\Tests\JsonApi; + +use Tobscure\JsonApi\Relationship; +use Tobscure\JsonApi\AbstractResource; + +class RelationshipTest extends AbstractTestCase +{ + public function testToArray() + { + $resource1 = new RelationshipResourceStub(); + $resource2 = new RelationshipResourceStub(); + + $relationship = new Relationship($resource1); + + $this->assertEquals([ + 'data' => ['type' => 'stub', 'id' => '1'] + ], $relationship->toArray()); + + $relationship = new Relationship([$resource1, $resource2]); + + $this->assertEquals([ + 'data' => [ + ['type' => 'stub', 'id' => '1'], + ['type' => 'stub', 'id' => '1'] + ] + ], $relationship->toArray()); + } +} + +class RelationshipResourceStub extends AbstractResource +{ + public function getType() + { + return 'stub'; + } + + public function getId() + { + return '1'; + } +} diff --git a/tests/ResourceTest.php b/tests/ResourceTest.php deleted file mode 100644 index 63246ca..0000000 --- a/tests/ResourceTest.php +++ /dev/null @@ -1,262 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\Tests\JsonApi\Element; - -use Tobscure\JsonApi\AbstractSerializer; -use Tobscure\JsonApi\Collection; -use Tobscure\JsonApi\Relationship; -use Tobscure\JsonApi\Resource; -use Tobscure\Tests\JsonApi\AbstractTestCase; - -class ResourceTest extends AbstractTestCase -{ - public function testToArrayReturnsArray() - { - $data = (object) ['id' => '123', 'foo' => 'bar', 'baz' => 'qux']; - - $resource = new Resource($data, new PostSerializer4WithLinksAndMeta); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'attributes' => [ - 'foo' => 'bar', - 'baz' => 'qux' - ], - 'links' => [ - 'self' => '/posts/123' - ], - 'meta' => [ - 'some-meta' => 'from-serializer-for-123' - ] - ], $resource->toArray()); - } - - public function testToIdentifierReturnsResourceIdentifier() - { - $data = (object) ['id' => '123', 'foo' => 'bar']; - - $resource = new Resource($data, new PostSerializer4); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123' - ], $resource->toIdentifier()); - - $resource->addMeta('foo', 'bar'); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'meta' => ['foo' => 'bar'] - ], $resource->toIdentifier()); - } - - public function testGetIdReturnsString() - { - $data = (object) ['id' => 123]; - - $resource = new Resource($data, new PostSerializer4); - - $this->assertSame('123', $resource->getId()); - } - - public function testGetIdWorksWithScalarData() - { - $resource = new Resource(123, new PostSerializer4); - - $this->assertSame('123', $resource->getId()); - } - - public function testCanFilterFields() - { - $data = (object) ['id' => '123', 'foo' => 'bar', 'baz' => 'qux']; - - $resource = new Resource($data, new PostSerializer4); - - $resource->fields(['posts' => ['baz']]); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'attributes' => [ - 'baz' => 'qux' - ] - ], $resource->toArray()); - } - - public function testCanMergeWithAnotherResource() - { - $post1 = (object) ['id' => '123', 'foo' => 'bar', 'comments' => [1]]; - $post2 = (object) ['id' => '123', 'baz' => 'qux', 'comments' => [1, 2]]; - - $resource1 = new Resource($post1, new PostSerializer4); - $resource2 = new Resource($post2, new PostSerializer4); - - $resource1->with(['comments']); - $resource2->with(['comments']); - - $resource1->merge($resource2); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'attributes' => [ - 'baz' => 'qux', - 'foo' => 'bar' - ], - 'relationships' => [ - 'comments' => [ - 'data' => [ - ['type' => 'comments', 'id' => '1'], - ['type' => 'comments', 'id' => '2'] - ] - ] - ] - ], $resource1->toArray()); - } - - public function testLinksMergeWithSerializerLinks() - { - $post1 = (object) ['id' => '123', 'foo' => 'bar', 'comments' => [1]]; - - $resource1 = new Resource($post1, new PostSerializer4WithLinksAndMeta()); - $resource1->addLink('self', 'overridden/by/resource'); - $resource1->addLink('related', '/some/other/comment'); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'attributes' => [ - 'foo' => 'bar' - ], - 'links' => [ - 'self' => 'overridden/by/resource', - 'related' => '/some/other/comment' - ], - 'meta' => [ - 'some-meta' => 'from-serializer-for-123' - ] - ], $resource1->toArray()); - } - - public function testMetaMergeWithSerializerLinks() - { - $post1 = (object) ['id' => '123', 'foo' => 'bar', 'comments' => [1]]; - - $resource1 = new Resource($post1, new PostSerializer4WithLinksAndMeta()); - $resource1->addMeta('some-meta', 'overridden-by-resource'); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'attributes' => [ - 'foo' => 'bar' - ], - 'links' => [ - 'self' => '/posts/123' - ], - 'meta' => [ - 'some-meta' => 'overridden-by-resource' - ] - ], $resource1->toArray()); - } - - public function testEmptyToOneRelationships() - { - $post1 = (object) ['id' => '123', 'foo' => 'bar']; - - $resource1 = new Resource($post1, new PostSerializer4()); - $resource1->with('author'); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'attributes' => [ - 'foo' => 'bar' - ], - 'relationships' => [ - 'author' => ['data' => null] - ] - ], $resource1->toArray()); - } - - public function testEmptyToManyRelationships() - { - $post1 = (object) ['id' => '123', 'foo' => 'bar']; - - $resource1 = new Resource($post1, new PostSerializer4()); - $resource1->with('likes'); - - $this->assertEquals([ - 'type' => 'posts', - 'id' => '123', - 'attributes' => [ - 'foo' => 'bar' - ], - 'relationships' => [ - 'likes' => ['data' => []] - ] - ], $resource1->toArray()); - } -} - -class PostSerializer4 extends AbstractSerializer -{ - protected $type = 'posts'; - - public function getAttributes($post, array $fields = null) - { - $attributes = []; - - if (isset($post->foo)) { - $attributes['foo'] = $post->foo; - } - if (isset($post->baz)) { - $attributes['baz'] = $post->baz; - } - - return $attributes; - } - - public function comments($post) - { - return new Relationship(new Collection($post->comments, new CommentSerializer)); - } - - public function author($post) - { - return new Relationship(new Resource(null, new CommentSerializer)); - } - - public function likes($post) - { - return new Relationship(new Collection([], new CommentSerializer)); - } -} -class PostSerializer4WithLinksAndMeta extends PostSerializer4 -{ - public function getLinks($post) - { - return ['self' => sprintf('/posts/%s', $post->id)]; - } - - public function getMeta($post) - { - return ['some-meta' => sprintf('from-serializer-for-%s', $post->id)]; - } -} - -class CommentSerializer extends AbstractSerializer -{ - protected $type = 'comments'; -} diff --git a/tests/UtilTest.php b/tests/UtilTest.php deleted file mode 100644 index 48f979b..0000000 --- a/tests/UtilTest.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\Tests\JsonApi; - -use Tobscure\JsonApi\Util; - -class UtilTest extends AbstractTestCase -{ - public function testParseRelationshipPaths() - { - $this->assertEquals( - ['user' => ['employer', 'employer.country'], 'comments' => []], - Util::parseRelationshipPaths(['user', 'user.employer', 'user.employer.country', 'comments']) - ); - - $this->assertEquals( - ['user' => ['employer.country']], - Util::parseRelationshipPaths(['user.employer.country']) - ); - } -} From 946cc45bfa1b932ee374e7152c4ac02ca78868a0 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 26 Mar 2017 07:00:13 +0000 Subject: [PATCH 08/37] Apply fixes from StyleCI --- tests/AbstractResourceTest.php | 2 -- tests/DocumentTest.php | 2 +- tests/RelationshipTest.php | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/AbstractResourceTest.php b/tests/AbstractResourceTest.php index 4f35796..c7d4a9f 100644 --- a/tests/AbstractResourceTest.php +++ b/tests/AbstractResourceTest.php @@ -12,9 +12,7 @@ namespace Tobscure\Tests\JsonApi; use Tobscure\JsonApi\AbstractResource; -use Tobscure\JsonApi\Collection; use Tobscure\JsonApi\Relationship; -use Tobscure\JsonApi\Resource; class AbstractResourceTest extends AbstractTestCase { diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index ac27ff5..a150f73 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -11,9 +11,9 @@ namespace Tobscure\Tests\JsonApi; -use Tobscure\JsonApi\ResourceInterface; use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Relationship; +use Tobscure\JsonApi\ResourceInterface; class DocumentTest extends AbstractTestCase { diff --git a/tests/RelationshipTest.php b/tests/RelationshipTest.php index 0cf22cb..1749496 100644 --- a/tests/RelationshipTest.php +++ b/tests/RelationshipTest.php @@ -11,8 +11,8 @@ namespace Tobscure\Tests\JsonApi; -use Tobscure\JsonApi\Relationship; use Tobscure\JsonApi\AbstractResource; +use Tobscure\JsonApi\Relationship; class RelationshipTest extends AbstractTestCase { From 6f9cf92e33d88f56ad369de2ba0462a8d24dfcd9 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 26 Mar 2017 17:44:17 +1030 Subject: [PATCH 09/37] Drop support for PHPUnit 5.0 for now As long as we support PHP 5.5 + PHPUnit 4.8, we need to use getMock in our tests, which is deprecated in 5.0 and thus causing tests to fail. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3061728..69aa0bb 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "^5.5.9 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" + "phpunit/phpunit": "^4.8" }, "autoload": { "psr-4": { From 24f092d68e0c23ae2ae5c9e48ba2c8948a8f9823 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 16:57:24 +1030 Subject: [PATCH 10/37] Update README --- README.md | 154 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 311cdda..b02d041 100644 --- a/README.md +++ b/README.md @@ -22,93 +22,124 @@ composer require tobscure/json-api ```php use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Collection; -// Create a new collection of posts, and specify relationships to be included. -$collection = (new Collection($posts, new PostSerializer)) - ->with(['author', 'comments']); +// Create a new resource to represent a post. +$resource = new PostResource($post); -// Create a new JSON-API document with that collection as the data. -$document = new Document($collection); +// Create a new JSON-API document with that resource as the data, and specify +// relationships to be included. +$document = new Document($resource); +$document->setInclude(['author', 'comments']); // Add metadata and links. $document->addMeta('total', count($posts)); $document->addLink('self', 'http://example.com/api/posts'); // Output the document as JSON. -echo json_encode($document); +echo $document; ``` -### Elements +### Resources & Collections -The JSON-API spec describes *resource objects* as objects containing information about a single resource, and *collection objects* as objects containing information about many resources. In this package: +The JSON-API spec describes *resource objects* as objects containing information about a single resource. A resource object is represented by the `Tobscure\JsonApi\ResourceInterface` interface. An `AbstractResource` class is provided with some basic functionality. At a minimum, subclasses must specify the resource `$type` and implement the `getId()` method: -- `Tobscure\JsonApi\Resource` represents a *resource object* -- `Tobscure\JsonApi\Collection` represents a *collection object* +```php +use Tobscure\JsonApi\AbstractResource; -Both Resources and Collections are termed as *Elements*. In conceptually the same way that the JSON-API spec describes, a Resource may have **relationships** with any number of other Elements (Resource for has-one relationships, Collection for has-many). Similarly, a Collection may contain many Resources. +class PostResource extends AbstractResource +{ + protected $type = 'posts'; -A JSON-API Document may contain one primary Element. The primary Element will be recursively parsed for relationships with other Elements; these Elements will be added to the Document as **included** resources. + protected $post; -#### Sparse Fieldsets + public function __construct(Post $post) + { + $this->post = $post; + } -You can specify which fields (attributes and relationships) are to be included on an Element using the `fields` method. You must provide a multidimensional array organized by resource type: + public function getId() + { + return $this->post->id; + } +} +``` + +An instantiated resource object can then be added to the JSON-API document: ```php -$collection->fields(['posts' => ['title', 'date']]); -``` +$resource = new PostResource($post); -### Serializers +$document = new Document($resource); +``` -A Serializer is responsible for building attributes and relationships for a certain resource type. Serializers must implement `Tobscure\JsonApi\SerializerInterface`. An `AbstractSerializer` is provided with some basic functionality. At a minimum, a serializer must specify its **type** and provide a method to transform **attributes**: +To create a collection of resources, simply map your data to an array of Resource objects: ```php -use Tobscure\JsonApi\AbstractSerializer; +$collection = array_map(function (Post $post) { + return new PostResource($post); +}, $posts); -class PostSerializer extends AbstractSerializer -{ - protected $type = 'posts'; +$document = new Document($collection); +``` + +#### Attributes & Sparse Fieldsets + +Resource objects may implement a `getAttributes()` method to specify attributes: - public function getAttributes($post, array $fields = null) +```php + public function getAttributes(array $fields = null) { return [ - 'title' => $post->title, - 'body' => $post->body, - 'date' => $post->date + 'title' => $this->post->title, + 'body' => $this->post->body, + 'date' => $this->post->date ]; } -} ``` -By default, a Resource object's **id** attribute will be set as the `id` property on the model. A serializer can provide a method to override this: +For sparse fieldsets, you may specify which fields (attributes and relationships) are to be included on the Document. You must provide a multidimensional array organized by resource type: ```php -public function getId($post) -{ - return $post->someOtherKey; -} +$document->setFields(['posts' => ['title', 'body']]); ``` -#### Relationships +The attributes returned by your resources will automatically be filtered according to the sparse fieldset for the resource type. If some attributes are expensive to calculate, then you may use the `$fields` argument to improve performance when sparse fieldsets are used. This argument will be `null` if no sparse fieldset has been specified for the resource type, or an `array` of fields if it has: -The `AbstractSerializer` allows you to define a public method for each relationship that exists for a resource. A relationship method should return a `Tobscure\JsonApi\Relationship` instance. +```php + public function getAttributes(array $fields = null) + { + // Calculate the "expensive" attribute only if this field will show up + // in the final output + if ($fields === null || in_array('expensive', $fields)) { + $attributes['expensive'] = $this->getExpensiveAttribute(); + } + + return $attributes; + } +``` + +#### Relationships + +A JSON-API Document may contain one primary resource, or a collection of resources. The primary resource(s) will be recursively parsed for relationships with other resources; these resources will be added to the Document as **included** resources. + +The `AbstractResource` class allows you to define a public method for each relationship that exists for a resource. A relationship method should return a `Tobscure\JsonApi\Relationship` instance. ```php -public function comments($post) -{ - $element = new Collection($post->comments, new CommentSerializer); + public function author() + { + $resource = new UserResource($this->post->author); - return new Relationship($element); -} + return new Relationship($resource); + } ``` -By default, the `AbstractSerializer` will convert relationship names from `kebab-case` and `snake_case` into a `camelCase` method name and call that on the serializer. If you wish to customize this behaviour, you may override the `getRelationship` method: +By default, the `AbstractResource` will convert relationship names from `kebab-case` and `snake_case` into a `camelCase` method name. If you wish to customize this behaviour, you may override the `getRelationship` method: ```php -public function getRelationship($model, $name) -{ - // resolve Relationship called $name for $model -} + public function getRelationship($name) + { + // resolve Relationship for $name + } ``` ### Meta & Links @@ -124,7 +155,7 @@ $document->setMeta(['key' => 'value']); They also allow you to add links in a similar way: ```php -$resource = new Resource($data, $serializer); +$resource = new PostResource($post); $resource->addLink('self', 'url'); $resource->setLinks(['key' => 'value']); ``` @@ -140,27 +171,26 @@ $document->addPaginationLinks( 100 // The total number of results ); ``` -Serializers can provide links and/or meta data as well: + +To define metadata and/or links on resources implicitly, call the appropriate methods in the constructor: ```php -use Tobscure\JsonApi\AbstractSerializer; +use Tobscure\JsonApi\AbstractResource; -class PostSerializer extends AbstractSerializer -{ - // ... - - public function getLinks($post) { - return ['self' => '/posts/' . $post->id]; - } +class PostResource extends AbstractResource +{ + public function __construct(Post $post) + { + $this->post = $post; - public function getMeta($post) { - return ['some' => 'metadata for ' . $post->id]; + $this->addLink('self', '/posts/' . $post->id); + $this->addMeta('some', 'metadata for ' . $post->id); } + + // ... } ``` -**Note:** Links and metadata of the resource overrule ones with the same key from the serializer! - ### Parameters The `Tobscure\JsonApi\Parameters` class allows you to easily parse and validate query parameters in accordance with the specification. @@ -234,6 +264,10 @@ try { } ``` +## Examples + +* [Flarum](https://github.com/flarum/core/tree/master/src/Api) is forum software that uses tobscure/json-api to power its API. + ## Contributing Feel free to send pull requests or create issues if you come across problems or have great ideas. Any input is appreciated! @@ -241,7 +275,7 @@ Feel free to send pull requests or create issues if you come across problems or ### Running Tests ```bash -$ phpunit +$ vendor/bin/phpunit ``` ## License From a350d254cad0ef59626a91851f8f7eb05653e083 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 20:42:45 +1030 Subject: [PATCH 11/37] Add some missing docs --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b02d041..20ed5e3 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ use Tobscure\JsonApi\Document; // Create a new resource to represent a post. $resource = new PostResource($post); -// Create a new JSON-API document with that resource as the data, and specify -// relationships to be included. +// Create a new JSON-API document with that resource as the data, and specify relationships to be included. $document = new Document($resource); $document->setInclude(['author', 'comments']); @@ -36,6 +35,7 @@ $document->addMeta('total', count($posts)); $document->addLink('self', 'http://example.com/api/posts'); // Output the document as JSON. +header('Content-Type: ' . $document::MEDIA_TYPE); echo $document; ``` @@ -142,6 +142,12 @@ By default, the `AbstractResource` will convert relationship names from `kebab-c } ``` +Once relationships are defined, you can specify which relationships should be included on the Document: + +```php +$document->setInclude(['author', 'comments', 'comments.author']); +``` + ### Meta & Links The `Document`, `Resource`, and `Relationship` classes allow you to add meta information: From 0d66e3b593ce1fbba5127b7e82411519d8fcfaca Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 20:43:53 +1030 Subject: [PATCH 12/37] Formatting --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 20ed5e3..dd17ba9 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,10 @@ use Tobscure\JsonApi\Document; // Create a new resource to represent a post. $resource = new PostResource($post); -// Create a new JSON-API document with that resource as the data, and specify relationships to be included. +// Create a new JSON-API document with that resource as the data. $document = new Document($resource); + +// Specify relationships to be included. $document->setInclude(['author', 'comments']); // Add metadata and links. From ea20221924eb1b345569121036f16c4c1e26abf7 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 20:44:58 +1030 Subject: [PATCH 13/37] Add sparse fieldsets to example --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd17ba9..eff48d8 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,9 @@ $resource = new PostResource($post); // Create a new JSON-API document with that resource as the data. $document = new Document($resource); -// Specify relationships to be included. +// Specify relationships and fields to be included. $document->setInclude(['author', 'comments']); +$document->setFields(['posts' => ['title', 'body']]); // Add metadata and links. $document->addMeta('total', count($posts)); From 2a3c32aca32dfd613b61af9dc3b040e98ab5798a Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 20:45:30 +1030 Subject: [PATCH 14/37] Tweak wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eff48d8..9c062b7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ $resource = new PostResource($post); // Create a new JSON-API document with that resource as the data. $document = new Document($resource); -// Specify relationships and fields to be included. +// Specify included relationships and sparse fieldsets. $document->setInclude(['author', 'comments']); $document->setFields(['posts' => ['title', 'body']]); From cb7702e464791f17dd446c22ca8fae5d960500e8 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 20:46:33 +1030 Subject: [PATCH 15/37] More tweaks --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c062b7..34810a4 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ composer require tobscure/json-api ```php use Tobscure\JsonApi\Document; -// Create a new resource to represent a post. +// Create a resource object for a post. $resource = new PostResource($post); -// Create a new JSON-API document with that resource as the data. +// Create a JSON-API document with that resource as the primary data. $document = new Document($resource); // Specify included relationships and sparse fieldsets. @@ -37,7 +37,7 @@ $document->setFields(['posts' => ['title', 'body']]); $document->addMeta('total', count($posts)); $document->addLink('self', 'http://example.com/api/posts'); -// Output the document as JSON. +// Output the document with the JSON-API media type. header('Content-Type: ' . $document::MEDIA_TYPE); echo $document; ``` From c7b8a326cdb07c69bad79a21b43b1c4b5f23deb1 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 21:14:28 +1030 Subject: [PATCH 16/37] Revert "Decode pagination link query string (no reason for it to be encoded)" This reverts commit 4e80ea80683af895f42ae8210519d951033d173f. --- src/LinksTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 5d9437c..0048d97 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -128,7 +128,7 @@ private function addPaginationLink($name, $url, array $queryParams, $offset, $li $page['limit'] = $limit; } - $queryString = urldecode(http_build_query($queryParams)); + $queryString = http_build_query($queryParams); $this->addLink($name, $url.($queryString ? '?'.$queryString : '')); } From 880a28d10fc935801e349f63ea99bf33771eebe3 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 21:17:17 +1030 Subject: [PATCH 17/37] Reverse non-encoding of pagination URLs (should've read the spec more carefully!) --- tests/LinksTraitTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/LinksTraitTest.php b/tests/LinksTraitTest.php index fef56a3..1f55b8c 100644 --- a/tests/LinksTraitTest.php +++ b/tests/LinksTraitTest.php @@ -22,17 +22,17 @@ public function testAddPaginationLinks() $this->assertEquals([ 'first' => 'http://example.org', - 'next' => 'http://example.org?page[offset]=20' + 'next' => 'http://example.org?page%5Boffset%5D=20' ], $document->getLinks()); $document = new LinksTraitStub; $document->addPaginationLinks('http://example.org', ['foo' => 'bar', 'page' => ['limit' => 20]], 30, 20, 100); $this->assertEquals([ - 'first' => 'http://example.org?foo=bar&page[limit]=20', - 'prev' => 'http://example.org?foo=bar&page[limit]=20&page[offset]=10', - 'next' => 'http://example.org?foo=bar&page[limit]=20&page[offset]=50', - 'last' => 'http://example.org?foo=bar&page[limit]=20&page[offset]=80' + 'first' => 'http://example.org?foo=bar&page%5Blimit%5D=20', + 'prev' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=10', + 'next' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=50', + 'last' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=80' ], $document->getLinks()); $document = new LinksTraitStub; @@ -40,9 +40,9 @@ public function testAddPaginationLinks() $this->assertEquals([ 'first' => 'http://example.org', - 'prev' => 'http://example.org?page[number]=2', - 'next' => 'http://example.org?page[number]=4', - 'last' => 'http://example.org?page[number]=5' + 'prev' => 'http://example.org?page%5Bnumber%5D=2', + 'next' => 'http://example.org?page%5Bnumber%5D=4', + 'last' => 'http://example.org?page%5Bnumber%5D=5' ], $document->getLinks()); } } From ac96c04acc023a9488058185f19f9861575a2897 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 21:25:26 +1030 Subject: [PATCH 18/37] Support page[size] in pagination links --- src/LinksTrait.php | 2 ++ tests/LinksTraitTest.php | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 0048d97..34fa367 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -126,6 +126,8 @@ private function addPaginationLink($name, $url, array $queryParams, $offset, $li if (isset($page['limit'])) { $page['limit'] = $limit; + } elseif (isset($page['size'])) { + $page['size'] = $limit; } $queryString = http_build_query($queryParams); diff --git a/tests/LinksTraitTest.php b/tests/LinksTraitTest.php index 1f55b8c..7b8861e 100644 --- a/tests/LinksTraitTest.php +++ b/tests/LinksTraitTest.php @@ -44,6 +44,15 @@ public function testAddPaginationLinks() 'next' => 'http://example.org?page%5Bnumber%5D=4', 'last' => 'http://example.org?page%5Bnumber%5D=5' ], $document->getLinks()); + + $document = new LinksTraitStub; + $document->addPaginationLinks('http://example.org', ['page' => ['number' => 3, 'size' => 1]], 2, 1, 2); + + $this->assertEquals([ + 'first' => 'http://example.org?page%5Bsize%5D=1', + 'prev' => 'http://example.org?page%5Bnumber%5D=2&page%5Bsize%5D=1', + 'last' => 'http://example.org?page%5Bnumber%5D=2&page%5Bsize%5D=1' + ], $document->getLinks()); } } From a071a76691eea44e3cc88a8aeee31239eb72daba Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 23:43:24 +1030 Subject: [PATCH 19/37] Improve README; add references to JSON-API spec --- README.md | 65 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 34810a4..d8aaf23 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,9 @@ echo $document; ### Resources & Collections -The JSON-API spec describes *resource objects* as objects containing information about a single resource. A resource object is represented by the `Tobscure\JsonApi\ResourceInterface` interface. An `AbstractResource` class is provided with some basic functionality. At a minimum, subclasses must specify the resource `$type` and implement the `getId()` method: +The JSON-API spec describes [resource objects](http://jsonapi.org/format/#document-resource-objects) as objects representing about a single resource. A resource object is represented by the `Tobscure\JsonApi\ResourceInterface` interface. + +You should create a class which implements this interface for each resource type in your API. A base `AbstractResource` class is provided with some basic functionality. At a minimum, subclasses must specify the resource `$type` and implement the `getId()` method: ```php use Tobscure\JsonApi\AbstractResource; @@ -73,9 +75,11 @@ An instantiated resource object can then be added to the JSON-API document: $resource = new PostResource($post); $document = new Document($resource); + +echo $document; // {"data": {"type": "posts", "id": "1"}} ``` -To create a collection of resources, simply map your data to an array of Resource objects: +To output a collection of resources, map your data to an array of Resource objects: ```php $collection = array_map(function (Post $post) { @@ -87,7 +91,7 @@ $document = new Document($collection); #### Attributes & Sparse Fieldsets -Resource objects may implement a `getAttributes()` method to specify attributes: +To add [attributes](http://jsonapi.org/format/#document-resource-object-attributes) to your resources, you may implement the `getAttributes()` method: ```php public function getAttributes(array $fields = null) @@ -100,13 +104,13 @@ Resource objects may implement a `getAttributes()` method to specify attributes: } ``` -For sparse fieldsets, you may specify which fields (attributes and relationships) are to be included on the Document. You must provide a multidimensional array organized by resource type: +To support [sparse fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets), you may specify which [fields](http://jsonapi.org/format/#document-resource-object-fields) (attributes and relationships) are to be included on the Document. You must provide a multidimensional array organized by resource type: ```php $document->setFields(['posts' => ['title', 'body']]); ``` -The attributes returned by your resources will automatically be filtered according to the sparse fieldset for the resource type. If some attributes are expensive to calculate, then you may use the `$fields` argument to improve performance when sparse fieldsets are used. This argument will be `null` if no sparse fieldset has been specified for the resource type, or an `array` of fields if it has: +The attributes returned by your Resources will automatically be filtered according to the sparse fieldset for the resource type. However, if some attributes are expensive to calculate, then you may use the `$fields` argument provided to `getAttributes()` to improve performance when sparse fieldsets are used. This argument will be `null` if no sparse fieldset has been specified for the resource type, or an `array` of fields if it has: ```php public function getAttributes(array $fields = null) @@ -123,12 +127,10 @@ The attributes returned by your resources will automatically be filtered accordi #### Relationships -A JSON-API Document may contain one primary resource, or a collection of resources. The primary resource(s) will be recursively parsed for relationships with other resources; these resources will be added to the Document as **included** resources. - -The `AbstractResource` class allows you to define a public method for each relationship that exists for a resource. A relationship method should return a `Tobscure\JsonApi\Relationship` instance. +To support the [inclusion of related resources](http://jsonapi.org/format/#fetching-includes) alongside the document's primary resources (and output [compound documents](http://jsonapi.org/format/#document-compound-documents)), first you must define the available relationships on your Resource implementation. The `AbstractResource` base class allows you to define a method for each relationship that exists for a resource type. Relationship methods should return a `Tobscure\JsonApi\Relationship` instance, containing the related Resource(s). ```php - public function author() + protected function author() { $resource = new UserResource($this->post->author); @@ -136,24 +138,24 @@ The `AbstractResource` class allows you to define a public method for each relat } ``` -By default, the `AbstractResource` will convert relationship names from `kebab-case` and `snake_case` into a `camelCase` method name. If you wish to customize this behaviour, you may override the `getRelationship` method: +You can then specify which relationships should be included on the Document: ```php - public function getRelationship($name) - { - // resolve Relationship for $name - } +$document->setInclude(['author', 'comments', 'comments.author']); ``` -Once relationships are defined, you can specify which relationships should be included on the Document: +By default, the `AbstractResource` implementation will convert included relationship names from `kebab-case` and `snake_case` into a `camelCase` method name. If you wish to customize this behaviour, you may override the `getRelationship` method: ```php -$document->setInclude(['author', 'comments', 'comments.author']); + public function getRelationship($name) + { + // resolve Relationship for $name + } ``` -### Meta & Links +### Meta Information & Links -The `Document`, `Resource`, and `Relationship` classes allow you to add meta information: +The `Document`, `Resource`, and `Relationship` classes allow you to add [meta information](http://jsonapi.org/format/#document-meta): ```php $document = new Document; @@ -161,7 +163,7 @@ $document->addMeta('key', 'value'); $document->setMeta(['key' => 'value']); ``` -They also allow you to add links in a similar way: +They also allow you to add [links](http://jsonapi.org/format/#document-links) in a similar way: ```php $resource = new PostResource($post); @@ -169,19 +171,19 @@ $resource->addLink('self', 'url'); $resource->setLinks(['key' => 'value']); ``` -You can also easily add pagination links: +You can also easily add [pagination](http://jsonapi.org/format/#fetching-pagination) links: ```php $document->addPaginationLinks( 'url', // The base URL for the links - [], // The query params provided in the request + $_GET, // The query params provided in the request 40, // The current offset 20, // The current limit 100 // The total number of results ); ``` -To define metadata and/or links on resources implicitly, call the appropriate methods in the constructor: +To define meta information and/or links globally for a resource type, call the appropriate methods in the constructor: ```php use Tobscure\JsonApi\AbstractResource; @@ -212,25 +214,29 @@ $parameters = new Parameters($_GET); #### getInclude -Get the relationships requested for inclusion. Provide an array of available relationship paths; if anything else is present, an `InvalidParameterException` will be thrown. +Get the relationships requested for [inclusion](http://jsonapi.org/format/#fetching-includes). Provide an array of available relationship paths; if anything else is present, an `InvalidParameterException` will be thrown. ```php // GET /api?include=author,comments $include = $parameters->getInclude(['author', 'comments', 'comments.author']); // ['author', 'comments'] + +$document->setInclude($include); ``` #### getFields -Get the fields requested for inclusion, keyed by resource type. +Get the [sparse fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets) requested for inclusion, keyed by resource type. ```php // GET /api?fields[articles]=title,body $fields = $parameters->getFields(); // ['articles' => ['title', 'body']] + +$document->setFields($fields); ``` #### getSort -Get the requested sort criteria. Provide an array of available fields that can be sorted by; if anything else is present, an `InvalidParameterException` will be thrown. +Get the requested [sort fields](http://jsonapi.org/format/#fetching-sorting). Provide an array of available fields that can be sorted by; if anything else is present, an `InvalidParameterException` will be thrown. ```php // GET /api?sort=-created,title @@ -239,7 +245,7 @@ $sort = $parameters->getSort(['title', 'created']); // ['created' => 'desc', 'ti #### getLimit and getOffset -Get the offset number and the number of resources to display using a page- or offset-based strategy. `getLimit` accepts an optional maximum. If the calculated offset is below zero, an `InvalidParameterException` will be thrown. +Get the offset number and the number of resources to display using a [page- or offset-based strategy](http://jsonapi.org/format/#fetching-pagination). `getLimit` accepts an optional maximum. If the calculated offset is below zero, an `InvalidParameterException` will be thrown. ```php // GET /api?page[number]=5&page[size]=20 @@ -251,11 +257,14 @@ $limit = $parameters->getLimit(100); // 100 $offset = $parameters->getOffset(); // 20 ``` -### Error Handling +#### getFilter -You can transform caught exceptions into JSON-API error documents using the `Tobscure\JsonApi\ErrorHandler` class. You must register the appropriate `Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface` instances. +Get the contents of the [filter](http://jsonapi.org/format/#fetching-filtering) query parameter. ```php +// GET /api?filter[author]=toby +$filter = $parameters->getFilter(); // ['author' => 'toby'] +``` try { // API handling code } catch (Exception $e) { From 44ed1543de67acaf607d64e1019a5ab82c62a59f Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 27 Mar 2017 23:44:08 +1030 Subject: [PATCH 20/37] New error handling API in README (not implemented yet) --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d8aaf23..f093d58 100644 --- a/README.md +++ b/README.md @@ -265,23 +265,96 @@ Get the contents of the [filter](http://jsonapi.org/format/#fetching-filtering) // GET /api?filter[author]=toby $filter = $parameters->getFilter(); // ['author' => 'toby'] ``` + +### Errors + +The `Tobscure\JsonApi\Error\ErrorResponseInterface` interface represents the information required to produce an error response: the HTTP status code to respond with, and an array of [error objects](http://jsonapi.org/format/#error-objects). + +As an example, the implementation for a generic internal server error response would be as follows: + +```php +use Tobscure\JsonApi\Error; +use Tobscure\JsonApi\Error\ErrorResponseInterface; + +class InternalServerErrorResponse implements ErrorResponseInterface +{ + public function getStatusCode() + { + return 500; + } + + public function getErrors() + { + $error = new Error; + $error->setStatusCode($this->getStatusCode()); + $error->setTitle('Internal Server Error'); + + return [$error]; + } +} +``` + +A Document containing the errors can then be created and outputted: + +```php +$response = new InternalServerErrorResponse; + +$document = Document::fromErrorResponse($response); + +http_response_code($response->getStatusCode()); +header('Content-Type: ' . $document::MEDIA_TYPE); +echo $document; +``` + +In order to easily translate a caught `Exception` into a JSON-API error response, you should implement the `ErrorResponseInterface` for each type of `Exception` you wish to handle: + +```php +class MyCustomErrorResponse implements ErrorResponseInterface +{ + protected $e; + + public function __construct(MyCustomException $e) + { + $this->e = $e; + } + + public function getStatusCode() + { + return 400; + } + + public function getErrors() + { + return [ /* ... */ ]; + } +} +``` + +You can then instantiate the `Tobscure\JsonApi\Error\ExceptionHandler` class, passing a map of `Exception` subclasses to the `ErrorResponseInterface` classes that should be used to handle them. This map will be iterated through; if the given `Exception` is an instance of the key, then the value will be instantiated and used as the response. + +```php +use Tobscure\JsonApi\Document; +use Tobscure\JsonApi\Error\ExceptionErrorResponseMap; + try { // API handling code } catch (Exception $e) { - $errors = new ErrorHandler; + $map = new ExceptionErrorResponseMap([ + MyCustomException::class => MyCustomErrorResponse::class + ]); - $errors->registerHandler(new InvalidParameterExceptionHandler); - $errors->registerHandler(new FallbackExceptionHandler); + $response = $map->errorResponseFor($e); - $response = $errors->handle($e); + $document = Document::fromErrorResponse($response); - $document = new Document; - $document->setErrors($response->getErrors()); - - return new JsonResponse($document, $response->getStatus()); + http_response_code($response->getStatusCode()); + header('Content-Type: ' . $document::MEDIA_TYPE); + echo $document; } ``` +The `ExceptionHandler` class automatically uses a special error response for `InvalidParameterException`s, unless this otherwise specified. It will fall back to an `InternalServerErrorResponse` if no handler is specified for the given exception type. + ## Examples * [Flarum](https://github.com/flarum/core/tree/master/src/Api) is forum software that uses tobscure/json-api to power its API. From 4ad47d1228a9f9dddbcb0917b5703883d76491c0 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 00:15:21 +1030 Subject: [PATCH 21/37] README: Make link methods more explicit, add Link objects (not implemented yet) --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f093d58..ad76c45 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ $document->setFields(['posts' => ['title', 'body']]); // Add metadata and links. $document->addMeta('total', count($posts)); -$document->addLink('self', 'http://example.com/api/posts'); +$document->setSelfLink('http://example.com/api/posts'); // Output the document with the JSON-API media type. header('Content-Type: ' . $document::MEDIA_TYPE); @@ -75,8 +75,6 @@ An instantiated resource object can then be added to the JSON-API document: $resource = new PostResource($post); $document = new Document($resource); - -echo $document; // {"data": {"type": "posts", "id": "1"}} ``` To output a collection of resources, map your data to an array of Resource objects: @@ -167,14 +165,15 @@ They also allow you to add [links](http://jsonapi.org/format/#document-links) in ```php $resource = new PostResource($post); -$resource->addLink('self', 'url'); -$resource->setLinks(['key' => 'value']); +$resource->setSelfLink(new Link('url', ['meta' => 'information'])); + +$relationship->setRelatedLink('url'); ``` You can also easily add [pagination](http://jsonapi.org/format/#fetching-pagination) links: ```php -$document->addPaginationLinks( +$document->setPaginationLinks( 'url', // The base URL for the links $_GET, // The query params provided in the request 40, // The current offset @@ -194,7 +193,7 @@ class PostResource extends AbstractResource { $this->post = $post; - $this->addLink('self', '/posts/' . $post->id); + $this->setSelfLink('/posts/' . $post->id); $this->addMeta('some', 'metadata for ' . $post->id); } From 332151314d99ef352aa7e129e3b4a257df1e0248 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 00:16:25 +1030 Subject: [PATCH 22/37] README: Simplify new error handling API: less magic, less surface area (still not implemented) --- README.md | 68 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index ad76c45..705343d 100644 --- a/README.md +++ b/README.md @@ -269,33 +269,16 @@ $filter = $parameters->getFilter(); // ['author' => 'toby'] The `Tobscure\JsonApi\Error\ErrorResponseInterface` interface represents the information required to produce an error response: the HTTP status code to respond with, and an array of [error objects](http://jsonapi.org/format/#error-objects). -As an example, the implementation for a generic internal server error response would be as follows: +A couple of implementations are already provided: -```php -use Tobscure\JsonApi\Error; -use Tobscure\JsonApi\Error\ErrorResponseInterface; - -class InternalServerErrorResponse implements ErrorResponseInterface -{ - public function getStatusCode() - { - return 500; - } +* `Tobscure\JsonApi\Error\InternalServerErrorResponse` for a generic 500 Internal Server Error response. +* `Tobscure\JsonApi\Error\InvalidParameterErrorResponse` for an error response corresponding to an `InvalidParameterException`. - public function getErrors() - { - $error = new Error; - $error->setStatusCode($this->getStatusCode()); - $error->setTitle('Internal Server Error'); - - return [$error]; - } -} -``` - -A Document containing the errors can then be created and outputted: +A Document containing the errors from an error response can be created and outputted using the `fromErrorResponse` constructor: ```php +use Tobscure\JsonApi\Error\InternalServerErrorResponse; + $response = new InternalServerErrorResponse; $document = Document::fromErrorResponse($response); @@ -305,7 +288,7 @@ header('Content-Type: ' . $document::MEDIA_TYPE); echo $document; ``` -In order to easily translate a caught `Exception` into a JSON-API error response, you should implement the `ErrorResponseInterface` for each type of `Exception` you wish to handle: +In order to translate a caught `Exception` into a JSON-API error response, you should implement the `ErrorResponseInterface` for each type of `Exception` you wish to handle: ```php class MyCustomErrorResponse implements ErrorResponseInterface @@ -324,25 +307,46 @@ class MyCustomErrorResponse implements ErrorResponseInterface public function getErrors() { - return [ /* ... */ ]; + $error = new Error; + + $error->setId('1'); + $error->setAboutLink('url'); + $error->setMeta(['key' => 'value']); + $error->setStatusCode(400); + $error->setErrorCode('my-custom-error-code'); + $error->setTitle('My Custom Error'); + $error->setDetail('You dun goofed!'); + $error->setSourcePointer('/data/attributes/custom'); + $error->setSourceParameter('include'); + + return [$error]; } } ``` -You can then instantiate the `Tobscure\JsonApi\Error\ExceptionHandler` class, passing a map of `Exception` subclasses to the `ErrorResponseInterface` classes that should be used to handle them. This map will be iterated through; if the given `Exception` is an instance of the key, then the value will be instantiated and used as the response. +You can then instantiate the correct error response according to the type of `Exception` that has been caught: ```php use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Error\ExceptionErrorResponseMap; +use Tobscure\JsonApi\Error\InternalServerErrorResponse; +use Tobscure\JsonApi\Error\InvalidParameterErrorResponse; +use Tobscure\JsonApi\Exception\InvalidParameterException; try { // API handling code } catch (Exception $e) { - $map = new ExceptionErrorResponseMap([ - MyCustomException::class => MyCustomErrorResponse::class - ]); + switch (true) { + case $e instanceof MyCustomException: + $response = new MyCustomErrorResponse($e); + break; - $response = $map->errorResponseFor($e); + case $e instanceof InvalidParameterException: + $response = new InvalidParameterErrorResponse($e); + break; + + default: + $response = new InternalServerErrorResponse; + } $document = Document::fromErrorResponse($response); @@ -352,8 +356,6 @@ try { } ``` -The `ExceptionHandler` class automatically uses a special error response for `InvalidParameterException`s, unless this otherwise specified. It will fall back to an `InternalServerErrorResponse` if no handler is specified for the given exception type. - ## Examples * [Flarum](https://github.com/flarum/core/tree/master/src/Api) is forum software that uses tobscure/json-api to power its API. From 27a25b5e0043b28af7abf926a813f4031c534732 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 00:18:06 +1030 Subject: [PATCH 23/37] Formatting --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 705343d..40caa2e 100644 --- a/README.md +++ b/README.md @@ -156,21 +156,19 @@ By default, the `AbstractResource` implementation will convert included relation The `Document`, `Resource`, and `Relationship` classes allow you to add [meta information](http://jsonapi.org/format/#document-meta): ```php -$document = new Document; $document->addMeta('key', 'value'); $document->setMeta(['key' => 'value']); ``` -They also allow you to add [links](http://jsonapi.org/format/#document-links) in a similar way: +They also allow you to add [links](http://jsonapi.org/format/#document-links): ```php -$resource = new PostResource($post); $resource->setSelfLink(new Link('url', ['meta' => 'information'])); $relationship->setRelatedLink('url'); ``` -You can also easily add [pagination](http://jsonapi.org/format/#fetching-pagination) links: +You can also easily generate [pagination](http://jsonapi.org/format/#fetching-pagination) links: ```php $document->setPaginationLinks( @@ -194,6 +192,7 @@ class PostResource extends AbstractResource $this->post = $post; $this->setSelfLink('/posts/' . $post->id); + $this->addMeta('some', 'metadata for ' . $post->id); } From afa7d3fff7aa5607488455d35a18a24d7c57e1d5 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 00:19:01 +1030 Subject: [PATCH 24/37] Formatting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40caa2e..62679cc 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ class PostResource extends AbstractResource $this->post = $post; $this->setSelfLink('/posts/' . $post->id); - + $this->addMeta('some', 'metadata for ' . $post->id); } @@ -266,7 +266,7 @@ $filter = $parameters->getFilter(); // ['author' => 'toby'] ### Errors -The `Tobscure\JsonApi\Error\ErrorResponseInterface` interface represents the information required to produce an error response: the HTTP status code to respond with, and an array of [error objects](http://jsonapi.org/format/#error-objects). +The `Tobscure\JsonApi\Error\ErrorResponseInterface` interface represents the information required to produce an error response: a HTTP status code to respond with, and an array of [error objects](http://jsonapi.org/format/#error-objects). A couple of implementations are already provided: From 2f15ded8f8df9b338650ed830e867b7609bfecba Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 00:19:59 +1030 Subject: [PATCH 25/37] Formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62679cc..b3fba3d 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ A couple of implementations are already provided: * `Tobscure\JsonApi\Error\InternalServerErrorResponse` for a generic 500 Internal Server Error response. * `Tobscure\JsonApi\Error\InvalidParameterErrorResponse` for an error response corresponding to an `InvalidParameterException`. -A Document containing the errors from an error response can be created and outputted using the `fromErrorResponse` constructor: +A Document containing the errors from an error response can be created using the `fromErrorResponse` constructor: ```php use Tobscure\JsonApi\Error\InternalServerErrorResponse; From 8c2a260c6f37e376cb77a9b21a4e9ce3a805f2e6 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 00:21:30 +1030 Subject: [PATCH 26/37] Tweaks --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3fba3d..e3f4564 100644 --- a/README.md +++ b/README.md @@ -273,9 +273,10 @@ A couple of implementations are already provided: * `Tobscure\JsonApi\Error\InternalServerErrorResponse` for a generic 500 Internal Server Error response. * `Tobscure\JsonApi\Error\InvalidParameterErrorResponse` for an error response corresponding to an `InvalidParameterException`. -A Document containing the errors from an error response can be created using the `fromErrorResponse` constructor: +A Document containing the errors from an error response can be created using the `fromErrorResponse()` constructor: ```php +use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Error\InternalServerErrorResponse; $response = new InternalServerErrorResponse; @@ -290,6 +291,9 @@ echo $document; In order to translate a caught `Exception` into a JSON-API error response, you should implement the `ErrorResponseInterface` for each type of `Exception` you wish to handle: ```php +use Tobscure\JsonApi\Error; +use Tobscure\JsonApi\Error\ErrorResponseInterface; + class MyCustomErrorResponse implements ErrorResponseInterface { protected $e; From b9923e3b0f5414b2cdf707977f93cbc5b4b0b733 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 00:27:58 +1030 Subject: [PATCH 27/37] Add default error response for ease --- README.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e3f4564..745c9d9 100644 --- a/README.md +++ b/README.md @@ -327,13 +327,11 @@ class MyCustomErrorResponse implements ErrorResponseInterface } ``` -You can then instantiate the correct error response according to the type of `Exception` that has been caught: +You can then instantiate the correct error response according to the type of `Exception` that has been caught. The `Tobscure\JsonApi\Error\DefaultErrorResponse` class can be used to automatically handle generation of an `InvalidParameterErrorResponse` and falling back to an `InternalServerErrorResponse`. ```php use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Error\InternalServerErrorResponse; -use Tobscure\JsonApi\Error\InvalidParameterErrorResponse; -use Tobscure\JsonApi\Exception\InvalidParameterException; +use Tobscure\JsonApi\Error\DefaultErrorResponse; try { // API handling code @@ -343,12 +341,8 @@ try { $response = new MyCustomErrorResponse($e); break; - case $e instanceof InvalidParameterException: - $response = new InvalidParameterErrorResponse($e); - break; - default: - $response = new InternalServerErrorResponse; + $response = DefaultErrorResponse::forException($e); } $document = Document::fromErrorResponse($response); From ffa2bf80daab2e384a2b420f6753b20e133345e3 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 21:48:18 +1030 Subject: [PATCH 28/37] Big changes * Static Document constructors * Rename meta/link methods * Remove error *handling* stuff - I think it's just slightly beyond the scope of this library * Refactor internal Document logic * Remove toArray methods, just use jsonSerialize Still not quite done. Still a bit unsure about meta/links method names... --- README.md | 118 ++------- src/AbstractResource.php | 1 + src/Document.php | 226 ++++++++---------- src/Error.php | 81 +++++++ src/ErrorHandler.php | 58 ----- .../Handler/ExceptionHandlerInterface.php | 36 --- .../Handler/FallbackExceptionHandler.php | 66 ----- .../InvalidParameterExceptionHandler.php | 47 ---- src/Exception/Handler/ResponseBag.php | 47 ---- src/Exception/InvalidParameterException.php | 4 +- src/Link.php | 42 ++++ src/LinksTrait.php | 98 +------- src/MetaTrait.php | 33 +-- src/PaginationLinksTrait.php | 94 ++++++++ src/Parameters.php | 9 +- src/RelatedLinkTrait.php | 36 +++ src/Relationship.php | 36 ++- src/ResourceInterface.php | 4 +- src/SelfLinkTrait.php | 36 +++ tests/DocumentTest.php | 63 ++--- tests/ErrorHandlerTest.php | 27 --- .../Handler/FallbackExceptionHandlerTest.php | 46 ---- .../InvalidParameterExceptionHandlerTest.php | 44 ---- ...tTest.php => PaginationLinksTraitTest.php} | 32 +-- tests/RelationshipTest.php | 6 +- 25 files changed, 513 insertions(+), 777 deletions(-) create mode 100644 src/Error.php delete mode 100644 src/ErrorHandler.php delete mode 100644 src/Exception/Handler/ExceptionHandlerInterface.php delete mode 100644 src/Exception/Handler/FallbackExceptionHandler.php delete mode 100644 src/Exception/Handler/InvalidParameterExceptionHandler.php delete mode 100644 src/Exception/Handler/ResponseBag.php create mode 100644 src/Link.php create mode 100644 src/PaginationLinksTrait.php create mode 100644 src/RelatedLinkTrait.php create mode 100644 src/SelfLinkTrait.php delete mode 100644 tests/ErrorHandlerTest.php delete mode 100644 tests/Exception/Handler/FallbackExceptionHandlerTest.php delete mode 100644 tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php rename tests/{LinksTraitTest.php => PaginationLinksTraitTest.php} (60%) diff --git a/README.md b/README.md index 745c9d9..c73cde2 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,14 @@ use Tobscure\JsonApi\Document; $resource = new PostResource($post); // Create a JSON-API document with that resource as the primary data. -$document = new Document($resource); +$document = Document::fromData($resource); // Specify included relationships and sparse fieldsets. $document->setInclude(['author', 'comments']); $document->setFields(['posts' => ['title', 'body']]); // Add metadata and links. -$document->addMeta('total', count($posts)); +$document->setMeta('total', count($posts)); $document->setSelfLink('http://example.com/api/posts'); // Output the document with the JSON-API media type. @@ -74,7 +74,7 @@ An instantiated resource object can then be added to the JSON-API document: ```php $resource = new PostResource($post); -$document = new Document($resource); +$document = Document::fromData($resource); ``` To output a collection of resources, map your data to an array of Resource objects: @@ -84,7 +84,7 @@ $collection = array_map(function (Post $post) { return new PostResource($post); }, $posts); -$document = new Document($collection); +$document = Document::fromData($collection); ``` #### Attributes & Sparse Fieldsets @@ -132,7 +132,7 @@ To support the [inclusion of related resources](http://jsonapi.org/format/#fetch { $resource = new UserResource($this->post->author); - return new Relationship($resource); + return Relationship::fromData($resource); } ``` @@ -156,19 +156,21 @@ By default, the `AbstractResource` implementation will convert included relation The `Document`, `Resource`, and `Relationship` classes allow you to add [meta information](http://jsonapi.org/format/#document-meta): ```php -$document->addMeta('key', 'value'); -$document->setMeta(['key' => 'value']); +$document->setMeta('key', 'value'); +$document->removeMeta('key'); +$document->replaceMeta(['key' => 'value']); ``` -They also allow you to add [links](http://jsonapi.org/format/#document-links): +They also allow you to add [links](http://jsonapi.org/format/#document-links). A link's value may be a string, or a `Tobscure\JsonApi\Link` instance. ```php -$resource->setSelfLink(new Link('url', ['meta' => 'information'])); +use Tobscure\JsonApi\Link; -$relationship->setRelatedLink('url'); +$resource->setSelfLink('url'); +$relationship->setRelatedLink(new Link('url', ['some' => 'metadata'])); ``` -You can also easily generate [pagination](http://jsonapi.org/format/#fetching-pagination) links: +You can also easily generate [pagination](http://jsonapi.org/format/#fetching-pagination) links on `Document` and `Relationship` instances: ```php $document->setPaginationLinks( @@ -192,8 +194,7 @@ class PostResource extends AbstractResource $this->post = $post; $this->setSelfLink('/posts/' . $post->id); - - $this->addMeta('some', 'metadata for ' . $post->id); + $this->setMeta('some', 'metadata for ' . $post->id); } // ... @@ -266,91 +267,24 @@ $filter = $parameters->getFilter(); // ['author' => 'toby'] ### Errors -The `Tobscure\JsonApi\Error\ErrorResponseInterface` interface represents the information required to produce an error response: a HTTP status code to respond with, and an array of [error objects](http://jsonapi.org/format/#error-objects). - -A couple of implementations are already provided: - -* `Tobscure\JsonApi\Error\InternalServerErrorResponse` for a generic 500 Internal Server Error response. -* `Tobscure\JsonApi\Error\InvalidParameterErrorResponse` for an error response corresponding to an `InvalidParameterException`. - -A Document containing the errors from an error response can be created using the `fromErrorResponse()` constructor: - -```php -use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Error\InternalServerErrorResponse; - -$response = new InternalServerErrorResponse; - -$document = Document::fromErrorResponse($response); - -http_response_code($response->getStatusCode()); -header('Content-Type: ' . $document::MEDIA_TYPE); -echo $document; -``` - -In order to translate a caught `Exception` into a JSON-API error response, you should implement the `ErrorResponseInterface` for each type of `Exception` you wish to handle: +You can create a `Document` containing [error objects](http://jsonapi.org/format/#error-objects) using `Tobscure\JsonApi\Error` instances: ```php use Tobscure\JsonApi\Error; -use Tobscure\JsonApi\Error\ErrorResponseInterface; - -class MyCustomErrorResponse implements ErrorResponseInterface -{ - protected $e; - public function __construct(MyCustomException $e) - { - $this->e = $e; - } - - public function getStatusCode() - { - return 400; - } - - public function getErrors() - { - $error = new Error; - - $error->setId('1'); - $error->setAboutLink('url'); - $error->setMeta(['key' => 'value']); - $error->setStatusCode(400); - $error->setErrorCode('my-custom-error-code'); - $error->setTitle('My Custom Error'); - $error->setDetail('You dun goofed!'); - $error->setSourcePointer('/data/attributes/custom'); - $error->setSourceParameter('include'); - - return [$error]; - } -} -``` - -You can then instantiate the correct error response according to the type of `Exception` that has been caught. The `Tobscure\JsonApi\Error\DefaultErrorResponse` class can be used to automatically handle generation of an `InvalidParameterErrorResponse` and falling back to an `InternalServerErrorResponse`. - -```php -use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Error\DefaultErrorResponse; - -try { - // API handling code -} catch (Exception $e) { - switch (true) { - case $e instanceof MyCustomException: - $response = new MyCustomErrorResponse($e); - break; - - default: - $response = DefaultErrorResponse::forException($e); - } +$error = new Error; - $document = Document::fromErrorResponse($response); +$error->setId('1'); +$error->setAboutLink('url'); +$error->setMeta('key', 'value'); +$error->setStatus(400); +$error->setCode('123'); +$error->setTitle('Something Went Wrong'); +$error->setDetail('You dun goofed!'); +$error->setSourcePointer('/data/attributes/body'); +$error->setSourceParameter('include'); - http_response_code($response->getStatusCode()); - header('Content-Type: ' . $document::MEDIA_TYPE); - echo $document; -} +$document = Document::fromErrors([$error]); ``` ## Examples diff --git a/src/AbstractResource.php b/src/AbstractResource.php index 4dcfd34..8646d7a 100644 --- a/src/AbstractResource.php +++ b/src/AbstractResource.php @@ -16,6 +16,7 @@ abstract class AbstractResource implements ResourceInterface { use LinksTrait; + use SelfLinkTrait; use MetaTrait; /** diff --git a/src/Document.php b/src/Document.php index 5d1c2ca..7b7b5e3 100644 --- a/src/Document.php +++ b/src/Document.php @@ -16,12 +16,15 @@ class Document implements JsonSerializable { use LinksTrait; + use SelfLinkTrait; + use PaginationLinksTrait; use MetaTrait; const MEDIA_TYPE = 'application/vnd.api+json'; + const DEFAULT_API_VERSION = '1.0'; /** - * The data object. + * The primary data. * * @var ResourceInterface|ResourceInterface[]|null */ @@ -30,7 +33,7 @@ class Document implements JsonSerializable /** * The errors array. * - * @var array|null + * @var Error[]|null */ protected $errors; @@ -55,16 +58,54 @@ class Document implements JsonSerializable */ protected $fields = []; + /** + * Use named constructors instead. + */ + private function __construct() + { + } + /** * @param ResourceInterface|ResourceInterface[] $data + * + * @return self */ - public function __construct($data = null) + public static function fromData($data) { - $this->data = $data; + $document = new self; + $document->setData($data); + + return $document; } /** - * Get the data object. + * @param array $meta + * + * @return self + */ + public static function fromMeta(array $meta) + { + $document = new self; + $document->replaceMeta($meta); + + return $document; + } + + /** + * @param Error[] $errors + * + * @return self + */ + public static function fromErrors(array $errors) + { + $document = new self; + $document->setErrors($errors); + + return $document; + } + + /** + * Get the primary data. * * @return ResourceInterface|ResourceInterface[]|null $data */ @@ -77,20 +118,16 @@ public function getData() * Set the data object. * * @param ResourceInterface|ResourceInterface[]|null $data - * - * @return $this */ public function setData($data) { $this->data = $data; - - return $this; } /** * Get the errors array. * - * @return array|null $errors + * @return Error[]|null $errors */ public function getErrors() { @@ -100,45 +137,37 @@ public function getErrors() /** * Set the errors array. * - * @param array|null $errors - * - * @return $this + * @param Error[]|null $errors */ public function setErrors(array $errors = null) { $this->errors = $errors; - - return $this; } /** - * Get the jsonapi array. + * Set the jsonapi version. * - * @return array|null $jsonapi + * @param string $version */ - public function getJsonapi() + public function setApiVersion($version) { - return $this->jsonapi; + $this->jsonapi['version'] = $version; } /** - * Set the jsonapi array. - * - * @param array|null $jsonapi - * - * @return $this + * Set the jsonapi meta information. + * + * @param array $meta */ - public function setJsonapi(array $jsonapi = null) + public function setApiMeta(array $meta) { - $this->jsonapi = $jsonapi; - - return $this; + $this->jsonapi['meta'] = $meta; } /** * Get the relationships to include. * - * @return array $include + * @return string[] $include */ public function getInclude() { @@ -148,21 +177,17 @@ public function getInclude() /** * Set the relationships to include. * - * @param array $include - * - * @return $this + * @param string[] $include */ public function setInclude(array $include) { $this->include = $include; - - return $this; } /** * Get the sparse fieldsets. * - * @return array $fields + * @return array[] $fields */ public function getFields() { @@ -172,29 +197,26 @@ public function getFields() /** * Set the sparse fieldsets. * - * @param array $fields - * - * @return $this + * @param array[] $fields */ public function setFields(array $fields) { $this->fields = $fields; - - return $this; } /** - * Build the JSON-API document as an array. + * Serialize for JSON usage. * * @return array */ - public function toArray() + public function jsonSerialize() { - $document = []; - - if ($this->links) { - $document['links'] = $this->links; - } + $document = [ + 'links' => $this->links, + 'meta' => $this->meta, + 'errors' => $this->errors, + 'jsonapi' => $this->jsonapi + ]; if ($this->data) { $isCollection = is_array($this->data); @@ -207,7 +229,7 @@ public function toArray() $map = []; $resources = $isCollection ? $this->data : [$this->data]; - $this->addResourcesToMap($map, $resources, $this->include); + $this->mergeResources($map, $resources, $this->include); // Now extract the document's primary resource(s) from the resource // map, and flatten the map's remaining resources to be included in @@ -222,28 +244,11 @@ public function toArray() } } - $included = call_user_func_array('array_merge', $map); - $document['data'] = $isCollection ? $primary : $primary[0]; - - if ($included) { - $document['included'] = $included; - } - } - - if ($this->meta) { - $document['meta'] = $this->meta; - } - - if ($this->errors) { - $document['errors'] = $this->errors; - } - - if ($this->jsonapi) { - $document['jsonapi'] = $this->jsonapi; + $document['included'] = call_user_func_array('array_merge', $map); } - return $document; + return array_filter($document); } /** @@ -253,17 +258,7 @@ public function toArray() */ public function __toString() { - return json_encode($this->toArray()); - } - - /** - * Serialize for JSON usage. - * - * @return array - */ - public function jsonSerialize() - { - return $this->toArray(); + return json_encode($this->jsonSerialize()); } /** @@ -273,7 +268,7 @@ public function jsonSerialize() * @param ResourceInterface[] $resources * @param array $include An array of relationship paths to include. */ - private function addResourcesToMap(array &$map, array $resources, array $include) + private function mergeResources(array &$map, array $resources, array $include) { // Index relationship paths so that we have a list of the direct // relationships that will be included on these resources, and arrays @@ -296,69 +291,52 @@ private function addResourcesToMap(array &$map, array $resources, array $include if ($data = $relationship->getData()) { $children = is_array($data) ? $data : [$data]; - $this->addResourcesToMap($map, $children, $nested); + $this->mergeResources($map, $children, $nested); } } // Serialize the resource into an array and add it to the map. If // it is already present, its properties will be merged into the // existing resource. - $this->addResourceToMap($map, $resource, $relationships); + $this->mergeResource($map, $resource, $relationships); } } /** - * Serialize the given resource as an array and add it to the given map. + * Merge the given resource into a resource map. * * If it is already present in the map, its properties will be merged into - * the existing array. + * the existing resource. * * @param array &$map * @param ResourceInterface $resource - * @param Relationship[] $resource + * @param Relationship[] $relationships */ - private function addResourceToMap(array &$map, ResourceInterface $resource, array $relationships) + private function mergeResource(array &$map, ResourceInterface $resource, array $relationships) { $type = $resource->getType(); $id = $resource->getId(); + $meta = $resource->getMeta(); + $links = $resource->getLinks(); - if (empty($map[$type][$id])) { - $map[$type][$id] = [ - 'type' => $type, - 'id' => $id - ]; - } + $fields = isset($this->fields[$type]) ? $this->fields[$type] : null; - $array = &$map[$type][$id]; - $fields = $this->getFieldsForType($type); - - if ($meta = $resource->getMeta()) { - $array['meta'] = array_replace_recursive(isset($array['meta']) ? $array['meta'] : [], $meta); - } + $attributes = $resource->getAttributes($fields); - if ($links = $resource->getLinks()) { - $array['links'] = array_replace_recursive(isset($array['links']) ? $array['links'] : [], $links); - } + if ($fields) { + $keys = array_flip($fields); - if ($attributes = $resource->getAttributes($fields)) { - if ($fields) { - $attributes = array_intersect_key($attributes, array_flip($fields)); - } - if ($attributes) { - $array['attributes'] = array_replace_recursive(isset($array['attributes']) ? $array['attributes'] : [], $attributes); - } + $attributes = array_intersect_key($attributes, $keys); + $relationships = array_intersect_key($relationships, $keys); } - if ($relationships && $fields) { - $relationships = array_intersect_key($relationships, array_flip($fields)); - } - if ($relationships) { - $relationships = array_map(function ($relationship) { - return $relationship->toArray(); - }, $relationships); + $props = array_filter(compact('attributes', 'relationships', 'links', 'meta')); - $array['relationships'] = array_replace_recursive(isset($array['relationships']) ? $array['relationships'] : [], $relationships); - } + if (empty($map[$type][$id])) { + $map[$type][$id] = compact('type', 'id') + $props; + } else { + $map[$type][$id] = array_replace_recursive($map[$type][$id], $props); + } } /** @@ -373,9 +351,9 @@ private function addResourceToMap(array &$map, ResourceInterface $resource, arra * * ['user' => ['employer', 'employer.country'], 'comments' => []] * - * @param array $paths + * @param string[] $paths * - * @return array + * @return array[] */ private function indexRelationshipPaths(array $paths) { @@ -395,16 +373,4 @@ private function indexRelationshipPaths(array $paths) return $tree; } - - /** - * Get the fields that should be included for resources of the given type. - * - * @param string $type - * - * @return array|null - */ - private function getFieldsForType($type) - { - return isset($this->fields[$type]) ? $this->fields[$type] : null; - } } diff --git a/src/Error.php b/src/Error.php new file mode 100644 index 0000000..c68f294 --- /dev/null +++ b/src/Error.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\JsonApi; + +use JsonSerializable; + +class Error implements JsonSerializable +{ + use LinksTrait; + use MetaTrait; + + private $id; + private $status; + private $code; + private $title; + private $detail; + private $source; + + public function setId($id) + { + $this->id = $id; + } + + public function setAboutLink($link) + { + $this->links['about'] = $link; + } + + public function setStatus($status) + { + $this->status = $status; + } + + public function setCode($code) + { + $this->code = $code; + } + + public function setTitle($title) + { + $this->title = $title; + } + + public function setDetail($detail) + { + $this->detail = $detail; + } + + public function setSourcePointer($pointer) + { + $this->source['pointer'] = $pointer; + } + + public function setSourceParameter($parameter) + { + $this->source['parameter'] = $parameter; + } + + public function jsonSerialize() + { + return array_filter([ + 'id' => $this->id, + 'links' => $this->links, + 'status' => $this->status, + 'code' => $this->code, + 'title' => $this->title, + 'detail' => $this->detail, + 'source' => $this->source, + 'meta' => $this->meta + ]); + } +} diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php deleted file mode 100644 index 1e3492d..0000000 --- a/src/ErrorHandler.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi; - -use Exception; -use RuntimeException; -use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; - -class ErrorHandler -{ - /** - * Stores the valid handlers. - * - * @var \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface[] - */ - private $handlers = []; - - /** - * Handle the exception provided. - * - * @param Exception $e - * - * @throws RuntimeException - * - * @return \Tobscure\JsonApi\Exception\Handler\ResponseBag - */ - public function handle(Exception $e) - { - foreach ($this->handlers as $handler) { - if ($handler->manages($e)) { - return $handler->handle($e); - } - } - - throw new RuntimeException('Exception handler for '.get_class($e).' not found.'); - } - - /** - * Register a new exception handler. - * - * @param \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface $handler - * - * @return void - */ - public function registerHandler(ExceptionHandlerInterface $handler) - { - $this->handlers[] = $handler; - } -} diff --git a/src/Exception/Handler/ExceptionHandlerInterface.php b/src/Exception/Handler/ExceptionHandlerInterface.php deleted file mode 100644 index 4108fc0..0000000 --- a/src/Exception/Handler/ExceptionHandlerInterface.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi\Exception\Handler; - -use Exception; - -interface ExceptionHandlerInterface -{ - /** - * If the exception handler is able to format a response for the provided exception, - * then the implementation should return true. - * - * @param \Exception $e - * - * @return bool - */ - public function manages(Exception $e); - - /** - * Handle the provided exception. - * - * @param \Exception $e - * - * @return \Tobscure\JsonApi\Exception\Handler\ResponseBag - */ - public function handle(Exception $e); -} diff --git a/src/Exception/Handler/FallbackExceptionHandler.php b/src/Exception/Handler/FallbackExceptionHandler.php deleted file mode 100644 index 2799d55..0000000 --- a/src/Exception/Handler/FallbackExceptionHandler.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi\Exception\Handler; - -use Exception; - -class FallbackExceptionHandler implements ExceptionHandlerInterface -{ - /** - * @var bool - */ - private $debug; - - /** - * @param bool $debug - */ - public function __construct($debug) - { - $this->debug = $debug; - } - - /** - * {@inheritdoc} - */ - public function manages(Exception $e) - { - return true; - } - - /** - * {@inheritdoc} - */ - public function handle(Exception $e) - { - $status = 500; - $error = $this->constructError($e, $status); - - return new ResponseBag($status, [$error]); - } - - /** - * @param \Exception $e - * @param $status - * - * @return array - */ - private function constructError(Exception $e, $status) - { - $error = ['code' => $status, 'title' => 'Internal server error']; - - if ($this->debug) { - $error['detail'] = (string) $e; - } - - return $error; - } -} diff --git a/src/Exception/Handler/InvalidParameterExceptionHandler.php b/src/Exception/Handler/InvalidParameterExceptionHandler.php deleted file mode 100644 index 374020a..0000000 --- a/src/Exception/Handler/InvalidParameterExceptionHandler.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi\Exception\Handler; - -use Exception; -use Tobscure\JsonApi\Exception\InvalidParameterException; - -class InvalidParameterExceptionHandler implements ExceptionHandlerInterface -{ - /** - * {@inheritdoc} - */ - public function manages(Exception $e) - { - return $e instanceof InvalidParameterException; - } - - /** - * {@inheritdoc} - */ - public function handle(Exception $e) - { - $status = 400; - $error = []; - - $code = $e->getCode(); - if ($code) { - $error['code'] = $code; - } - - $invalidParameter = $e->getInvalidParameter(); - if ($invalidParameter) { - $error['source'] = ['parameter' => $invalidParameter]; - } - - return new ResponseBag($status, [$error]); - } -} diff --git a/src/Exception/Handler/ResponseBag.php b/src/Exception/Handler/ResponseBag.php deleted file mode 100644 index 0da7d88..0000000 --- a/src/Exception/Handler/ResponseBag.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\JsonApi\Exception\Handler; - -/** - * DTO to manage JSON error response handling. - */ -class ResponseBag -{ - private $status; - private $errors; - - /** - * @param int $status - * @param array $errors - */ - public function __construct($status, array $errors) - { - $this->status = $status; - $this->errors = $errors; - } - - /** - * @return array - */ - public function getErrors() - { - return $this->errors; - } - - /** - * @return int - */ - public function getStatus() - { - return $this->status; - } -} diff --git a/src/Exception/InvalidParameterException.php b/src/Exception/InvalidParameterException.php index 72027c0..4d561b2 100644 --- a/src/Exception/InvalidParameterException.php +++ b/src/Exception/InvalidParameterException.php @@ -33,7 +33,9 @@ public function __construct($message = '', $code = 0, $previous = null, $invalid } /** - * @return string The parameter that caused this exception. + * Get the parameter that caused this exception. + * + * @return string */ public function getInvalidParameter() { diff --git a/src/Link.php b/src/Link.php new file mode 100644 index 0000000..1b97b19 --- /dev/null +++ b/src/Link.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\JsonApi; + +use JsonSerializable; + +class Link implements JsonSerializable +{ + use MetaTrait; + + protected $href; + + public function __construct($href, $meta = null) + { + $this->href = $href; + $this->meta = $meta; + } + + public function getHref() + { + return $this->href; + } + + public function setHref($href) + { + $this->href = $href; + } + + public function jsonSerialize() + { + return $this->meta ? ['href' => $this->href, 'meta' => $this->meta] : $this->href; + } +} diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 34fa367..68e43e2 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -14,11 +14,11 @@ trait LinksTrait { /** - * The links array. + * The links. * * @var array */ - protected $links; + protected $links = []; /** * Get the links. @@ -31,107 +31,33 @@ public function getLinks() } /** - * Set the links. + * Replace the links. * * @param array $links - * - * @return $this */ - public function setLinks(array $links) + public function replaceLinks(array $links) { $this->links = $links; - - return $this; } /** - * Add a link. + * Set a link. * * @param string $key - * @param string $value - * - * @return $this + * @param string|Link $value */ - public function addLink($key, $value) + public function setLink($key, $value) { $this->links[$key] = $value; - - return $this; } /** - * Add pagination links (first, prev, next, and last). - * - * @param string $url The base URL for pagination links. - * @param array $queryParams The query params provided in the request. - * @param int $offset The current offset. - * @param int $limit The current limit. - * @param int|null $total The total number of results, or null if unknown. - * - * @return void - */ - public function addPaginationLinks($url, array $queryParams, $offset, $limit, $total = null) - { - if (isset($queryParams['page']['number'])) { - $offset = floor($offset / $limit) * $limit; - } - - $this->addPaginationLink('first', $url, $queryParams, 0, $limit); - - if ($offset > 0) { - $this->addPaginationLink('prev', $url, $queryParams, max(0, $offset - $limit), $limit); - } - - if ($total === null || $offset + $limit < $total) { - $this->addPaginationLink('next', $url, $queryParams, $offset + $limit, $limit); - } - - if ($total) { - $this->addPaginationLink('last', $url, $queryParams, floor(($total - 1) / $limit) * $limit, $limit); - } - } - - /** - * Add a pagination link. - * - * @param string $name The name of the link. - * @param string $url The base URL for pagination links. - * @param array $queryParams The query params provided in the request. - * @param int $offset The offset to link to. - * @param int $limit The current limit. - * - * @return void + * Remove a link. + * + * @param string $key */ - private function addPaginationLink($name, $url, array $queryParams, $offset, $limit) + public function removeLink($key) { - if (! isset($queryParams['page']) || ! is_array($queryParams['page'])) { - $queryParams['page'] = []; - } - - $page = &$queryParams['page']; - - if (isset($page['number'])) { - $page['number'] = floor($offset / $limit) + 1; - - if ($page['number'] <= 1) { - unset($page['number']); - } - } else { - $page['offset'] = $offset; - - if ($page['offset'] <= 0) { - unset($page['offset']); - } - } - - if (isset($page['limit'])) { - $page['limit'] = $limit; - } elseif (isset($page['size'])) { - $page['size'] = $limit; - } - - $queryString = http_build_query($queryParams); - - $this->addLink($name, $url.($queryString ? '?'.$queryString : '')); + unset($this->links[$key]); } } diff --git a/src/MetaTrait.php b/src/MetaTrait.php index 5572446..86acb46 100644 --- a/src/MetaTrait.php +++ b/src/MetaTrait.php @@ -14,14 +14,14 @@ trait MetaTrait { /** - * The meta data array. + * The meta data. * * @var array */ - protected $meta; + protected $meta = []; /** - * Get the meta. + * Get the meta data. * * @return array */ @@ -31,31 +31,34 @@ public function getMeta() } /** - * Set the meta data array. + * Set the meta data. * * @param array $meta - * - * @return $this */ - public function setMeta(array $meta) + public function replaceMeta(array $meta) { $this->meta = $meta; - - return $this; } /** - * Add meta data. + * Set a piece of meta data. * * @param string $key - * @param string $value - * - * @return $this + * @param mixed $value */ - public function addMeta($key, $value) + public function setMeta($key, $value) { $this->meta[$key] = $value; + } - return $this; + /** + * Remove a piece of meta data. + * + * @param string $key + * @param mixed $value + */ + public function removeMeta($key) + { + unset($this->meta[$key]); } } diff --git a/src/PaginationLinksTrait.php b/src/PaginationLinksTrait.php new file mode 100644 index 0000000..078e2fc --- /dev/null +++ b/src/PaginationLinksTrait.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\JsonApi; + +trait PaginationLinksTrait +{ + abstract public function setLink($key, $value); + abstract public function removeLink($key); + + /** + * Set pagination links (first, prev, next, and last). + * + * @param string $url The base URL for pagination links. + * @param array $queryParams The query params provided in the request. + * @param int $offset The current offset. + * @param int $limit The current limit. + * @param int|null $total The total number of results, or null if unknown. + */ + public function setPaginationLinks($url, array $queryParams, $offset, $limit, $total = null) + { + if (isset($queryParams['page']['number'])) { + $offset = floor($offset / $limit) * $limit; + } + + $this->setPaginationLink('first', $url, $queryParams, 0, $limit); + + $this->removeLink('prev'); + $this->removeLink('next'); + $this->removeLink('last'); + + if ($offset > 0) { + $this->setPaginationLink('prev', $url, $queryParams, max(0, $offset - $limit), $limit); + } + + if ($total === null || $offset + $limit < $total) { + $this->setPaginationLink('next', $url, $queryParams, $offset + $limit, $limit); + } + + if ($total) { + $this->setPaginationLink('last', $url, $queryParams, floor(($total - 1) / $limit) * $limit, $limit); + } + } + + /** + * Set a pagination link. + * + * @param string $name The name of the link. + * @param string $url The base URL for pagination links. + * @param array $queryParams The query params provided in the request. + * @param int $offset The offset to link to. + * @param int $limit The current limit. + */ + private function setPaginationLink($name, $url, array $queryParams, $offset, $limit) + { + if (! isset($queryParams['page']) || ! is_array($queryParams['page'])) { + $queryParams['page'] = []; + } + + $page = &$queryParams['page']; + + if (isset($page['number'])) { + $page['number'] = floor($offset / $limit) + 1; + + if ($page['number'] <= 1) { + unset($page['number']); + } + } else { + $page['offset'] = $offset; + + if ($page['offset'] <= 0) { + unset($page['offset']); + } + } + + if (isset($page['limit'])) { + $page['limit'] = $limit; + } elseif (isset($page['size'])) { + $page['size'] = $limit; + } + + $queryString = http_build_query($queryParams); + + $this->setLink($name, $url.($queryString ? '?'.$queryString : '')); + } +} diff --git a/src/Parameters.php b/src/Parameters.php index 2f044b2..09212d0 100644 --- a/src/Parameters.php +++ b/src/Parameters.php @@ -77,7 +77,12 @@ public function getOffset($perPage = null) $offset = (int) $this->getPage('offset'); if ($offset < 0) { - throw new InvalidParameterException('page[offset] must be >=0', 2, null, 'page[offset]'); + throw new InvalidParameterException( + 'page[offset] must be >=0', + 2, + null, + 'page[offset]' + ); } return $offset; @@ -92,7 +97,7 @@ public function getOffset($perPage = null) * * @return int */ - protected function getOffsetFromNumber($perPage) + private function getOffsetFromNumber($perPage) { $page = (int) $this->getPage('number'); diff --git a/src/RelatedLinkTrait.php b/src/RelatedLinkTrait.php new file mode 100644 index 0000000..7010c5d --- /dev/null +++ b/src/RelatedLinkTrait.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\JsonApi; + +trait RelatedLinkTrait +{ + abstract public function setLink($key, $value); + abstract public function removeLink($key); + + /** + * Set the related link. + * + * @param string|Link $value + */ + public function setRelatedLink($value) + { + return $this->setLink('related', $value); + } + + /** + * Remove the related link. + */ + public function removeRelatedLink() + { + return $this->removeLink('related'); + } +} diff --git a/src/Relationship.php b/src/Relationship.php index 12ff618..891b9fb 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -11,9 +11,14 @@ namespace Tobscure\JsonApi; -class Relationship +use JsonSerializable; + +class Relationship implements JsonSerializable { use LinksTrait; + use SelfLinkTrait; + use RelatedLinkTrait; + use PaginationLinksTrait; use MetaTrait; /** @@ -47,14 +52,10 @@ public function getData() * Set the data object. * * @param \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null $data - * - * @return $this */ public function setData($data) { $this->data = $data; - - return $this; } /** @@ -62,27 +63,20 @@ public function setData($data) * * @return array */ - public function toArray() + public function jsonSerialize() { - $array = []; + $relationship = []; if ($this->data) { - if (is_array($this->data)) { - $array['data'] = array_map([$this, 'buildIdentifier'], $this->data); - } else { - $array['data'] = $this->buildIdentifier($this->data); - } - } - - if ($this->meta) { - $array['meta'] = $this->meta; - } - - if ($this->links) { - $array['links'] = $this->links; + $relationship['data'] = is_array($this->data) + ? array_map([$this, 'buildIdentifier'], $this->data) + : $this->buildIdentifier($this->data); } - return $array; + return array_filter($relationship + [ + 'meta' => $this->meta, + 'links' => $this->links + ]); } /** diff --git a/src/ResourceInterface.php b/src/ResourceInterface.php index df6ba8c..5fda24a 100644 --- a/src/ResourceInterface.php +++ b/src/ResourceInterface.php @@ -30,7 +30,7 @@ public function getId(); /** * Get the resource attributes. * - * @param array|null $fields + * @param string[]|null $fields * * @return array|null */ @@ -44,7 +44,7 @@ public function getAttributes(array $fields = null); public function getLinks(); /** - * Get the resource meta. + * Get the resource meta information. * * @return array|null */ diff --git a/src/SelfLinkTrait.php b/src/SelfLinkTrait.php new file mode 100644 index 0000000..1076179 --- /dev/null +++ b/src/SelfLinkTrait.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\JsonApi; + +trait SelfLinkTrait +{ + abstract public function setLink($key, $value); + abstract public function removeLink($key); + + /** + * Set the self link. + * + * @param string|Link $value + */ + public function setSelfLink($value) + { + return $this->setLink('self', $value); + } + + /** + * Remove the self link. + */ + public function removeSelfLink() + { + return $this->removeLink('self'); + } +} diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index a150f73..dd1b8de 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -17,20 +17,15 @@ class DocumentTest extends AbstractTestCase { - public function testItCanBeSerializedToJson() - { - $this->assertEquals('[]', (string) new Document()); - } - public function testResource() { $resource = $this->mockResource('a', '1'); - $document = new Document($resource); + $document = Document::fromData($resource); - $this->assertEquals([ + $this->assertJsonStringEqualsJsonString(json_encode([ 'data' => ['type' => 'a', 'id' => '1'] - ], $document->toArray()); + ]), json_encode($document)); } public function testCollection() @@ -38,14 +33,14 @@ public function testCollection() $resource1 = $this->mockResource('a', '1'); $resource2 = $this->mockResource('a', '2'); - $document = new Document([$resource1, $resource2]); + $document = Document::fromData([$resource1, $resource2]); - $this->assertEquals([ + $this->assertJsonStringEqualsJsonString(json_encode([ 'data' => [ ['type' => 'a', 'id' => '1'], ['type' => 'a', 'id' => '2'] ] - ], $document->toArray()); + ]), json_encode($document)); } public function testMergeResource() @@ -56,9 +51,9 @@ public function testMergeResource() $resource1 = $this->mockResource('a', '1', $array1, $array1, $array1); $resource2 = $this->mockResource('a', '1', $array2, $array2, $array2); - $document = new Document([$resource1, $resource2]); + $document = Document::fromData([$resource1, $resource2]); - $this->assertEquals([ + $this->assertJsonStringEqualsJsonString(json_encode([ 'data' => [ [ 'type' => 'a', @@ -68,7 +63,7 @@ public function testMergeResource() 'links' => $merged ] ] - ], $document->toArray()); + ]), json_encode($document)); } public function testSparseFieldsets() @@ -77,16 +72,16 @@ public function testSparseFieldsets() $resource->expects($this->once())->method('getAttributes')->with($this->equalTo(['present'])); - $document = new Document($resource); + $document = Document::fromData($resource); $document->setFields(['a' => ['present']]); - $this->assertEquals([ + $this->assertJsonStringEqualsJsonString(json_encode([ 'data' => [ 'type' => 'a', 'id' => '1', 'attributes' => ['present' => 1] ] - ], $document->toArray()); + ]), json_encode($document)); } public function testIncludeRelationships() @@ -99,11 +94,11 @@ public function testIncludeRelationships() $relationshipA = $this->getMock(Relationship::class); $relationshipA->method('getData')->willReturn($resource2); - $relationshipA->method('toArray')->willReturn($relationshipArray); + $relationshipA->method('jsonSerialize')->willReturn($relationshipArray); $relationshipB = $this->getMock(Relationship::class); $relationshipB->method('getData')->willReturn($resource3); - $relationshipB->method('toArray')->willReturn($relationshipArray); + $relationshipB->method('jsonSerialize')->willReturn($relationshipArray); $resource1 ->expects($this->once()) @@ -117,10 +112,10 @@ public function testIncludeRelationships() ->with($this->equalTo('b')) ->willReturn($relationshipB); - $document = new Document($resource1); + $document = Document::fromData($resource1); $document->setInclude(['a', 'a.b']); - $this->assertEquals([ + $this->assertJsonStringEqualsJsonString(json_encode([ 'data' => [ 'type' => 'a', 'id' => '1', @@ -137,39 +132,29 @@ public function testIncludeRelationships() 'relationships' => ['b' => $relationshipArray] ] ] - ], $document->toArray()); + ]), json_encode($document)); } public function testErrors() { - $document = new Document(); - $document->setErrors(['a']); - - $this->assertEquals(['errors' => ['a']], $document->toArray()); - } - - public function testJsonapi() - { - $document = new Document(); - $document->setJsonapi(['a']); + $document = Document::fromErrors(['a']); - $this->assertEquals(['jsonapi' => ['a']], $document->toArray()); + $this->assertJsonStringEqualsJsonString(json_encode(['errors' => ['a']]), json_encode($document)); } public function testLinks() { - $document = new Document(); - $document->setLinks(['a']); + $document = Document::fromData(null); + $document->setLink('a', 'b'); - $this->assertEquals(['links' => ['a']], $document->toArray()); + $this->assertJsonStringEqualsJsonString(json_encode(['links' => ['a' => 'b']]), json_encode($document)); } public function testMeta() { - $document = new Document(); - $document->setMeta(['a']); + $document = Document::fromMeta(['a' => 'b']); - $this->assertEquals(['meta' => ['a']], $document->toArray()); + $this->assertJsonStringEqualsJsonString(json_encode(['meta' => ['a' => 'b']]), json_encode($document)); } private function mockResource($type, $id, $attributes = [], $meta = [], $links = []) diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php deleted file mode 100644 index 4d49a98..0000000 --- a/tests/ErrorHandlerTest.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\Tests\JsonApi; - -use Exception; -use Tobscure\JsonApi\ErrorHandler; - -class ErrorHandlerTest extends AbstractTestCase -{ - public function testThrowExceptionWhenNoHandlersPresent() - { - $this->setExpectedException('RuntimeException'); - - $handler = new ErrorHandler; - - $handler->handle(new Exception); - } -} diff --git a/tests/Exception/Handler/FallbackExceptionHandlerTest.php b/tests/Exception/Handler/FallbackExceptionHandlerTest.php deleted file mode 100644 index 37ba9ed..0000000 --- a/tests/Exception/Handler/FallbackExceptionHandlerTest.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\Tests\Exception\Handler; - -use Exception; -use Tobscure\JsonApi\Exception\Handler\FallbackExceptionHandler; -use Tobscure\JsonApi\Exception\Handler\ResponseBag; - -class FallbackExceptionHandlerTest extends \PHPUnit_Framework_TestCase -{ - public function testHandlerCanManageExceptions() - { - $handler = new FallbackExceptionHandler(false); - - $this->assertTrue($handler->manages(new Exception)); - } - - public function testErrorHandlingWithoutDebugMode() - { - $handler = new FallbackExceptionHandler(false); - $response = $handler->handle(new Exception); - - $this->assertInstanceOf(ResponseBag::class, $response); - $this->assertEquals(500, $response->getStatus()); - $this->assertEquals([['code' => 500, 'title' => 'Internal server error']], $response->getErrors()); - } - - public function testErrorHandlingWithDebugMode() - { - $handler = new FallbackExceptionHandler(true); - $response = $handler->handle(new Exception); - - $this->assertInstanceOf(ResponseBag::class, $response); - $this->assertEquals(500, $response->getStatus()); - $this->assertArrayHasKey('detail', $response->getErrors()[0]); - } -} diff --git a/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php b/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php deleted file mode 100644 index eb01ec2..0000000 --- a/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tobscure\Tests\Exception\Handler; - -use Exception; -use Tobscure\JsonApi\Exception\Handler\InvalidParameterExceptionHandler; -use Tobscure\JsonApi\Exception\Handler\ResponseBag; -use Tobscure\JsonApi\Exception\InvalidParameterException; - -class InvalidParameterExceptionHandlerTest extends \PHPUnit_Framework_TestCase -{ - public function testHandlerCanManageInvalidParameterExceptions() - { - $handler = new InvalidParameterExceptionHandler(); - - $this->assertTrue($handler->manages(new InvalidParameterException)); - } - - public function testHandlerCanNotManageOtherExceptions() - { - $handler = new InvalidParameterExceptionHandler(); - - $this->assertFalse($handler->manages(new Exception)); - } - - public function testErrorHandling() - { - $handler = new InvalidParameterExceptionHandler(); - $response = $handler->handle(new InvalidParameterException('error', 1, null, 'include')); - - $this->assertInstanceOf(ResponseBag::class, $response); - $this->assertEquals(400, $response->getStatus()); - $this->assertEquals([['code' => 1, 'source' => ['parameter' => 'include']]], $response->getErrors()); - } -} diff --git a/tests/LinksTraitTest.php b/tests/PaginationLinksTraitTest.php similarity index 60% rename from tests/LinksTraitTest.php rename to tests/PaginationLinksTraitTest.php index 7b8861e..3c2da0a 100644 --- a/tests/LinksTraitTest.php +++ b/tests/PaginationLinksTraitTest.php @@ -12,51 +12,53 @@ namespace Tobscure\Tests\JsonApi; use Tobscure\JsonApi\LinksTrait; +use Tobscure\JsonApi\PaginationLinksTrait; -class LinksTraitTest extends AbstractTestCase +class PaginationLinksTraitTest extends AbstractTestCase { - public function testAddPaginationLinks() + public function testSetPaginationLinks() { - $document = new LinksTraitStub; - $document->addPaginationLinks('http://example.org', [], 0, 20); + $stub = new PaginationLinksTraitStub; + $stub->setPaginationLinks('http://example.org', [], 0, 20); $this->assertEquals([ 'first' => 'http://example.org', 'next' => 'http://example.org?page%5Boffset%5D=20' - ], $document->getLinks()); + ], $stub->getLinks()); - $document = new LinksTraitStub; - $document->addPaginationLinks('http://example.org', ['foo' => 'bar', 'page' => ['limit' => 20]], 30, 20, 100); + $stub = new PaginationLinksTraitStub; + $stub->setPaginationLinks('http://example.org', ['foo' => 'bar', 'page' => ['limit' => 20]], 30, 20, 100); $this->assertEquals([ 'first' => 'http://example.org?foo=bar&page%5Blimit%5D=20', 'prev' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=10', 'next' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=50', 'last' => 'http://example.org?foo=bar&page%5Blimit%5D=20&page%5Boffset%5D=80' - ], $document->getLinks()); + ], $stub->getLinks()); - $document = new LinksTraitStub; - $document->addPaginationLinks('http://example.org', ['page' => ['number' => 2]], 50, 20, 100); + $stub = new PaginationLinksTraitStub; + $stub->setPaginationLinks('http://example.org', ['page' => ['number' => 2]], 50, 20, 100); $this->assertEquals([ 'first' => 'http://example.org', 'prev' => 'http://example.org?page%5Bnumber%5D=2', 'next' => 'http://example.org?page%5Bnumber%5D=4', 'last' => 'http://example.org?page%5Bnumber%5D=5' - ], $document->getLinks()); + ], $stub->getLinks()); - $document = new LinksTraitStub; - $document->addPaginationLinks('http://example.org', ['page' => ['number' => 3, 'size' => 1]], 2, 1, 2); + $stub = new PaginationLinksTraitStub; + $stub->setPaginationLinks('http://example.org', ['page' => ['number' => 3, 'size' => 1]], 2, 1, 2); $this->assertEquals([ 'first' => 'http://example.org?page%5Bsize%5D=1', 'prev' => 'http://example.org?page%5Bnumber%5D=2&page%5Bsize%5D=1', 'last' => 'http://example.org?page%5Bnumber%5D=2&page%5Bsize%5D=1' - ], $document->getLinks()); + ], $stub->getLinks()); } } -class LinksTraitStub +class PaginationLinksTraitStub { use LinksTrait; + use PaginationLinksTrait; } diff --git a/tests/RelationshipTest.php b/tests/RelationshipTest.php index 1749496..4e18255 100644 --- a/tests/RelationshipTest.php +++ b/tests/RelationshipTest.php @@ -16,7 +16,7 @@ class RelationshipTest extends AbstractTestCase { - public function testToArray() + public function testJsonSerialize() { $resource1 = new RelationshipResourceStub(); $resource2 = new RelationshipResourceStub(); @@ -25,7 +25,7 @@ public function testToArray() $this->assertEquals([ 'data' => ['type' => 'stub', 'id' => '1'] - ], $relationship->toArray()); + ], $relationship->jsonSerialize()); $relationship = new Relationship([$resource1, $resource2]); @@ -34,7 +34,7 @@ public function testToArray() ['type' => 'stub', 'id' => '1'], ['type' => 'stub', 'id' => '1'] ] - ], $relationship->toArray()); + ], $relationship->jsonSerialize()); } } From 9cf83360ab4135122b88bda61797cb2e35242998 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 11:18:58 +0000 Subject: [PATCH 29/37] Apply fixes from StyleCI --- src/Document.php | 4 ++-- src/Exception/InvalidParameterException.php | 2 +- src/LinksTrait.php | 2 +- src/PaginationLinksTrait.php | 5 +++-- src/RelatedLinkTrait.php | 1 + src/SelfLinkTrait.php | 1 + 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Document.php b/src/Document.php index 7b7b5e3..c8cec58 100644 --- a/src/Document.php +++ b/src/Document.php @@ -156,7 +156,7 @@ public function setApiVersion($version) /** * Set the jsonapi meta information. - * + * * @param array $meta */ public function setApiMeta(array $meta) @@ -336,7 +336,7 @@ private function mergeResource(array &$map, ResourceInterface $resource, array $ $map[$type][$id] = compact('type', 'id') + $props; } else { $map[$type][$id] = array_replace_recursive($map[$type][$id], $props); - } + } } /** diff --git a/src/Exception/InvalidParameterException.php b/src/Exception/InvalidParameterException.php index 4d561b2..6f3e978 100644 --- a/src/Exception/InvalidParameterException.php +++ b/src/Exception/InvalidParameterException.php @@ -34,7 +34,7 @@ public function __construct($message = '', $code = 0, $previous = null, $invalid /** * Get the parameter that caused this exception. - * + * * @return string */ public function getInvalidParameter() diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 68e43e2..73f75e8 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -53,7 +53,7 @@ public function setLink($key, $value) /** * Remove a link. - * + * * @param string $key */ public function removeLink($key) diff --git a/src/PaginationLinksTrait.php b/src/PaginationLinksTrait.php index 078e2fc..cc32169 100644 --- a/src/PaginationLinksTrait.php +++ b/src/PaginationLinksTrait.php @@ -14,8 +14,9 @@ trait PaginationLinksTrait { abstract public function setLink($key, $value); + abstract public function removeLink($key); - + /** * Set pagination links (first, prev, next, and last). * @@ -90,5 +91,5 @@ private function setPaginationLink($name, $url, array $queryParams, $offset, $li $queryString = http_build_query($queryParams); $this->setLink($name, $url.($queryString ? '?'.$queryString : '')); - } + } } diff --git a/src/RelatedLinkTrait.php b/src/RelatedLinkTrait.php index 7010c5d..bd76324 100644 --- a/src/RelatedLinkTrait.php +++ b/src/RelatedLinkTrait.php @@ -14,6 +14,7 @@ trait RelatedLinkTrait { abstract public function setLink($key, $value); + abstract public function removeLink($key); /** diff --git a/src/SelfLinkTrait.php b/src/SelfLinkTrait.php index 1076179..42cd3bc 100644 --- a/src/SelfLinkTrait.php +++ b/src/SelfLinkTrait.php @@ -14,6 +14,7 @@ trait SelfLinkTrait { abstract public function setLink($key, $value); + abstract public function removeLink($key); /** From d1a008f3dcef74011716af483888150979949b1c Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 21:58:05 +1030 Subject: [PATCH 30/37] Relationship static constructors --- src/Relationship.php | 36 +++++++++++++++++++++++++++------- tests/AbstractResourceTest.php | 2 +- tests/DocumentTest.php | 4 ++-- tests/RelationshipTest.php | 4 ++-- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/Relationship.php b/src/Relationship.php index 891b9fb..789e85b 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -28,14 +28,36 @@ class Relationship implements JsonSerializable */ protected $data; - /** - * Create a new relationship. - * - * @param \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null $data - */ - public function __construct($data = null) + private function __construct() { - $this->data = $data; + } + + public static function fromMeta($meta) + { + $r = new self; + $r->replaceMeta($meta); + return $r; + } + + public static function fromSelfLink($link) + { + $r = new self; + $r->setSelfLink($link); + return $r; + } + + public static function fromRelatedLink($link) + { + $r = new self; + $r->setRelatedLink($link); + return $r; + } + + public static function fromData($data) + { + $r = new self; + $r->data = $data; + return $r; } /** diff --git a/tests/AbstractResourceTest.php b/tests/AbstractResourceTest.php index c7d4a9f..8682e4a 100644 --- a/tests/AbstractResourceTest.php +++ b/tests/AbstractResourceTest.php @@ -65,7 +65,7 @@ public function getId() public function valid() { - return new Relationship(); + return Relationship::fromData(null); } public function invalid() diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index dd1b8de..2615d77 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -92,11 +92,11 @@ public function testIncludeRelationships() $relationshipArray = ['data' => 'stub']; - $relationshipA = $this->getMock(Relationship::class); + $relationshipA = $this->getMockBuilder(Relationship::class)->disableOriginalConstructor()->getMock(); $relationshipA->method('getData')->willReturn($resource2); $relationshipA->method('jsonSerialize')->willReturn($relationshipArray); - $relationshipB = $this->getMock(Relationship::class); + $relationshipB = $this->getMockBuilder(Relationship::class)->disableOriginalConstructor()->getMock(); $relationshipB->method('getData')->willReturn($resource3); $relationshipB->method('jsonSerialize')->willReturn($relationshipArray); diff --git a/tests/RelationshipTest.php b/tests/RelationshipTest.php index 4e18255..389bad1 100644 --- a/tests/RelationshipTest.php +++ b/tests/RelationshipTest.php @@ -21,13 +21,13 @@ public function testJsonSerialize() $resource1 = new RelationshipResourceStub(); $resource2 = new RelationshipResourceStub(); - $relationship = new Relationship($resource1); + $relationship = Relationship::fromData($resource1); $this->assertEquals([ 'data' => ['type' => 'stub', 'id' => '1'] ], $relationship->jsonSerialize()); - $relationship = new Relationship([$resource1, $resource2]); + $relationship = Relationship::fromData([$resource1, $resource2]); $this->assertEquals([ 'data' => [ From a39459df0af79208f4c029a85f11d1a0f5eff812 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 28 Mar 2017 11:30:14 +0000 Subject: [PATCH 31/37] Apply fixes from StyleCI --- src/Relationship.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Relationship.php b/src/Relationship.php index 789e85b..8d36518 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -36,6 +36,7 @@ public static function fromMeta($meta) { $r = new self; $r->replaceMeta($meta); + return $r; } @@ -43,6 +44,7 @@ public static function fromSelfLink($link) { $r = new self; $r->setSelfLink($link); + return $r; } @@ -50,6 +52,7 @@ public static function fromRelatedLink($link) { $r = new self; $r->setRelatedLink($link); + return $r; } @@ -57,6 +60,7 @@ public static function fromData($data) { $r = new self; $r->data = $data; + return $r; } From d50ba8ad8d01ea50a9b845c0fbfcdd0aaedb662d Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 29 Mar 2017 21:05:54 +1030 Subject: [PATCH 32/37] More big changes, hopefully the last lot * Add internal `ResourceIdentifier` and `ResourceObject` classes so that all serialization is done via `JsonSerializable` * Change `AbstractResource` relationship method name format to `getXxxRelationship` (rather than just `xxx`) * Further internal refactor of `Document` and resource merging process * Rename links/meta methods again: `setLinks`, `setLink`, `removeLink` `setMeta`, `setMetaItem`, `removeMetaItem` * Remove getters where they are not essential, make properties private * Finalize README --- README.md | 51 +++---- src/AbstractResource.php | 48 +++--- src/Document.php | 236 ++++++++--------------------- src/Error.php | 25 +-- src/Link.php | 5 - src/LinksTrait.php | 21 +-- src/MetaTrait.php | 23 +-- src/Relationship.php | 45 +----- src/ResourceIdentifier.php | 40 +++++ src/ResourceObject.php | 65 ++++++++ tests/AbstractResourceTest.php | 4 +- tests/DocumentTest.php | 10 +- tests/PaginationLinksTraitTest.php | 5 + tests/RelationshipTest.php | 8 +- 14 files changed, 259 insertions(+), 327 deletions(-) create mode 100644 src/ResourceIdentifier.php create mode 100644 src/ResourceObject.php diff --git a/README.md b/README.md index c73cde2..fe63f4f 100644 --- a/README.md +++ b/README.md @@ -23,30 +23,23 @@ composer require tobscure/json-api ```php use Tobscure\JsonApi\Document; -// Create a resource object for a post. $resource = new PostResource($post); -// Create a JSON-API document with that resource as the primary data. $document = Document::fromData($resource); -// Specify included relationships and sparse fieldsets. $document->setInclude(['author', 'comments']); $document->setFields(['posts' => ['title', 'body']]); -// Add metadata and links. -$document->setMeta('total', count($posts)); -$document->setSelfLink('http://example.com/api/posts'); +$document->setMetaItem('total', count($posts)); +$document->setSelfLink('http://example.com/api/posts/1'); -// Output the document with the JSON-API media type. header('Content-Type: ' . $document::MEDIA_TYPE); -echo $document; +echo json_encode($document); ``` -### Resources & Collections +### Resources -The JSON-API spec describes [resource objects](http://jsonapi.org/format/#document-resource-objects) as objects representing about a single resource. A resource object is represented by the `Tobscure\JsonApi\ResourceInterface` interface. - -You should create a class which implements this interface for each resource type in your API. A base `AbstractResource` class is provided with some basic functionality. At a minimum, subclasses must specify the resource `$type` and implement the `getId()` method: +Resources are used to create JSON-API [resource objects](http://jsonapi.org/format/#document-resource-objects). They must implement `Tobscure\JsonApi\ResourceInterface`. An `AbstractResource` class is provided with some basic functionality. Subclasses must specify the resource `$type` and implement the `getId()` method: ```php use Tobscure\JsonApi\AbstractResource; @@ -69,7 +62,7 @@ class PostResource extends AbstractResource } ``` -An instantiated resource object can then be added to the JSON-API document: +A JSON-API document can then be created from an instantiated resource: ```php $resource = new PostResource($post); @@ -77,19 +70,19 @@ $resource = new PostResource($post); $document = Document::fromData($resource); ``` -To output a collection of resources, map your data to an array of Resource objects: +To output a collection of resource objects, map your data to an array of resources: ```php -$collection = array_map(function (Post $post) { +$resources = array_map(function (Post $post) { return new PostResource($post); }, $posts); -$document = Document::fromData($collection); +$document = Document::fromData($resources); ``` #### Attributes & Sparse Fieldsets -To add [attributes](http://jsonapi.org/format/#document-resource-object-attributes) to your resources, you may implement the `getAttributes()` method: +To add [attributes](http://jsonapi.org/format/#document-resource-object-attributes) to your resource objects, you may implement the `getAttributes()` method in your resource: ```php public function getAttributes(array $fields = null) @@ -102,13 +95,13 @@ To add [attributes](http://jsonapi.org/format/#document-resource-object-attribut } ``` -To support [sparse fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets), you may specify which [fields](http://jsonapi.org/format/#document-resource-object-fields) (attributes and relationships) are to be included on the Document. You must provide a multidimensional array organized by resource type: +To output resource objects with a [sparse fieldset](http://jsonapi.org/format/#fetching-sparse-fieldsets), pass in an array of [fields](http://jsonapi.org/format/#document-resource-object-fields) (attributes and relationships), organised by resource type: ```php $document->setFields(['posts' => ['title', 'body']]); ``` -The attributes returned by your Resources will automatically be filtered according to the sparse fieldset for the resource type. However, if some attributes are expensive to calculate, then you may use the `$fields` argument provided to `getAttributes()` to improve performance when sparse fieldsets are used. This argument will be `null` if no sparse fieldset has been specified for the resource type, or an `array` of fields if it has: +The attributes returned by your resources will automatically be filtered according to the sparse fieldset for the resource type. However, if some attributes are expensive to calculate, then you can use the `$fields` argument provided to `getAttributes()`. This will be an `array` of fields, or `null` if no sparse fieldset has been specified. ```php public function getAttributes(array $fields = null) @@ -125,10 +118,10 @@ The attributes returned by your Resources will automatically be filtered accordi #### Relationships -To support the [inclusion of related resources](http://jsonapi.org/format/#fetching-includes) alongside the document's primary resources (and output [compound documents](http://jsonapi.org/format/#document-compound-documents)), first you must define the available relationships on your Resource implementation. The `AbstractResource` base class allows you to define a method for each relationship that exists for a resource type. Relationship methods should return a `Tobscure\JsonApi\Relationship` instance, containing the related Resource(s). +You can [include related resources](http://jsonapi.org/format/#document-compound-documents) alongside the document's primary data. First you must define the available relationships on your resource. The `AbstractResource` base class allows you to define a method for each relationship. Relationship methods should return a `Tobscure\JsonApi\Relationship` instance, containing the related resource(s). ```php - protected function author() + protected function getAuthorRelationship() { $resource = new UserResource($this->post->author); @@ -136,13 +129,13 @@ To support the [inclusion of related resources](http://jsonapi.org/format/#fetch } ``` -You can then specify which relationships should be included on the Document: +You can then specify which relationship paths should be included on the document: ```php $document->setInclude(['author', 'comments', 'comments.author']); ``` -By default, the `AbstractResource` implementation will convert included relationship names from `kebab-case` and `snake_case` into a `camelCase` method name. If you wish to customize this behaviour, you may override the `getRelationship` method: +By default, the `AbstractResource` implementation will convert included relationship names from `kebab-case` and `snake_case` into a `getCamelCaseRelationship` method name. If you wish to customize this behaviour, you may override the `getRelationship` method: ```php public function getRelationship($name) @@ -153,12 +146,12 @@ By default, the `AbstractResource` implementation will convert included relation ### Meta Information & Links -The `Document`, `Resource`, and `Relationship` classes allow you to add [meta information](http://jsonapi.org/format/#document-meta): +The `Document`, `AbstractResource`, and `Relationship` classes allow you to add [meta information](http://jsonapi.org/format/#document-meta): ```php -$document->setMeta('key', 'value'); -$document->removeMeta('key'); -$document->replaceMeta(['key' => 'value']); +$document->setMeta(['key' => 'value']); +$document->setMetaItem('key', 'value'); +$document->removeMetaItem('key'); ``` They also allow you to add [links](http://jsonapi.org/format/#document-links). A link's value may be a string, or a `Tobscure\JsonApi\Link` instance. @@ -170,7 +163,7 @@ $resource->setSelfLink('url'); $relationship->setRelatedLink(new Link('url', ['some' => 'metadata'])); ``` -You can also easily generate [pagination](http://jsonapi.org/format/#fetching-pagination) links on `Document` and `Relationship` instances: +You can also easily generate [pagination links](http://jsonapi.org/format/#fetching-pagination) on `Document` and `Relationship` instances: ```php $document->setPaginationLinks( @@ -194,7 +187,7 @@ class PostResource extends AbstractResource $this->post = $post; $this->setSelfLink('/posts/' . $post->id); - $this->setMeta('some', 'metadata for ' . $post->id); + $this->setMetaItem('some', 'metadata for ' . $post->id); } // ... diff --git a/src/AbstractResource.php b/src/AbstractResource.php index 8646d7a..0e72bc3 100644 --- a/src/AbstractResource.php +++ b/src/AbstractResource.php @@ -15,9 +15,7 @@ abstract class AbstractResource implements ResourceInterface { - use LinksTrait; - use SelfLinkTrait; - use MetaTrait; + use LinksTrait, SelfLinkTrait, MetaTrait; /** * The resource type. @@ -42,24 +40,42 @@ public function getAttributes(array $fields = null) return []; } + /** + * Get the links. + * + * @return array + */ + public function getLinks() + { + return $this->links; + } + + /** + * Get the meta data. + * + * @return array + */ + public function getMeta() + { + return $this->meta; + } + /** * {@inheritdoc} * - * @throws \LogicException + * @throws LogicException */ public function getRelationship($name) { $method = $this->getRelationshipMethodName($name); - if (method_exists($this, $method)) { - $relationship = $this->$method(); + $relationship = $this->$method(); - if ($relationship !== null && ! ($relationship instanceof Relationship)) { - throw new LogicException('Relationship method must return null or an instance of Tobscure\JsonApi\Relationship'); - } - - return $relationship; + if ($relationship !== null && ! ($relationship instanceof Relationship)) { + throw new LogicException('Relationship method must return null or an instance of Tobscure\JsonApi\Relationship'); } + + return $relationship; } /** @@ -73,14 +89,6 @@ public function getRelationship($name) */ private function getRelationshipMethodName($name) { - if (stripos($name, '-')) { - $name = lcfirst(implode('', array_map('ucfirst', explode('-', $name)))); - } - - if (stripos($name, '_')) { - $name = lcfirst(implode('', array_map('ucfirst', explode('_', $name)))); - } - - return $name; + return 'get'.implode(array_map('ucfirst', preg_split('/[-_]/', $name))).'Relationship'; } } diff --git a/src/Document.php b/src/Document.php index c8cec58..defbb8b 100644 --- a/src/Document.php +++ b/src/Document.php @@ -15,52 +15,17 @@ class Document implements JsonSerializable { - use LinksTrait; - use SelfLinkTrait; - use PaginationLinksTrait; - use MetaTrait; + use LinksTrait, SelfLinkTrait, PaginationLinksTrait, MetaTrait; const MEDIA_TYPE = 'application/vnd.api+json'; - const DEFAULT_API_VERSION = '1.0'; - /** - * The primary data. - * - * @var ResourceInterface|ResourceInterface[]|null - */ - protected $data; - - /** - * The errors array. - * - * @var Error[]|null - */ - protected $errors; - - /** - * The jsonapi array. - * - * @var array|null - */ - protected $jsonapi; - - /** - * Relationships to include. - * - * @var array - */ - protected $include = []; + private $data; + private $errors; + private $jsonapi; - /** - * Sparse fieldsets. - * - * @var array - */ - protected $fields = []; + private $include = []; + private $fields = []; - /** - * Use named constructors instead. - */ private function __construct() { } @@ -86,7 +51,7 @@ public static function fromData($data) public static function fromMeta(array $meta) { $document = new self; - $document->replaceMeta($meta); + $document->setMeta($meta); return $document; } @@ -105,17 +70,7 @@ public static function fromErrors(array $errors) } /** - * Get the primary data. - * - * @return ResourceInterface|ResourceInterface[]|null $data - */ - public function getData() - { - return $this->data; - } - - /** - * Set the data object. + * Set the primary data. * * @param ResourceInterface|ResourceInterface[]|null $data */ @@ -124,16 +79,6 @@ public function setData($data) $this->data = $data; } - /** - * Get the errors array. - * - * @return Error[]|null $errors - */ - public function getErrors() - { - return $this->errors; - } - /** * Set the errors array. * @@ -165,41 +110,21 @@ public function setApiMeta(array $meta) } /** - * Get the relationships to include. - * - * @return string[] $include - */ - public function getInclude() - { - return $this->include; - } - - /** - * Set the relationships to include. - * + * Set the relationship paths to include. + * * @param string[] $include */ - public function setInclude(array $include) + public function setInclude($include) { $this->include = $include; } - /** - * Get the sparse fieldsets. - * - * @return array[] $fields - */ - public function getFields() - { - return $this->fields; - } - /** * Set the sparse fieldsets. - * - * @param array[] $fields + * + * @param array $fields */ - public function setFields(array $fields) + public function setFields($fields) { $this->fields = $fields; } @@ -220,67 +145,38 @@ public function jsonSerialize() if ($this->data) { $isCollection = is_array($this->data); - - // Build a multi-dimensional map of all of the distinct resources - // that are present in the document, indexed by type and ID. This is - // done by recursively looping through each of the resources and - // their included relationships. We do this so that any resources - // that are duplicated may be merged back into a single instance. - $map = []; $resources = $isCollection ? $this->data : [$this->data]; - $this->mergeResources($map, $resources, $this->include); + $map = $this->buildResourceMap($resources); - // Now extract the document's primary resource(s) from the resource - // map, and flatten the map's remaining resources to be included in - // the document's "included" array. - foreach ($resources as $resource) { - $type = $resource->getType(); - $id = $resource->getId(); - - if (isset($map[$type][$id])) { - $primary[] = $map[$type][$id]; - unset($map[$type][$id]); - } - } + $primary = $this->extractResourcesFromMap($map, $resources); $document['data'] = $isCollection ? $primary : $primary[0]; - $document['included'] = call_user_func_array('array_merge', $map); + + if ($map) { + $document['included'] = call_user_func_array('array_merge', $map); + } } - return array_filter($document); + return (object) array_filter($document); } - /** - * Build the JSON-API document and encode it as a JSON string. - * - * @return string - */ - public function __toString() + private function buildResourceMap(array $resources) { - return json_encode($this->jsonSerialize()); + $map = []; + + $include = $this->buildRelationshipTree($this->include); + + $this->mergeResources($map, $resources, $include); + + return $map; } - /** - * Recursively add the given resources and their relationships to a map. - * - * @param array &$map The map to merge resources into. - * @param ResourceInterface[] $resources - * @param array $include An array of relationship paths to include. - */ private function mergeResources(array &$map, array $resources, array $include) { - // Index relationship paths so that we have a list of the direct - // relationships that will be included on these resources, and arrays - // of their respective nested relationships. - $include = $this->indexRelationshipPaths($include); - foreach ($resources as $resource) { $relationships = []; - // Get each of the relationships we're including on this resource, - // and add their resources (and their relationships, and so on) to - // the map. foreach ($include as $name => $nested) { if (! ($relationship = $resource->getRelationship($name))) { continue; @@ -295,29 +191,16 @@ private function mergeResources(array &$map, array $resources, array $include) } } - // Serialize the resource into an array and add it to the map. If - // it is already present, its properties will be merged into the - // existing resource. $this->mergeResource($map, $resource, $relationships); } } - /** - * Merge the given resource into a resource map. - * - * If it is already present in the map, its properties will be merged into - * the existing resource. - * - * @param array &$map - * @param ResourceInterface $resource - * @param Relationship[] $relationships - */ private function mergeResource(array &$map, ResourceInterface $resource, array $relationships) { $type = $resource->getType(); $id = $resource->getId(); - $meta = $resource->getMeta(); $links = $resource->getLinks(); + $meta = $resource->getMeta(); $fields = isset($this->fields[$type]) ? $this->fields[$type] : null; @@ -330,44 +213,47 @@ private function mergeResource(array &$map, ResourceInterface $resource, array $ $relationships = array_intersect_key($relationships, $keys); } - $props = array_filter(compact('attributes', 'relationships', 'links', 'meta')); - if (empty($map[$type][$id])) { - $map[$type][$id] = compact('type', 'id') + $props; - } else { - $map[$type][$id] = array_replace_recursive($map[$type][$id], $props); + $map[$type][$id] = new ResourceObject($type, $id); } + + array_map([$map[$type][$id], 'setAttribute'], array_keys($attributes), $attributes); + array_map([$map[$type][$id], 'setRelationship'], array_keys($relationships), $relationships); + array_map([$map[$type][$id], 'setLink'], array_keys($links), $links); + array_map([$map[$type][$id], 'setMetaItem'], array_keys($meta), $meta); } - /** - * Index relationship paths by top-level relationships. - * - * Given an array of relationship paths such as: - * - * ['user', 'user.employer', 'user.employer.country', 'comments'] - * - * Returns an array with key-value pairs of top-level relationships and - * their nested relationships: - * - * ['user' => ['employer', 'employer.country'], 'comments' => []] - * - * @param string[] $paths - * - * @return array[] - */ - private function indexRelationshipPaths(array $paths) + private function extractResourcesFromMap(array &$map, array $resources) + { + return array_filter( + array_map(function ($resource) use (&$map) { + $type = $resource->getType(); + $id = $resource->getId(); + + if (isset($map[$type][$id])) { + $resource = $map[$type][$id]; + unset($map[$type][$id]); + + return $resource; + } + }, $resources) + ); + } + + private function buildRelationshipTree(array $paths) { $tree = []; foreach ($paths as $path) { - list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null); + $keys = explode('.', $path); + $array = &$tree; - if (! isset($tree[$primary])) { - $tree[$primary] = []; - } + foreach ($keys as $key) { + if (! isset($array[$key])) { + $array[$key] = []; + } - if ($nested) { - $tree[$primary][] = $nested; + $array = &$array[$key]; } } diff --git a/src/Error.php b/src/Error.php index c68f294..061c7fd 100644 --- a/src/Error.php +++ b/src/Error.php @@ -15,8 +15,7 @@ class Error implements JsonSerializable { - use LinksTrait; - use MetaTrait; + use LinksTrait, MetaTrait; private $id; private $status; @@ -67,15 +66,17 @@ public function setSourceParameter($parameter) public function jsonSerialize() { - return array_filter([ - 'id' => $this->id, - 'links' => $this->links, - 'status' => $this->status, - 'code' => $this->code, - 'title' => $this->title, - 'detail' => $this->detail, - 'source' => $this->source, - 'meta' => $this->meta - ]); + return array_filter( + [ + 'id' => $this->id, + 'links' => $this->links, + 'status' => $this->status, + 'code' => $this->code, + 'title' => $this->title, + 'detail' => $this->detail, + 'source' => $this->source, + 'meta' => $this->meta + ] + ); } } diff --git a/src/Link.php b/src/Link.php index 1b97b19..d3a2c31 100644 --- a/src/Link.php +++ b/src/Link.php @@ -25,11 +25,6 @@ public function __construct($href, $meta = null) $this->meta = $meta; } - public function getHref() - { - return $this->href; - } - public function setHref($href) { $this->href = $href; diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 73f75e8..2eac1d4 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -13,29 +13,14 @@ trait LinksTrait { - /** - * The links. - * - * @var array - */ - protected $links = []; - - /** - * Get the links. - * - * @return array - */ - public function getLinks() - { - return $this->links; - } + private $links = []; /** - * Replace the links. + * Set the links. * * @param array $links */ - public function replaceLinks(array $links) + public function setLinks(array $links) { $this->links = $links; } diff --git a/src/MetaTrait.php b/src/MetaTrait.php index 86acb46..e7d3816 100644 --- a/src/MetaTrait.php +++ b/src/MetaTrait.php @@ -13,29 +13,14 @@ trait MetaTrait { - /** - * The meta data. - * - * @var array - */ - protected $meta = []; - - /** - * Get the meta data. - * - * @return array - */ - public function getMeta() - { - return $this->meta; - } + private $meta = []; /** * Set the meta data. * * @param array $meta */ - public function replaceMeta(array $meta) + public function setMeta(array $meta) { $this->meta = $meta; } @@ -46,7 +31,7 @@ public function replaceMeta(array $meta) * @param string $key * @param mixed $value */ - public function setMeta($key, $value) + public function setMetaItem($key, $value) { $this->meta[$key] = $value; } @@ -57,7 +42,7 @@ public function setMeta($key, $value) * @param string $key * @param mixed $value */ - public function removeMeta($key) + public function removeMetaItem($key) { unset($this->meta[$key]); } diff --git a/src/Relationship.php b/src/Relationship.php index 8d36518..ab0bbb5 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -15,18 +15,9 @@ class Relationship implements JsonSerializable { - use LinksTrait; - use SelfLinkTrait; - use RelatedLinkTrait; - use PaginationLinksTrait; - use MetaTrait; - - /** - * The data object. - * - * @var \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null - */ - protected $data; + use LinksTrait, SelfLinkTrait, RelatedLinkTrait, PaginationLinksTrait, MetaTrait; + + private $data; private function __construct() { @@ -64,31 +55,16 @@ public static function fromData($data) return $r; } - /** - * Get the data object. - * - * @return \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null - */ public function getData() { return $this->data; } - /** - * Set the data object. - * - * @param \Tobscure\JsonApi\ResourceInterface|\Tobscure\JsonApi\ResourceInterface[]|null $data - */ public function setData($data) { $this->data = $data; } - /** - * Build the relationship as an array. - * - * @return array - */ public function jsonSerialize() { $relationship = []; @@ -105,18 +81,11 @@ public function jsonSerialize() ]); } - /** - * Build an idenitfier array for the given resource. - * - * @param ResourceInterface $resource - * - * @return array - */ private function buildIdentifier(ResourceInterface $resource) { - return [ - 'type' => $resource->getType(), - 'id' => $resource->getId() - ]; + $id = new ResourceIdentifier($resource->getType(), $resource->getId()); + $id->setMeta($resource->getMeta()); + + return $id; } } diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php new file mode 100644 index 0000000..28e47b7 --- /dev/null +++ b/src/ResourceIdentifier.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\JsonApi; + +use JsonSerializable; +use Tobscure\JsonApi\MetaTrait; + +class ResourceIdentifier implements JsonSerializable +{ + use MetaTrait; + + private $type; + private $id; + + public function __construct($type, $id) + { + $this->type = $type; + $this->id = $id; + } + + public function jsonSerialize() + { + return array_filter( + [ + 'type' => $this->type, + 'id' => $this->id, + 'meta' => $this->meta + ] + ); + } +} diff --git a/src/ResourceObject.php b/src/ResourceObject.php new file mode 100644 index 0000000..cbb3803 --- /dev/null +++ b/src/ResourceObject.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\JsonApi; + +use LogicException; +use InvalidArgumentException; + +class ResourceObject extends ResourceIdentifier +{ + use LinksTrait, SelfLinkTrait; + + private $attributes = []; + private $relationships = []; + + public function setAttribute($name, $value) + { + if (! $this->validateField($name)) { + throw new InvalidArgumentException('Invalid attribute name'); + } + + if (isset($this->relationships[$name])) { + throw new LogicException("Field $name already exists in relationships"); + } + + $this->attributes[$name] = $value; + } + + public function setRelationship($name, Relationship $value) + { + if (! $this->validateField($name)) { + throw new InvalidArgumentException('Invalid relationship name'); + } + + if (isset($this->attributes[$name])) { + throw new LogicException("Field $name already exists in attributes"); + } + + $this->relationships[$name] = $value; + } + + public function jsonSerialize() + { + return array_filter( + parent::jsonSerialize() + [ + 'attributes' => $this->attributes, + 'relationships' => $this->relationships, + 'links' => $this->links + ] + ); + } + + private function validateField($name) + { + return ! in_array($name, ['id', 'type']); + } +} diff --git a/tests/AbstractResourceTest.php b/tests/AbstractResourceTest.php index 8682e4a..96253c3 100644 --- a/tests/AbstractResourceTest.php +++ b/tests/AbstractResourceTest.php @@ -63,12 +63,12 @@ public function getId() { } - public function valid() + public function getValidRelationship() { return Relationship::fromData(null); } - public function invalid() + public function getInvalidRelationship() { return 'invalid'; } diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 2615d77..4bfa956 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -94,11 +94,11 @@ public function testIncludeRelationships() $relationshipA = $this->getMockBuilder(Relationship::class)->disableOriginalConstructor()->getMock(); $relationshipA->method('getData')->willReturn($resource2); - $relationshipA->method('jsonSerialize')->willReturn($relationshipArray); + $relationshipA->method('jsonSerialize')->willReturn($relationshipJson); $relationshipB = $this->getMockBuilder(Relationship::class)->disableOriginalConstructor()->getMock(); $relationshipB->method('getData')->willReturn($resource3); - $relationshipB->method('jsonSerialize')->willReturn($relationshipArray); + $relationshipB->method('jsonSerialize')->willReturn($relationshipJson); $resource1 ->expects($this->once()) @@ -119,7 +119,7 @@ public function testIncludeRelationships() 'data' => [ 'type' => 'a', 'id' => '1', - 'relationships' => ['a' => $relationshipArray] + 'relationships' => ['a' => $relationshipJson] ], 'included' => [ [ @@ -129,7 +129,7 @@ public function testIncludeRelationships() [ 'type' => 'a', 'id' => '2', - 'relationships' => ['b' => $relationshipArray] + 'relationships' => ['b' => $relationshipJson] ] ] ]), json_encode($document)); @@ -144,7 +144,7 @@ public function testErrors() public function testLinks() { - $document = Document::fromData(null); + $document = Document::fromMeta([]); $document->setLink('a', 'b'); $this->assertJsonStringEqualsJsonString(json_encode(['links' => ['a' => 'b']]), json_encode($document)); diff --git a/tests/PaginationLinksTraitTest.php b/tests/PaginationLinksTraitTest.php index 3c2da0a..076f30d 100644 --- a/tests/PaginationLinksTraitTest.php +++ b/tests/PaginationLinksTraitTest.php @@ -61,4 +61,9 @@ class PaginationLinksTraitStub { use LinksTrait; use PaginationLinksTrait; + + public function getLinks() + { + return $this->links; + } } diff --git a/tests/RelationshipTest.php b/tests/RelationshipTest.php index 389bad1..c9584bf 100644 --- a/tests/RelationshipTest.php +++ b/tests/RelationshipTest.php @@ -23,18 +23,18 @@ public function testJsonSerialize() $relationship = Relationship::fromData($resource1); - $this->assertEquals([ + $this->assertJsonStringEqualsJsonString(json_encode([ 'data' => ['type' => 'stub', 'id' => '1'] - ], $relationship->jsonSerialize()); + ]), json_encode($relationship)); $relationship = Relationship::fromData([$resource1, $resource2]); - $this->assertEquals([ + $this->assertJsonStringEqualsJsonString(json_encode([ 'data' => [ ['type' => 'stub', 'id' => '1'], ['type' => 'stub', 'id' => '1'] ] - ], $relationship->jsonSerialize()); + ]), json_encode($relationship)); } } From e940b9fb7befe8feff39366a99b3edec15edf1a5 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 29 Mar 2017 10:39:40 +0000 Subject: [PATCH 33/37] Apply fixes from StyleCI --- src/AbstractResource.php | 2 +- src/Document.php | 6 +++--- src/ResourceIdentifier.php | 1 - src/ResourceObject.php | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/AbstractResource.php b/src/AbstractResource.php index 0e72bc3..b975161 100644 --- a/src/AbstractResource.php +++ b/src/AbstractResource.php @@ -49,7 +49,7 @@ public function getLinks() { return $this->links; } - + /** * Get the meta data. * diff --git a/src/Document.php b/src/Document.php index defbb8b..e7bf0c2 100644 --- a/src/Document.php +++ b/src/Document.php @@ -111,7 +111,7 @@ public function setApiMeta(array $meta) /** * Set the relationship paths to include. - * + * * @param string[] $include */ public function setInclude($include) @@ -121,7 +121,7 @@ public function setInclude($include) /** * Set the sparse fieldsets. - * + * * @param array $fields */ public function setFields($fields) @@ -237,7 +237,7 @@ private function extractResourcesFromMap(array &$map, array $resources) return $resource; } }, $resources) - ); + ); } private function buildRelationshipTree(array $paths) diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php index 28e47b7..d4c4915 100644 --- a/src/ResourceIdentifier.php +++ b/src/ResourceIdentifier.php @@ -12,7 +12,6 @@ namespace Tobscure\JsonApi; use JsonSerializable; -use Tobscure\JsonApi\MetaTrait; class ResourceIdentifier implements JsonSerializable { diff --git a/src/ResourceObject.php b/src/ResourceObject.php index cbb3803..bd130a7 100644 --- a/src/ResourceObject.php +++ b/src/ResourceObject.php @@ -11,8 +11,8 @@ namespace Tobscure\JsonApi; -use LogicException; use InvalidArgumentException; +use LogicException; class ResourceObject extends ResourceIdentifier { From 6bf587313e246a93cda0792903f7abe8ee99fbc2 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 29 Mar 2017 21:28:34 +1030 Subject: [PATCH 34/37] Fix failing test --- tests/DocumentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 4bfa956..be338a0 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -90,7 +90,7 @@ public function testIncludeRelationships() $resource2 = $this->mockResource('a', '2'); $resource3 = $this->mockResource('b', '1'); - $relationshipArray = ['data' => 'stub']; + $relationshipJson = ['data' => 'stub']; $relationshipA = $this->getMockBuilder(Relationship::class)->disableOriginalConstructor()->getMock(); $relationshipA->method('getData')->willReturn($resource2); From de7305582552fb93aa003269bd4ad6a673ec8e11 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 30 Mar 2017 14:35:50 +1030 Subject: [PATCH 35/37] Fix Relationship constructors --- src/Relationship.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Relationship.php b/src/Relationship.php index ab0bbb5..d28843d 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -26,7 +26,7 @@ private function __construct() public static function fromMeta($meta) { $r = new self; - $r->replaceMeta($meta); + $r->setMeta($meta); return $r; } @@ -50,7 +50,7 @@ public static function fromRelatedLink($link) public static function fromData($data) { $r = new self; - $r->data = $data; + $r->setData($data); return $r; } From 10adcf3b85a56b157565a6b28aa65d28fc5ebd10 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Wed, 13 Sep 2017 20:25:56 -0700 Subject: [PATCH 36/37] DRY-ed up tests --- src/Document.php | 2 +- tests/AbstractTestCase.php | 11 ++++ tests/DocumentTest.php | 109 +++++++++++++++++++++---------------- tests/RelationshipTest.php | 24 +++++--- 4 files changed, 89 insertions(+), 57 deletions(-) diff --git a/src/Document.php b/src/Document.php index e7bf0c2..a5c509a 100644 --- a/src/Document.php +++ b/src/Document.php @@ -132,7 +132,7 @@ public function setFields($fields) /** * Serialize for JSON usage. * - * @return array + * @return object */ public function jsonSerialize() { diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index 8a046e0..050bc7d 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -15,4 +15,15 @@ abstract class AbstractTestCase extends PHPUnit_Framework_TestCase { + /** + * Asserts the two values encode to json and produce same result. + * + * @param mixed $expected + * @param mixed $actual + * @param string $message + */ + public static function assertProduceSameJson($expected, $actual, $message = '') + { + self::assertJsonStringEqualsJsonString(json_encode($expected), json_encode($actual), $message); + } } diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index be338a0..ecfd37e 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -23,9 +23,12 @@ public function testResource() $document = Document::fromData($resource); - $this->assertJsonStringEqualsJsonString(json_encode([ - 'data' => ['type' => 'a', 'id' => '1'] - ]), json_encode($document)); + $this->assertProduceSameJson( + [ + 'data' => ['type' => 'a', 'id' => '1'], + ], + $document + ); } public function testCollection() @@ -35,12 +38,15 @@ public function testCollection() $document = Document::fromData([$resource1, $resource2]); - $this->assertJsonStringEqualsJsonString(json_encode([ - 'data' => [ - ['type' => 'a', 'id' => '1'], - ['type' => 'a', 'id' => '2'] - ] - ]), json_encode($document)); + $this->assertProduceSameJson( + [ + 'data' => [ + ['type' => 'a', 'id' => '1'], + ['type' => 'a', 'id' => '2'], + ], + ], + $document + ); } public function testMergeResource() @@ -53,17 +59,20 @@ public function testMergeResource() $document = Document::fromData([$resource1, $resource2]); - $this->assertJsonStringEqualsJsonString(json_encode([ - 'data' => [ - [ - 'type' => 'a', - 'id' => '1', - 'attributes' => $merged = array_merge($array1, $array2), - 'meta' => $merged, - 'links' => $merged - ] - ] - ]), json_encode($document)); + $this->assertProduceSameJson( + [ + 'data' => [ + [ + 'type' => 'a', + 'id' => '1', + 'attributes' => $merged = array_merge($array1, $array2), + 'meta' => $merged, + 'links' => $merged, + ], + ], + ], + $document + ); } public function testSparseFieldsets() @@ -75,13 +84,16 @@ public function testSparseFieldsets() $document = Document::fromData($resource); $document->setFields(['a' => ['present']]); - $this->assertJsonStringEqualsJsonString(json_encode([ - 'data' => [ - 'type' => 'a', - 'id' => '1', - 'attributes' => ['present' => 1] - ] - ]), json_encode($document)); + $this->assertProduceSameJson( + [ + 'data' => [ + 'type' => 'a', + 'id' => '1', + 'attributes' => ['present' => 1], + ], + ], + $document + ); } public function testIncludeRelationships() @@ -115,31 +127,34 @@ public function testIncludeRelationships() $document = Document::fromData($resource1); $document->setInclude(['a', 'a.b']); - $this->assertJsonStringEqualsJsonString(json_encode([ - 'data' => [ - 'type' => 'a', - 'id' => '1', - 'relationships' => ['a' => $relationshipJson] - ], - 'included' => [ - [ - 'type' => 'b', - 'id' => '1' - ], - [ + $this->assertProduceSameJson( + [ + 'data' => [ 'type' => 'a', - 'id' => '2', - 'relationships' => ['b' => $relationshipJson] - ] - ] - ]), json_encode($document)); + 'id' => '1', + 'relationships' => ['a' => $relationshipJson], + ], + 'included' => [ + [ + 'type' => 'b', + 'id' => '1', + ], + [ + 'type' => 'a', + 'id' => '2', + 'relationships' => ['b' => $relationshipJson], + ], + ], + ], + $document + ); } public function testErrors() { $document = Document::fromErrors(['a']); - $this->assertJsonStringEqualsJsonString(json_encode(['errors' => ['a']]), json_encode($document)); + $this->assertProduceSameJson(['errors' => ['a']], $document); } public function testLinks() @@ -147,14 +162,14 @@ public function testLinks() $document = Document::fromMeta([]); $document->setLink('a', 'b'); - $this->assertJsonStringEqualsJsonString(json_encode(['links' => ['a' => 'b']]), json_encode($document)); + $this->assertProduceSameJson(['links' => ['a' => 'b']], $document); } public function testMeta() { $document = Document::fromMeta(['a' => 'b']); - $this->assertJsonStringEqualsJsonString(json_encode(['meta' => ['a' => 'b']]), json_encode($document)); + $this->assertProduceSameJson(['meta' => ['a' => 'b']], $document); } private function mockResource($type, $id, $attributes = [], $meta = [], $links = []) diff --git a/tests/RelationshipTest.php b/tests/RelationshipTest.php index c9584bf..340a4b5 100644 --- a/tests/RelationshipTest.php +++ b/tests/RelationshipTest.php @@ -23,18 +23,24 @@ public function testJsonSerialize() $relationship = Relationship::fromData($resource1); - $this->assertJsonStringEqualsJsonString(json_encode([ - 'data' => ['type' => 'stub', 'id' => '1'] - ]), json_encode($relationship)); + $this->assertProduceSameJson( + [ + 'data' => ['type' => 'stub', 'id' => '1'], + ], + $relationship + ); $relationship = Relationship::fromData([$resource1, $resource2]); - $this->assertJsonStringEqualsJsonString(json_encode([ - 'data' => [ - ['type' => 'stub', 'id' => '1'], - ['type' => 'stub', 'id' => '1'] - ] - ]), json_encode($relationship)); + $this->assertProduceSameJson( + [ + 'data' => [ + ['type' => 'stub', 'id' => '1'], + ['type' => 'stub', 'id' => '1'], + ], + ], + $relationship + ); } } From 50ffec70b9c65c117d01fbab81c84ff55f271307 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Fri, 2 Mar 2018 17:06:23 -0800 Subject: [PATCH 37/37] Added a benchmark --- tests/benchmarks/compound10k.php | 180 +++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/benchmarks/compound10k.php diff --git a/tests/benchmarks/compound10k.php b/tests/benchmarks/compound10k.php new file mode 100644 index 0000000..f636220 --- /dev/null +++ b/tests/benchmarks/compound10k.php @@ -0,0 +1,180 @@ + 'Dan', + 'last-name' => 'Gebhardt', + 'twitter' => 'dgeb' + ]; + } + + public function getLinks() + { + return [ + 'self' => 'http://example.com/people/9' + ]; + } + + public function getMeta() + { + return []; + } + + public function getRelationship($name) + { + } +}; + +class Comment05 implements ResourceInterface +{ + public function getType() + { + return 'articles'; + } + + public function getId() + { + return '5'; + } + + public function getAttributes(array $fields = null) + { + return [ + "body" => "First!" + ]; + } + + public function getLinks() + { + return [ + "self" => "http://example.com/comments/5" + ]; + } + + public function getMeta() + { + return []; + } + + public function getRelationship($name) + { + if ($name === 'author') { + return Relationship::fromData(new ResourceIdentifier('people', '2')); + } + } +} + +class Comment12 implements ResourceInterface +{ + public function getType() + { + return 'articles'; + } + + public function getId() + { + return '12'; + } + + public function getAttributes(array $fields = null) + { + return [ + "body" => "I like XML better" + ]; + } + + public function getLinks() + { + return [ + "self" => "http://example.com/comments/12" + ]; + } + + public function getMeta() + { + return []; + } + + public function getRelationship($name) + { + if ($name === 'author') { + return Relationship::fromData(new Dan()); + } + } +} + +class Article implements ResourceInterface { + public function getType() + { + return 'articles'; + } + + public function getId() + { + return '1'; + } + + public function getAttributes(array $fields = null) + { + return [ + 'title' => 'JSON API paints my bikeshed!' + ]; + } + + public function getLinks() + { + return [ + 'self' => 'http://example.com/articles/1' + ]; + } + + public function getMeta() + { + return []; + } + + public function getRelationship($name) + { + if ($name === 'author') { + $author = Relationship::fromData(new Dan()); + $author->setLink('self', 'http://example.com/articles/1/relationships/author'); + $author->setLink('related', 'http://example.com/articles/1/author'); + return $author; + } + if ($name === 'comments') { + $comments = Relationship::fromData([new Comment05(), new Comment12()]); + $comments->setLink("self", "http://example.com/articles/1/relationships/comments"); + $comments->setLink("related", "http://example.com/articles/1/comments"); + return $comments; + } + } +}; +$article = new Article(); +for ($i = 0; $i < 10000; $i++) { + $doc = Document::fromData($article); + $doc->setLink('self', 'http://example.com/articles'); + $doc->setLink('next', 'http://example.com/articles?page[offset]=2'); + $doc->setLink('last', 'http://example.com/articles?page[offset]=10'); + $doc->setInclude(['author', 'comments']); + $json = json_encode($doc); +} +echo json_encode($doc, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); \ No newline at end of file