├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json ├── config └── validation.php ├── helpers.php ├── phpunit.xml ├── src ├── AbstractContainer.php ├── Adapter │ ├── AbstractResourceAdapter.php │ └── HydratesAttributesTrait.php ├── Authorizer │ ├── AbstractAuthorizer.php │ └── ReadOnlyAuthorizer.php ├── Contracts │ ├── Adapter │ │ ├── HasManyAdapterInterface.php │ │ ├── RelationshipAdapterInterface.php │ │ └── ResourceAdapterInterface.php │ ├── Authorizer │ │ └── AuthorizerInterface.php │ ├── ContainerInterface.php │ ├── Document │ │ └── MutableErrorInterface.php │ ├── Encoder │ │ └── SerializerInterface.php │ ├── Exceptions │ │ ├── ErrorIdAllocatorInterface.php │ │ └── ExceptionParserInterface.php │ ├── Factories │ │ └── FactoryInterface.php │ ├── Http │ │ ├── Client │ │ │ └── ClientInterface.php │ │ ├── Requests │ │ │ ├── InboundRequestInterface.php │ │ │ └── RequestInterface.php │ │ └── Responses │ │ │ ├── ErrorResponseInterface.php │ │ │ └── ResponseInterface.php │ ├── Object │ │ ├── DocumentInterface.php │ │ ├── MetaMemberInterface.php │ │ ├── RelationshipInterface.php │ │ ├── RelationshipsInterface.php │ │ ├── ResourceIdentifierCollectionInterface.php │ │ ├── ResourceIdentifierInterface.php │ │ ├── ResourceObjectCollectionInterface.php │ │ └── ResourceObjectInterface.php │ ├── Pagination │ │ └── PageInterface.php │ ├── Repositories │ │ ├── CodecMatcherRepositoryInterface.php │ │ ├── ErrorRepositoryInterface.php │ │ └── SchemasRepositoryInterface.php │ ├── Resolver │ │ └── ResolverInterface.php │ ├── Store │ │ ├── AdapterInterface.php │ │ ├── StoreAwareInterface.php │ │ └── StoreInterface.php │ ├── Utils │ │ ├── ConfigurableInterface.php │ │ ├── ErrorReporterInterface.php │ │ ├── ErrorsAwareInterface.php │ │ └── ReplacerInterface.php │ └── Validators │ │ ├── AcceptRelatedResourceInterface.php │ │ ├── AttributesValidatorInterface.php │ │ ├── DocumentValidatorInterface.php │ │ ├── QueryValidatorInterface.php │ │ ├── RelationshipValidatorInterface.php │ │ ├── RelationshipsValidatorInterface.php │ │ ├── ResourceValidatorInterface.php │ │ ├── ValidatorErrorFactoryInterface.php │ │ ├── ValidatorFactoryInterface.php │ │ └── ValidatorProviderInterface.php ├── Document │ └── Error.php ├── Encoder │ └── Encoder.php ├── Exceptions │ ├── AuthorizationException.php │ ├── DocumentRequiredException.php │ ├── InvalidArgumentException.php │ ├── InvalidJsonException.php │ ├── MutableErrorCollection.php │ ├── RecordNotFoundException.php │ ├── RuntimeException.php │ └── ValidationException.php ├── Factories │ └── Factory.php ├── Http │ ├── Client │ │ ├── GuzzleClient.php │ │ └── SendsRequestsTrait.php │ ├── Middleware │ │ ├── AuthorizesRequests.php │ │ ├── NegotiatesContent.php │ │ ├── ParsesServerRequests.php │ │ └── ValidatesRequests.php │ ├── Query │ │ ├── ChecksQueryParameters.php │ │ └── ValidationQueryChecker.php │ ├── Requests │ │ └── InboundRequest.php │ └── Responses │ │ ├── AbstractResponses.php │ │ ├── ErrorResponse.php │ │ └── Response.php ├── Object │ ├── Document.php │ ├── IdentifiableTrait.php │ ├── MetaMemberTrait.php │ ├── Relationship.php │ ├── Relationships.php │ ├── ResourceIdentifier.php │ ├── ResourceIdentifierCollection.php │ ├── ResourceObject.php │ └── ResourceObjectCollection.php ├── Pagination │ └── Page.php ├── Repositories │ ├── CodecMatcherRepository.php │ ├── ErrorRepository.php │ └── SchemasRepository.php ├── Resolver │ └── NamespaceResolver.php ├── Schema │ └── ExtractsAttributesTrait.php ├── Store │ ├── IdentityMap.php │ ├── Store.php │ └── StoreAwareTrait.php ├── Utils │ ├── ErrorCreatorTrait.php │ ├── ErrorsAwareTrait.php │ ├── Http.php │ ├── Pointer.php │ ├── Replacer.php │ └── Str.php └── Validators │ ├── AbstractRelationshipValidator.php │ ├── AcceptImmutableRelationship.php │ ├── AcceptRelatedResourceCallback.php │ ├── HasManyValidator.php │ ├── HasOneValidator.php │ ├── RelationshipDocumentValidator.php │ ├── RelationshipValidator.php │ ├── RelationshipsValidator.php │ ├── ResourceDocumentValidator.php │ ├── ResourceValidator.php │ ├── ValidatorErrorFactory.php │ └── ValidatorFactory.php └── tests ├── AbstractContainerTest.php ├── Adapter ├── AbstractResourceAdapterTest.php └── TestAdapter.php ├── Authorizer └── ReadOnlyAuthorizerTest.php ├── Document └── ErrorTest.php ├── Encoder └── EncoderTest.php ├── Exceptions ├── AuthorizationExceptionTest.php ├── MutableErrorCollectionTest.php └── ValidationExceptionTest.php ├── HelpersTest.php ├── Http ├── Client │ └── GuzzleClientTest.php ├── Middleware │ └── ValidatesRequestsTest.php ├── Requests │ └── InboundRequestTest.php └── Responses │ └── ErrorResponseTest.php ├── Object ├── DocumentTest.php ├── RelationshipTest.php ├── RelationshipsTest.php ├── ResourceIdentifierCollectionTest.php ├── ResourceIdentifierTest.php ├── ResourceObjectCollectionTest.php └── ResourceObjectTest.php ├── Repositories ├── CodecMatcherRepositoryTest.php └── SchemasRepositoryTest.php ├── Resolver └── NamespaceResolverTest.php ├── Schema ├── SchemaTest.php └── TestSchema.php ├── Store └── StoreTest.php ├── TestCase.php ├── Utils ├── ErrorCreatorTraitTest.php └── StrTest.php └── Validators ├── HasManyDocumentValidatorTest.php ├── HasOneDocumentValidatorTest.php ├── ResourceDocumentValidatorTest.php ├── TestCase.php └── TestContextValidator.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 4 10 | 11 | [*.{md,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | vendor/ 3 | build/ 4 | composer.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '5.6' 5 | - '7.0' 6 | - '7.1' 7 | - '7.2' 8 | 9 | before_script: 10 | - travis_retry composer install --no-interaction --prefer-dist 11 | 12 | script: 13 | - vendor/bin/phpunit 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/cloudcreativity/json-api.svg?branch=master)](https://travis-ci.org/cloudcreativity/json-api) 2 | 3 | # DEPRECATED: cloudcreativity/json-api 4 | 5 | This package has been deprecated and is no longer supported. It was originally built as a framework agnostic 6 | extension of the [neomerx/json-api](https://github.com/neomerx/json-api) package. We built it in a framework 7 | agnostic way as we were using it in both Laravel and Zend 2 at the time. However, we now only use it in 8 | Laravel and have therefore deprecated this package to focus on our Laravel package. 9 | 10 | If you need to use JSON API in a Laravel application, check out our 11 | [cloudcreativity/laravel-json-api](https://github.com/cloudcreativity/laravel-json-api) package. 12 | 13 | ### License 14 | 15 | Apache License (Version 2.0). Please see [License File](LICENSE) for more information. 16 | 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudcreativity/json-api", 3 | "description": "Framework agnostic JSON API serialization and deserialization.", 4 | "keywords": [ 5 | "jsonapi.org", 6 | "json-api", 7 | "jsonapi", 8 | "cloudcreativity", 9 | "json", 10 | "api" 11 | ], 12 | "homepage": "https://github.com/cloudcreativity/json-api", 13 | "support": { 14 | "issues": "https://github.com/cloudcreativity/json-api/issues" 15 | }, 16 | "license": "Apache-2.0", 17 | "authors": [ 18 | { 19 | "name": "Cloud Creativity Ltd", 20 | "email": "info@cloudcreativity.co.uk" 21 | } 22 | ], 23 | "require": { 24 | "php": "^5.6.4|^7.0", 25 | "cloudcreativity/utils-object": "^1.0", 26 | "neomerx/json-api": "^1.0.3" 27 | }, 28 | "require-dev": { 29 | "guzzlehttp/guzzle": "^6.3", 30 | "phpunit/phpunit": "^5.7" 31 | }, 32 | "suggest": { 33 | "guzzlehttp/guzzle": "Guzzle v6 required to use the JSON API client." 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "CloudCreativity\\JsonApi\\": "src/" 38 | }, 39 | "files": [ 40 | "helpers.php" 41 | ] 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "CloudCreativity\\JsonApi\\": "tests/" 46 | } 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-develop": "1.x-dev" 51 | } 52 | }, 53 | "minimum-stability": "stable", 54 | "prefer-stable": true, 55 | "config": { 56 | "sort-packages": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /helpers.php: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | ./tests/ 8 | 9 | 10 | 11 | 12 | src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Authorizer/AbstractAuthorizer.php: -------------------------------------------------------------------------------- 1 | errorRepository = $errorRepository; 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function canReadRelatedResource($relationshipKey, $record, EncodingParametersInterface $parameters) 55 | { 56 | return $this->canRead($record, $parameters); 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function canReadRelationship($relationshipKey, $record, EncodingParametersInterface $parameters) 63 | { 64 | return $this->canReadRelatedResource($relationshipKey, $record, $parameters); 65 | } 66 | 67 | /** 68 | * @inheritdoc 69 | */ 70 | protected function getErrorRepository() 71 | { 72 | return $this->errorRepository; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Authorizer/ReadOnlyAuthorizer.php: -------------------------------------------------------------------------------- 1 | ["1", "2"], 94 | * "bar" => ["99"] 95 | * ] 96 | * ``` 97 | * 98 | * If the method call is provided with the an array `['foo' => 'FooModel', 'bar' => 'FoobarModel']`, then the 99 | * returned mapped array will be: 100 | * 101 | * ``` 102 | * [ 103 | * "FooModel" => ["1", "2"], 104 | * "FoobarModel" => ["99"] 105 | * ] 106 | * ``` 107 | * 108 | * @param string[]|null $typeMap 109 | * if an array, map the identifier types to the supplied types. 110 | * @return mixed 111 | */ 112 | public function map(array $typeMap = null); 113 | } 114 | -------------------------------------------------------------------------------- /src/Contracts/Object/ResourceIdentifierInterface.php: -------------------------------------------------------------------------------- 1 | getHttpStatus($defaultHttpCode), $previous); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exceptions/DocumentRequiredException.php: -------------------------------------------------------------------------------- 1 | jsonError = $jsonError; 68 | $this->jsonErrorMessage = $jsonErrorMessage; 69 | } 70 | 71 | /** 72 | * @return int|null 73 | */ 74 | public function getJsonError() 75 | { 76 | return $this->jsonError; 77 | } 78 | 79 | /** 80 | * @return string|null 81 | */ 82 | public function getJsonErrorMessage() 83 | { 84 | return $this->jsonErrorMessage; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Exceptions/RecordNotFoundException.php: -------------------------------------------------------------------------------- 1 | getType(), $identifier->getId()); 50 | parent::__construct($message, $code, $previous); 51 | $this->identifier = $identifier; 52 | } 53 | 54 | /** 55 | * @return ResourceIdentifierInterface 56 | */ 57 | public function getIdentifier() 58 | { 59 | return $this->identifier; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Exceptions/RuntimeException.php: -------------------------------------------------------------------------------- 1 | getHttpStatus($defaultHttpCode), $previous); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Http/Middleware/NegotiatesContent.php: -------------------------------------------------------------------------------- 1 | createHeaderParametersParser(); 50 | $checker = $httpFactory->createHeadersChecker($codecMatcher); 51 | 52 | $checker->checkHeaders($parser->parse($request, http_contains_body($request))); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Http/Query/ValidationQueryChecker.php: -------------------------------------------------------------------------------- 1 | queryChecker = $checker; 53 | $this->queryValidator = $validator; 54 | } 55 | 56 | /** 57 | * @param EncodingParametersInterface $parameters 58 | */ 59 | public function checkQuery(EncodingParametersInterface $parameters) 60 | { 61 | $this->queryChecker->checkQuery($parameters); 62 | 63 | if ($this->queryValidator) { 64 | $this->validateQuery($parameters); 65 | } 66 | } 67 | 68 | /** 69 | * @param EncodingParametersInterface $parameters 70 | * @return void 71 | */ 72 | protected function validateQuery(EncodingParametersInterface $parameters) 73 | { 74 | if (!$this->queryValidator->isValid($parameters)) { 75 | throw new ValidationException( 76 | $this->queryValidator->getErrors(), 77 | ValidationException::HTTP_CODE_BAD_REQUEST 78 | ); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Http/Responses/ErrorResponse.php: -------------------------------------------------------------------------------- 1 | errors = Errors::cast($errors); 59 | $this->defaultHttpCode = $defaultHttpCode; 60 | $this->headers = $headers; 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function getErrors() 67 | { 68 | return $this->errors; 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function getHttpCode() 75 | { 76 | return $this->errors->getHttpStatus($this->defaultHttpCode); 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | public function getHeaders() 83 | { 84 | return $this->headers; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/Http/Responses/Response.php: -------------------------------------------------------------------------------- 1 | response = $response; 52 | $this->document = $document; 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function getPsrResponse() 59 | { 60 | return $this->response; 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function getDocument() 67 | { 68 | return $this->document; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Object/Document.php: -------------------------------------------------------------------------------- 1 | has(self::DATA)) { 43 | throw new RuntimeException('Data member is not present.'); 44 | } 45 | 46 | $data = $this->get(self::DATA); 47 | 48 | if (is_array($data) || is_null($data)) { 49 | return $data; 50 | } 51 | 52 | if ($data instanceof StandardObjectInterface) { 53 | throw new RuntimeException('Data member is not an object or null.'); 54 | } 55 | 56 | return $data; 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function getResource() 63 | { 64 | $data = $this->{self::DATA}; 65 | 66 | if (!is_object($data)) { 67 | throw new RuntimeException('Data member is not an object.'); 68 | } 69 | 70 | return new ResourceObject($data); 71 | } 72 | 73 | /** 74 | * @inheritDoc 75 | */ 76 | public function getResources() 77 | { 78 | $data = $this->get(self::DATA); 79 | 80 | if (!is_array($data)) { 81 | throw new RuntimeException('Data member is not an array.'); 82 | } 83 | 84 | return ResourceObjectCollection::create($data); 85 | } 86 | 87 | /** 88 | * @inheritdoc 89 | */ 90 | public function getRelationship() 91 | { 92 | return new Relationship($this->proxy); 93 | } 94 | 95 | /** 96 | * @inheritDoc 97 | */ 98 | public function getIncluded() 99 | { 100 | if (!$this->has(self::INCLUDED)) { 101 | return null; 102 | } 103 | 104 | if (!is_array($data = $this->{self::INCLUDED})) { 105 | throw new RuntimeException('Included member is not an array.'); 106 | } 107 | 108 | return ResourceObjectCollection::create($data); 109 | } 110 | 111 | /** 112 | * @inheritDoc 113 | */ 114 | public function getErrors() 115 | { 116 | if (!$this->has(self::ERRORS)) { 117 | return null; 118 | } 119 | 120 | if (!is_array($data = $this->{self::ERRORS})) { 121 | throw new RuntimeException('Errors member is not an array.'); 122 | } 123 | 124 | return Error::createMany($data); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/Object/IdentifiableTrait.php: -------------------------------------------------------------------------------- 1 | has(DocumentInterface::KEYWORD_TYPE)) { 40 | throw new RuntimeException('Type member not present.'); 41 | } 42 | 43 | $type = $this->get(DocumentInterface::KEYWORD_TYPE); 44 | 45 | if (!is_string($type) || empty($type)) { 46 | throw new RuntimeException('Type member is not a string, or is empty.'); 47 | } 48 | 49 | return $type; 50 | } 51 | 52 | /** 53 | * @return bool 54 | */ 55 | public function hasType() 56 | { 57 | return $this->has(DocumentInterface::KEYWORD_TYPE); 58 | } 59 | 60 | /** 61 | * @return string|int 62 | * @throws RuntimeException 63 | * if the id member is not present, or is not a string/int, or is an empty string. 64 | */ 65 | public function getId() 66 | { 67 | if (!$this->has(DocumentInterface::KEYWORD_ID)) { 68 | throw new RuntimeException('Id member not present.'); 69 | } 70 | 71 | $id = $this->get(DocumentInterface::KEYWORD_ID); 72 | 73 | if (!is_string($id)) { 74 | throw new RuntimeException('Id member is not a string.'); 75 | } 76 | 77 | if (empty($id)) { 78 | throw new RuntimeException('Id member is an empty string.'); 79 | } 80 | 81 | return $id; 82 | } 83 | 84 | /** 85 | * @return bool 86 | */ 87 | public function hasId() 88 | { 89 | return $this->has(DocumentInterface::KEYWORD_ID); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Object/MetaMemberTrait.php: -------------------------------------------------------------------------------- 1 | hasMeta() ? $this->get(DocumentInterface::KEYWORD_META) : new StandardObject(); 44 | 45 | if (!is_null($meta) && !$meta instanceof StandardObjectInterface) { 46 | throw new RuntimeException('Data member is not an object.'); 47 | } 48 | 49 | return $meta; 50 | } 51 | 52 | /** 53 | * @return bool 54 | */ 55 | public function hasMeta() 56 | { 57 | return $this->has(DocumentInterface::KEYWORD_META); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Object/Relationship.php: -------------------------------------------------------------------------------- 1 | isHasMany()) { 41 | return $this->getIdentifiers(); 42 | } elseif (!$this->isHasOne()) { 43 | throw new RuntimeException('No data member or data member is not a valid relationship.'); 44 | } 45 | 46 | return $this->hasIdentifier() ? $this->getIdentifier() : null; 47 | } 48 | 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public function getIdentifier() 54 | { 55 | if (!$this->isHasOne()) { 56 | throw new RuntimeException('No data member or data member is not a valid has-one relationship.'); 57 | } 58 | 59 | $data = $this->{self::DATA}; 60 | 61 | if (!$data) { 62 | throw new RuntimeException('No resource identifier - relationship is empty.'); 63 | } 64 | 65 | return new ResourceIdentifier($data); 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | public function hasIdentifier() 72 | { 73 | return is_object($this->{self::DATA}); 74 | } 75 | 76 | /** 77 | * @inheritdoc 78 | */ 79 | public function isHasOne() 80 | { 81 | if (!$this->has(self::DATA)) { 82 | return false; 83 | } 84 | 85 | $data = $this->{self::DATA}; 86 | 87 | return is_null($data) || is_object($data); 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | */ 93 | public function getIdentifiers() 94 | { 95 | if (!$this->isHasMany()) { 96 | throw new RuntimeException('No data member of data member is not a valid has-many relationship.'); 97 | } 98 | 99 | return ResourceIdentifierCollection::create($this->{self::DATA}); 100 | } 101 | 102 | /** 103 | * @inheritdoc 104 | */ 105 | public function isHasMany() 106 | { 107 | return is_array($this->{self::DATA}); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Object/Relationships.php: -------------------------------------------------------------------------------- 1 | keys() as $key) { 39 | yield $key => $this->getRelationship($key); 40 | } 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function getRelationship($key) 47 | { 48 | if (!$this->has($key)) { 49 | throw new RuntimeException("Relationship member '$key' is not present."); 50 | } 51 | 52 | $value = $this->{$key}; 53 | 54 | if (!is_object($value)) { 55 | throw new RuntimeException("Relationship member '$key' is not an object.'"); 56 | } 57 | 58 | return new Relationship($value); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/Object/ResourceIdentifier.php: -------------------------------------------------------------------------------- 1 | set(self::TYPE, $type) 46 | ->set(self::ID, $id); 47 | 48 | return $identifier; 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function isType($typeOrTypes) 55 | { 56 | return in_array($this->get(self::TYPE), (array) $typeOrTypes, true); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function mapType(array $map) 63 | { 64 | $type = $this->getType(); 65 | 66 | if (array_key_exists($type, $map)) { 67 | return $map[$type]; 68 | } 69 | 70 | throw new RuntimeException(sprintf('Type "%s" is not in the supplied map.', $type)); 71 | } 72 | 73 | /** 74 | * @inheritDoc 75 | */ 76 | public function isComplete() 77 | { 78 | return $this->hasType() && $this->hasId(); 79 | } 80 | 81 | /** 82 | * @inheritDoc 83 | */ 84 | public function isSame(ResourceIdentifierInterface $identifier) 85 | { 86 | return $this->getType() === $identifier->getType() && 87 | $this->getId() === $identifier->getId(); 88 | } 89 | 90 | /** 91 | * @inheritDoc 92 | */ 93 | public function toString() 94 | { 95 | return sprintf('%s:%s', $this->getType(), $this->getId()); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Object/ResourceObject.php: -------------------------------------------------------------------------------- 1 | getType(), $this->getId()); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function getAttributes() 50 | { 51 | $attributes = $this->hasAttributes() ? $this->get(self::ATTRIBUTES) : new StandardObject(); 52 | 53 | if (!$attributes instanceof StandardObjectInterface) { 54 | throw new RuntimeException('Attributes member is not an object.'); 55 | } 56 | 57 | return $attributes; 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function hasAttributes() 64 | { 65 | return $this->has(self::ATTRIBUTES); 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | public function getRelationships() 72 | { 73 | $relationships = $this->hasRelationships() ? $this->{self::RELATIONSHIPS} : null; 74 | 75 | if (!is_null($relationships) && !is_object($relationships)) { 76 | throw new RuntimeException('Relationships member is not an object.'); 77 | } 78 | 79 | return new Relationships($relationships); 80 | } 81 | 82 | /** 83 | * @inheritdoc 84 | */ 85 | public function hasRelationships() 86 | { 87 | return $this->has(self::RELATIONSHIPS); 88 | } 89 | 90 | /** 91 | * @inheritDoc 92 | */ 93 | public function getRelationship($key) 94 | { 95 | $relationships = $this->getRelationships(); 96 | 97 | return $relationships->has($key) ? $relationships->getRelationship($key) : null; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/Pagination/Page.php: -------------------------------------------------------------------------------- 1 | data = $data; 88 | $this->first = $first; 89 | $this->previous = $previous; 90 | $this->next = $next; 91 | $this->last = $last; 92 | $this->meta = $meta; 93 | $this->metaKey = $metaKey; 94 | } 95 | 96 | /** 97 | * @inheritDoc 98 | */ 99 | public function getData() 100 | { 101 | return $this->data; 102 | } 103 | 104 | /** 105 | * @inheritDoc 106 | */ 107 | public function getFirstLink() 108 | { 109 | return $this->first; 110 | } 111 | 112 | /** 113 | * @inheritDoc 114 | */ 115 | public function getPreviousLink() 116 | { 117 | return $this->previous; 118 | } 119 | 120 | /** 121 | * @inheritDoc 122 | */ 123 | public function getNextLink() 124 | { 125 | return $this->next; 126 | } 127 | 128 | /** 129 | * @inheritDoc 130 | */ 131 | public function getLastLink() 132 | { 133 | return $this->last; 134 | } 135 | 136 | /** 137 | * @inheritDoc 138 | */ 139 | public function getMeta() 140 | { 141 | return $this->meta; 142 | } 143 | 144 | /** 145 | * @inheritDoc 146 | */ 147 | public function getMetaKey() 148 | { 149 | return $this->metaKey; 150 | } 151 | 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/Repositories/ErrorRepository.php: -------------------------------------------------------------------------------- 1 | replacer = $replacer; 53 | } 54 | 55 | /** 56 | * Add error configuration. 57 | * 58 | * @param array $config 59 | * @return $this 60 | */ 61 | public function configure(array $config) 62 | { 63 | $this->errors = array_merge($this->errors, $config); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | public function exists($key) 72 | { 73 | return isset($this->errors[$key]); 74 | } 75 | 76 | /** 77 | * @inheritDoc 78 | */ 79 | public function errors(...$keys) 80 | { 81 | $errors = new MutableErrorCollection(); 82 | 83 | foreach ($keys as $error) { 84 | if (is_string($error)) { 85 | $error = $this->error($error); 86 | } 87 | 88 | $errors->add($error); 89 | } 90 | 91 | return $errors; 92 | } 93 | 94 | 95 | /** 96 | * @inheritdoc 97 | */ 98 | public function error($key, array $values = []) 99 | { 100 | return $this->make($key, $values); 101 | } 102 | 103 | /** 104 | * @inheritdoc 105 | */ 106 | public function errorWithPointer($key, $pointer, array $values = []) 107 | { 108 | $error = $this->make($key, $values); 109 | $error->setSourcePointer($pointer); 110 | 111 | return $error; 112 | } 113 | 114 | /** 115 | * @inheritdoc 116 | */ 117 | public function errorWithParameter($key, $parameter, array $values = []) 118 | { 119 | $error = $this->make($key, $values); 120 | $error->setSourceParameter($parameter); 121 | 122 | return $error; 123 | } 124 | 125 | /** 126 | * @param $key 127 | * @return array 128 | */ 129 | protected function get($key) 130 | { 131 | return isset($this->errors[$key]) ? (array) $this->errors[$key] : []; 132 | } 133 | 134 | /** 135 | * @param $key 136 | * @param array $values 137 | * @return Error 138 | */ 139 | protected function make($key, array $values) 140 | { 141 | $error = Error::create($this->get($key)); 142 | 143 | if ($this->replacer && $error->hasDetail()) { 144 | $detail = $this->replacer->replace($error->getDetail(), $values); 145 | $error->setDetail($detail); 146 | } 147 | 148 | return $error; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/Repositories/SchemasRepository.php: -------------------------------------------------------------------------------- 1 | [ 37 | * 'Author' => 'AuthorSchema', 38 | * 'Post' => 'PostSchema', 39 | * ], 40 | * 'foo' => [ 41 | * 'Comment' => 'CommentSchema', 42 | * ], 43 | * ] 44 | * ```` 45 | * 46 | * If the 'foo' schema is requested, the return array will have Author, Schema and Comment in it. 47 | * 48 | * This repository also accepts non-namespaced schemas. I.e. if the config array does not have a 'defaults' key, it 49 | * will be loaded as the default schemas. 50 | */ 51 | class SchemasRepository implements SchemasRepositoryInterface 52 | { 53 | 54 | /** 55 | * @var SchemaFactoryInterface 56 | */ 57 | private $factory; 58 | 59 | /** 60 | * @var bool 61 | */ 62 | private $namespaced = false; 63 | 64 | /** 65 | * @var array 66 | */ 67 | private $schemas = []; 68 | 69 | /** 70 | * @param SchemaFactoryInterface|null $factory 71 | */ 72 | public function __construct(SchemaFactoryInterface $factory = null) 73 | { 74 | $this->factory = $factory ?: new Factory(); 75 | } 76 | 77 | /** 78 | * @param string $name 79 | * @return ContainerInterface 80 | */ 81 | public function getSchemas($name = null) 82 | { 83 | $name = ($name) ?: static::DEFAULTS; 84 | 85 | if (static::DEFAULTS !== $name && !$this->namespaced) { 86 | throw new \RuntimeException(sprintf('Schemas configuration is not namespaced, so cannot get "%s".', $name)); 87 | } 88 | 89 | $defaults = $this->get(static::DEFAULTS); 90 | $schemas = (static::DEFAULTS === $name) ? $defaults : array_merge($defaults, $this->get($name)); 91 | 92 | return $this->factory->createContainer($schemas); 93 | } 94 | 95 | /** 96 | * @param array $config 97 | * @return $this 98 | */ 99 | public function configure(array $config) 100 | { 101 | if (!isset($config[static::DEFAULTS])) { 102 | $config = [static::DEFAULTS => $config]; 103 | $this->namespaced = false; 104 | } else { 105 | $this->namespaced = true; 106 | } 107 | 108 | $this->schemas = $config; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * @param $key 115 | * @return array 116 | */ 117 | private function get($key) 118 | { 119 | return array_key_exists($key, $this->schemas) ? (array) $this->schemas[$key] : []; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/Schema/ExtractsAttributesTrait.php: -------------------------------------------------------------------------------- 1 | attributeKeys($record) as $recordKey => $attributeKey) { 52 | if (is_numeric($recordKey)) { 53 | $recordKey = $attributeKey; 54 | $attributeKey = $this->keyForAttribute($attributeKey); 55 | } 56 | 57 | $value = $this->extractAttribute($record, $recordKey); 58 | $attributes[$attributeKey] = $this->serializeAttribute($value, $record, $recordKey); 59 | } 60 | 61 | return $attributes; 62 | } 63 | 64 | /** 65 | * Get a list of attributes that are to be extracted. 66 | * 67 | * @param object $record 68 | * @return array 69 | */ 70 | protected function attributeKeys($record) 71 | { 72 | $keys = property_exists($this, 'attributes') ? $this->attributes : null; 73 | 74 | if (is_null($keys)) { 75 | return array_keys(get_object_vars($record)); 76 | } 77 | 78 | return (array) $keys; 79 | } 80 | 81 | /** 82 | * Convert a record key into a resource attribute key. 83 | * 84 | * @param $recordKey 85 | * @return string 86 | */ 87 | protected function keyForAttribute($recordKey) 88 | { 89 | $dasherized = property_exists($this, 'dasherize') ? $this->dasherize : true; 90 | 91 | return $dasherized ? Str::dasherize($recordKey) : $recordKey; 92 | } 93 | 94 | /** 95 | * @param $value 96 | * @param object $record 97 | * @param $recordKey 98 | * @return string 99 | */ 100 | protected function serializeAttribute($value, $record, $recordKey) 101 | { 102 | if (method_exists($this, $method = $this->methodForSerializer($recordKey))) { 103 | $value = call_user_func([$this, $method], $value, $record); 104 | } 105 | 106 | if ($value instanceof DateTime) { 107 | $value = $this->serializeDateTime($value, $record); 108 | } 109 | 110 | return $value; 111 | } 112 | 113 | /** 114 | * @param DateTime $value 115 | * @param object $record 116 | * @return string 117 | */ 118 | protected function serializeDateTime(DateTime $value, $record) 119 | { 120 | return $value->format($this->dateFormat()); 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | protected function dateFormat() 127 | { 128 | $format = property_exists($this, 'dateFormat') ? $this->dateFormat : null; 129 | 130 | return $format ?: DateTime::W3C; 131 | } 132 | 133 | /** 134 | * @param $recordKey 135 | * @return string 136 | */ 137 | protected function methodForSerializer($recordKey) 138 | { 139 | return 'serialize' . Str::classify($recordKey) . 'Attribute'; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Store/IdentityMap.php: -------------------------------------------------------------------------------- 1 | lookup($identifier); 55 | 56 | if (is_object($existing) && is_bool($record)) { 57 | throw new InvalidArgumentException('Attempting to push a boolean into the map in place of an object.'); 58 | } 59 | 60 | $this->map[$identifier->toString()] = $record; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Does the identity map know that ths supplied identifier exists? 67 | * 68 | * @param ResourceIdentifierInterface $identifier 69 | * @return bool|null 70 | * the answer, or null if the identity map does not know 71 | */ 72 | public function exists(ResourceIdentifierInterface $identifier) 73 | { 74 | $record = $this->lookup($identifier); 75 | 76 | return is_object($record) ? true : $record; 77 | } 78 | 79 | /** 80 | * Get the record from the identity map. 81 | * 82 | * @param ResourceIdentifierInterface $identifier 83 | * @return object|bool|null 84 | * the record, false if it is known not to exist, or null if the identity map does not have the object. 85 | */ 86 | public function find(ResourceIdentifierInterface $identifier) 87 | { 88 | $record = $this->lookup($identifier); 89 | 90 | if (false === $record) { 91 | return false; 92 | } 93 | 94 | return is_object($record) ? $record : null; 95 | } 96 | 97 | /** 98 | * @param ResourceIdentifierInterface $identifier 99 | * @return object|bool|null 100 | */ 101 | private function lookup(ResourceIdentifierInterface $identifier) 102 | { 103 | $key = $identifier->toString(); 104 | 105 | return isset($this->map[$key]) ? $this->map[$key] : null; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Store/StoreAwareTrait.php: -------------------------------------------------------------------------------- 1 | store = $store; 43 | } 44 | 45 | /** 46 | * @return StoreInterface 47 | */ 48 | protected function store() 49 | { 50 | if (!$this->store) { 51 | throw new RuntimeException('No store injected.'); 52 | } 53 | 54 | return $this->store; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Utils/ErrorCreatorTrait.php: -------------------------------------------------------------------------------- 1 | errors instanceof Errors) { 51 | $this->errors = new Errors(); 52 | } 53 | 54 | return $this->errors; 55 | } 56 | 57 | /** 58 | * @param ErrorInterface|string $error 59 | * @param array $values 60 | * @return MutableErrorInterface 61 | * the error that was added. 62 | */ 63 | public function addError($error, array $values = []) 64 | { 65 | if ($error instanceof ErrorInterface) { 66 | $error = Error::cast($error); 67 | } else { 68 | $error = $this->getErrorRepository()->error($error, $values); 69 | } 70 | 71 | $this->getErrors()->add($error); 72 | 73 | return $error; 74 | } 75 | 76 | /** 77 | * @param $error 78 | * @param $pointer 79 | * @param array $values 80 | * @return MutableErrorInterface 81 | */ 82 | public function addErrorWithPointer($error, $pointer, array $values = []) 83 | { 84 | $error = $this->addError($error, $values); 85 | $error->setSourcePointer($pointer); 86 | 87 | return $error; 88 | } 89 | 90 | /** 91 | * @param $error 92 | * @param $parameter 93 | * @param array $values 94 | * @return MutableErrorInterface 95 | */ 96 | public function addErrorWithParameter($error, $parameter, array $values = []) 97 | { 98 | $error = $this->addError($error, $values); 99 | $error->setSourceParameter($parameter); 100 | 101 | return $error; 102 | } 103 | 104 | /** 105 | * Clear all errors 106 | * 107 | * @return $this 108 | */ 109 | protected function reset() 110 | { 111 | $this->errors = null; 112 | 113 | return $this; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Utils/ErrorsAwareTrait.php: -------------------------------------------------------------------------------- 1 | errors instanceof Errors) { 44 | $this->errors = new Errors(); 45 | } 46 | 47 | return $this->errors; 48 | } 49 | 50 | /** 51 | * @param ErrorInterface $error 52 | * @return $this 53 | */ 54 | protected function addError(ErrorInterface $error) 55 | { 56 | $this->getErrors()->add($error); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * @param ErrorCollection|ErrorInterface[] $errors 63 | * @return $this 64 | */ 65 | protected function addErrors($errors) 66 | { 67 | /** @var ErrorInterface $error */ 68 | foreach ($errors as $error) { 69 | $this->getErrors()->add($error); 70 | } 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @return $this 77 | */ 78 | protected function reset() 79 | { 80 | $this->errors = null; 81 | 82 | return $this; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Utils/Http.php: -------------------------------------------------------------------------------- 1 | hasHeader('Transfer-Encoding')) { 48 | return true; 49 | }; 50 | 51 | if (!$contentLength = $request->getHeader('Content-Length')) { 52 | return false; 53 | } 54 | 55 | return 0 < $contentLength[0]; 56 | } 57 | 58 | /** 59 | * Does the HTTP response contain body content? 60 | * 61 | * "For response messages, whether or not a message-body is included with a message is dependent 62 | * on both the request method and the response status code (section 6.1.1). All responses to the 63 | * HEAD request method MUST NOT include a message-body, even though the presence of entity-header 64 | * fields might lead one to believe they do. All 1xx (informational), 204 (no content), and 304 65 | * (not modified) responses MUST NOT include a message-body. All other responses do include a 66 | * message-body, although it MAY be of zero length." 67 | * https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 68 | * 69 | * @param RequestInterface $request 70 | * @param ResponseInterface $response 71 | * @return bool 72 | */ 73 | public static function doesResponseHaveBody(RequestInterface $request, ResponseInterface $response) 74 | { 75 | if ('HEAD' === strtoupper($request->getMethod())) { 76 | return false; 77 | } 78 | 79 | $status = $response->getStatusCode(); 80 | 81 | if ((100 <= $status && 200 > $status) || 204 === $status || 304 === $status) { 82 | return false; 83 | } 84 | 85 | if ($response->hasHeader('Transfer-Encoding')) { 86 | return true; 87 | }; 88 | 89 | if (!$contentLength = $response->getHeader('Content-Length')) { 90 | return false; 91 | } 92 | 93 | return 0 < $contentLength[0]; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Utils/Replacer.php: -------------------------------------------------------------------------------- 1 | $value) { 44 | $message = str_replace( 45 | $this->parseKey($key), 46 | $this->parseValue($value), 47 | $message 48 | ); 49 | } 50 | 51 | return $message; 52 | } 53 | 54 | /** 55 | * @param $key 56 | * @return string 57 | */ 58 | protected function parseKey($key) 59 | { 60 | return sprintf('{%s}', $key); 61 | } 62 | 63 | /** 64 | * @param $value 65 | * @return string 66 | */ 67 | protected function parseValue($value) 68 | { 69 | if (is_object($value)) { 70 | return ''; 71 | } elseif (is_null($value)) { 72 | return 'null'; 73 | } elseif (is_bool($value)) { 74 | return $value ? 'true' : 'false'; 75 | } elseif (is_scalar($value)) { 76 | return (string) $value; 77 | } 78 | 79 | $ret = []; 80 | 81 | foreach ((array) $value as $v) { 82 | $ret[] = $this->parseValue($v); 83 | } 84 | 85 | return implode(', ', $ret); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Utils/Str.php: -------------------------------------------------------------------------------- 1 | current = ResourceIdentifier::create($type, (string) $id); 49 | } 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function accept( 56 | ResourceIdentifierInterface $identifier, 57 | $record = null, 58 | $key = null, 59 | ResourceObjectInterface $resource = null 60 | ) { 61 | if (!$this->current) { 62 | return true; 63 | } 64 | 65 | return $this->current->getType() == $identifier->getType() && 66 | $this->current->getId() == $identifier->getId(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Validators/AcceptRelatedResourceCallback.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | */ 51 | public function accept( 52 | ResourceIdentifierInterface $identifier, 53 | $record = null, 54 | $key = null, 55 | ResourceObjectInterface $resource = null 56 | ) { 57 | $callback = $this->callback; 58 | 59 | return $callback($identifier, $record, $key, $resource); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Validators/HasManyValidator.php: -------------------------------------------------------------------------------- 1 | reset(); 42 | 43 | if (!$this->validateRelationship($relationship, $key)) { 44 | return false; 45 | } 46 | 47 | return $this->validateHasMany($relationship, $record, $key, $resource); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Validators/HasOneValidator.php: -------------------------------------------------------------------------------- 1 | reset(); 42 | 43 | if (!$this->validateRelationship($relationship, $key)) { 44 | return false; 45 | } 46 | 47 | return $this->validateHasOne($relationship, $record, $key, $resource); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Validators/RelationshipDocumentValidator.php: -------------------------------------------------------------------------------- 1 | errorFactory = $errorFactory; 58 | $this->relationshipValidator = $validator; 59 | } 60 | 61 | /** 62 | * @inheritdoc 63 | */ 64 | public function isValid(DocumentInterface $document, $record = null) 65 | { 66 | $this->reset(); 67 | 68 | if (!$this->relationshipValidator->isValid($document->getRelationship(), $record)) { 69 | $this->addErrors($this->relationshipValidator->getErrors()); 70 | return false; 71 | } 72 | 73 | return true; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Validators/RelationshipValidator.php: -------------------------------------------------------------------------------- 1 | reset(); 42 | 43 | /** Check that it is a valid relationship object. */ 44 | if (!$this->validateRelationship($relationship, $key)) { 45 | return false; 46 | } 47 | 48 | if ($relationship->isHasOne()) { 49 | return $this->validateHasOne($relationship, $record, $key, $resource); 50 | } 51 | 52 | return $this->validateHasMany($relationship, $record, $key, $resource); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Validators/ResourceDocumentValidator.php: -------------------------------------------------------------------------------- 1 | errorFactory = $errorFactory; 59 | $this->resourceValidator = $validator; 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function isValid(DocumentInterface $document, $record = null) 66 | { 67 | $this->reset(); 68 | 69 | if (!$document->has(DocumentInterface::DATA)) { 70 | $this->addError($this->errorFactory->memberRequired(DocumentInterface::DATA, P::root())); 71 | return false; 72 | } 73 | 74 | $data = $document->get(DocumentInterface::DATA); 75 | 76 | if (!is_object($data)) { 77 | $this->addError($this->errorFactory->memberObjectExpected(DocumentInterface::DATA, P::data())); 78 | return false; 79 | } 80 | 81 | if (!$this->resourceValidator->isValid($document->getResource(), $record)) { 82 | $this->addErrors($this->resourceValidator->getErrors()); 83 | return false; 84 | } 85 | 86 | return true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Adapter/TestAdapter.php: -------------------------------------------------------------------------------- 1 | destroyed = true; 71 | 72 | return true; 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function exists($resourceId) 79 | { 80 | // TODO: Implement exists() method. 81 | } 82 | 83 | /** 84 | * @inheritDoc 85 | */ 86 | public function find($resourceId) 87 | { 88 | // TODO: Implement find() method. 89 | } 90 | 91 | /** 92 | * @inheritDoc 93 | */ 94 | public function findMany(array $resourceIds) 95 | { 96 | // TODO: Implement findMany() method. 97 | } 98 | 99 | /** 100 | * @inheritDoc 101 | */ 102 | public function related($relationshipName) 103 | { 104 | // TODO: Implement related() method. 105 | } 106 | 107 | /** 108 | * @inheritDoc 109 | */ 110 | protected function createRecord(ResourceObjectInterface $resource) 111 | { 112 | $id = $resource->getId(); 113 | 114 | return (object) compact('id'); 115 | } 116 | 117 | /** 118 | * @inheritDoc 119 | */ 120 | protected function hydrateRelationships( 121 | $record, 122 | RelationshipsInterface $relationships, 123 | EncodingParametersInterface $parameters 124 | ) { 125 | // TODO: Implement hydrateRelationships() method. 126 | } 127 | 128 | /** 129 | * @inheritDoc 130 | */ 131 | protected function hydrateAttribute($record, $attrKey, $value) 132 | { 133 | $record->{$attrKey} = $value; 134 | } 135 | 136 | /** 137 | * @param $record 138 | * @param $value 139 | */ 140 | protected function hydrateTitleField($record, $value) 141 | { 142 | $record->title = ucwords($value); 143 | } 144 | 145 | /** 146 | * @inheritDoc 147 | */ 148 | protected function persist($record) 149 | { 150 | if (!isset($record->id)) { 151 | $record->id = 'new'; 152 | } 153 | 154 | $record->saved = true; 155 | 156 | return $record; 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /tests/Authorizer/ReadOnlyAuthorizerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(EncodingParametersInterface::class)->getMock(); 40 | $authorizer = new ReadOnlyAuthorizer(new ErrorRepository()); 41 | $record = new StandardObject(); 42 | 43 | $this->assertTrue($authorizer->canReadMany('posts', $parameters)); 44 | $this->assertTrue($authorizer->canRead($record, $parameters)); 45 | $this->assertTrue($authorizer->canReadRelationship('comments', $record, $parameters)); 46 | 47 | $this->assertFalse($authorizer->canCreate('posts', new ResourceObject(), $parameters)); 48 | $this->assertFalse($authorizer->canUpdate($record, new ResourceObject(), $parameters)); 49 | $this->assertFalse($authorizer->canDelete($record, $parameters)); 50 | $this->assertFalse($authorizer->canModifyRelationship('comments', $record, new Relationship(), $parameters)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Encoder/EncoderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Encoder::class, $encoder); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Exceptions/AuthorizationExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertSame(AuthorizationException::HTTP_CODE_FORBIDDEN, $ex->getHttpCode()); 36 | } 37 | 38 | public function testErrorStatus() 39 | { 40 | $err = new Error(null, null, 401); 41 | $ex = new AuthorizationException($err); 42 | $this->assertEquals(401, $ex->getHttpCode()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Exceptions/MutableErrorCollectionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $errors->getArrayCopy()); 44 | $this->assertEquals($expected, iterator_to_array($errors)); 45 | } 46 | 47 | public function testMerge() 48 | { 49 | $a = new BaseError(null, null, 422); 50 | $b = new Error(null, null, 400); 51 | $c = new BaseError(null, null, 500); 52 | 53 | $merge = new ErrorCollection(); 54 | $merge->add($a)->add($b); 55 | 56 | $errors = new MutableErrorCollection([$c]); 57 | $expected = [Error::cast($c), Error::cast($a), $b]; 58 | 59 | $this->assertEquals($errors, $errors->merge($merge)); 60 | $this->assertEquals($expected, $errors->getArrayCopy()); 61 | } 62 | 63 | public function testCastReturnsSame() 64 | { 65 | $errors = new MutableErrorCollection(); 66 | $this->assertSame($errors, MutableErrorCollection::cast($errors)); 67 | } 68 | 69 | public function testCastError() 70 | { 71 | $error = new BaseError(null, null, 422); 72 | $expected = new MutableErrorCollection([$error]); 73 | $this->assertEquals($expected, MutableErrorCollection::cast($error)); 74 | } 75 | 76 | public function testCastBaseCollection() 77 | { 78 | $error = new Error(null, null, 422); 79 | $expected = new MutableErrorCollection([$error]); 80 | $base = new ErrorCollection(); 81 | $base->add($error); 82 | $this->assertEquals($expected, MutableErrorCollection::cast($base)); 83 | } 84 | 85 | public function testCastArray() 86 | { 87 | $arr = [new Error(null, null, 500)]; 88 | $expected = new MutableErrorCollection($arr); 89 | $this->assertEquals($expected, MutableErrorCollection::cast($arr)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Exceptions/ValidationExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertSame(ValidationException::HTTP_CODE_BAD_REQUEST, $ex->getHttpCode()); 36 | } 37 | 38 | public function testErrorStatus() 39 | { 40 | $err = new Error(null, null, 401); 41 | $ex = new ValidationException($err); 42 | $this->assertEquals(401, $ex->getHttpCode()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/HelpersTest.php: -------------------------------------------------------------------------------- 1 | ['{ "data": { "type": "foo" }', true], 19 | 'empty string' => [''], 20 | 'null' => ['NULL'], 21 | 'integer' => ['1'], 22 | 'bool' => ['true'], 23 | 'string' => ['foo'], 24 | ]; 25 | } 26 | 27 | /** 28 | * @param $content 29 | * @param bool $jsonError 30 | * @dataProvider invalidJsonProvider 31 | */ 32 | public function testInvalidJson($content, $jsonError = false) 33 | { 34 | try { 35 | json_decode($content); 36 | $this->fail('No exception thrown.'); 37 | } catch (InvalidJsonException $ex) { 38 | if ($jsonError) { 39 | $this->assertJsonError($ex); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function requestContainsBodyProvider() 48 | { 49 | return [ 50 | 'neither header' => [[], false], 51 | 'content-length' => [['Content-Length' => '120'], true], 52 | 'zero content-length' => [['Content-Length' => '0'], false], 53 | 'empty content-length' => [['Content-Length' => ''], false], 54 | 'transfer-encoding 1' => [['Transfer-Encoding' => 'chunked'], true], 55 | 'transfer-encoding 2' => [['Transfer-Encoding' => 'gzip, chunked'], true], 56 | 'content-type no content-length' => [['Content-Type' => 'text/plain'], false], 57 | ]; 58 | } 59 | 60 | /** 61 | * @param array $headers 62 | * @param $expected 63 | * @dataProvider requestContainsBodyProvider 64 | */ 65 | public function testRequestContainsBody(array $headers, $expected) 66 | { 67 | $request = new Request('GET', '/api/posts', $headers); 68 | 69 | $this->assertSame($expected, http_contains_body($request)); 70 | } 71 | 72 | /** 73 | * @return array 74 | */ 75 | public function responseContainsBodyProvider() 76 | { 77 | return [ 78 | 'head never contains body' => [false, 'HEAD', 200], 79 | '1xx never contain body' => [false, 'POST', 100], 80 | '204 never contains body' => [false, 'GET', 204], 81 | '304 never contains body' => [false, 'GET', 304], 82 | '200 with zero content length' => [false, 'GET', 200, ['Content-Length' => '0']], 83 | '200 with content' => [true, 'GET', 200, ['Content-Length' => '3'], 'foo'], 84 | '201 with content' => [true, 'POST', 201, ['Content-Length' => '3'], 'foo'], 85 | '200 with transfer encoding' => [true, 'GET', 200, ['Transfer-Encoding' => 'chunked']], 86 | ]; 87 | } 88 | 89 | /** 90 | * @param $expected 91 | * @param $method 92 | * @param $status 93 | * @param array $headers 94 | * @param $body 95 | * @dataProvider responseContainsBodyProvider 96 | */ 97 | public function testResponseContainsBody($expected, $method, $status, $headers = [], $body = null) 98 | { 99 | $request = new Request($method, '/api/posts'); 100 | $response = new Response($status, $headers, $body); 101 | 102 | $this->assertSame($expected, http_contains_body($request, $response)); 103 | } 104 | 105 | /** 106 | * @param InvalidJsonException $ex 107 | */ 108 | private function assertJsonError(InvalidJsonException $ex) 109 | { 110 | $this->assertEquals(json_last_error(), $ex->getJsonError()); 111 | $this->assertEquals(json_last_error_msg(), $ex->getJsonErrorMessage()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/Http/Responses/ErrorResponseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(JsonApiException::DEFAULT_HTTP_CODE, $response->getHttpCode()); 37 | } 38 | 39 | public function testResolveErrorStatusUsesDefaultWithMultiple() 40 | { 41 | $response = new ErrorResponse([new Error(), new Error()], 499); 42 | $this->assertEquals(499, $response->getHttpCode()); 43 | } 44 | 45 | public function testResolveErrorStatusUsesErrorStatus() 46 | { 47 | $response = new ErrorResponse([new Error(), new Error(null, null, 422)]); 48 | $this->assertEquals(422, $response->getHttpCode()); 49 | } 50 | 51 | public function testResolveErrorStatus4xx() 52 | { 53 | $response = new ErrorResponse([new Error(null, null, 422), new Error(null, null, 415)]); 54 | $this->assertEquals(400, $response->getHttpCode()); 55 | } 56 | 57 | public function testResolveErrorStatus5xx() 58 | { 59 | $response = new ErrorResponse([new Error(null, null, 501), new Error(null, null, 503)]); 60 | $this->assertEquals(500, $response->getHttpCode()); 61 | } 62 | 63 | public function testResolveErrorStatusMixed() 64 | { 65 | $a = new Error(null, null, 422); 66 | $b = new Error(null, null, 501); 67 | $response = new ErrorResponse([$a, $b]); 68 | 69 | $this->assertEquals(500, $response->getHttpCode()); 70 | $this->assertSame([$a, $b], $response->getErrors()->getArrayCopy()); 71 | } 72 | 73 | public function testHeaders() 74 | { 75 | $headers = ['X-Custom' => 'Foobar']; 76 | $response = new ErrorResponse([], null, $headers); 77 | $this->assertEquals($headers, $response->getHeaders()); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /tests/Object/RelationshipsTest.php: -------------------------------------------------------------------------------- 1 | {ResourceIdentifier::TYPE} = 'foo'; 44 | $belongsTo->{ResourceIdentifier::ID} = 123; 45 | 46 | $a = new stdClass(); 47 | $a->{Relationship::DATA} = $belongsTo; 48 | 49 | $b = new stdClass(); 50 | $b->{Relationship::DATA} = null; 51 | 52 | $this->data = new stdClass(); 53 | $this->data->{self::KEY_A} = $a; 54 | $this->data->{self::KEY_B} = $b; 55 | } 56 | 57 | public function testGet() 58 | { 59 | $object = new Relationships($this->data); 60 | $a = new Relationship($this->data->{self::KEY_A}); 61 | $b = new Relationship($this->data->{self::KEY_B}); 62 | 63 | $this->assertEquals($a, $object->getRelationship(self::KEY_A)); 64 | $this->assertEquals($b, $object->getRelationship(self::KEY_B)); 65 | 66 | return $object; 67 | } 68 | 69 | /** 70 | * @depends testGet 71 | */ 72 | public function testAll(Relationships $object) 73 | { 74 | $expected = [ 75 | self::KEY_A => $object->getRelationship(self::KEY_A), 76 | self::KEY_B => $object->getRelationship(self::KEY_B), 77 | ]; 78 | 79 | $this->assertEquals($expected, iterator_to_array($object->getAll())); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /tests/Repositories/SchemasRepositoryTest.php: -------------------------------------------------------------------------------- 1 | [ 40 | 'Author' => 'AuthorSchema', 41 | 'Comment' => 'CommentSchema', 42 | ], 43 | self::A => [ 44 | 'Post' => 'PostSchema', 45 | ], 46 | self::B => [ 47 | 'Like' => 'LikeSchema', 48 | ], 49 | ]; 50 | 51 | private $defaults; 52 | private $a; 53 | private $b; 54 | 55 | /** 56 | * @var SchemasRepository 57 | */ 58 | private $repository; 59 | 60 | protected function setUp() 61 | { 62 | $factory = new Factory(); 63 | 64 | $this->repository = new SchemasRepository($factory); 65 | $this->repository->configure($this->config); 66 | 67 | $defaults = $this->config[SchemasRepository::DEFAULTS]; 68 | $this->defaults = $factory->createContainer($defaults); 69 | $this->a = $factory->createContainer(array_merge($defaults, $this->config[static::A])); 70 | $this->b = $factory->createContainer(array_merge($defaults, $this->config[static::B])); 71 | } 72 | 73 | public function testDefaults() 74 | { 75 | $this->assertEquals($this->defaults, $this->repository->getSchemas()); 76 | $this->assertEquals($this->defaults, $this->repository->getSchemas(SchemasRepository::DEFAULTS)); 77 | } 78 | 79 | public function testVariantA() 80 | { 81 | $this->assertEquals($this->a, $this->repository->getSchemas(static::A)); 82 | } 83 | 84 | public function testVariantB() 85 | { 86 | $this->assertEquals($this->b, $this->repository->getSchemas(static::B)); 87 | } 88 | 89 | /** 90 | * @depends testDefaults 91 | */ 92 | public function testRootConfig() 93 | { 94 | $defaults = $this->config[SchemasRepository::DEFAULTS]; 95 | 96 | $repository = new SchemasRepository(new Factory()); 97 | $repository->configure($defaults); 98 | 99 | $this->assertEquals($this->defaults, $repository->getSchemas()); 100 | 101 | $this->setExpectedException(\RuntimeException::class); 102 | $repository->getSchemas('foo'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Schema/TestSchema.php: -------------------------------------------------------------------------------- 1 | id; 68 | } 69 | 70 | /** 71 | * @inheritDoc 72 | */ 73 | protected function extractAttribute($record, $recordKey) 74 | { 75 | return $record->{$recordKey}; 76 | } 77 | 78 | /** 79 | * @param $value 80 | * @param $record 81 | * @return string 82 | */ 83 | protected function serializeFooAttribute($value, $record) 84 | { 85 | return strtoupper($value); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | factory = new Factory(); 45 | } 46 | 47 | /** 48 | * @param $content 49 | * @return Contracts\Object\DocumentInterface 50 | */ 51 | protected function decode($content) 52 | { 53 | return new Document(json_decode($content)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Utils/ErrorCreatorTraitTest.php: -------------------------------------------------------------------------------- 1 | repository = new ErrorRepository(new Replacer()); 46 | } 47 | 48 | public function testAddErrorObject() 49 | { 50 | $expected = new Error('123'); 51 | 52 | $this->assertSame($expected, $this->addError($expected)); 53 | $this->assertError($expected); 54 | } 55 | 56 | public function testAddErrorKey() 57 | { 58 | $expected = new Error('123'); 59 | $expected->setDetail('Expecting to see bar as the value'); 60 | 61 | $this->willSee('my-error', '123', 'Expecting to see {foo} as the value'); 62 | $this->assertEquals($expected, $this->addError('my-error', ['foo' => 'bar'])); 63 | $this->assertError($expected); 64 | } 65 | 66 | public function testAddErrorWithPointer() 67 | { 68 | $expected = new Error('123'); 69 | $expected->setDetail('Expecting to see bar as the value'); 70 | $expected->setSourcePointer($pointer = '/foo/bar'); 71 | 72 | $this->willSee('my-error', '123', 'Expecting to see {foo} as the value'); 73 | $this->assertEquals($expected, $this->addErrorWithPointer('my-error', $pointer, ['foo' => 'bar'])); 74 | $this->assertError($expected); 75 | } 76 | 77 | public function testAddErrorWithParameter() 78 | { 79 | $expected = new Error('123'); 80 | $expected->setDetail('Expecting to see bar as the value'); 81 | $expected->setSourceParameter($param = 'foobar'); 82 | 83 | $this->willSee('my-error', '123', 'Expecting to see {foo} as the value'); 84 | $this->assertEquals($expected, $this->addErrorWithParameter('my-error', $param, ['foo' => 'bar'])); 85 | $this->assertError($expected); 86 | } 87 | 88 | /** 89 | * @param $key 90 | * @param $id 91 | * @param $detail 92 | * @return $this 93 | */ 94 | private function willSee($key, $id, $detail = null) 95 | { 96 | $this->repository->configure([ 97 | $key => [ 98 | MutableErrorInterface::ID => $id, 99 | MutableErrorInterface::DETAIL => $detail, 100 | ], 101 | ]); 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @param ErrorInterface $expected 108 | * @return $this 109 | */ 110 | private function assertError(ErrorInterface $expected) 111 | { 112 | $expected = new MutableErrorCollection([$expected]); 113 | $actual = $this->getErrors(); 114 | 115 | $this->assertEquals($expected, $actual); 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * @return ErrorRepository 122 | */ 123 | protected function getErrorRepository() 124 | { 125 | return $this->repository; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Utils/StrTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, Str::dasherize($value)); 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | public function decamelizeProvider() 58 | { 59 | return [ 60 | ['foo', 'foo'], 61 | ['fooBar', 'foo_bar'], 62 | ['fooBarBazBat', 'foo_bar_baz_bat'], 63 | ['foo_bar', 'foo_bar'], 64 | ]; 65 | } 66 | 67 | /** 68 | * @param $value 69 | * @param $expected 70 | * @dataProvider decamelizeProvider 71 | */ 72 | public function testDecamelize($value, $expected) 73 | { 74 | $this->assertSame($expected, Str::decamelize($value)); 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | public function underscoreProvider() 81 | { 82 | return [ 83 | ['foo', 'foo'], 84 | ['fooBar', 'foo_bar'], 85 | ['fooBarBazBat', 'foo_bar_baz_bat'], 86 | ['foo_bar', 'foo_bar'], 87 | ['foo-bar', 'foo_bar'], 88 | ['foo-bar-baz-bat', 'foo_bar_baz_bat'], 89 | ]; 90 | } 91 | 92 | /** 93 | * @param $value 94 | * @param $expected 95 | * @dataProvider underscoreProvider 96 | */ 97 | public function testUnderscore($value, $expected) 98 | { 99 | $this->assertSame($expected, Str::underscore($value)); 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function camelizeProvider() 106 | { 107 | return [ 108 | ['foo', 'foo'], 109 | ['foo-bar', 'fooBar'], 110 | ['foo_bar', 'fooBar'], 111 | ['foo_bar_baz_bat', 'fooBarBazBat'], 112 | ['fooBar', 'fooBar'], 113 | ]; 114 | } 115 | 116 | /** 117 | * @param $value 118 | * @param $expected 119 | * @dataProvider camelizeProvider 120 | */ 121 | public function testCamelizeAndClassify($value, $expected) 122 | { 123 | $this->assertSame($expected, Str::camelize($value), 'camelize'); 124 | $this->assertSame(ucfirst($expected), Str::classify($value), 'classify'); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Validators/TestContextValidator.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 48 | } 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public function isValid(ResourceObjectInterface $resource, $record = null) 54 | { 55 | $callback = $this->callback; 56 | 57 | return (bool) $callback($resource, $record, $this); 58 | } 59 | 60 | } 61 | --------------------------------------------------------------------------------