├── .gitignore ├── composer.json ├── composer.lock ├── config └── api.php ├── documentation ├── controllers.md ├── getting-started.md └── repository.md ├── phpspec.yml ├── readme.md ├── spec ├── ApiFacadeSpec.php ├── CollectionHelperSpec.php ├── Config │ ├── ApiSpec.php │ └── ApiVersionSpec.php ├── Factory │ ├── FactorySpec.php │ └── VersionFactorySpec.php ├── MediaType │ └── JsonSpec.php ├── PayloadCreatorSpec.php ├── Repository │ └── Helper │ │ └── RelationalSpec.php ├── RepresentationCreatorSpec.php ├── Request │ ├── AcceptHeaderSpec.php │ ├── ParserSpec.php │ └── PayloadSpec.php ├── Resource │ ├── LaravelCollectionSpec.php │ └── LaravelEntitySpec.php └── Transformer │ └── AbstractTransformerSpec.php └── src ├── ApiFacade.php ├── CollectionHelper.php ├── Config ├── Api.php └── ApiVersion.php ├── Contracts ├── ApiFacade.php ├── CollectionHelper.php ├── Config │ ├── Api.php │ └── ApiVersion.php ├── Entity │ ├── Entity.php │ └── Transformer │ │ ├── EntityTransformer.php │ │ ├── ForwardTransformer.php │ │ ├── ReverseTransformer.php │ │ └── Transformer.php ├── Event │ └── Event.php ├── Exceptions │ └── ApiExceptionHandler.php ├── Factory │ ├── Factory.php │ └── VersionFactory.php ├── MediaType │ ├── Json.php │ └── MediaType.php ├── Nullable.php ├── PayloadCreator.php ├── Repository │ ├── Actions │ │ └── REST.php │ ├── Helper │ │ └── Relational.php │ └── Repository.php ├── RepresentationCreator.php ├── Request │ ├── AcceptHeader.php │ ├── Parser.php │ └── Payload.php ├── Resource │ ├── Collection.php │ ├── Entity.php │ └── Type.php └── Validation │ ├── Entity.php │ ├── Relation.php │ └── Transform.php ├── Exceptions ├── ApiExceptionHandler.php ├── InvalidArgumentException.php ├── RefactorNeededException.php ├── RepositoryException.php ├── RequestException.php ├── StupidProgrammerMistakeException.php ├── TeapotException.php └── ValidationException.php ├── Factory ├── Factory.php └── VersionFactory.php ├── FractalFactory.php ├── Http └── Controllers │ └── AbstractRestController.php ├── Implementations ├── PayloadCreator.php └── RepresentationCreator.php ├── MediaType └── Json.php ├── Model └── AbstractModel.php ├── Providers └── ApiServiceProvider.php ├── Repository ├── Factory.php ├── Helper │ └── Relational.php └── Repository.php ├── Request ├── AcceptHeader.php ├── Parser.php └── Payload.php ├── Resource ├── LaravelCollection.php └── LaravelEntity.php └── Transformer └── AbstractTransformer.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "larapackage/api", 3 | "version": "0.1.5", 4 | "description": "A Laravel API Abstraction", 5 | "require": { 6 | "laravel/framework": "~5.1", 7 | "league/fractal": "0.12.*", 8 | "prometheusapi/utilities": "~0.0.1", 9 | "larapackage/randomid": "~0.0.1" 10 | }, 11 | "require-dev": { 12 | "phpspec/phpspec": "^2.2" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "LaraPackage\\Api\\": "src/" 17 | } 18 | }, 19 | "license": "MIT", 20 | "authors": [ 21 | { 22 | "name": "Andy Wendt", 23 | "email": "andy@awendt.com" 24 | } 25 | ], 26 | "minimum-stability": "stable" 27 | } 28 | -------------------------------------------------------------------------------- /config/api.php: -------------------------------------------------------------------------------- 1 | 'v', 10 | 'versions' => [ 11 | 1 => [ 12 | 'vendor' => 'vnd.your_api.', 13 | 'media' => [ 14 | 'types' => ['json'], 15 | 'default' => 'json', 16 | ], 17 | 'collection' => [ 18 | 'page_size' => 10, 19 | 'current_position' => 'current', 20 | ], 21 | // This is typically used for testing for getting ids for routes that are non-standard. 22 | 'resourceIdsMap' => [ 23 | '/your/{random_id}/route' => function () { 24 | return (new \LaraPackage\RandomId\TableFetcher)->getRandomColumnEntries('table', ['column_id']); 25 | }, 26 | ], 27 | 'factory' => \LaraPackage\Api\Factory\VersionFactory::class, 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /documentation/controllers.md: -------------------------------------------------------------------------------- 1 | ### Controllers 2 | 3 | Extending `\LaraPackage\Api\Http\Controllers\AbstractRestController` provides GET, POST, PUT, PATCH, DELETE routes out of the box. 4 | 5 | You need to use the repository and transformer for the controller. 6 | 7 | Example usage: 8 | 9 | ```php 10 | 11 | use App\Repositories; 12 | use App\v1\Transformer; 13 | use LaraPackage\Api\CollectionHelper; 14 | use LaraPackage\Api\Http\Controllers\AbstractRestController; 15 | 16 | class YourController extends AbstractRestController 17 | { 18 | 19 | /** 20 | * @param Repositories\YourRepository $repository 21 | * @param Transformer\YourTransformer $transformer 22 | * @param \LaraPackage\Api\ApiFacade $api 23 | * @param CollectionHelper $collectionHelper 24 | */ 25 | public function __construct(Repositories\YourRepository $repository, Transformer\YourTransformer $transformer, \LaraPackage\Api\ApiFacade $api, CollectionHelper $collectionHelper) 26 | { 27 | parent::__construct($attribute, $transformer, $api, $collectionHelper); 28 | } 29 | } 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /documentation/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Setup 4 | 5 | 1. Add `\LaraPackage\Api\Providers\ApiServiceProvider::class,` to your provider's array in `config/app.php` 6 | 7 | 2. Publish the config: `php artisan vendor:publish` 8 | 9 | - Change the config definitions to your needs. 10 | 11 | 3. Use versioning in `app/Http/routes.php` file 12 | -------------------------------------------------------------------------------- /documentation/repository.md: -------------------------------------------------------------------------------- 1 | # Repository 2 | 3 | 4 | Extending `\LaraPackage\Api\Repository\Repository` provides collection, entity, and relational functionality. 5 | 6 | Example repository implementation: 7 | 8 | ```php 9 | use LaraPackage\Api\Model\AbstractModel; 10 | use Illuminate\Contracts\Validation\Factory as Validator; 11 | use LaraPackage\Api\Repository\Factory; 12 | use LaraPackage\Api\Repository\Repository; 13 | 14 | class YourRepository extends Repository implements \App\Contracts\Repository\YourRepository 15 | { 16 | protected $model; 17 | 18 | /** 19 | * @param AbstractModel $model 20 | * @param Validator $validator 21 | * @param Factory $factory 22 | */ 23 | public function __construct(AbstractModel $model, Validator $validator, Factory $factory) 24 | { 25 | parent::__construct($model, $validator, $factory); 26 | } 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | suites: 2 | main: 3 | namespace: LaraPackage\Api 4 | psr4_prefix: LaraPackage\Api 5 | src_path: src 6 | spec_prefix: spec 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # LaraPackage/Api 2 | 3 | A convenient Laravel package to speed api development. 4 | 5 | * [Getting Started](documentation/getting-started.md) 6 | * [Controllers](documentation/controllers.md) 7 | * [Repositories](documentation/repository.md) 8 | 9 | -------------------------------------------------------------------------------- /spec/ApiFacadeSpec.php: -------------------------------------------------------------------------------- 1 | errorExpectations(400, $message, $factory, $representationCreator, $response); 31 | $this->badRequest($exception)->shouldReturn($response); 32 | } 33 | 34 | function it_creates_a_collection_response( 35 | PayloadCreator $payload, 36 | RepresentationCreator $representationCreator, 37 | Factory $factory, 38 | Collection $cursor, 39 | Response $response, 40 | Transformer $transformer 41 | ) 42 | { 43 | $factory->makePayloadCreator()->shouldBeCalled(1)->willReturn($payload); 44 | $factory->makeRepresentationCreator()->shouldBeCalled(1)->willReturn($representationCreator); 45 | $payload->cursor($cursor, $transformer)->shouldBeCalled(1); 46 | $representationCreator->make($payload)->shouldBeCalled()->willReturn($response); 47 | 48 | $this->collection($cursor, $transformer)->shouldReturn($response); 49 | } 50 | 51 | function it_creates_a_conflict_response( 52 | Factory $factory, 53 | RepresentationCreator $representationCreator, 54 | Response $response, 55 | \Exception $exception 56 | ) { 57 | $message = 'The request could not be completed due to a conflict with the current state of the resource.'; 58 | $this->errorExpectations(409, $message, $factory, $representationCreator, $response); 59 | $this->conflict($exception)->shouldReturn($response); 60 | } 61 | 62 | function it_creates_a_i_am_a_teapot_response( 63 | Factory $factory, 64 | RepresentationCreator $representationCreator, 65 | Response $response, 66 | \Exception $exception 67 | ) { 68 | $message = 'If you think this is not an http status code--you are wrong: (RFC 2324)'; 69 | $this->errorExpectations(418, $message, $factory, $representationCreator, $response); 70 | $this->iAmATeapot($exception)->shouldReturn($response); 71 | } 72 | 73 | function it_creates_a_method_not_allowed_response( 74 | Factory $factory, 75 | RepresentationCreator $representationCreator, 76 | Response $response, 77 | MethodNotAllowedHttpException $exception 78 | ) { 79 | $message = 'The method specified in the Request-Line is not allowed for the resource identified by the Request-URI.'; 80 | $this->errorExpectations(405, $message, $factory, $representationCreator, $response); 81 | $this->methodNotAllowed($exception)->shouldReturn($response); 82 | } 83 | 84 | function it_creates_a_not_found_error_response( 85 | Factory $factory, 86 | RepresentationCreator $representationCreator, 87 | Response $response, 88 | \Exception $exception 89 | ) { 90 | $message = 'The server did not find anything matching the requested URI.'; 91 | $this->errorExpectations(404, $message, $factory, $representationCreator, $response); 92 | $this->notFound($exception)->shouldReturn($response); 93 | } 94 | 95 | function it_creates_a_validation_error_response( 96 | Factory $factory, 97 | RepresentationCreator $representationCreator, 98 | Response $response, 99 | \LaraPackage\Api\Exceptions\ValidationException $exception 100 | ) { 101 | $failed = ['test_field' => 'no spaces']; 102 | $message = json_encode($failed); 103 | 104 | $exception->failed()->shouldBeCalled()->willReturn($failed); 105 | $this->errorExpectations(400, $message, $factory, $representationCreator, $response); 106 | $this->validationError($exception)->shouldReturn($response); 107 | } 108 | 109 | function it_creates_an_entity_response( 110 | PayloadCreator $payloadCreator, 111 | Response $response, 112 | Factory $factory, 113 | Entity $entity, 114 | RepresentationCreator $representationCreator, 115 | Transformer $transformer 116 | ) 117 | { 118 | $factory->makePayloadCreator()->shouldBeCalled(1)->willReturn($payloadCreator); 119 | $payloadCreator->entity($entity, $transformer)->shouldBeCalled(1)->willReturn($payloadCreator); 120 | $factory->makeRepresentationCreator()->shouldBeCalled(1)->willReturn($representationCreator); 121 | 122 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($representationCreator); 123 | $representationCreator->make($payloadCreator, 200)->shouldBeCalled()->willReturn($response); 124 | 125 | $this->entity($entity, $transformer)->shouldReturn($response); 126 | } 127 | 128 | function it_creates_an_internal_error_response( 129 | Factory $factory, 130 | RepresentationCreator $representationCreator, 131 | Response $response, 132 | \Exception $exception 133 | ) { 134 | $message = 'The server encountered an unexpected condition which prevented it from fulfilling the request.'; 135 | $this->errorExpectations(500, $message, $factory, $representationCreator, $response); 136 | $this->internalError($exception)->shouldReturn($response); 137 | } 138 | 139 | function it_creates_an_unauthorized_response( 140 | Factory $factory, 141 | RepresentationCreator $representationCreator, 142 | Response $response, 143 | \Exception $exception 144 | ) { 145 | $message = 'The request requires user authentication.'; 146 | $this->errorExpectations(401, $message, $factory, $representationCreator, $response); 147 | $this->unauthorized($exception)->shouldReturn($response); 148 | } 149 | 150 | function it_deletes_a_relational_collection( 151 | Repository $repository, 152 | Response $response, 153 | Factory $factory, 154 | RepresentationCreator $responseCreator, 155 | Relational $relational 156 | ) { 157 | $table = 'image_product_site'; 158 | $relationIds = ['site_id' => 2, 'product_id' => 7]; 159 | 160 | $relational->table()->shouldBeCalled()->willReturn($table); 161 | $relational->relationIds()->shouldBeCalled()->willReturn($relationIds); 162 | 163 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($responseCreator); 164 | $responseCreator->make([], 204)->shouldBeCalled()->willReturn($response); 165 | 166 | $this->deleteRelationalCollection($repository)->shouldReturn($response); 167 | } 168 | 169 | function it_deletes_a_resource( 170 | Repository $repository, 171 | Factory $factory, 172 | RepresentationCreator $representationCreator, 173 | Response $response 174 | ) 175 | { 176 | $ids = '1,2'; 177 | $idsArray = ['1', '2']; 178 | 179 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($representationCreator); 180 | $representationCreator->make([], 204)->shouldBeCalled()->willReturn($response); 181 | $repository->delete($idsArray)->shouldBeCalled(); 182 | $this->delete($ids, $repository)->shouldReturn($response); 183 | } 184 | 185 | function it_deletes_relations( 186 | Repository $repository, 187 | Response $response, 188 | Factory $factory, 189 | RepresentationCreator $representationCreator, 190 | Relational $relational 191 | ) { 192 | $table = 'image_product_site'; 193 | $relationIds = ['site_id' => 2, 'product_id' => 7]; 194 | $itemColumn = 'image_id'; 195 | $itemIds = [6, 7, 10]; 196 | 197 | $relational->table()->shouldBeCalled()->willReturn($table); 198 | $relational->relationIds()->shouldBeCalled()->willReturn($relationIds); 199 | $relational->itemColumn()->shouldBeCalled()->willReturn($itemColumn); 200 | $relational->itemIds()->shouldBeCalled()->willReturn($itemIds); 201 | 202 | $repository->deleteRelation($table, $relationIds, $itemColumn, $itemIds, null)->shouldBeCalled(); 203 | 204 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($representationCreator); 205 | $representationCreator->make([], 204)->shouldBeCalled()->willReturn($response); 206 | 207 | $this->deleteRelation($repository)->shouldReturn($response); 208 | } 209 | 210 | function it_patches_a_resource( 211 | Payload $payload, 212 | Factory $factory, 213 | PayloadCreator $payloadCreator, 214 | Repository $dbRepository, 215 | Transformer $transformer, 216 | Entity $entity, 217 | RepresentationCreator $representationCreator, 218 | Response $response 219 | ) 220 | { 221 | $id = 3; 222 | $array = ['test']; 223 | $factory->getRequestPayload()->shouldBeCalled()->willReturn($payload); 224 | $payload->getIterator()->shouldBeCalled()->willReturn($array); 225 | $transformer->reverseTransform($array)->shouldBeCalled()->willReturn($array); 226 | $dbRepository->patch($id, $array)->shouldBeCalled()->willReturn($entity); 227 | 228 | $factory->makePayloadCreator()->shouldBeCalled()->willReturn($payloadCreator); 229 | $payloadCreator->entity($entity, $transformer)->shouldBeCalled()->willReturn($payloadCreator); 230 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($representationCreator); 231 | 232 | $representationCreator->make($payloadCreator, 200)->shouldBeCalled()->willReturn($response); 233 | 234 | 235 | $this->patch($id, $dbRepository, $transformer)->shouldReturn($response); 236 | } 237 | 238 | function it_posts_a_resource( 239 | Payload $payload, 240 | Factory $factory, 241 | PayloadCreator $payloadCreator, 242 | Repository $dbRepository, 243 | Transformer $transformer, 244 | Entity $entity, 245 | Factory $factory, 246 | RepresentationCreator $representationCreator, 247 | Response $response 248 | ) { 249 | $array = ['test']; 250 | 251 | $factory->getRequestPayload()->shouldBeCalled()->willReturn($payload); 252 | $payload->getIterator()->shouldBeCalled()->willReturn($array); 253 | $transformer->reverseTransform($array)->shouldBeCalled()->willReturn($array); 254 | $dbRepository->post($array)->shouldBeCalled()->willReturn($entity); 255 | 256 | $factory->makePayloadCreator()->shouldBeCalled()->willReturn($payloadCreator); 257 | $payloadCreator->entity($entity, $transformer)->shouldBeCalled()->willReturn($payloadCreator); 258 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($representationCreator); 259 | 260 | $representationCreator->make($payloadCreator, 201)->shouldBeCalled()->willReturn($response); 261 | 262 | $this->post($dbRepository, $transformer)->shouldReturn($response); 263 | } 264 | 265 | function it_posts_multiple_relations( 266 | Repository $repository, 267 | Response $response, 268 | Payload $payload, 269 | Factory $factory, 270 | RepresentationCreator $responseCreator, 271 | ReverseTransformer $transformer, 272 | Relational $relational 273 | ) 274 | { 275 | $table = 'image_product_site'; 276 | $columnsValues = [ 277 | ['image_id' => 5, 'product_id' => 7, 'site_id' => 10], 278 | ['image_id' => 7, 'product_id' => 7, 'site_id' => 10], 279 | ]; 280 | 281 | $columnsValuesWithTimestamps = $this->addTimestampsToCollection($columnsValues); 282 | 283 | $rawBody = [['id' => 5], ['id', 7]]; 284 | $body = [['image_id' => 5], ['image_id', 7]]; 285 | 286 | $factory->getRequestPayload()->shouldBeCalled()->willReturn($payload); 287 | 288 | $payload->getIterator()->shouldBeCalled()->willReturn($rawBody); 289 | $transformer->reverseTransform($rawBody)->shouldBeCalled()->willReturn($body); 290 | 291 | $relational->addRelationalIdsToEachItemInCollection($body)->shouldBeCalled()->willReturn($columnsValues); 292 | $relational->addTimestampsToEachItemInCollection($columnsValues)->shouldBeCalled()->willReturn($columnsValuesWithTimestamps); 293 | 294 | $relational->table()->shouldBeCalled()->willReturn($table); 295 | $repository->postRelations($table, $columnsValuesWithTimestamps)->shouldBeCalled(); 296 | 297 | $this->postRelation($repository, $transformer); 298 | } 299 | 300 | function it_puts_a_collection_to_a_relation( 301 | Repository $repository, 302 | Response $response, 303 | Payload $payload, 304 | Factory $factory, 305 | RepresentationCreator $responseCreator, 306 | ReverseTransformer $transformer, 307 | Relational $relational 308 | ) 309 | { 310 | $table = 'image_product_site'; 311 | $columnsValues = [ 312 | ['image_id' => 5, 'product_id' => 7, 'site_id' => 10], 313 | ['image_id' => 7, 'product_id' => 7, 'site_id' => 10], 314 | ]; 315 | 316 | $columnsValuesWithTimestamps = $this->addTimestampsToCollection($columnsValues); 317 | 318 | $rawBody = [['id' => 5], ['id', 7]]; 319 | $body = [['image_id' => 5], ['image_id', 7]]; 320 | 321 | $relationIds = ['site_id' => 10, 'product_id' => 7]; 322 | 323 | 324 | $relational->relationIds()->shouldBeCalled()->willReturn($relationIds); 325 | 326 | $factory->getRequestPayload()->shouldBeCalled()->willReturn($payload); 327 | 328 | $payload->getIterator()->shouldBeCalled()->willReturn($rawBody); 329 | $transformer->reverseTransform($rawBody)->shouldBeCalled()->willReturn($body); 330 | 331 | $relational->addRelationalIdsToEachItemInCollection($body)->shouldBeCalled()->willReturn($columnsValues); 332 | $relational->addTimestampsToEachItemInCollection($columnsValues)->shouldBeCalled()->willReturn($columnsValuesWithTimestamps); 333 | 334 | $relational->table()->shouldBeCalled()->willReturn($table); 335 | $repository->putRelations($table, $relationIds, $columnsValuesWithTimestamps)->shouldBeCalled(); 336 | 337 | $this->putRelationalCollection($repository, $transformer); 338 | } 339 | 340 | function it_puts_a_resource( 341 | Payload $payload, 342 | Factory $factory, 343 | PayloadCreator $payloadCreator, 344 | Repository $dbRepository, 345 | Transformer $transformer, 346 | Entity $entity, 347 | RepresentationCreator $representationCreator, 348 | Response $response 349 | ) { 350 | $id = 3; 351 | $array = ['test']; 352 | $factory->getRequestPayload()->shouldBeCalled()->willReturn($payload); 353 | $payload->getIterator()->shouldBeCalled()->willReturn($array); 354 | $transformer->reverseTransform($array)->shouldBeCalled()->willReturn($array); 355 | $dbRepository->put($id, $array)->shouldBeCalled()->willReturn($entity); 356 | 357 | $factory->makePayloadCreator()->shouldBeCalled()->willReturn($payloadCreator); 358 | $payloadCreator->entity($entity, $transformer)->shouldBeCalled()->willReturn($payloadCreator); 359 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($representationCreator); 360 | 361 | $representationCreator->make($payloadCreator, 200)->shouldBeCalled()->willReturn($response); 362 | 363 | $this->put($id, $dbRepository, $transformer)->shouldReturn($response); 364 | } 365 | 366 | function let(Factory $factory, Relational $relational) 367 | { 368 | $this->beConstructedWith($factory, $relational); 369 | } 370 | 371 | protected function addTimestampsToArray(array $array) 372 | { 373 | return array_merge($array, ['created_at' => 'now', 'updated_at' => 'now']); 374 | } 375 | 376 | protected function addTimestampsToCollection(array $collection) 377 | { 378 | $out = []; 379 | 380 | foreach ($collection as $item) { 381 | $out[] = $this->addTimestampsToArray($item); 382 | } 383 | 384 | return $out; 385 | } 386 | 387 | protected function errorExpectations( 388 | $statusCode, 389 | $message, 390 | Factory $factory, 391 | RepresentationCreator $representationCreator, 392 | Response $response, 393 | $headers = [] 394 | ) 395 | { 396 | $responseArray = [ 397 | 'error' => [ 398 | 'http_status' => $statusCode, 399 | 'message' => $message, 400 | ], 401 | ]; 402 | 403 | $factory->makeRepresentationCreator()->shouldBeCalled()->willReturn($representationCreator); 404 | $representationCreator->make($responseArray, $statusCode, $headers)->shouldBeCalled()->willReturn($response); 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /spec/CollectionHelperSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('LaraPackage\Api\CollectionHelper'); 16 | } 17 | 18 | function let(Parser $parser, ApiVersion $apiVersion) 19 | { 20 | $this->beConstructedWith($parser, $apiVersion); 21 | } 22 | 23 | function it_returns_the_query(Parser $parser, Transformer $transformer) 24 | { 25 | $query = ['foo' => 'bar', 'baz' => 'boo']; 26 | $transformed = ['foofoo' => 'bar', 'bazbaz' => 'boo']; 27 | $parser->query()->shouldBeCalled()->willReturn($query); 28 | $transformer->reverseTransform($query)->shouldBeCalled()->willReturn($transformed); 29 | 30 | $this->query($transformer)->shouldReturn($transformed); 31 | } 32 | 33 | function it_returns_the_page_size(Parser $parser, ApiVersion $apiVersion) 34 | { 35 | $version = 4; 36 | $expectedPageSize = 10; 37 | $parser->version()->shouldBeCalled()->willReturn($version); 38 | 39 | $apiVersion->collectionPageSize($version)->shouldBeCalled()->willReturn($expectedPageSize); 40 | 41 | $this->pageSize()->shouldReturn($expectedPageSize); 42 | } 43 | 44 | function it_returns_the_current_position(Parser $parser, ApiVersion $apiVersion) 45 | { 46 | $version = 4; 47 | $postionString = 'current'; 48 | $currentPosition = 20; 49 | 50 | $parser->version()->shouldBeCalled()->willReturn($version); 51 | $apiVersion->collectionCurrentPositionString($version)->shouldBeCalled()->willReturn($postionString); 52 | $parser->query($postionString)->shouldBeCalled()->willReturn($currentPosition); 53 | 54 | $this->currentPosition()->shouldReturn(20); 55 | } 56 | 57 | function it_returns_the_current_position_even_if_it_is_null(Parser $parser, ApiVersion $apiVersion) 58 | { 59 | $version = 4; 60 | $postionString = 'current'; 61 | 62 | $parser->version()->shouldBeCalled()->willReturn($version); 63 | $apiVersion->collectionCurrentPositionString($version)->shouldBeCalled()->willReturn($postionString); 64 | $parser->query($postionString)->shouldBeCalled()->willReturn(null); 65 | 66 | $this->currentPosition()->shouldReturn(0); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /spec/Config/ApiSpec.php: -------------------------------------------------------------------------------- 1 | appExpectations($app, $config); 19 | $config->get('api.'.$index)->shouldBeCalledTimes(1)->willReturn($return); 20 | 21 | $this->getIndex($index)->shouldReturn($return); 22 | } 23 | 24 | function it_gets_an_item_for_an_api_version(App $app, Config\Repository $config) 25 | { 26 | $item = 'foo'; 27 | $version = 4; 28 | $return = 'bar'; 29 | 30 | $this->appExpectations($app, $config); 31 | $config->get('api.versions.'.$version.'.'.$item)->shouldBeCalledTimes(1)->willReturn($return); 32 | 33 | $this->getIndexForVersion($item, $version)->shouldReturn($return); 34 | } 35 | 36 | function it_is_initializable() 37 | { 38 | $this->shouldHaveType('LaraPackage\Api\Config\Api'); 39 | } 40 | 41 | function let(App $app) 42 | { 43 | $this->beConstructedWith($app); 44 | } 45 | 46 | /** 47 | * @param AppStub $app 48 | * @param Config\Repository $config 49 | */ 50 | protected function appExpectations(App $app, Config\Repository $config) 51 | { 52 | $app->offsetGet('config')->shouldBeCalled()->willReturn($config); 53 | } 54 | } 55 | 56 | 57 | class AppStub implements \Illuminate\Contracts\Container\Container, \ArrayAccess 58 | { 59 | 60 | /** 61 | * Register a new after resolving callback. 62 | * 63 | * @param string $abstract 64 | * @param \Closure $callback 65 | * 66 | * @return void 67 | */ 68 | public function afterResolving($abstract, Closure $callback = null) 69 | { 70 | // TODO: Implement afterResolving() method. 71 | } 72 | 73 | /** 74 | * Alias a type to a different name. 75 | * 76 | * @param string $abstract 77 | * @param string $alias 78 | * 79 | * @return void 80 | */ 81 | public function alias($abstract, $alias) 82 | { 83 | // TODO: Implement alias() method. 84 | } 85 | 86 | /** 87 | * Register a binding with the container. 88 | * 89 | * @param string|array $abstract 90 | * @param \Closure|string|null $concrete 91 | * @param bool $shared 92 | * 93 | * @return void 94 | */ 95 | public function bind($abstract, $concrete = null, $shared = false) 96 | { 97 | // TODO: Implement bind() method. 98 | } 99 | 100 | /** 101 | * Register a binding if it hasn't already been registered. 102 | * 103 | * @param string $abstract 104 | * @param \Closure|string|null $concrete 105 | * @param bool $shared 106 | * 107 | * @return void 108 | */ 109 | public function bindIf($abstract, $concrete = null, $shared = false) 110 | { 111 | // TODO: Implement bindIf() method. 112 | } 113 | 114 | /** 115 | * Determine if the given type has been bound. 116 | * 117 | * @param string $abstract 118 | * 119 | * @return bool 120 | */ 121 | public function bound($abstract) 122 | { 123 | // TODO: Implement bound() method. 124 | } 125 | 126 | /** 127 | * Call the given Closure / class@method and inject its dependencies. 128 | * 129 | * @param callable|string $callback 130 | * @param array $parameters 131 | * @param string|null $defaultMethod 132 | * 133 | * @return mixed 134 | */ 135 | public function call($callback, array $parameters = [], $defaultMethod = null) 136 | { 137 | // TODO: Implement call() method. 138 | } 139 | 140 | /** 141 | * "Extend" an type in the container. 142 | * 143 | * @param string $abstract 144 | * @param \Closure $closure 145 | * 146 | * @return void 147 | * 148 | * @throws \InvalidArgumentException 149 | */ 150 | public function extend($abstract, Closure $closure) 151 | { 152 | // TODO: Implement extend() method. 153 | } 154 | 155 | /** 156 | * Register an existing instance as shared in the container. 157 | * 158 | * @param string $abstract 159 | * @param mixed $instance 160 | * 161 | * @return void 162 | */ 163 | public function instance($abstract, $instance) 164 | { 165 | // TODO: Implement instance() method. 166 | } 167 | 168 | /** 169 | * Resolve the given type from the container. 170 | * 171 | * @param string $abstract 172 | * @param array $parameters 173 | * 174 | * @return mixed 175 | */ 176 | public function make($abstract, array $parameters = []) 177 | { 178 | // TODO: Implement make() method. 179 | } 180 | 181 | /** 182 | * (PHP 5 >= 5.0.0)
183 | * Whether a offset exists 184 | * 185 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php 186 | * 187 | * @param mixed $offset

188 | * An offset to check for. 189 | *

190 | * 191 | * @return boolean true on success or false on failure. 192 | *

193 | *

194 | * The return value will be casted to boolean if non-boolean was returned. 195 | */ 196 | public function offsetExists($offset) 197 | { 198 | // TODO: Implement offsetExists() method. 199 | } 200 | 201 | /** 202 | * (PHP 5 >= 5.0.0)
203 | * Offset to retrieve 204 | * 205 | * @link http://php.net/manual/en/arrayaccess.offsetget.php 206 | * 207 | * @param mixed $offset

208 | * The offset to retrieve. 209 | *

210 | * 211 | * @return mixed Can return all value types. 212 | */ 213 | public function offsetGet($offset) 214 | { 215 | // TODO: Implement offsetGet() method. 216 | } 217 | 218 | /** 219 | * (PHP 5 >= 5.0.0)
220 | * Offset to set 221 | * 222 | * @link http://php.net/manual/en/arrayaccess.offsetset.php 223 | * 224 | * @param mixed $offset

225 | * The offset to assign the value to. 226 | *

227 | * @param mixed $value

228 | * The value to set. 229 | *

230 | * 231 | * @return void 232 | */ 233 | public function offsetSet($offset, $value) 234 | { 235 | // TODO: Implement offsetSet() method. 236 | } 237 | 238 | /** 239 | * (PHP 5 >= 5.0.0)
240 | * Offset to unset 241 | * 242 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php 243 | * 244 | * @param mixed $offset

245 | * The offset to unset. 246 | *

247 | * 248 | * @return void 249 | */ 250 | public function offsetUnset($offset) 251 | { 252 | // TODO: Implement offsetUnset() method. 253 | } 254 | 255 | /** 256 | * Determine if the given type has been resolved. 257 | * 258 | * @param string $abstract 259 | * 260 | * @return bool 261 | */ 262 | public function resolved($abstract) 263 | { 264 | // TODO: Implement resolved() method. 265 | } 266 | 267 | /** 268 | * Register a new resolving callback. 269 | * 270 | * @param string $abstract 271 | * @param \Closure $callback 272 | * 273 | * @return void 274 | */ 275 | public function resolving($abstract, Closure $callback = null) 276 | { 277 | // TODO: Implement resolving() method. 278 | } 279 | 280 | /** 281 | * Register a shared binding in the container. 282 | * 283 | * @param string $abstract 284 | * @param \Closure|string|null $concrete 285 | * 286 | * @return void 287 | */ 288 | public function singleton($abstract, $concrete = null) 289 | { 290 | // TODO: Implement singleton() method. 291 | } 292 | 293 | /** 294 | * Assign a set of tags to a given binding. 295 | * 296 | * @param array|string $abstracts 297 | * @param array|mixed ...$tags 298 | * 299 | * @return void 300 | */ 301 | public function tag($abstracts, $tags) 302 | { 303 | // TODO: Implement tag() method. 304 | } 305 | 306 | /** 307 | * Resolve all of the bindings for a given tag. 308 | * 309 | * @param array $tag 310 | * 311 | * @return array 312 | */ 313 | public function tagged($tag) 314 | { 315 | // TODO: Implement tagged() method. 316 | } 317 | 318 | /** 319 | * Define a contextual binding. 320 | * 321 | * @param string $concrete 322 | * 323 | * @return \Illuminate\Contracts\Container\ContextualBindingBuilder 324 | */ 325 | public function when($concrete) 326 | { 327 | // TODO: Implement when() method. 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /spec/Config/ApiVersionSpec.php: -------------------------------------------------------------------------------- 1 | 'test', 10 => 'test', 1 => 'foobar']; 13 | 14 | function it_checks_a_version_to_make_sure_it_is_valid(\LaraPackage\Api\Contracts\Config\Api $config) 15 | { 16 | $this->configGetApiItemExpectation($config, 'versions', $this->versionArray); 17 | $this->isValid($this->version)->shouldReturn(true); 18 | } 19 | 20 | function it_checks_to_see_if_a_media_type_is_valid_for_the_version(\LaraPackage\Api\Contracts\Config\Api $config) 21 | { 22 | $lookFor = 'yml'; 23 | $mediaTypes = ['json', 'yml']; 24 | $this->configGetApiVersionItemExpecation($config, 'media.types', $mediaTypes); 25 | $this->mediaTypeIsValid($lookFor, $this->version)->shouldReturn(true); 26 | } 27 | 28 | function it_gets_all_available_versions(\LaraPackage\Api\Contracts\Config\Api $config) 29 | { 30 | $expected = \array_keys($this->versionArray); 31 | $this->configGetApiItemExpectation($config, 'versions', $this->versionArray); 32 | $this->availableVersions()->shouldReturn($expected); 33 | } 34 | 35 | function it_gets_an_item_out_of_the_resource_id_map(\LaraPackage\Api\Contracts\Config\Api $config) 36 | { 37 | $idMap = [ 38 | '/attributes/{random_id}' => function () { 39 | return [5, 6]; 40 | }, 41 | '/catalogs/{random_id}/catalogtabs' => function () { 42 | return [8, 9]; 43 | }, 44 | ]; 45 | $this->configGetApiVersionItemExpecation($config, 'resourceIdsMap', $idMap); 46 | $this->resourceIdMap('/catalogs/{random_id}/catalogtabs', 4)->shouldReturn([8, 9]); 47 | } 48 | 49 | function it_gets_the_collection_size_for_the_version(\LaraPackage\Api\Contracts\Config\Api $config) 50 | { 51 | $collectionPageSize = 10; 52 | $this->configGetApiVersionItemExpecation($config, 'collection.page_size', $collectionPageSize); 53 | $this->collectionPageSize($this->version)->shouldReturn($collectionPageSize); 54 | } 55 | 56 | function it_gets_the_current_position_string_for_a_version(\LaraPackage\Api\Contracts\Config\Api $config) 57 | { 58 | $currentPositionString = 'current'; 59 | $this->configGetApiVersionItemExpecation($config, 'collection.current_position', $currentPositionString); 60 | $this->collectionCurrentPositionString($this->version)->shouldReturn($currentPositionString); 61 | } 62 | 63 | function it_gets_the_default_media_type_for_a_version(\LaraPackage\Api\Contracts\Config\Api $config) 64 | { 65 | $defaultMediaType = 'json'; 66 | $this->configGetApiVersionItemExpecation($config, 'media.default', $defaultMediaType); 67 | $this->defaultMediaType($this->version)->shouldReturn($defaultMediaType); 68 | } 69 | 70 | function it_is_initializable() 71 | { 72 | $this->shouldHaveType('LaraPackage\Api\Config\ApiVersion'); 73 | } 74 | 75 | function it_provides_the_version_designator(\LaraPackage\Api\Contracts\Config\Api $config) 76 | { 77 | $version = 4; 78 | $versionDesignator = 'v'; 79 | $this->configGetApiItemExpectation($config, 'version_designator', $versionDesignator); 80 | $this->versionDesignator($version)->shouldReturn($versionDesignator.$version); 81 | } 82 | 83 | function it_returns_an_array_of_media_types_for_the_version(\LaraPackage\Api\Contracts\Config\Api $config) 84 | { 85 | $mediaTypes = ['json', 'yml']; 86 | $this->configGetApiVersionItemExpecation($config, 'media.types', $mediaTypes); 87 | $this->mediaTypes($this->version)->shouldReturn($mediaTypes); 88 | } 89 | 90 | function it_returns_false_if_an_item_is_not_found_in_the_resource_id_map(\LaraPackage\Api\Contracts\Config\Api $config) 91 | { 92 | $idMap = [ 93 | '/attributes/{random_id}' => [5, 6], 94 | '/catalogs/{random_id}/catalogtabs' => [8, 9], 95 | ]; 96 | $this->configGetApiVersionItemExpecation($config, 'resourceIdsMap', $idMap); 97 | $this->resourceIdMap('fubar', 4)->shouldReturn(false); 98 | } 99 | 100 | function it_returns_the_latest_version(\LaraPackage\Api\Contracts\Config\Api $config) 101 | { 102 | $this->configGetApiItemExpectation($config, 'versions', $this->versionArray); 103 | $this->latest()->shouldReturn(10); 104 | } 105 | 106 | function it_returns_the_vendor_string_for_the_version(\LaraPackage\Api\Contracts\Config\Api $config) 107 | { 108 | $vendorString = 'vnd.wps_api.'; 109 | $this->configGetApiVersionItemExpecation($config, 'vendor', $vendorString); 110 | $this->vendor($this->version)->shouldReturn($vendorString); 111 | } 112 | 113 | function it_returns_a_version_factory(\LaraPackage\Api\Contracts\Config\Api $config, \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory) 114 | { 115 | $this->configGetApiVersionItemExpecation($config, 'factory', $versionFactory); 116 | $this->factory($this->version)->shouldReturn($versionFactory); 117 | } 118 | 119 | function let(\LaraPackage\Api\Contracts\Config\Api $config) 120 | { 121 | $this->beConstructedWith($config); 122 | } 123 | 124 | /** 125 | * @param \LaraPackage\Api\Contracts\Config\Api $config 126 | * @param $key 127 | * @param $return 128 | */ 129 | protected function configGetApiItemExpectation(\LaraPackage\Api\Contracts\Config\Api $config, $key, $return) 130 | { 131 | $config->getIndex($key)->shouldBeCalled()->willReturn($return); 132 | } 133 | 134 | /** 135 | * @param \LaraPackage\Api\Contracts\Config\Api $config 136 | * @param $key 137 | * @param $return 138 | * 139 | * @internal param $this->version 140 | */ 141 | protected function configGetApiVersionItemExpecation(\LaraPackage\Api\Contracts\Config\Api $config, $key, $return) 142 | { 143 | $config->getIndexForVersion($key, $this->version)->shouldBeCalled()->willReturn($return); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /spec/Factory/FactorySpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('LaraPackage\Api\Factory\Factory'); 18 | } 19 | 20 | function it_makes_a_payload_creator_for_a_version( 21 | App $app, 22 | \LaraPackage\Api\Contracts\Request\Parser $requestParser, 23 | \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory, 24 | \LaraPackage\Api\Contracts\PayloadCreator $resourceCreator, 25 | ApiVersion $apiVersion 26 | ) { 27 | $this->versionFactoryExpectations($app, $requestParser, $versionFactory, $apiVersion); 28 | $versionFactory->makePayloadCreator()->shouldBeCalledTimes(1)->willReturn($resourceCreator); 29 | $this->makePayloadCreator()->shouldReturn($resourceCreator); 30 | } 31 | 32 | function it_makes_a_representation_creator( 33 | App $app, 34 | \LaraPackage\Api\Contracts\Request\Parser $requestParser, 35 | \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory, 36 | \LaraPackage\Api\Contracts\RepresentationCreator $responseCreator, 37 | ApiVersion $apiVersion 38 | ) { 39 | $this->versionFactoryExpectations($app, $requestParser, $versionFactory, $apiVersion); 40 | $versionFactory->makeRepresentationCreator()->shouldBeCalledTimes(1)->willReturn($responseCreator); 41 | $this->makeRepresentationCreator()->shouldReturn($responseCreator); 42 | } 43 | 44 | function let(App $app, \LaraPackage\Api\Contracts\Request\Parser $requestParser, ApiVersion $apiVersion) 45 | { 46 | $this->beConstructedWith($app, $requestParser, $apiVersion); 47 | } 48 | 49 | /** 50 | * @param App $app 51 | * @param \LaraPackage\Api\Contracts\Request\Parser $requestParser 52 | * @param \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory 53 | */ 54 | protected function versionFactoryExpectations( 55 | App $app, 56 | \LaraPackage\Api\Contracts\Request\Parser $requestParser, 57 | \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory, 58 | ApiVersion $apiVersion 59 | ) { 60 | $requestParser->version()->shouldBeCalled()->willReturn($this->version); 61 | $apiVersion->factory($this->version)->shouldBeCalled()->willReturn(VersionFactory::class); 62 | $app->instance(\LaraPackage\Api\Contracts\Factory\VersionFactory::class, $versionFactory)->shouldBeCalled(); 63 | $app->make(VersionFactory::class)->shouldBeCalledTimes(1)->willReturn($versionFactory); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /spec/Factory/VersionFactorySpec.php: -------------------------------------------------------------------------------- 1 | make(\LaraPackage\Api\Request\Payload::class)->shouldBeCalled()->willReturn($payload); 17 | $this->getRequestPayload()->shouldReturn($payload); 18 | } 19 | 20 | function it_is_initializable() 21 | { 22 | $this->shouldHaveType('LaraPackage\Api\Factory\VersionFactory'); 23 | } 24 | 25 | function it_makes_a_media_type() 26 | { 27 | $this->makeMediaType('json')->shouldReturnAnInstanceOf(\LaraPackage\Api\MediaType\Json::class); 28 | } 29 | 30 | function it_makes_a_payload_creator(App $app, PayloadCreator $payloadCreator) 31 | { 32 | $app->make(\LaraPackage\Api\Implementations\PayloadCreator::class)->shouldBeCalled()->willReturn($payloadCreator); 33 | $this->makePayloadCreator()->shouldReturn($payloadCreator); 34 | } 35 | 36 | function it_makes_a_representation_creator(App $app, RepresentationCreator $responseCreator) 37 | { 38 | $app->make(\LaraPackage\Api\Implementations\RepresentationCreator::class)->shouldBeCalled()->willReturn($responseCreator); 39 | $this->makeRepresentationCreator()->shouldReturn($responseCreator); 40 | } 41 | 42 | function let(App $app) 43 | { 44 | $this->beConstructedWith($app); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spec/MediaType/JsonSpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow(\LaraPackage\Api\Exceptions\InvalidArgumentException::class) 14 | ->during('format', [$notIterable]); 15 | } 16 | 17 | function it_formats_an_array_into_json() 18 | { 19 | $array = ['foo' => 'bar']; 20 | $expected = json_encode($array); 21 | 22 | $this->format($array)->shouldReturn($expected); 23 | } 24 | 25 | function it_formats_an_iterator_aggregate_into_json() 26 | { 27 | $array = ['foo' => 'bar']; 28 | $iteratorAggregate = new IteratorAggregateStub($array); 29 | $expected = json_encode($array); 30 | 31 | $this->format($iteratorAggregate)->shouldReturn($expected); 32 | } 33 | 34 | function it_is_initializable() 35 | { 36 | $this->shouldHaveType('LaraPackage\Api\MediaType\Json'); 37 | } 38 | } 39 | 40 | class IteratorAggregateStub implements \IteratorAggregate 41 | { 42 | 43 | /** 44 | * @var array 45 | */ 46 | private $array; 47 | 48 | public function __construct(array $array) 49 | { 50 | $this->array = $array; 51 | } 52 | 53 | public function getIterator() 54 | { 55 | return new \ArrayIterator($this->array); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /spec/PayloadCreatorSpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow(\LaraPackage\Api\Exceptions\InvalidArgumentException::class)->during('getIterator'); 20 | } 21 | 22 | function it_builds_a_cursor_resource( 23 | \LaraPackage\Api\Contracts\Resource\Collection $cursor, 24 | Fractal\Pagination\Cursor $fractalCursor, 25 | FractalFactory $fractalFactory, 26 | \LaraPackage\Api\Contracts\Entity\Transformer\Transformer $transformer, 27 | Fractal\Resource\Collection $fractalCollection, 28 | Fractal\Manager $manager, 29 | Fractal\Scope $scope 30 | ) { 31 | $expected = ['test']; 32 | $cursorData = new \ArrayIterator($expected); 33 | $current = 10; 34 | $previous = 5; 35 | $next = 15; 36 | $count = 5; 37 | 38 | $fractalFactory->createCollection($cursorData, $transformer)->shouldBeCalled()->willReturn($fractalCollection); 39 | $fractalFactory->createCursor($current, $previous, $next, $count)->shouldBeCalled()->willReturn($fractalCursor); 40 | 41 | $this->setPayloadAssertions($fractalFactory, $fractalCollection, $manager, $scope, $cursorData); 42 | 43 | $fractalCollection->setCursor($fractalCursor)->shouldBeCalled(); 44 | 45 | $cursor->getData()->shouldBeCalled()->willReturn($cursorData); 46 | $cursor->getCurrent()->shouldBeCalled()->willReturn($current); 47 | $cursor->getPrevious()->shouldBeCalled()->willReturn($previous); 48 | $cursor->getNext()->shouldBeCalled()->willReturn($next); 49 | $cursor->getCount()->shouldBeCalled()->willReturn($count); 50 | 51 | $this->cursor($cursor, $transformer); 52 | $this->getIterator()->getArrayCopy()->shouldReturn($expected); 53 | } 54 | 55 | function it_builds_an_entity_resource( 56 | \LaraPackage\Api\Contracts\Resource\Entity $entity, 57 | FractalFactory $fractalFactory, 58 | \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory, 59 | \LaraPackage\Api\Contracts\Entity\Transformer\Transformer $transformer, 60 | Fractal\Resource\Item $item, 61 | Fractal\Manager $manager, 62 | Fractal\Scope $scope 63 | ) { 64 | $expected = ['test']; 65 | $entityData = new \ArrayIterator(['test']); 66 | 67 | $entity->getData()->shouldBeCalled()->willReturn($entityData); 68 | $fractalFactory->createEntity($entityData, $transformer)->shouldBeCalled()->willReturn($item); 69 | 70 | $this->setPayloadAssertions($fractalFactory, $item, $manager, $scope, $entityData); 71 | 72 | $this->entity($entity, $transformer); 73 | 74 | $this->getIterator()->getArrayCopy()->shouldReturn($expected); 75 | } 76 | 77 | function it_is_initializable() 78 | { 79 | $this->shouldHaveType('LaraPackage\Api\Implementations\PayloadCreator'); 80 | } 81 | 82 | function it_returns_an_empty_collection_for_no_results( 83 | \LaraPackage\Api\Contracts\Resource\Collection $cursor, 84 | Fractal\Pagination\Cursor $fractalCursor, 85 | FractalFactory $fractalFactory, 86 | \LaraPackage\Api\Contracts\Entity\Transformer\Transformer $transformer, 87 | Fractal\Resource\Collection $fractalCollection, 88 | Fractal\Manager $manager, 89 | Fractal\Scope $scope 90 | ) 91 | { 92 | $expected = []; 93 | $cursorData = new \ArrayIterator($expected); 94 | $current = 0; 95 | $previous = 0; 96 | $next = 0; 97 | $count = 0; 98 | 99 | $fractalFactory->createCollection($cursorData, $transformer)->shouldBeCalled()->willReturn($fractalCollection); 100 | $fractalFactory->createCursor($current, $previous, $next, $count)->shouldBeCalled()->willReturn($fractalCursor); 101 | 102 | $this->setPayloadAssertions($fractalFactory, $fractalCollection, $manager, $scope, $cursorData); 103 | 104 | $fractalCollection->setCursor($fractalCursor)->shouldBeCalled(); 105 | 106 | $cursor->getData()->shouldBeCalled()->willReturn($cursorData); 107 | $cursor->getCurrent()->shouldBeCalled()->willReturn($current); 108 | $cursor->getPrevious()->shouldBeCalled()->willReturn($previous); 109 | $cursor->getNext()->shouldBeCalled()->willReturn($next); 110 | $cursor->getCount()->shouldBeCalled()->willReturn($count); 111 | 112 | $this->cursor($cursor, $transformer); 113 | $this->getIterator()->getArrayCopy()->shouldReturn($expected); 114 | } 115 | 116 | function let(\LaraPackage\Api\Contracts\Request\Parser $requestParser, FractalFactory $fractalFactory, \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory) 117 | { 118 | $this->beConstructedWith($requestParser, $fractalFactory, $versionFactory); 119 | } 120 | 121 | /** 122 | * @param FractalFactory $fractalFactory 123 | * @param Fractal\Resource\ResourceInterface $resource 124 | * @param Fractal\Manager $manager 125 | * @param Fractal\Scope $scope 126 | * @param \ArrayIterator $data 127 | */ 128 | protected function setPayloadAssertions( 129 | FractalFactory $fractalFactory, 130 | Fractal\Resource\ResourceInterface $resource, 131 | Fractal\Manager $manager, 132 | Fractal\Scope $scope, 133 | \ArrayIterator $data 134 | ) { 135 | $fractalFactory->createManager()->shouldBeCalled()->willReturn($manager); 136 | $manager->createData($resource)->shouldBeCalled()->willReturn($scope); 137 | $scope->toArray()->shouldBeCalled()->willReturn($data->getArrayCopy()); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /spec/Repository/Helper/RelationalSpec.php: -------------------------------------------------------------------------------- 1 | function (array $array, array $keys) { 18 | return $this->keysAreInArray($keys, $array); 19 | }, 20 | 'haveKeysInCollectionItems' => function (array $collection, array $keys) { 21 | $result = []; 22 | foreach ($collection as $item) { 23 | $result[] = $this->keysAreInArray($keys, $item); 24 | } 25 | 26 | return !in_array(false, $result, true); 27 | }, 28 | ]; 29 | } 30 | 31 | function it_adds_relational_ids_to_a_collection( 32 | Request $request, 33 | Parser $parser, 34 | TableHelper $tableHelper 35 | ) { 36 | $uri = '/sites/1/products/2/images'; 37 | $entities = ['sites', 'products', 'images']; 38 | $idEntities = ['sites', 'products']; 39 | $ids = [1, 2]; 40 | $idEntityColumnNames = ['site_id', 'product_id']; 41 | $collection = [['item_id' => 1], ['item_id' => 2]]; 42 | $resultingCollection = [ 43 | ['site_id' => 1, 'product_id' => 2, 'item_id' => 1], 44 | ['site_id' => 1, 'product_id' => 2, 'item_id' => 2], 45 | ]; 46 | 47 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 48 | 49 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 50 | $parser->idEntities($uri)->shouldBeCalled()->willReturn($idEntities); 51 | $parser->ids($uri)->shouldBeCalled()->willReturn($ids); 52 | 53 | $tableHelper->getIdColumnNames($entities, $idEntities)->willReturn($idEntityColumnNames); 54 | 55 | $this->addRelationalIdsToEachItemInCollection($collection)->shouldReturn($resultingCollection); 56 | } 57 | 58 | function it_adds_relational_ids_to_an_item( 59 | Request $request, 60 | Parser $parser, 61 | TableHelper $tableHelper 62 | ) { 63 | $uri = '/sites/1/products/2/images/1'; 64 | $entities = ['sites', 'products', 'images']; 65 | $idEntities = ['sites', 'products', 'images']; 66 | $ids = [1, 2, 1]; 67 | $idEntityColumnNames = ['site_id', 'product_id', 'image_id']; 68 | $item = ['item_id' => 1]; 69 | $resultingItem = ['site_id' => 1, 'product_id' => 2, 'item_id' => 1]; 70 | 71 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 72 | 73 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 74 | $parser->idEntities($uri)->shouldBeCalled()->willReturn($idEntities); 75 | $parser->ids($uri)->shouldBeCalled()->willReturn($ids); 76 | 77 | $tableHelper->getIdColumnNames($entities, $idEntities)->shouldBeCalled()->willReturn($idEntityColumnNames); 78 | 79 | $this->addRelationalIdsToItem($item)->shouldReturn($resultingItem); 80 | } 81 | 82 | function it_adds_timestamps_to_a_collection() 83 | { 84 | $collection = [['test' => 5], ['y' => 7]]; 85 | $this->addTimestampsToEachItemInCollection($collection)->shouldHaveKeysInCollectionItems(['created_at', 'updated_at']); 86 | } 87 | 88 | function it_adds_timestamps_to_an_item() 89 | { 90 | $item = ['test' => 5]; 91 | $this->addTimestampsToItem($item)->shouldHaveKeysInArray(['created_at', 'updated_at']); 92 | } 93 | 94 | function it_blows_up_if_the_last_entity_is_not_an_id( 95 | Request $request, 96 | Parser $parser 97 | ) { 98 | $uri = '/sites/1/products/2/images'; 99 | $entities = ['sites', 'products', 'images']; 100 | $idEntities = ['sites', 'products']; 101 | 102 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 103 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 104 | $parser->idEntities($uri)->shouldBeCalled()->willReturn($idEntities); 105 | 106 | $this->shouldThrow(StupidProgrammerMistakeException::class)->during('itemColumn'); 107 | } 108 | 109 | function it_gets_a_table_name(Request $request, Parser $parser, TableHelper $tableHelper) 110 | { 111 | $uri = '/sites/1/products/2/images/1,2,3'; 112 | $entities = ['sites', 'products', 'images']; 113 | $table = 'image_product_site'; 114 | 115 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 116 | 117 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 118 | $tableHelper->getTable($entities)->shouldBeCalled()->willReturn($table); 119 | 120 | $this->table()->shouldReturn($table); 121 | } 122 | 123 | function it_gets_relation_ids_from_a_collection_resource( 124 | Request $request, 125 | Parser $parser, 126 | TableHelper $tableHelper 127 | ) { 128 | $uri = '/sites/1/products/2/images'; 129 | $entities = ['sites', 'products', 'images']; 130 | $idEntities = ['sites', 'products']; 131 | $ids = [1, 2]; 132 | $result = ['site_id' => 1, 'product_id' => 2]; 133 | 134 | $idEntityColumnNames = ['site_id', 'product_id']; 135 | 136 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 137 | 138 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 139 | $parser->idEntities($uri)->shouldBeCalled()->willReturn($idEntities); 140 | $parser->ids($uri)->shouldBeCalled()->willReturn($ids); 141 | 142 | $tableHelper->getIdColumnNames($entities, $idEntities)->willReturn($idEntityColumnNames); 143 | 144 | $this->relationIds()->shouldReturn($result); 145 | } 146 | 147 | function it_gets_relation_ids_from_an_item_resource( 148 | Request $request, 149 | Parser $parser, 150 | TableHelper $tableHelper 151 | ) { 152 | $uri = '/sites/1/products/2/images/1,4,5'; 153 | $entities = ['sites', 'products', 'images']; 154 | $idEntities = ['sites', 'products', 'images']; 155 | $ids = [1, 2, [1, 4, 5]]; 156 | $result = ['site_id' => 1, 'product_id' => 2]; 157 | $idEntityColumnNames = ['site_id', 'product_id', 'image_id']; 158 | 159 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 160 | 161 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 162 | $parser->idEntities($uri)->shouldBeCalled()->willReturn($idEntities); 163 | $parser->ids($uri)->shouldBeCalled()->willReturn($ids); 164 | 165 | $tableHelper->getIdColumnNames($entities, $idEntities)->willReturn($idEntityColumnNames); 166 | 167 | $this->relationIds()->shouldReturn($result); 168 | } 169 | 170 | function it_is_initializable() 171 | { 172 | $this->shouldHaveType('LaraPackage\Api\Repository\Helper\Relational'); 173 | } 174 | 175 | function it_returns_an_item_column( 176 | Request $request, 177 | Parser $parser, 178 | TableHelper $tableHelper 179 | ) { 180 | $uri = '/sites/1/products/2/images/1,2,4'; 181 | $entities = ['sites', 'products', 'images']; 182 | $idEntities = ['sites', 'products', 'images']; 183 | $result = 'image_id'; 184 | 185 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 186 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 187 | $parser->idEntities($uri)->shouldBeCalled()->willReturn($idEntities); 188 | 189 | $tableHelper->getLastEntityAsIdColumnName($entities)->willReturn($result); 190 | 191 | $this->itemColumn()->shouldReturn($result); 192 | } 193 | 194 | function it_returns_an_item_name(Request $request, Parser $parser, TableHelper $tableHelper) 195 | { 196 | $uri = '/sites/1/products/2/images/1,4,5'; 197 | $entities = ['sites', 'products', 'images']; 198 | $rawItem = 'images'; 199 | $expected = 'Image'; 200 | 201 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 202 | $parser->entities($uri)->shouldBeCalled()->willReturn($entities); 203 | $tableHelper->singularize($rawItem)->shouldBeCalled()->willReturn($expected); 204 | $this->itemName()->shouldReturn($expected); 205 | } 206 | 207 | function it_returns_item_ids( 208 | Request $request, 209 | Parser $parser 210 | ) { 211 | $uri = '/sites/1/products/2/images/1,4,5'; 212 | $ids = [1, 2, [1, 4, 5]]; 213 | $result = [1, 4, 5]; 214 | 215 | $request->getRequestUri()->shouldBeCalled()->willReturn($uri); 216 | 217 | $parser->ids($uri)->shouldBeCalled()->willReturn($ids); 218 | 219 | $this->itemIds()->shouldReturn($result); 220 | } 221 | 222 | function let(Request $request, Parser $parser, TableHelper $tableHelper) 223 | { 224 | $this->beConstructedWith($request, $parser, $tableHelper); 225 | } 226 | 227 | protected function keysAreInArray(array $keys, array $array) 228 | { 229 | $result = []; 230 | foreach ($keys as $search) { 231 | $result[] = array_key_exists($search, $array); 232 | } 233 | 234 | return !in_array(false, $result, true); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /spec/RepresentationCreatorSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('LaraPackage\Api\Implementations\RepresentationCreator'); 15 | } 16 | 17 | function it_makes_a_response( 18 | \LaraPackage\Api\Contracts\Request\Parser $requestParser, 19 | ResponseFactory $response, 20 | \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory, 21 | \LaraPackage\Api\Contracts\MediaType\Json $media, 22 | \Illuminate\Http\Response $illuminateResponse, 23 | \LaraPackage\Api\Contracts\Config\ApiVersion $versionInfoRetriever 24 | ) { 25 | $mediaType = 'json'; 26 | $version = 4; 27 | $vendor = 'vnd.wps_api.'; 28 | $versionDesignator = 'v4'; 29 | $dataArray = ['data' => 'array']; 30 | $jsonData = json_encode($dataArray); 31 | 32 | $requestParser->acceptedMediaType()->shouldBeCalled()->willReturn($mediaType); 33 | 34 | $versionInfoRetriever->vendor($version)->shouldBeCalled()->willReturn($vendor); 35 | $versionInfoRetriever->versionDesignator($version)->shouldBeCalled()->willReturn($versionDesignator); 36 | 37 | $versionFactory->makeMediaType($mediaType)->shouldBeCalled()->willReturn($media); 38 | 39 | $media->format($dataArray)->shouldBeCalled()->willReturn($jsonData); 40 | 41 | $response->make($jsonData, 200, ['Content-Type' => 'application/'.$vendor.$versionDesignator.'+'.$mediaType])->shouldBeCalled()->willReturn($illuminateResponse); 42 | 43 | $this->make($dataArray)->shouldReturn($illuminateResponse); 44 | } 45 | 46 | function let( 47 | \LaraPackage\Api\Contracts\Request\Parser $requestParser, 48 | ResponseFactory $response, 49 | \LaraPackage\Api\Contracts\Factory\VersionFactory $versionFactory, 50 | \LaraPackage\Api\Contracts\Config\ApiVersion $versionInfoRetriever 51 | ) { 52 | $this->beConstructedWith($requestParser, $response, $versionFactory, $versionInfoRetriever); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/Request/AcceptHeaderSpec.php: -------------------------------------------------------------------------------- 1 | requestHeaderExpectation($request, 'application/vnd.wps_api.v4+json'); 14 | $version->isValid(4)->shouldBeCalledTimes(1)->willReturn(true); 15 | $version->mediaTypeIsValid('json', 4)->shouldBeCalledTimes(1)->willReturn(true); 16 | 17 | $this->acceptedMediaType()->shouldReturn('json'); 18 | } 19 | 20 | function it_gets_the_media_type_from_the_accept_header_if_vendor_is_not_specified(Request $request, \LaraPackage\Api\Contracts\Config\ApiVersion $version) 21 | { 22 | $this->requestHeaderExpectation($request, 'application/json'); 23 | $version->latest()->shouldBeCalledTimes(2)->willReturn(4); 24 | $version->isValid(4)->shouldBeCalledTimes(2)->willReturn(true); 25 | 26 | $version->mediaTypeIsValid(false, 4)->shouldBeCalledTimes(1)->willReturn(false); 27 | $version->mediaTypeIsValid('json', 4)->shouldBeCalledTimes(1)->willReturn(true); 28 | 29 | $this->acceptedMediaType()->shouldReturn('json'); 30 | } 31 | 32 | function it_gets_the_version_from_the_accept_header(Request $request, \LaraPackage\Api\Contracts\Config\ApiVersion $version) 33 | { 34 | $this->requestHeaderExpectation($request, 'application/vnd.wps_api.v4+json'); 35 | $version->isValid(4)->shouldBeCalledTimes(1)->willReturn(true); 36 | 37 | $this->version()->shouldReturn(4); 38 | } 39 | 40 | function it_is_initializable() 41 | { 42 | $this->shouldHaveType('LaraPackage\Api\Request\AcceptHeader'); 43 | } 44 | 45 | function it_returns_the_default_if_content_type_is_not_specified(Request $request, \LaraPackage\Api\Contracts\Config\ApiVersion $version) 46 | { 47 | $this->requestHeaderExpectation($request, ''); 48 | $version->latest()->shouldBeCalledTimes(3)->willReturn(4); 49 | $version->isValid(4)->shouldBeCalledTimes(3)->willReturn(true); 50 | $version->mediaTypeIsValid(false, 4)->shouldBeCalledTimes(2)->willReturn(false); 51 | $version->defaultMediaType(4)->shouldBeCalledTimes(1)->willReturn('json'); 52 | 53 | $this->acceptedMediaType()->shouldReturn('json'); 54 | } 55 | 56 | function it_returns_the_latest_version_if_no_version_is_requested(Request $request, \LaraPackage\Api\Contracts\Config\ApiVersion $version) 57 | { 58 | $this->requestHeaderExpectation($request, 'application/json'); 59 | $version->latest()->shouldBeCalledTimes(1)->willReturn(4); 60 | $version->isValid(4)->shouldBeCalledTimes(1)->willReturn(true); 61 | 62 | $this->version()->shouldReturn(4); 63 | } 64 | 65 | function it_returns_the_latest_version_if_the_requested_version_is_invalid(Request $request, \LaraPackage\Api\Contracts\Config\ApiVersion $version) 66 | { 67 | $this->requestHeaderExpectation($request, 'application/vnd.wps_api.v500012+json'); 68 | $version->isValid(500012)->shouldBeCalledTimes(1)->willReturn(false); 69 | $version->latest()->shouldBeCalledTimes(1)->willReturn(4); 70 | 71 | $this->version()->shouldReturn(4); 72 | } 73 | 74 | function let(Request $request, \LaraPackage\Api\Contracts\Config\ApiVersion $version) 75 | { 76 | $this->beConstructedWith($request, $version); 77 | } 78 | 79 | /** 80 | * @param Request $request 81 | * @param $willReturn 82 | */ 83 | protected function requestHeaderExpectation(Request $request, $willReturn) 84 | { 85 | $request->server('HTTP_ACCEPT', '')->shouldBeCalled()->willReturn($willReturn); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /spec/Request/ParserSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($request, $acceptHeaderParser); 17 | } 18 | 19 | function it_is_initializable() 20 | { 21 | $this->shouldHaveType('LaraPackage\Api\Request\Parser'); 22 | } 23 | 24 | function it_gets_the_version_from_the_accept_header(\LaraPackage\Api\Contracts\Request\AcceptHeader $acceptHeaderParser) 25 | { 26 | $acceptHeaderParser->version()->shouldBeCalledTimes(1)->willReturn(4); 27 | $this->version()->shouldReturn(4); 28 | } 29 | 30 | function it_gets_the_media_type_from_the_accept_header(\LaraPackage\Api\Contracts\Request\AcceptHeader $acceptHeaderParser) 31 | { 32 | $acceptHeaderParser->acceptedMediaType()->shouldBeCalledTimes(1)->willReturn('json'); 33 | $this->acceptedMediaType()->shouldReturn('json'); 34 | } 35 | 36 | function it_gets_the_includes_from_the_request(Request $request, ParameterBag $parameterBag) 37 | { 38 | $includes = 'product,image'; 39 | 40 | $request->query = $parameterBag; 41 | $parameterBag->get('include')->shouldBeCalledTimes(1)->willReturn($includes); 42 | $this->includes()->shouldReturn($includes); 43 | } 44 | 45 | function it_checks_if_an_item_is_in_the_includes(Request $request, ParameterBag $parameterBag) 46 | { 47 | $includes = 'product,image'; 48 | $entity = 'image'; 49 | 50 | $request->query = $parameterBag; 51 | $parameterBag->get('include')->shouldBeCalledTimes(1)->willReturn($includes); 52 | $this->inIncludes($entity)->shouldReturn(true); 53 | } 54 | 55 | function it_gets_an_item_from_the_query(Request $request) 56 | { 57 | $item = 'foo'; 58 | $return = 'bar'; 59 | 60 | $request->query($item)->shouldBeCalled()->willReturn($return); 61 | 62 | $this->query($item)->shouldReturn($return); 63 | } 64 | 65 | function it_returns_the_query_as_an_array(Request $request) 66 | { 67 | $item = null; 68 | $return = ['foo' => 'bar', 'baz' => 'boo']; 69 | $request->query($item)->shouldBeCalled()->willReturn($return); 70 | 71 | $this->query()->shouldReturn($return); 72 | } 73 | 74 | function it_gets_an_item_from_the_header(Request $request, HeaderBag $headerBag) 75 | { 76 | $item = 'foo'; 77 | $return = 'bar'; 78 | 79 | $request->headers = $headerBag; 80 | $headerBag->get($item)->shouldBeCalled(1)->willReturn($return); 81 | 82 | $this->header($item)->shouldReturn($return); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /spec/Request/PayloadSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($request); 14 | } 15 | 16 | function it_is_initializable() 17 | { 18 | $this->shouldHaveType('LaraPackage\Api\Request\Payload'); 19 | } 20 | 21 | function it_retrieves_the_payload(Request $request) 22 | { 23 | 24 | $array = ['test']; 25 | $request->getContent()->shouldBeCalled()->willReturn($this->json($array)); 26 | $this->getIterator()->shouldReturnAnInstanceOf(\ArrayIterator::class); 27 | } 28 | 29 | protected function json(array $array) 30 | { 31 | return json_encode($array, true); 32 | } 33 | 34 | function it_throws_if_the_payload_cannot_be_parsed(Request $request) 35 | { 36 | $request->getContent()->shouldBeCalled()->willReturn($this->gibberish()); 37 | $this->shouldThrow(\LaraPackage\Api\Exceptions\RequestException::class)->during('getIterator'); 38 | } 39 | 40 | protected function gibberish() 41 | { 42 | return 'alawejjafsadoif asfdlkjsadf asdlfjk asdf;lkjsadflkj sadfdfsdsf'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spec/Resource/LaravelCollectionSpec.php: -------------------------------------------------------------------------------- 1 | getIterator()->shouldBeCalledTimes(1)->willReturn($expected); 17 | $this->getData()->shouldReturn($expected); 18 | } 19 | 20 | function it_gets_the_count(Paginator $paginator) 21 | { 22 | $expected = 5; 23 | $this->countExpectations($paginator, $expected); 24 | $this->getCount()->shouldReturn($expected); 25 | } 26 | 27 | function it_gets_the_current_cursor_position(\LaraPackage\Api\Contracts\Config\Api $config, \LaraPackage\Api\Contracts\Request\Parser $requestParser) 28 | { 29 | $expectedReturn = 43; 30 | $this->currentExpectations($config, $requestParser, $expectedReturn); 31 | $this->getCurrent()->shouldReturn($expectedReturn); 32 | } 33 | 34 | function it_gets_the_next_position(Paginator $paginator, Collection $collection) 35 | { 36 | $next = 4; 37 | $model = new \stdClass(); 38 | $model->id = $next; 39 | $paginator->getCollection()->shouldBeCalledTimes(1)->willReturn($collection); 40 | $collection->last()->shouldBeCalledTimes(1)->willReturn($model); 41 | $this->getNext()->shouldReturn($next); 42 | } 43 | 44 | function it_gets_the_page_size(Paginator $paginator) 45 | { 46 | $perPage = 10; 47 | $this->pageSizeExpectations($paginator, $perPage); 48 | $this->getPageSize()->shouldReturn($perPage); 49 | } 50 | 51 | function it_gets_the_previous_position(Paginator $paginator, \LaraPackage\Api\Contracts\Config\Api $config, \LaraPackage\Api\Contracts\Request\Parser $requestParser) 52 | { 53 | $current = 10; 54 | $count = 5; 55 | 56 | $this->currentExpectations($config, $requestParser, $current); 57 | $this->countExpectations($paginator, $count); 58 | 59 | $this->getPrevious()->shouldReturn(($current - $count)); 60 | } 61 | 62 | function it_is_initializable() 63 | { 64 | $this->shouldHaveType('LaraPackage\Api\Resource\LaravelCollection'); 65 | } 66 | 67 | function it_return_0_for_the_previous_if_current_minus_count_is_equal_or_less(Paginator $paginator, \LaraPackage\Api\Contracts\Config\Api $config, \LaraPackage\Api\Contracts\Request\Parser $requestParser) 68 | { 69 | $current = 3; 70 | $count = 4; 71 | 72 | $this->currentExpectations($config, $requestParser, $current); 73 | $this->countExpectations($paginator, $count); 74 | 75 | $this->getPrevious()->shouldReturn(0); 76 | } 77 | 78 | function it_returns_0_for_current_if_it_is_empty_in_the_query(\LaraPackage\Api\Contracts\Config\Api $config, \LaraPackage\Api\Contracts\Request\Parser $requestParser) 79 | { 80 | $requestParserReturn = null; 81 | $expectedReturn = 0; 82 | $this->currentExpectations($config, $requestParser, $requestParserReturn); 83 | $this->getCurrent()->shouldReturn($expectedReturn); 84 | } 85 | 86 | function let(Paginator $paginator, \LaraPackage\Api\Contracts\Config\Api $config, \LaraPackage\Api\Contracts\Request\Parser $requestParser) 87 | { 88 | $this->beConstructedWith($paginator, $config, $requestParser); 89 | } 90 | 91 | /** 92 | * @param Paginator $paginator 93 | * @param $paginatorCountReturn 94 | */ 95 | protected function countExpectations(Paginator $paginator, $paginatorCountReturn) 96 | { 97 | $paginator->count()->shouldBeCalled()->willReturn($paginatorCountReturn); 98 | } 99 | 100 | /** 101 | * @param \LaraPackage\Api\Contracts\Config\Api $config 102 | * @param \LaraPackage\Api\Contracts\Request\Parser $requestParser 103 | * @param $requestParserReturn 104 | */ 105 | protected function currentExpectations(\LaraPackage\Api\Contracts\Config\Api $config, \LaraPackage\Api\Contracts\Request\Parser $requestParser, $requestParserReturn) 106 | { 107 | $version = 4; 108 | $currentString = 'current'; 109 | 110 | $requestParser->version()->shouldBeCalled()->willReturn($version); 111 | $config->getIndexForVersion('collection.current_position', $version)->shouldBeCalled()->willReturn($currentString); 112 | $requestParser->query($currentString)->shouldBeCalled()->willReturn($requestParserReturn); 113 | } 114 | 115 | /** 116 | * @param Paginator $paginator 117 | * @param $perPage 118 | */ 119 | protected function pageSizeExpectations(Paginator $paginator, $perPage) 120 | { 121 | $paginator->perPage()->shouldBeCalled()->willReturn($perPage); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /spec/Resource/LaravelEntitySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($entityModelStub); 14 | $this->getData()->shouldReturnAnInstanceOf(\LaraPackage\Api\Contracts\Entity\Entity::class); 15 | } 16 | 17 | function it_blows_up_if_it_is_passed_a_model_that_is_not_also_an_entity(Model $model) 18 | { 19 | $this->shouldThrow(\LaraPackage\Api\Exceptions\InvalidArgumentException::class)->during('__construct', [$model]); 20 | } 21 | 22 | function it_is_initializable(EntityModelStub $entityModelStub) 23 | { 24 | $this->beConstructedWith($entityModelStub); 25 | $this->shouldHaveType('LaraPackage\Api\Resource\LaravelEntity'); 26 | } 27 | } 28 | 29 | class EntityModelStub extends Model implements \LaraPackage\Api\Contracts\Entity\Entity 30 | { 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function createValidationRules() 36 | { 37 | // TODO: Implement createValidationRules() method. 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function deleteValidationRules() 44 | { 45 | // TODO: Implement deleteValidationRules() method. 46 | } 47 | 48 | /** 49 | * @param array $array 50 | * 51 | * @return array 52 | */ 53 | public function forwardTransformAttributeKeyNames(array $array) 54 | { 55 | // TODO: Implement forwardTransformAttributeKeyNames() method. 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function frontEndAttributeNames() 62 | { 63 | // TODO: Implement frontEndAttributeNames() method. 64 | } 65 | 66 | /** 67 | * @param $id 68 | * 69 | * @return array 70 | */ 71 | public function replaceValidationRules($id) 72 | { 73 | // TODO: Implement replaceValidationRules() method. 74 | } 75 | 76 | /** 77 | * @param $id 78 | * 79 | * @return array 80 | */ 81 | public function updateValidationRules($id) 82 | { 83 | // TODO: Implement updateValidationRules() method. 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | public function validationMessages() 90 | { 91 | // TODO: Implement validationMessages() method. 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /spec/Transformer/AbstractTransformerSpec.php: -------------------------------------------------------------------------------- 1 | 5, 15 | 'title' => 'foobar', 16 | ]; 17 | 18 | $interalArray = [ 19 | 'id' => 5, 20 | 'name' => 'foobar', 21 | ]; 22 | 23 | $this->reverseTransform($externalArray)->shouldReturn($interalArray); 24 | } 25 | 26 | function it_changes_an_arrays_keys_from_internal_to_external() 27 | { 28 | $interalArray = [ 29 | 'id' => 5, 30 | 'name' => 'foobar', 31 | ]; 32 | 33 | $externalArray = [ 34 | 'identifier' => 5, 35 | 'title' => 'foobar', 36 | ]; 37 | 38 | $this->forwardTransform($interalArray)->shouldReturn($externalArray); 39 | } 40 | 41 | function it_is_initializable() 42 | { 43 | $this->shouldHaveType('LaraPackage\Api\Transformer\AbstractTransformer'); 44 | } 45 | 46 | function it_transforms_an_associative_array_in_place_and_not_as_a_collection() 47 | { 48 | $interalArray = [ 49 | 'id' => [5], 50 | 'name' => ['foobar'], 51 | ]; 52 | 53 | $externalArray = [ 54 | 'identifier' => [5], 55 | 'title' => ['foobar'], 56 | ]; 57 | 58 | $this->forwardTransform($interalArray)->shouldReturn($externalArray); 59 | $this->reverseTransform($externalArray)->shouldReturn($interalArray); 60 | } 61 | 62 | function it_transforms_using_an_interator() 63 | { 64 | $interalArray = [ 65 | 'id' => [5], 66 | 'name' => ['foobar'], 67 | ]; 68 | 69 | $externalArray = [ 70 | 'identifier' => [5], 71 | 'title' => ['foobar'], 72 | ]; 73 | 74 | $interalIterator = new \ArrayIterator($interalArray); 75 | $externalIterator = new \ArrayIterator($externalArray); 76 | 77 | $this->forwardTransform($interalIterator)->shouldReturn($externalArray); 78 | $this->reverseTransform($externalIterator)->shouldReturn($interalArray); 79 | } 80 | 81 | function let() 82 | { 83 | $this->beAnInstanceOf('spec\LaraPackage\Api\Transformer\AbstractTransformerStub'); 84 | } 85 | } 86 | 87 | class AbstractTransformerStub extends AbstractTransformer 88 | { 89 | /** 90 | * [ privateKey => publicKey ] 91 | * 92 | * @return array 93 | */ 94 | public function mappings() 95 | { 96 | return [ 97 | 'id' => 'identifier', 98 | 'name' => 'title', 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ApiFacade.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 32 | $this->relational = $relational; 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function badRequest(\Exception $e, $message = '') 39 | { 40 | $message = $message ?: 'The request could not be understood by the server due to malformed syntax.'; 41 | 42 | return $this->error(400, $message, $e); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function collection(\LaraPackage\Api\Contracts\Resource\Collection $cursor, \LaraPackage\Api\Contracts\Entity\Transformer\Transformer $transformer) 49 | { 50 | $payloadCreator = $this->factory->makePayloadCreator(); 51 | $payloadCreator->cursor($cursor, $transformer); 52 | 53 | return $this->factory->makeRepresentationCreator()->make($payloadCreator); 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | public function conflict(\Exception $e, $message = '') 60 | { 61 | $message = $message ?: 'The request could not be completed due to a conflict with the current state of the resource.'; 62 | 63 | return $this->error(409, $message, $e); 64 | } 65 | 66 | /** 67 | * @inheritdoc 68 | */ 69 | public function delete($idsString, Repository $repository) 70 | { 71 | $repository->delete($this->getArrayOfIdsFromString($idsString)); 72 | 73 | return $this->factory->makeRepresentationCreator()->make([], 204); 74 | } 75 | 76 | /** 77 | * @inheritdoc 78 | */ 79 | public function deleteRelation(Repository $repository, Event $event = null) 80 | { 81 | 82 | $repository->deleteRelation( 83 | $this->relational->table(), 84 | $this->relational->relationIds(), 85 | $this->relational->itemColumn(), 86 | $this->relational->itemIds(), 87 | $event 88 | ); 89 | 90 | return $this->factory->makeRepresentationCreator()->make([], 204); 91 | } 92 | 93 | /** 94 | * @inheritdoc 95 | */ 96 | public function deleteRelationalCollection(Repository $repository, Event $event = null) 97 | { 98 | $repository->deleteRelation( 99 | $this->relational->table(), 100 | $this->relational->relationIds(), 101 | null, 102 | [], 103 | $event 104 | ); 105 | 106 | return $this->factory->makeRepresentationCreator()->make([], 204); 107 | } 108 | 109 | /** 110 | * @inheritdoc 111 | */ 112 | public function entity(\LaraPackage\Api\Contracts\Resource\Entity $entity, \LaraPackage\Api\Contracts\Entity\Transformer\Transformer $transformer, $responseCode = 200) 113 | { 114 | return $this->buildEntityResponse($transformer, $entity, $responseCode); 115 | } 116 | 117 | /** 118 | * @inheritdoc 119 | */ 120 | public function iAmATeapot(\Exception $e, $message = '') 121 | { 122 | $message = $message ?: 'If you think this is not an http status code--you are wrong: (RFC 2324)'; 123 | 124 | return $this->error(418, $message, $e); 125 | } 126 | 127 | /** 128 | * @inheritdoc 129 | */ 130 | public function internalError(\Exception $e, $message = '') 131 | { 132 | $message = $message ?: 'The server encountered an unexpected condition which prevented it from fulfilling the request.'; 133 | 134 | return $this->error(500, $message, $e); 135 | } 136 | 137 | /** 138 | * @inheritdoc 139 | */ 140 | public function methodNotAllowed(MethodNotAllowedHttpException $e, $message = '') 141 | { 142 | $message = $message ?: 'The method specified in the Request-Line is not allowed for the resource identified by the Request-URI.'; 143 | 144 | return $this->error(405, $message, $e); 145 | } 146 | 147 | /** 148 | * @inheritdoc 149 | */ 150 | public function notFound(\Exception $e, $message = '') 151 | { 152 | $message = $message ?: 'The server did not find anything matching the requested URI.'; 153 | 154 | return $this->error(404, $message, $e); 155 | } 156 | 157 | /** 158 | * @inheritdoc 159 | */ 160 | public function patch($id, Repository $repository, ReverseTransformer $transformer) 161 | { 162 | $payload = $this->factory->getRequestPayload()->getIterator(); 163 | $payload = $transformer->reverseTransform($payload); 164 | $entity = $repository->patch($id, $payload); 165 | 166 | return $this->buildEntityResponse($transformer, $entity, 200); 167 | } 168 | 169 | /** 170 | * @inheritdoc 171 | */ 172 | public function post(Repository $repository, Transformer $transformer) 173 | { 174 | $payload = $this->factory->getRequestPayload()->getIterator(); 175 | $payload = $transformer->reverseTransform($payload); 176 | $entity = $repository->post($payload); 177 | 178 | return $this->buildEntityResponse($transformer, $entity, 201); 179 | } 180 | 181 | /** 182 | * @inheritdoc 183 | */ 184 | public function postRelation(Repository $repository, ReverseTransformer $transformer) 185 | { 186 | $payload = $this->factory->getRequestPayload()->getIterator(); 187 | $body = $transformer->reverseTransform($payload); 188 | 189 | $insert = $this->relational->addRelationalIdsToEachItemInCollection($body); 190 | $insert = $this->relational->addTimestampsToEachItemInCollection($insert); 191 | 192 | $repository->postRelations($this->relational->table(), $insert); 193 | } 194 | 195 | /** 196 | * @inheritdoc 197 | */ 198 | public function put($id, Repository $repository, ReverseTransformer $transformer) 199 | { 200 | $payload = $this->factory->getRequestPayload()->getIterator(); 201 | $payload = $transformer->reverseTransform($payload); 202 | $entity = $repository->put($id, $payload); 203 | 204 | return $this->buildEntityResponse($transformer, $entity, 200); 205 | } 206 | 207 | /** 208 | * @inheritdoc 209 | */ 210 | public function putRelationalCollection(Repository $repository, ReverseTransformer $transformer) 211 | { 212 | $payload = $this->factory->getRequestPayload()->getIterator(); 213 | $body = $transformer->reverseTransform($payload); 214 | $collection = $this->relational->addRelationalIdsToEachItemInCollection($body); 215 | $collection = $this->relational->addTimestampsToEachItemInCollection($collection); 216 | $repository->putRelations($this->relational->table(), $this->relational->relationIds(), $collection); 217 | } 218 | 219 | /** 220 | * @inheritdoc 221 | */ 222 | public function unauthorized(\Exception $e, $message = '') 223 | { 224 | $message = $message ?: 'The request requires user authentication.'; 225 | 226 | return $this->error(401, $message, $e); 227 | } 228 | 229 | /** 230 | * @inheritdoc 231 | */ 232 | public function validationError(Exceptions\ValidationException $e) 233 | { 234 | return $this->error(400, json_encode($e->failed()), $e); 235 | } 236 | 237 | /** 238 | * @inheritdoc 239 | */ 240 | protected function buildEntityResponse(ReverseTransformer $transformer, $entity, $statusCode) 241 | { 242 | $representation = $this->factory->makePayloadCreator(); 243 | $representation->entity($entity, $transformer); 244 | 245 | return $this->factory->makeRepresentationCreator()->make($representation, $statusCode); 246 | } 247 | 248 | /** 249 | * @inheritdoc 250 | */ 251 | protected function getArrayOfIdsFromString($idsString) 252 | { 253 | $ids = explode(',', $idsString); 254 | 255 | return $ids; 256 | } 257 | 258 | /** 259 | * @inheritdoc 260 | */ 261 | protected function isCollection(array $array) 262 | { 263 | $filtered = array_filter($array, function ($value) { 264 | if (!is_array($value)) { 265 | return $value; 266 | } 267 | }); 268 | 269 | if (!empty($filtered)) { 270 | // some of the elements are not arrays 271 | return false; 272 | } 273 | 274 | // all of the elements are arrays 275 | return true; 276 | } 277 | 278 | /** 279 | * @inheritdoc 280 | */ 281 | private function debug(\Exception $e) 282 | { 283 | return $debug = [ 284 | 'message' => $e->getMessage(), 285 | 'file' => $e->getFile(), 286 | 'line' => $e->getLine(), 287 | 'stacktrace' => $e->getTraceAsString(), 288 | ]; 289 | } 290 | 291 | /** 292 | * @inheritdoc 293 | */ 294 | private function error($statusCode, $message, \Exception $e, $headers = []) 295 | { 296 | 297 | $responseArray = [ 298 | 'error' => [ 299 | 'http_status' => $statusCode, 300 | 'message' => $message, 301 | ], 302 | ]; 303 | 304 | // Other than production, debug information will be included with 305 | // the returned query if an exception is thrown and caught by the handler 306 | $appEnv = getenv('APP_ENV'); 307 | if ($appEnv AND $appEnv !== 'production') { 308 | $responseArray['debug'] = $this->debug($e); 309 | } 310 | 311 | return $this->factory->makeRepresentationCreator()->make($responseArray, $statusCode, $headers); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/CollectionHelper.php: -------------------------------------------------------------------------------- 1 | requestParser = $requestParser; 25 | $this->version = $version; 26 | } 27 | 28 | /** 29 | * @inheritdoc 30 | */ 31 | public static function addToArrayIfSet($outKey, $inKey, $inArray, &$outArray) 32 | { 33 | if (isset($inArray[$inKey])) { 34 | $outArray[$outKey] = $inArray[$inKey]; 35 | } 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public static function isAssociative(array $array) 42 | { 43 | return (bool)count(array_filter(array_keys($array), 'is_string')); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public static function isCollection(array $array) 50 | { 51 | if (self::isAssociative($array)) { 52 | return false; 53 | } 54 | 55 | $arrayElementCount = count(array_filter($array, 'is_array')); 56 | if (count($array) === $arrayElementCount && !empty($array)) { 57 | return true; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function currentPosition() 67 | { 68 | return (int)$this->requestParser->query($this->version->collectionCurrentPositionString($this->requestParser->version())) ?: 0; 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function pageSize() 75 | { 76 | return $this->version->collectionPageSize($this->requestParser->version()); 77 | } 78 | 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function query(Transformer $transformer) 84 | { 85 | $queryArray = $this->requestParser->query(); 86 | 87 | return $transformer->reverseTransform($queryArray); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Config/Api.php: -------------------------------------------------------------------------------- 1 | app = $app; 21 | } 22 | 23 | /** 24 | * Retrieves an index from the API config file 25 | * 26 | * @param string $index 27 | * 28 | * @return mixed 29 | */ 30 | public function getIndex($index) 31 | { 32 | return $this->config()->get(self::API.$index); 33 | } 34 | 35 | /** 36 | * Retrieves an index for a version from the API config file 37 | * 38 | * @param string $index 39 | * @param int $version 40 | * 41 | * @return mixed 42 | */ 43 | public function getIndexForVersion($index, $version) 44 | { 45 | return $this->config()->get(self::API.'versions.'.$version.'.'.$index); 46 | } 47 | 48 | /** 49 | * @return \Illuminate\Config\Repository 50 | */ 51 | protected function config() 52 | { 53 | return $this->app->offsetGet('config'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Config/ApiVersion.php: -------------------------------------------------------------------------------- 1 | config = $config; 17 | } 18 | 19 | /** 20 | * @return array 21 | */ 22 | public function availableVersions() 23 | { 24 | return \array_keys($this->config->getIndex('versions')); 25 | } 26 | 27 | /** 28 | * Returns the query parameter that is to be used to denote the current position in a collection 29 | * 30 | * @param int $version 31 | * 32 | * @return string 33 | */ 34 | public function collectionCurrentPositionString($version) 35 | { 36 | return $this->config->getIndexForVersion('collection.current_position', $version); 37 | } 38 | 39 | /** 40 | * The collection page size for the version 41 | * 42 | * @param int $version 43 | * 44 | * @return int 45 | */ 46 | public function collectionPageSize($version) 47 | { 48 | return (int)$this->config->getIndexForVersion('collection.page_size', $version); 49 | } 50 | 51 | /** 52 | * @param int $version 53 | * 54 | * @return string 55 | */ 56 | public function defaultMediaType($version) 57 | { 58 | return $this->config->getIndexForVersion('media.default', $version); 59 | } 60 | 61 | /** 62 | * Checks to see if the specified version is valid 63 | * 64 | * @param int $version 65 | * 66 | * @return bool 67 | */ 68 | public function isValid($version) 69 | { 70 | return \in_array($version, $this->availableVersions()); 71 | } 72 | 73 | /** 74 | * Gets the latest version 75 | * 76 | * @return int 77 | */ 78 | public function latest() 79 | { 80 | return (int)\max(\array_keys($this->config->getIndex('versions'))); 81 | } 82 | 83 | /** 84 | * Checks if a media type is valid for a version 85 | * 86 | * @param string $mediaType 87 | * @param int $version 88 | * 89 | * @return bool 90 | */ 91 | public function mediaTypeIsValid($mediaType, $version) 92 | { 93 | return \in_array($mediaType, $this->mediaTypes($version)); 94 | } 95 | 96 | /** 97 | * Returns an array of media types for a version 98 | * 99 | * @param int $version 100 | * 101 | * @return array 102 | */ 103 | public function mediaTypes($version) 104 | { 105 | return $this->config->getIndexForVersion('media.types', $version); 106 | } 107 | 108 | /** 109 | * Returns an array of ids for a resource 110 | * or false if the resource is not found 111 | * 112 | * This is used for testing to retrieve random ids manually if the automatic way 113 | * doesn't work. 114 | * 115 | * @param string $resource 116 | * @param int $version 117 | * 118 | * @return array|false 119 | */ 120 | public function resourceIdMap($resource, $version) 121 | { 122 | $idMap = $this->config->getIndexForVersion('resourceIdsMap', $version); 123 | if (array_key_exists($resource, $idMap)) { 124 | return $idMap[$resource](); 125 | } 126 | 127 | return false; 128 | } 129 | 130 | /** 131 | * Returns the vendor string for the version 132 | * 133 | * @param int $version 134 | * 135 | * @return string 136 | */ 137 | public function vendor($version) 138 | { 139 | return $this->config->getIndexForVersion('vendor', $version); 140 | } 141 | 142 | /** 143 | * Returns the version designator for all versions 144 | * 145 | * @param int $version 146 | * 147 | * @return string 148 | */ 149 | public function versionDesignator($version) 150 | { 151 | return $this->config->getIndex('version_designator').$version; 152 | } 153 | 154 | /** 155 | * @param $version 156 | * 157 | * @return \LaraPackage\Api\Contracts\Factory\VersionFactory 158 | */ 159 | public function factory($version) 160 | { 161 | return $this->config->getIndexForVersion('factory', $version); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Contracts/ApiFacade.php: -------------------------------------------------------------------------------- 1 | 1 12 | * ] 13 | * 14 | * transforms to 15 | * 16 | * [ 17 | * 'public_api' => 1 18 | * ] 19 | * 20 | * This uses mappings() 21 | * 22 | * @param array|\Traversable $iterable 23 | * 24 | * @return array 25 | */ 26 | public function forwardTransform($iterable); 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Entity/Transformer/ReverseTransformer.php: -------------------------------------------------------------------------------- 1 | public key mappings 8 | * 9 | * [ privateKey => publicKey ] 10 | * 11 | * @return array 12 | */ 13 | public function mappings(); 14 | 15 | /** 16 | * Transforms an array from the public api to the private api 17 | * 18 | * e.g. 19 | * [ 20 | * 'public_api' => 1 21 | * ] 22 | * 23 | * transforms to 24 | * 25 | * [ 26 | * 'private_api' => 1 27 | * ] 28 | * 29 | * This uses mappings() 30 | * 31 | * @param array|\Traversable $iterable 32 | * 33 | * @return array 34 | */ 35 | public function reverseTransform($iterable); 36 | } 37 | -------------------------------------------------------------------------------- /src/Contracts/Entity/Transformer/Transformer.php: -------------------------------------------------------------------------------- 1 | 5, 'product_id' => 7, 'site_id' => 10] 83 | * 84 | * @return bool 85 | */ 86 | public function postRelations($table, array $collection); 87 | 88 | /** 89 | * Replace a record 90 | * 91 | * @param int $id 92 | * @param array $data 93 | * 94 | * @return \LaraPackage\Api\Contracts\Resource\Entity 95 | */ 96 | public function put($id, array $data); 97 | 98 | /** 99 | * Deletes then replaces the collection 100 | * 101 | * @param string $table 102 | * @param array $collection 103 | * 104 | * [ 105 | * ['image_id' => 5, 'product_id' => 7, 'site_id' => 10], 106 | * ['image_id' => 7, 'product_id' => 7, 'site_id' => 10], 107 | * ] 108 | * 109 | * @return bool 110 | */ 111 | public function putRelations($table, array $relationIds, array $collection); 112 | } 113 | -------------------------------------------------------------------------------- /src/Contracts/Repository/Helper/Relational.php: -------------------------------------------------------------------------------- 1 | 1], 13 | * ['image_id' => 2], 14 | * ] 15 | * 16 | * @return array 17 | * 18 | * [ 19 | * ['image_id' => 1, 'product_id' => 78], 20 | * ['image_id' => 2, 'product_id' => 78], 21 | * ] 22 | */ 23 | public function addRelationalIdsToEachItemInCollection($collection); 24 | 25 | /** 26 | * @param array $item ['image_id' => 6] 27 | * 28 | * @return array ['image_id' => 6, 'site_id' => 7, 'product_id' => 5] 29 | */ 30 | public function addRelationalIdsToItem(array $item); 31 | 32 | /** 33 | * @param array $collection 34 | * 35 | * @return array 36 | */ 37 | public function addTimestampsToEachItemInCollection(array $collection); 38 | 39 | /** 40 | * @param array $item 41 | * 42 | * @return array 43 | */ 44 | public function addTimestampsToItem(array $item); 45 | 46 | /** 47 | * @return array 48 | */ 49 | public function columns(); 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function ids(); 55 | 56 | /** 57 | * @return string e.g. 'site_id' 58 | */ 59 | public function itemColumn(); 60 | 61 | /** 62 | * @return array [2,6,5] 63 | */ 64 | public function itemIds(); 65 | 66 | /** 67 | * @return string e.g. 'site' 68 | */ 69 | public function itemName(); 70 | 71 | /** 72 | * @return array ['site_id' => 1, 'product_id' => 2] 73 | * @throws StupidProgrammerMistakeException 74 | */ 75 | public function relationIds(); 76 | 77 | /** 78 | * @return string 79 | */ 80 | public function table(); 81 | } 82 | -------------------------------------------------------------------------------- /src/Contracts/Repository/Repository.php: -------------------------------------------------------------------------------- 1 | 1 12 | * ] 13 | * 14 | * transforms to 15 | * 16 | * [ 17 | * 'public_api' => 1 18 | * ] 19 | * 20 | * @param array $array 21 | * 22 | * @return array 23 | */ 24 | public function forwardTransformAttributeKeyNames(array $array); 25 | 26 | /** 27 | * Returns an array of public keys 28 | * 29 | * @return array 30 | */ 31 | public function frontEndAttributeNames(); 32 | } 33 | -------------------------------------------------------------------------------- /src/Exceptions/ApiExceptionHandler.php: -------------------------------------------------------------------------------- 1 | apiFacade = $apiFacade; 28 | } 29 | 30 | 31 | public function handle(\Exception $e) 32 | { 33 | if ($this->exceptionIs(NotFoundHttpException::class, $e)) { 34 | return $this->apiFacade->notFound($e, $e->getMessage()); 35 | } 36 | 37 | if ($this->exceptionIs(MethodNotAllowedHttpException::class, $e)) { 38 | return $this->apiFacade->methodNotAllowed($e, $e->getMessage()); 39 | } 40 | 41 | if ($this->exceptionIs(BadRequestHttpException::class, $e)) { 42 | return $this->apiFacade->badRequest($e, $e->getMessage()); 43 | } 44 | 45 | if ($this->exceptionIs(UnauthorizedHttpException::class, $e)) { 46 | return $this->apiFacade->unauthorized($e, $e->getMessage()); 47 | } 48 | 49 | if ($this->exceptionIs(ConflictHttpException::class, $e)) { 50 | return $this->apiFacade->conflict($e, $e->getMessage()); 51 | } 52 | 53 | if ($this->exceptionIs(QueryException::class, $e)) { 54 | return $this->apiFacade->internalError($e, $e->getMessage()); 55 | } 56 | 57 | if ($this->exceptionIs(ErrorException::class, $e)) { 58 | return $this->apiFacade->internalError($e, $e->getMessage()); 59 | } 60 | 61 | if ($this->exceptionIs(ValidationException::class, $e)) { 62 | return $this->apiFacade->validationError($e, $e->getMessage()); 63 | } 64 | 65 | if ($this->exceptionIs(BindingResolutionException::class, $e)) { 66 | return $this->apiFacade->internalError($e, $e->getMessage()); 67 | } 68 | 69 | if ($this->exceptionIs(\RuntimeException::class, $e)) { 70 | return $this->apiFacade->internalError($e, $e->getMessage()); 71 | } 72 | 73 | if ($this->exceptionIs(TeapotException::class, $e)) { 74 | return $this->apiFacade->iAmATeapot($e, $e->getMessage()); 75 | } 76 | 77 | return $this->apiFacade->internalError($e, 'Unknown error'); 78 | } 79 | 80 | /** 81 | * @param $exceptionType 82 | * @param $e 83 | * 84 | * @return bool 85 | */ 86 | private function exceptionIs($exceptionType, $e) 87 | { 88 | return is_a($e, $exceptionType); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | failed = $failed; 23 | } 24 | 25 | /** 26 | * returns the failed messages 27 | * 28 | * @return array 29 | */ 30 | public function failed() 31 | { 32 | return $this->failed; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Factory/Factory.php: -------------------------------------------------------------------------------- 1 | app = $app; 36 | $this->requestParser = $requestParser; 37 | $this->apiVersion = $apiVersion; 38 | } 39 | 40 | /** 41 | * @return \LaraPackage\Api\Contracts\Request\Payload 42 | */ 43 | public function getRequestPayload() 44 | { 45 | return $this->createVersionFactory()->getRequestPayload(); 46 | } 47 | 48 | /** 49 | * @return \LaraPackage\Api\Contracts\PayloadCreator 50 | */ 51 | public function makePayloadCreator() 52 | { 53 | return $this->createVersionFactory()->makePayloadCreator(); 54 | } 55 | 56 | /** 57 | * @return \LaraPackage\Api\Contracts\RepresentationCreator 58 | */ 59 | public function makeRepresentationCreator() 60 | { 61 | return $this->createVersionFactory()->makeRepresentationCreator(); 62 | } 63 | 64 | /** 65 | * Create the VersionFactory for the Version 66 | * 67 | * @return \LaraPackage\Api\Contracts\Factory\VersionFactory 68 | * @internal param $version 69 | * 70 | */ 71 | protected function createVersionFactory() 72 | { 73 | $version = $this->requestParser->version(); 74 | $factory = $this->apiVersion->factory($version); 75 | $instance = $this->app->make($factory); 76 | $this->app->instance(\LaraPackage\Api\Contracts\Factory\VersionFactory::class, $instance); 77 | 78 | return $instance; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Factory/VersionFactory.php: -------------------------------------------------------------------------------- 1 | app = $app; 18 | } 19 | 20 | /** 21 | * @return \LaraPackage\Api\Contracts\Request\Payload 22 | */ 23 | public function getRequestPayload() 24 | { 25 | return $this->app->make(\LaraPackage\Api\Contracts\Request\Payload::class); 26 | } 27 | 28 | /** 29 | * @param string $mediaType 30 | * 31 | * @return \LaraPackage\Api\Contracts\MediaType\MediaType 32 | */ 33 | public function makeMediaType($mediaType) 34 | { 35 | return new Json(); 36 | } 37 | 38 | /** 39 | * @return \LaraPackage\Api\Contracts\PayloadCreator 40 | */ 41 | public function makePayloadCreator() 42 | { 43 | return $this->app->make(\LaraPackage\Api\Contracts\PayloadCreator::class); 44 | } 45 | 46 | /** 47 | * @return \LaraPackage\Api\Contracts\RepresentationCreator 48 | */ 49 | public function makeRepresentationCreator() 50 | { 51 | return $this->app->make(\LaraPackage\Api\Contracts\RepresentationCreator::class); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/FractalFactory.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 44 | $this->transformer = $transformer; 45 | $this->api = $api; 46 | $this->collectionHelper = $collectionHelper; 47 | } 48 | 49 | /** 50 | * @return \Illuminate\Http\Response 51 | */ 52 | public function create() 53 | { 54 | return $this->api->post($this->repository, $this->transformer); 55 | } 56 | 57 | /** 58 | * @param int $id 59 | * 60 | * @return \Illuminate\Http\Response 61 | */ 62 | public function destroy($id) 63 | { 64 | return $this->api->delete($id, $this->repository); 65 | } 66 | 67 | /** 68 | * @return \Illuminate\Http\Response 69 | */ 70 | public function index() 71 | { 72 | $cursor = $this->repository->collection($this->collectionHelper->currentPosition(), $this->collectionHelper->pageSize(), 73 | $this->collectionHelper->query($this->transformer)); 74 | 75 | return $this->api->collection($cursor, $this->transformer); 76 | } 77 | 78 | /** 79 | * @param int $id 80 | * 81 | * @return \Illuminate\Http\Response 82 | */ 83 | public function replace($id) 84 | { 85 | return $this->api->put($id, $this->repository, $this->transformer); 86 | } 87 | 88 | /** 89 | * @param int $id 90 | * 91 | * @return \Illuminate\Http\Response 92 | */ 93 | public function show($id) 94 | { 95 | $entity = $this->repository->entity($id); 96 | 97 | return $this->api->entity($entity, $this->transformer); 98 | } 99 | 100 | /** 101 | * @param int $id 102 | * 103 | * @return \Illuminate\Http\Response 104 | */ 105 | public function update($id) 106 | { 107 | return $this->api->patch($id, $this->repository, $this->transformer); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Implementations/PayloadCreator.php: -------------------------------------------------------------------------------- 1 | requestParser = $requestParser; 34 | $this->fractalFactory = $fractalFactory; 35 | $this->versionFactory = $versionFactory; 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function cursor(\LaraPackage\Api\Contracts\Resource\Collection $cursor, \LaraPackage\Api\Contracts\Entity\Transformer\Transformer $transformer) 42 | { 43 | $resource = $this->fractalFactory->createCollection($cursor->getData(), $transformer); 44 | 45 | $fractalCursor = $this->fractalFactory->createCursor($cursor->getCurrent(), $cursor->getPrevious(), $cursor->getNext(), $cursor->getCount()); 46 | 47 | $resource->setCursor($fractalCursor); 48 | 49 | $this->setPayload($resource); 50 | 51 | $this->removePrevCursor($payload); 52 | } 53 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | public function entity(\LaraPackage\Api\Contracts\Resource\Entity $entity, \LaraPackage\Api\Contracts\Entity\Transformer\Transformer $transformer) 58 | { 59 | $resource = $this->fractalFactory->createEntity($entity->getData(), $transformer); 60 | 61 | $this->setPayload($resource); 62 | } 63 | 64 | /** 65 | * @return \ArrayIterator 66 | */ 67 | public function getIterator() 68 | { 69 | if (!is_array($this->payload)) { 70 | throw new InvalidArgumentException('You must run a payload method before trying to use getIterator'); 71 | } 72 | 73 | return new \ArrayIterator($this->payload); 74 | } 75 | 76 | /** 77 | * @param &$payload 78 | */ 79 | protected function removePrevCursor(&$payload) 80 | { 81 | // this removes the previous count from the cursor 82 | if (isset($payload['meta']['cursor']['prev'])) { 83 | unset($payload['meta']['cursor']['prev']); 84 | } 85 | } 86 | 87 | /** 88 | * @param Fractal\Resource\ResourceInterface $resource 89 | * 90 | * @return void 91 | */ 92 | protected function setPayload(Fractal\Resource\ResourceInterface $resource) 93 | { 94 | $includes = $this->requestParser->includes(); 95 | $manager = $this->fractalFactory->createManager(); 96 | 97 | if ($includes) { 98 | $manager = $manager->parseIncludes($includes); 99 | } 100 | 101 | $payload = $manager->createData($resource)->toArray(); 102 | 103 | $this->payload = $payload; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Implementations/RepresentationCreator.php: -------------------------------------------------------------------------------- 1 | requestParser = $requestParser; 37 | $this->response = $response; 38 | $this->versionFactory = $versionFactory; 39 | $this->versionInfoRetriever = $versionInfoRetriever; 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function make($data, $statusCode = 200, array $headers = []) 46 | { 47 | $acceptedMediaType = $this->requestParser->acceptedMediaType(); 48 | $mediaType = $this->versionFactory->makeMediaType($acceptedMediaType); 49 | 50 | $representation = $mediaType->format($data); 51 | 52 | $headers = $this->setContentTypeHeader($headers, $acceptedMediaType); 53 | 54 | return $this->response->make($representation, $statusCode, $headers); 55 | } 56 | 57 | /** 58 | * @param array $headers 59 | * @param $mediaType 60 | * 61 | * @return array 62 | */ 63 | protected function setContentTypeHeader(array $headers, $mediaType) 64 | { 65 | $headers['Content-Type'] = 'application/'. 66 | $this->versionInfoRetriever->vendor($this->version). 67 | $this->versionInfoRetriever->versionDesignator($this->version).'+'.$mediaType; 68 | 69 | return $headers; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MediaType/Json.php: -------------------------------------------------------------------------------- 1 | transformer()->forwardTransform($array); 19 | } 20 | 21 | /** 22 | * @inheritdoc 23 | */ 24 | public function frontEndAttributeNames() 25 | { 26 | return $this->transformer()->mappings(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Providers/ApiServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([__DIR__.'/../../config/api.php' => config_path('api.php')]); 18 | } 19 | 20 | /** 21 | * Register the application services. 22 | * 23 | * @return void 24 | */ 25 | public function register() 26 | { 27 | $this->app->singleton(\LaraPackage\Api\Contracts\Request\Parser::class, \LaraPackage\Api\Request\Parser::class); 28 | $this->app->singleton(\LaraPackage\Api\Contracts\Factory\Factory::class, \LaraPackage\Api\Factory\Factory::class); 29 | $this->app->singleton(\LaraPackage\Api\Contracts\Config\Api::class, \LaraPackage\Api\Config\Api::class); 30 | $this->app->singleton(\LaraPackage\Api\Contracts\Request\AcceptHeader::class, \LaraPackage\Api\Request\AcceptHeader::class); 31 | $this->app->singleton(\LaraPackage\Api\Contracts\Config\ApiVersion::class, \LaraPackage\Api\Config\ApiVersion::class); 32 | $this->app->singleton(\LaraPackage\Api\Contracts\ApiFacade::class, \LaraPackage\Api\ApiFacade::class); 33 | $this->app->singleton(\PrometheusApi\Utilities\Contracts\Uri\Parser::class, \PrometheusApi\Utilities\Uri\Parser::class); 34 | $this->app->singleton(\LaraPackage\RandomId\Contracts\Retriever::class, \LaraPackage\RandomId\Retriever::class); 35 | $this->app->singleton(\LaraPackage\Api\Contracts\Repository\Helper\Relational::class, \LaraPackage\Api\Repository\Helper\Relational::class); 36 | 37 | $this->app->singleton(\LaraPackage\Api\Contracts\Exceptions\ApiExceptionHandler::class, \LaraPackage\Api\Exceptions\ApiExceptionHandler::class); 38 | 39 | $this->app->singleton(\LaraPackage\Api\Contracts\PayloadCreator::class, \LaraPackage\Api\Implementations\PayloadCreator::class); 40 | $this->app->singleton(\LaraPackage\Api\Contracts\RepresentationCreator::class, \LaraPackage\Api\Implementations\RepresentationCreator::class); 41 | 42 | $this->app->bind(\LaraPackage\Api\Contracts\Resource\Collection::class, function ($app, $params) { 43 | $paginator = $params[0]; 44 | 45 | return new \LaraPackage\Api\Resource\LaravelCollection( 46 | $paginator, 47 | $this->app->make(\LaraPackage\Api\Contracts\Config\Api::class), 48 | $this->app->make(\LaraPackage\Api\Contracts\Request\Parser::class) 49 | ); 50 | }); 51 | 52 | $this->app->bind(\LaraPackage\Api\Contracts\Resource\Entity::class, function ($app, $parameters) { 53 | $model = $parameters[0]; 54 | 55 | return new \LaraPackage\Api\Resource\LaravelEntity($model); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Repository/Factory.php: -------------------------------------------------------------------------------- 1 | app = $app; 18 | } 19 | 20 | /** 21 | * @param Paginator $paginator 22 | * 23 | * @return \LaraPackage\Api\Resource\LaravelCollection 24 | */ 25 | public function cursor(Paginator $paginator) 26 | { 27 | return new \LaraPackage\Api\Resource\LaravelCollection( 28 | $paginator, 29 | $this->app->make(\LaraPackage\Api\Contracts\Config\Api::class), 30 | $this->app->make(\LaraPackage\Api\Contracts\Request\Parser::class) 31 | ); 32 | } 33 | 34 | /** 35 | * @param Model $model 36 | * 37 | * @return \LaraPackage\Api\Resource\LaravelEntity 38 | */ 39 | public function entity(Model $model) 40 | { 41 | return new \LaraPackage\Api\Resource\LaravelEntity($model); 42 | } 43 | 44 | /** 45 | * @param $collection 46 | * @param $pageSize 47 | * @param $current 48 | * 49 | * @return Paginator 50 | */ 51 | public function paginator($collection, $pageSize, $current) 52 | { 53 | return new Paginator($collection, $pageSize, $current); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Repository/Helper/Relational.php: -------------------------------------------------------------------------------- 1 | request = $request; 39 | $this->parser = $parser; 40 | $this->tableHelper = $tableHelper; 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function addRelationalIdsToEachItemInCollection($collection) 47 | { 48 | $relationIds = $this->relationIds(); 49 | 50 | return $this->addToEachItemOfCollection($relationIds, $collection); 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function addRelationalIdsToItem(array $item) 57 | { 58 | $relationIds = $this->relationIds(); 59 | 60 | return $this->addToItem($relationIds, $item); 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function addTimestampsToEachItemInCollection(array $collection) 67 | { 68 | return $this->addToEachItemOfCollection($this->timestamps(), $collection); 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function addTimestampsToItem(array $item) 75 | { 76 | return $this->addToItem($this->timestamps(), $item); 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | public function columns() 83 | { 84 | return $this->tableHelper->getIdColumnNames($this->entities(), $this->idEntities()); 85 | } 86 | 87 | /** 88 | * @inheritdoc 89 | */ 90 | public function ids() 91 | { 92 | return $this->parser->ids($this->uri()); 93 | } 94 | 95 | /** 96 | * @inheritdoc 97 | */ 98 | public function itemColumn() 99 | { 100 | $this->isAnItemResource(); 101 | 102 | return $this->tableHelper->getLastEntityAsIdColumnName($this->entities()); 103 | } 104 | 105 | /** 106 | * @inheritdoc 107 | */ 108 | public function itemIds() 109 | { 110 | $ids = $this->ids(); 111 | 112 | return (array)array_pop($ids); 113 | } 114 | 115 | /** 116 | * @inheritdoc 117 | */ 118 | public function itemName() 119 | { 120 | $entities = $this->entities(); 121 | 122 | return $this->tableHelper->singularize(array_pop($entities)); 123 | } 124 | 125 | /** 126 | * @inheritdoc 127 | */ 128 | public function relationIds() 129 | { 130 | $entities = $this->entities(); 131 | $idEntities = $this->idEntities(); 132 | $ids = $this->ids(); 133 | $columnNames = $this->tableHelper->getIdColumnNames($entities, $idEntities); 134 | 135 | if (count($entities) === count($idEntities)) { 136 | array_pop($ids); 137 | array_pop($columnNames); 138 | 139 | return array_combine($columnNames, $ids); 140 | } elseif (count($entities) > count($idEntities)) { 141 | return array_combine($columnNames, $ids); 142 | } 143 | 144 | throw new StupidProgrammerMistakeException('the entities identities count is off'); 145 | } 146 | 147 | /** 148 | * @inheritdoc 149 | */ 150 | public function table() 151 | { 152 | return $this->tableHelper->getTable($this->entities()); 153 | } 154 | 155 | /** 156 | * @param array $add 157 | * @param array $collection 158 | * 159 | * @return array 160 | */ 161 | protected function addToEachItemOfCollection(array $add, array $collection) 162 | { 163 | return array_map(function ($item) use ($add) { 164 | return $this->addToItem($add, $item); 165 | }, $collection); 166 | } 167 | 168 | /** 169 | * @param array $add 170 | * 171 | * @param array $item 172 | * 173 | * @return array 174 | */ 175 | protected function addToItem(array $add, array $item) 176 | { 177 | return array_merge($add, $item); 178 | } 179 | 180 | /** 181 | * @return array 182 | */ 183 | protected function entities() 184 | { 185 | return $this->parser->entities($this->uri()); 186 | } 187 | 188 | /** 189 | * @return array 190 | */ 191 | protected function idEntities() 192 | { 193 | return $this->parser->idEntities($this->uri()); 194 | } 195 | 196 | /** 197 | * @throws StupidProgrammerMistakeException 198 | */ 199 | protected function isACollectionResource() 200 | { 201 | if (!(count($this->entities()) > count($this->idEntities()))) { 202 | throw new StupidProgrammerMistakeException('this is not a collection resource'); 203 | } 204 | } 205 | 206 | /** 207 | * @throws StupidProgrammerMistakeException 208 | */ 209 | protected function isAnItemResource() 210 | { 211 | if (count($this->entities()) > count($this->idEntities())) { 212 | throw new StupidProgrammerMistakeException('this is not an item resource'); 213 | } 214 | } 215 | 216 | /** 217 | * @return array 218 | */ 219 | protected function timestamps() 220 | { 221 | $timestamps = []; 222 | $timestamps['created_at'] = Carbon::now(); 223 | $timestamps['updated_at'] = Carbon::now(); 224 | 225 | return $timestamps; 226 | } 227 | 228 | /** 229 | * @return string 230 | */ 231 | protected function uri() 232 | { 233 | return $this->request->getRequestUri(); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Repository/Repository.php: -------------------------------------------------------------------------------- 1 | model = $model; 46 | $this->validator = $validator; 47 | $this->factory = $factory; 48 | } 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public function collection($currentPosition, $pageSize, $where = [], $with = null) 54 | { 55 | $query = $this->model->where('id', '>', $currentPosition); 56 | 57 | foreach ($where as $key => $value) { 58 | $query->where($key, $value); 59 | } 60 | 61 | if ($with) { 62 | $query = $query->with($with); 63 | } 64 | 65 | $paginator = $query->simplePaginate($pageSize); 66 | 67 | return $this->cursor($paginator); 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function delete(array $ids) 74 | { 75 | return $this->model->destroy($ids); 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function deleteRelation($table, array $relationIds, $itemColumn = null, array $itemIds = [], Event $event = null) 82 | { 83 | $itemColumnIsDefined = count($itemIds) > 0; 84 | 85 | $db = $this->deleteWhereRelation($table, $relationIds); 86 | 87 | if ($itemColumnIsDefined) { 88 | $db->whereIn($itemColumn, $itemIds); 89 | } 90 | 91 | $numberDeleted = $db->delete(); 92 | 93 | if ($event !== null) { 94 | \Event::fire($event); 95 | } 96 | 97 | return $numberDeleted; 98 | } 99 | 100 | /** 101 | * @inheritdoc 102 | */ 103 | public function entity($id) 104 | { 105 | $model = $this->model->find($id); 106 | 107 | if (is_null($model)) { 108 | throw new NotFoundHttpException('Model id not found'); 109 | } 110 | 111 | return $this->makeEntity($model); 112 | } 113 | 114 | /** 115 | * @inheritdoc 116 | */ 117 | public function patch($id, array $data) 118 | { 119 | $data = $this->addIdToKeyedArray($id, $data); 120 | $this->validate($data, $this->model->updateValidationRules($id)); 121 | 122 | return $this->update($id, $data); 123 | } 124 | 125 | /** 126 | * @inheritdoc 127 | */ 128 | public function post(array $data) 129 | { 130 | $this->validate($data, $this->model->createValidationRules()); 131 | 132 | return $this->create($data); 133 | } 134 | 135 | /** 136 | * @inheritdoc 137 | */ 138 | public function postRelations($table, array $collection) 139 | { 140 | $this->validate($collection, $this->model->createRelationsValidationRules()); 141 | 142 | return $this->db()->table($table)->insert($collection); 143 | } 144 | 145 | /** 146 | * @inheritdoc 147 | */ 148 | public function put($id, array $data) 149 | { 150 | $data = $this->addIdToKeyedArray($id, $data); 151 | $this->validate($data, $this->model->replaceValidationRules($id)); 152 | 153 | return $this->update($id, $data); 154 | } 155 | 156 | /** 157 | * @inheritdoc 158 | */ 159 | public function putRelations($table, array $relationIds, array $collection) 160 | { 161 | $this->validate($collection, $this->model->replaceRelationsValidationRules()); 162 | 163 | $delete = $this->deleteWhereRelation($table, $relationIds); 164 | 165 | $delete->delete(); 166 | 167 | return $this->db()->table($table)->insert($collection); 168 | } 169 | 170 | /** 171 | * @param int $id 172 | * @param array $data 173 | * 174 | * @return array 175 | */ 176 | protected function addIdToKeyedArray($id, array $data) 177 | { 178 | $data['id'] = $id; 179 | 180 | return $data; 181 | } 182 | 183 | /** 184 | * @param array $array 185 | * 186 | * @return \LaraPackage\Api\Contracts\Resource\Entity 187 | */ 188 | protected function create(array $array) 189 | { 190 | $model = $this->model->create($array); 191 | 192 | return $this->makeEntity($model); 193 | } 194 | 195 | /** 196 | * @param array $ids 197 | * 198 | * @return array 199 | */ 200 | protected function createDeleteDataArray(array $ids) 201 | { 202 | return ['id' => $ids]; 203 | } 204 | 205 | /** 206 | * @param Paginator $paginator 207 | * 208 | * @return \LaraPackage\Api\Contracts\Resource\Collection 209 | */ 210 | protected function cursor(Paginator $paginator) 211 | { 212 | return $this->factory->cursor($paginator); 213 | } 214 | 215 | /** 216 | * @param int $id the id to find in the model 217 | * @param int $current 218 | * @param int $pageSize 219 | * 220 | * @return \LaraPackage\Api\Contracts\Resource\Collection 221 | */ 222 | protected function cursorFromRelation($id, $current, $pageSize, $relation = null, $with = null) 223 | { 224 | if (is_null($relation)) { 225 | $relation = debug_backtrace(false)[1]['function']; 226 | } 227 | 228 | $model = $this->model->find($id); 229 | 230 | if ($model === null) { 231 | throw new InvalidArgumentException('Model id not found for cursorFromRelation'); 232 | } 233 | 234 | $collection = $model->{$relation}()->where('id', '>', $current); 235 | 236 | if ($with) { 237 | $collection = $collection->with($with); 238 | } 239 | 240 | $collection = $collection->take($pageSize)->get()->unique(); 241 | 242 | $paginator = $this->factory->paginator($collection, $pageSize, $current); 243 | 244 | return $this->cursor($paginator); 245 | } 246 | 247 | /** 248 | * @return \Illuminate\Database\DatabaseManager 249 | */ 250 | protected function db() 251 | { 252 | return \App::make('db'); 253 | } 254 | 255 | /** 256 | * @param string $table 257 | * @param array $relationIds 258 | * 259 | * @return mixed 260 | */ 261 | protected function deleteWhereRelation($table, array $relationIds) 262 | { 263 | $delete = $this->db()->table($table); 264 | 265 | foreach ($relationIds as $column => $id) { 266 | $delete->where($column, $id); 267 | } 268 | 269 | return $delete; 270 | } 271 | 272 | /** 273 | * @param Model $model 274 | * 275 | * @return \LaraPackage\Api\Contracts\Resource\Entity 276 | */ 277 | protected function makeEntity(Model $model) 278 | { 279 | return $this->factory->entity($model); 280 | } 281 | 282 | /** 283 | * @param array $data 284 | * @param array $rules 285 | * 286 | * @return array 287 | */ 288 | protected function runValidation(array $data, array $rules) 289 | { 290 | $validation = $this->validator->make( 291 | $data, 292 | $rules, 293 | $this->model->validationMessages() 294 | ); 295 | 296 | if ($validation->fails()) { 297 | $validation->setAttributeNames($this->model->frontEndAttributeNames()); 298 | 299 | $messagesArray = $validation->messages()->getMessages(); 300 | 301 | return $this->model->forwardTransformAttributeKeyNames($messagesArray); 302 | } 303 | 304 | return null; 305 | } 306 | 307 | /** 308 | * @param int $id 309 | * @param array $array 310 | * 311 | * @return \LaraPackage\Api\Contracts\Resource\Entity 312 | * @throws RepositoryException 313 | */ 314 | protected function update($id, array $array) 315 | { 316 | /** @var AbstractModel $model */ 317 | $model = $this->model->find($id); 318 | 319 | if (!$model->update($array)) { 320 | throw new RepositoryException('Could not be saved'); 321 | } 322 | 323 | return $this->makeEntity($model); 324 | } 325 | 326 | /** 327 | * @param array $data an associative array of fields that map to the model's fields 328 | * @param array $rules 329 | * 330 | * @return $this 331 | * @throws ValidationException 332 | */ 333 | protected function validate(array $data, array $rules) 334 | { 335 | if (CollectionHelper::isCollection($data)) { 336 | $validationMessages = []; 337 | $i = 0; 338 | foreach ($data as $item) { 339 | $validationMessages['collection_index_'.$i] = $this->runValidation($item, $rules); 340 | $i++; 341 | } 342 | 343 | $validationMessages = array_filter($validationMessages); 344 | } else { 345 | $validationMessages = $this->runValidation($data, $rules); 346 | } 347 | 348 | if (count($validationMessages) > 0) { 349 | throw new ValidationException($validationMessages, 'Validation failed'); 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/Request/AcceptHeader.php: -------------------------------------------------------------------------------- 1 | request = $request; 26 | $this->version = $version; 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | public function acceptedMediaType() 33 | { 34 | if ($this->mediaTypeAfterVendorIsValid()) { 35 | return $this->acceptHeaderMediaTypeAfterVendor(); 36 | } 37 | 38 | if ($this->mediaTypeAfterApplicationIsValid()) { 39 | return $this->acceptHeaderMediaTypeAfterApplication(); 40 | } 41 | 42 | return $this->version->defaultMediaType($this->version()); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function version() 49 | { 50 | $version = $this->parseVersion($this->acceptHeader()); 51 | 52 | if ($this->invalidVersion($version)) { 53 | return $this->version->latest(); 54 | } 55 | 56 | return (int)$version; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | protected function acceptHeader() 63 | { 64 | $header = $this->request->server('HTTP_ACCEPT', ''); 65 | 66 | // Sometimes the accept header may contain something after application/json; 67 | return explode(';', $header)[0]; 68 | } 69 | 70 | /** 71 | * @return bool|string 72 | */ 73 | protected function acceptHeaderMediaTypeAfterApplication() 74 | { 75 | return $this->getAfter('/', $this->acceptHeader()); 76 | } 77 | 78 | /** 79 | * @return bool|string 80 | */ 81 | protected function acceptHeaderMediaTypeAfterVendor() 82 | { 83 | return $this->getAfter('+', $this->acceptHeader()); 84 | } 85 | 86 | /** 87 | * @return string 88 | */ 89 | protected function application() 90 | { 91 | return 'application'; 92 | } 93 | 94 | /** 95 | * @param string $character 96 | * @param string $string 97 | * 98 | * @return bool|string 99 | */ 100 | protected function getAfter($character, $string) 101 | { 102 | if (($pos = strpos($string, $character)) !== false) { 103 | return substr($string, $pos + 1); 104 | } 105 | 106 | return false; 107 | } 108 | 109 | /** 110 | * @param string $start 111 | * @param string $end 112 | * @param string $string 113 | * 114 | * @return bool 115 | * @throws \Exception 116 | */ 117 | protected function getBetween($start, $end, $string) 118 | { 119 | $success = \preg_match('/'.$start.'(.*?)'.$end.'/s', $string, $matches); 120 | if ($success) { 121 | return $matches[1]; 122 | } 123 | if ($success === false) { 124 | throw new \Exception('preg_match did not work.'); 125 | } 126 | 127 | return false; 128 | } 129 | 130 | /** 131 | * @param $version 132 | * 133 | * @return bool 134 | */ 135 | protected function invalidVersion($version) 136 | { 137 | return $this->version->isValid($version) === false; 138 | } 139 | 140 | /** 141 | * @return string 142 | */ 143 | protected function mediaTypeAfterApplicationIsValid() 144 | { 145 | return $this->version->mediaTypeIsValid($this->acceptHeaderMediaTypeAfterApplication(), $this->version()); 146 | } 147 | 148 | /** 149 | * @return string 150 | */ 151 | protected function mediaTypeAfterVendorIsValid() 152 | { 153 | return $this->version->mediaTypeIsValid($this->acceptHeaderMediaTypeAfterVendor(), $this->version()); 154 | } 155 | 156 | /** 157 | * @param string $string 158 | * 159 | * @return int 160 | */ 161 | protected function parseVersion($string) 162 | { 163 | 164 | $version = \preg_replace('/[^0-9]/', '', $string); 165 | 166 | if (!is_numeric($version)) { 167 | return (int)$this->version->latest(); 168 | } 169 | 170 | return (int)$version; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Request/Parser.php: -------------------------------------------------------------------------------- 1 | request = $request; 33 | $this->acceptHeader = $acceptHeader; 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | public function acceptedMediaType() 40 | { 41 | return $this->acceptHeader->acceptedMediaType(); 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function header($item) 48 | { 49 | return $this->request->headers->get($item); 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function inIncludes($entity) 56 | { 57 | $includesArray = \explode(',', $this->includes()); 58 | 59 | return \in_array($entity, $includesArray); 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function includes() 66 | { 67 | return $this->request->query->get('include'); 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function query($item = null) 74 | { 75 | return $this->request->query($item); 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function requestedMediaType() 82 | { 83 | $header = $this->request->header('Content-Type'); 84 | 85 | return explode(';', $header)[0]; 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function version() 92 | { 93 | return $this->acceptHeader->version(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Request/Payload.php: -------------------------------------------------------------------------------- 1 | request = $request; 21 | } 22 | 23 | /** 24 | * @inheritdoc 25 | */ 26 | public function getIterator() 27 | { 28 | $content = $this->request->getContent(); 29 | $array = json_decode($content, true); 30 | 31 | if (is_null($array)) { 32 | throw new RequestException('Payload could not be parsed from json'); 33 | } 34 | 35 | return new \ArrayIterator($array); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Resource/LaravelCollection.php: -------------------------------------------------------------------------------- 1 | paginator = $paginator; 31 | $this->config = $config; 32 | $this->requestParser = $requestParser; 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function getCount() 39 | { 40 | return (int)$this->paginator->count(); 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function getCurrent() 47 | { 48 | 49 | $currentPositionParameter = $this->config->getIndexForVersion('collection.current_position', $this->requestParser->version()); 50 | $current = $this->requestParser->query($currentPositionParameter); 51 | if (!empty($current)) { 52 | return (int)$current; 53 | } 54 | 55 | return 0; 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public function getData() 62 | { 63 | return $this->paginator->getIterator(); 64 | } 65 | 66 | /** 67 | * @inheritdoc 68 | */ 69 | public function getNext() 70 | { 71 | $last = $this->paginator->getCollection()->last(); 72 | 73 | if (is_null($last)) { 74 | return 0; 75 | } 76 | 77 | return (int)$last->id; 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function getPageSize() 84 | { 85 | return (int)$this->paginator->perPage(); 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function getPrevious() 92 | { 93 | return (int)($this->getCurrent() - $this->getCount()) >= 0 ? $this->getCurrent() - $this->getCount() : 0; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Resource/LaravelEntity.php: -------------------------------------------------------------------------------- 1 | model = $model; 24 | } 25 | 26 | /** 27 | * @inheritdoc 28 | */ 29 | public function getData() 30 | { 31 | return $this->model; 32 | } 33 | 34 | /** 35 | * @inheritdoc 36 | */ 37 | public function getId() 38 | { 39 | return $this->model->id; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Transformer/AbstractTransformer.php: -------------------------------------------------------------------------------- 1 | toArray($iterable); 16 | 17 | if (CollectionHelper::isCollection($iterable)) { 18 | 19 | return array_map(function ($array) { 20 | return $this->forward($this->mappings(), $array); 21 | }, $iterable); 22 | } 23 | 24 | return $this->forward($this->mappings(), $iterable); 25 | } 26 | 27 | 28 | /** 29 | * @inheritdoc 30 | */ 31 | public function reverseTransform($iterable) 32 | { 33 | $iterable = $this->toArray($iterable); 34 | 35 | if (CollectionHelper::isCollection($iterable)) { 36 | 37 | return array_map(function ($array) { 38 | return $this->reverse($this->mappings(), $array); 39 | }, $iterable); 40 | } 41 | 42 | return $this->reverse($this->mappings(), $iterable); 43 | } 44 | 45 | /** 46 | * @param string $outKey 47 | * @param string $inKey 48 | * @param array $inArray 49 | * @param array $outArray 50 | */ 51 | protected function addElementToArrayIfSet($outKey, $inKey, $inArray, &$outArray) 52 | { 53 | CollectionHelper::addToArrayIfSet($outKey, $inKey, $inArray, $outArray); 54 | } 55 | 56 | protected function forward($mappings, $array) 57 | { 58 | $outArray = []; 59 | 60 | foreach ($mappings as $privateKey => $publicKey) { 61 | $this->addElementToArrayIfSet($publicKey, $privateKey, $array, $outArray); 62 | } 63 | 64 | return $outArray; 65 | } 66 | 67 | /** 68 | * @param string $value 69 | * 70 | * @return int|null 71 | */ 72 | protected function intOrNull($value) 73 | { 74 | return $value ? (int)$value : null; 75 | } 76 | 77 | /** 78 | * @param array $mappings 79 | * @param array $array 80 | * 81 | * @return array 82 | */ 83 | protected function reverse($mappings, $array) 84 | { 85 | $outArray = []; 86 | 87 | foreach ($mappings as $privateKey => $publicKey) { 88 | $this->addElementToArrayIfSet($privateKey, $publicKey, $array, $outArray); 89 | } 90 | 91 | return $outArray; 92 | } 93 | 94 | /** 95 | * @param $iterable 96 | * 97 | * @return array 98 | */ 99 | protected function toArray($iterable) 100 | { 101 | if ($iterable instanceof \Traversable) { 102 | $iterable = iterator_to_array($iterable); 103 | 104 | return $iterable; 105 | } 106 | 107 | return $iterable; 108 | } 109 | } 110 | --------------------------------------------------------------------------------