├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── src ├── Exceptions │ ├── ApiException.php │ ├── BadRequestException.php │ ├── Handler.php │ ├── InvalidAttributeException.php │ ├── NotAcceptableException.php │ ├── UnauthorizedHttpException.php │ └── UnsupportedMediaTypeException.php ├── Helpers │ ├── Api.php │ ├── ApiController.php │ ├── ApiObjects.php │ └── ApiValidation.php ├── Http │ ├── Middleware │ │ ├── Auth │ │ │ └── JsonApiAuthBasicMiddleware.php │ │ └── JsonApiMiddleware.php │ └── Responses │ │ └── ApiResponse.php ├── Providers │ ├── .gitkeep │ └── GenericServiceProvider.php ├── Traits │ ├── ControllerTrait.php │ ├── ModelTrait.php │ └── SearchableTrait.php ├── Transformers │ ├── ApiTransformer.php │ └── KeysTransformer.php ├── config │ └── jsonapi.php └── lang │ └── en │ └── errors.php └── tests ├── AcceptanceTestCase.php ├── AcceptanceTests ├── AuthTest.php ├── CrudTest.php ├── HeadersTest.php ├── IncludesTest.php ├── JsonTest.php ├── ListTest.php ├── PatchTest.php └── PostTest.php ├── App ├── Database │ ├── .gitignore │ └── Migrations │ │ ├── .gitkeep │ │ ├── 2014_10_12_000000_create_users_table.php │ │ ├── 2016_03_09_190517_create_profiles_table.php │ │ └── 2016_03_09_223910_create_profiles_lookup_table.php ├── Http │ ├── Controllers │ │ ├── ProfileController.php │ │ └── UserController.php │ └── routes.php ├── Profiles.php ├── ProfilesAlt.php ├── Providers │ └── RouteServiceProvider.php ├── User.php └── UserAlt.php ├── BaseTestCase.php ├── IntegrationTestCase.php ├── IntegrationTests ├── CrudTest.php └── SearchTest.php ├── Main.json.postman_collection ├── SeeOrSaveJsonStructure.php ├── UnitTestCase.php └── UnitTests └── ExceptionTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | node_modules/ 3 | 4 | # Laravel 4 specific 5 | bootstrap/compiled.php 6 | app/storage/ 7 | 8 | # Laravel 5 & Lumen specific 9 | bootstrap/cache/ 10 | storage/ 11 | .env.*.php 12 | .env.php 13 | .env 14 | .env.example 15 | logs 16 | build 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "5.6" 4 | - "7.0" 5 | - "hhvm" 6 | 7 | before_script: 8 | - curl -sS https://getcomposer.org/installer | php -- --filename=composer 9 | - chmod +x composer 10 | - composer install -n 11 | 12 | script: 13 | - php vendor/bin/phpunit 14 | 15 | after_script: 16 | - php vendor/bin/codacycoverage clover build/logs/clover.xml 17 | 18 | matrix: 19 | allow_failures: 20 | - php: "hhvm" 21 | branches: 22 | only: 23 | - master 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Askedio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![laravel-cruddy](http://i.imgur.com/TmEh1m6.jpgg) 2 | 3 | A really simple package that provides a CRUD JSON API for your Laravel 5 application. 4 | 5 | [![Build Status](https://travis-ci.org/Askedio/laravel-Cruddy.svg?branch=master)](https://travis-ci.org/Askedio/laravel-Cruddy) 6 | [![StyleCI](https://styleci.io/repos/52752552/shield)](https://styleci.io/repos/52752552) 7 | [![Code Climate](https://codeclimate.com/github/Askedio/laravel-Cruddy/badges/gpa.svg)](https://codeclimate.com/github/Askedio/laravel-Cruddy) 8 | [![Codacy Badge](https://api.codacy.com/project/badge/grade/c2f2291fe3af4ea3a511afa64ddc034b)](https://www.codacy.com/app/gcphost/laravel-Cruddy) 9 | [![Codacy Badge](https://api.codacy.com/project/badge/coverage/c2f2291fe3af4ea3a511afa64ddc034b)](https://www.codacy.com/app/gcphost/laravel-Cruddy) 10 | 11 | * [Live Demo](https://cruddy.io/app/). 12 | * [Laravel 5.2 Example Package](https://github.com/Askedio/Laravel-5-CRUD-Example). 13 | * Plays well with [jQuery CRUDdy](https://github.com/Askedio/jQuery-Cruddy). 14 | 15 | 16 | 17 | # Installation 18 | ### Composer: require 19 | ~~~ 20 | composer require askedio/laravel-cruddy:dev-master 21 | ~~~ 22 | 23 | 24 | ### Providers: config/app.php 25 | Add the Service Provider to your providers array. 26 | ~~~ 27 | 'providers' => [ 28 | Askedio\Laravel5ApiController\Providers\GenericServiceProvider::class, 29 | ... 30 | ~~~ 31 | 32 | 33 | 34 | 35 | ### Model: app/User.php 36 | Add the traits to your Model to enable the Api and Search features. [More Details & Options.](https://github.com/Askedio/laravel-Cruddy/wiki/Models) 37 | ~~~ 38 | class User extends Authenticatable 39 | { 40 | 41 | use \Askedio\Laravel5ApiController\Traits\ModelTrait; 42 | use \Askedio\Laravel5ApiController\Traits\SearchableTrait; 43 | ... 44 | ~~~ 45 | 46 | 47 | 48 | 49 | ### Controller: app/Http/Controllers/Api/UserController.php 50 | Create a new controller for your API. [More Details & Options](https://github.com/Askedio/laravel-Cruddy/wiki/Controllers). 51 | ~~~ 52 | 'api', 'middleware' => ['api', 'jsonapi']], function() 69 | { 70 | Route::resource('user', 'Api\UserController'); 71 | }); 72 | ~~~ 73 | 74 |
75 |
76 | 77 | 78 | 79 | # Usage 80 | Consume the API using Laravels resource routes, GET, PATCH, POST and DELETE. [More Details & Options](https://github.com/Askedio/laravel-Cruddy/wiki/Usage). 81 | 82 | ### Example 83 | ~~~ 84 | GET /api/user/1 85 | ~~~ 86 | 87 | ~~~ 88 | HTTP/1.1 200 OK 89 | Content-Type: application/vnd.api+json 90 | 91 | { 92 | "data": { 93 | "type": "users", 94 | "id": 1, 95 | "attributes": { 96 | "id": 1, 97 | "name": "Test User", 98 | "email": "test@test.com" 99 | } 100 | }, 101 | "links": { 102 | "self": "/api/user/1" 103 | }, 104 | "jsonapi": { 105 | "version": "1.0", 106 | "self": "v1" 107 | } 108 | } 109 | ~~~ 110 | 111 | 112 |

113 | 114 | 115 | # Comments 116 | My goal is a plug-n-play json api for Laravel. You shouldn't need to configure much of anything to enable the api on your models but if you still want advanced features like relations, searching, etc, you get that too. 117 | 118 | If you have any comments, opinions or can code review please reach me here or on twitter, [@asked_io](https://twitter.com/asked_io). You can also follow me on my website, [asked.io](https://asked.io). 119 | 120 | 121 | Thank you. 122 | 123 | -William 124 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "askedio/laravel-cruddy", 3 | "description": "A JSON API CRUD Package for Laravel 5", 4 | "keywords": ["laravel", "json", "jsonapi", "crud"], 5 | "license": "MIT", 6 | "type": "library", 7 | "require": { 8 | "php": ">=5.5.9", 9 | "laravel/framework": "5.2.*" 10 | }, 11 | "require-dev": { 12 | "laravel/laravel": "5.*", 13 | "phpunit/phpunit": "4.*", 14 | "codacy/coverage": "dev-master" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Askedio\\Laravel5ApiController\\": "src/" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Askedio\\Tests\\" : "tests" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ./src/ 30 | 31 | ./vendor/ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Exceptions/ApiException.php: -------------------------------------------------------------------------------- 1 | getDetails($this->error); 33 | } 34 | 35 | /** 36 | * Get the status. 37 | * 38 | * @return int 39 | */ 40 | public function getStatusCode() 41 | { 42 | return (int) $this->status; 43 | } 44 | 45 | /** 46 | * Store exception details. 47 | * 48 | * @param mixed $details 49 | */ 50 | public function withDetails($details) 51 | { 52 | $this->exceptionDetails = $details; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Store exception errors details. 59 | * 60 | * @param array $details 61 | */ 62 | public function withErrors($errors) 63 | { 64 | $this->exceptionErrors = $errors; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Build the error results. 71 | * 72 | * @return array 73 | */ 74 | public function getDetails($template) 75 | { 76 | if ($this->exceptionErrors) { 77 | return $this->exceptionErrors; 78 | } 79 | $details = $this->exceptionDetails; 80 | 81 | /* Not pre-rendered errors, build from template */ 82 | if (! is_array($details)) { 83 | $details = [$details]; 84 | } 85 | 86 | return array_map(function ($detail) use ($template) { 87 | return $this->item($template, $detail); 88 | }, $details); 89 | } 90 | 91 | /** 92 | * Render the item. 93 | * 94 | * @return array 95 | */ 96 | private function item($template, $detail) 97 | { 98 | $insert = $template; 99 | $replace = $template['detail']; 100 | 101 | $insert['detail'] = vsprintf($replace, $detail); 102 | if (isset($template['source'])) { 103 | $insert['source'] = []; 104 | $insert['source'][$template['source']['type']] = vsprintf($template['source']['value'], $detail); 105 | } 106 | 107 | return $insert; 108 | } 109 | 110 | /** 111 | * Build the Exception details from the custom exception class. 112 | * 113 | * @return void 114 | */ 115 | protected function build(array $args) 116 | { 117 | 118 | /* Nothing to build if no type. */ 119 | if (! isset($args[0])) { 120 | return false; 121 | } 122 | 123 | $settings = $this->settings($args); 124 | $this->error = $settings; 125 | $this->status = $settings['code']; 126 | } 127 | 128 | /** 129 | * Generate settings array from errors config. 130 | * 131 | * @return array 132 | */ 133 | private function settings($args) 134 | { 135 | $base = [ 136 | 'title' => '', 137 | 'detail' => '', 138 | 'code' => isset($args[1]) ? $args[1] : $this->status, 139 | ]; 140 | 141 | $tpl = trans(sprintf('jsonapi::errors.%s', $args[0])); 142 | 143 | return array_merge($base, is_array($tpl) ? $tpl : []); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Exceptions/BadRequestException.php: -------------------------------------------------------------------------------- 1 | build(func_get_args()); 18 | 19 | parent::__construct(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | is(config('jsonapi.url'))) { 44 | return parent::render($request, $exception); 45 | } 46 | 47 | return $this->handle($request, $exception); 48 | } 49 | 50 | /** 51 | * Convert the Exception into a JSON HTTP Response. 52 | * 53 | * @param Request $request 54 | * @param Exception $exception 55 | * 56 | * @return ApiResponse 57 | */ 58 | private function handle($code, Exception $exception) 59 | { 60 | 61 | /* custom exception class */ 62 | if ($exception instanceof ApiException) { 63 | return response()->jsonapi($exception->getStatusCode(), ['errors' => $exception->getErrors()]); 64 | } 65 | 66 | /* not an exception we manage so generic error or if debug, the real exception */ 67 | // TO-DO: Need a way to get coverage on something like.. if (!env('APP_DEBUG', false)) { 68 | $code = method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500; 69 | $detail = method_exists($exception, 'getMessage') ? $exception->getMessage() : 'Unknown Exception.'; 70 | $data = array_filter([ 71 | 'status' => $code, 72 | 'detail' => $detail, 73 | ]); 74 | 75 | return response()->jsonapi($code, ['errors' => $data]); 76 | // } 77 | // return parent::render($code, $exception); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidAttributeException.php: -------------------------------------------------------------------------------- 1 | build(func_get_args()); 18 | 19 | parent::__construct(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/NotAcceptableException.php: -------------------------------------------------------------------------------- 1 | build(func_get_args()); 18 | 19 | parent::__construct(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/UnauthorizedHttpException.php: -------------------------------------------------------------------------------- 1 | build(func_get_args()); 18 | 19 | parent::__construct(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/UnsupportedMediaTypeException.php: -------------------------------------------------------------------------------- 1 | build(func_get_args()); 18 | 19 | parent::__construct(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Helpers/Api.php: -------------------------------------------------------------------------------- 1 | version ?: config('jsonapi.version'); 18 | } 19 | 20 | /** 21 | * Set version. 22 | * 23 | * @param string $version 24 | * 25 | * @return void 26 | */ 27 | public function setVersion($version) 28 | { 29 | $this->version = $version; 30 | } 31 | 32 | /** 33 | * List of included options from input. 34 | * 35 | * @return Illuminate\Http\Request 36 | */ 37 | public function includes() 38 | { 39 | return collect(request()->input('include') ? explode(',', request()->input('include')) : []); 40 | } 41 | 42 | /** 43 | * List of fields from input. 44 | * 45 | * @return array 46 | */ 47 | public function fields() 48 | { 49 | $results = []; 50 | foreach (array_filter(request()->input('fields', [])) as $type => $members) { 51 | foreach (explode(',', $members) as $member) { 52 | $results[$type][] = $member; 53 | } 54 | } 55 | 56 | return collect($results); 57 | } 58 | 59 | public function jsonBody() 60 | { 61 | return collect(request()->json()->all())->get('data'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Helpers/ApiController.php: -------------------------------------------------------------------------------- 1 | model = new $parent->model(); 21 | $this->object = $this->model; 22 | 23 | new ApiValidation($this->model->getObjects()); 24 | 25 | $this->setAuth($parent); 26 | } 27 | 28 | private function setAuth($parent) 29 | { 30 | if ($parent->getAuth()) { 31 | $table = $this->model->getTable(); 32 | $user = auth()->user(); 33 | $this->object = $user->$table(); 34 | } 35 | } 36 | 37 | /** 38 | * index. 39 | * 40 | * @return pagination class.. 41 | */ 42 | public function index() 43 | { 44 | $results = $this->object->setSort(request()->input('sort')); 45 | 46 | if (request()->input('search') && $this->model->isSearchable()) { 47 | $results->search(request()->input('search')); 48 | } 49 | 50 | return $results->paginate(request()->input('page.size', 10), ['*'], 'page', request()->input('page.number', 1) 51 | ); 52 | } 53 | 54 | /** 55 | * Store. 56 | * 57 | * @return Illuminate\Database\Eloquent\Model 58 | */ 59 | public function store() 60 | { 61 | $this->validate('create'); 62 | 63 | return $this->object->create($this->getRequest()); 64 | } 65 | 66 | /** 67 | * Show. 68 | * 69 | * @return Illuminate\Database\Eloquent\Model 70 | */ 71 | public function show($idd) 72 | { 73 | return $this->object->find($idd); 74 | } 75 | 76 | /** 77 | * Update. 78 | * 79 | * @return Illuminate\Database\Eloquent\Model 80 | */ 81 | public function update($idd) 82 | { 83 | $this->validate('update'); 84 | 85 | if ($model = $this->object->find($idd)) { 86 | return $model->update($this->getRequest()) ? $model : false; 87 | } 88 | 89 | return false; 90 | } 91 | 92 | /** 93 | * Destroy. 94 | * 95 | * @return Illuminate\Database\Eloquent\Model 96 | */ 97 | public function destroy($idd) 98 | { 99 | $model = $this->object->find($idd); 100 | 101 | return $model ? $model->delete() : false; 102 | } 103 | 104 | /** 105 | * Get request body data->attibutes. 106 | * 107 | * @return array 108 | */ 109 | private function getRequest() 110 | { 111 | $requst = app('api')->jsonBody(); 112 | 113 | return isset($requst['attributes']) ? $requst['attributes'] : []; 114 | } 115 | 116 | /** 117 | * Validate Form. 118 | * 119 | * @param string $action 120 | * 121 | * @return void 122 | */ 123 | private function validate($action) 124 | { 125 | $validator = validator()->make($this->getRequest(), $this->model->getRule($action)); 126 | $errors = []; 127 | foreach ($validator->errors()->toArray() as $field => $err) { 128 | array_push($errors, [ 129 | //'code' => 0, # TO-DO: report valid json api error code base on validation error. 130 | 'source' => ['pointer' => $field], 131 | 'title' => trans('jsonapi::errors.invalid_attribute.title'), 132 | 'detail' => implode(' ', $err), 133 | ]); 134 | } 135 | 136 | if (! empty($errors)) { 137 | throw (new InvalidAttributeException('invalid_attribute', 403))->withErrors($errors); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Helpers/ApiObjects.php: -------------------------------------------------------------------------------- 1 | baseObject = $object; 33 | $this->fillables = collect([]); 34 | $this->includes = collect([]); 35 | $this->columns = collect([]); 36 | $this->relations = collect($this->includes($object)); 37 | } 38 | 39 | /** 40 | * Return a collection of all fillable items. 41 | * 42 | * @return collection 43 | */ 44 | public function getFillables() 45 | { 46 | return $this->fillables; 47 | } 48 | 49 | /** 50 | * Return a collection of all includes. 51 | * 52 | * @return collection 53 | */ 54 | public function getIncludes() 55 | { 56 | return $this->includes; 57 | } 58 | 59 | /** 60 | * Return a collection of all columns. 61 | * 62 | * @return collection 63 | */ 64 | public function getColumns() 65 | { 66 | return $this->columns; 67 | } 68 | 69 | /** 70 | * Itterate over object, build a relations, fillable and includes collection. 71 | * 72 | * @param model $object the model to iterate over 73 | * 74 | * @return array 75 | */ 76 | private function includes($object) 77 | { 78 | $fillable = $object->getFillable(); 79 | $includes = $object->getIncludes(); 80 | $table = $object->getTable(); 81 | $columns = $object->columns(); 82 | 83 | $results[$table] = []; 84 | 85 | if (! empty($includes)) { 86 | foreach ($includes as $include) { 87 | $results[$table] = [ 88 | 'object' => $object, 89 | 'includes' => $this->includes(new $include()), 90 | ]; 91 | } 92 | } 93 | 94 | $this->fillables->put($table, $fillable); 95 | $this->includes->push($table, $table); 96 | $this->columns->put($table, $columns); 97 | 98 | return $results; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Helpers/ApiValidation.php: -------------------------------------------------------------------------------- 1 | objects = $objects; 23 | $this->validateIncludes(); 24 | $this->validateFields(); 25 | $this->validateRequests(); 26 | } 27 | 28 | /** 29 | * Validate post and patch requests to make sure the elements are fillable (needs updating to json api spec posts/patch). 30 | * 31 | * @return [type] [description] 32 | */ 33 | public function validateRequests() 34 | { 35 | if (! request()->isMethod('post') && ! request()->isMethod('patch')) { 36 | return; 37 | } 38 | 39 | $request = app('api')->jsonBody(); 40 | 41 | $fillable = $this->objects->getFillables(); 42 | 43 | if (! isset($request['attributes'])) { 44 | throw (new BadRequestException('invalid_filter'))->withDetails(['data.attributes']); 45 | } 46 | 47 | $errors = array_diff(array_keys($request['attributes']), $fillable->flatten()->all()); 48 | if (! empty($errors)) { 49 | throw (new BadRequestException('invalid_filter'))->withDetails($errors); 50 | } 51 | } 52 | 53 | /** 54 | * Validate include= variables to make sure our models have them (needs updated for sub includes, like include=users,profiles.addresses - by passes profiles for addresses). 55 | * 56 | * @return void 57 | */ 58 | public function validateIncludes() 59 | { 60 | $allowed = $this->objects->getIncludes(); 61 | $includes = app('api')->includes(); 62 | 63 | $errors = array_diff($includes->all(), $allowed->all()); 64 | 65 | if (! empty($errors)) { 66 | throw (new BadRequestException('invalid_include'))->withDetails($errors); 67 | } 68 | } 69 | 70 | /** 71 | * Validate that fields[]= variables are in our models. 72 | * 73 | * @return array 74 | */ 75 | public function validateFields() 76 | { 77 | $fields = app('api')->fields(); 78 | $columns = $this->objects->getColumns(); 79 | $includes = $this->objects->getIncludes(); 80 | 81 | $errors = array_diff($fields->keys()->all(), $includes->all()); 82 | if (! empty($errors)) { 83 | throw (new BadRequestException('invalid_filter'))->withDetails($errors); 84 | } 85 | 86 | $errors = $fields->map(function ($item, $key) use ($columns) { 87 | return array_diff($item, $columns->get($key)); 88 | })->flatten()->all(); 89 | 90 | if (! empty($errors)) { 91 | throw (new BadRequestException('invalid_filter'))->withDetails($errors); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Http/Middleware/Auth/JsonApiAuthBasicMiddleware.php: -------------------------------------------------------------------------------- 1 | onceBasic())) { 20 | throw new UnauthorizedHttpException('invalid-credentials'); 21 | } 22 | 23 | return $next($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Middleware/JsonApiMiddleware.php: -------------------------------------------------------------------------------- 1 | request = $request; 25 | 26 | $this->checkGetVars(); 27 | $this->checkAccept(); 28 | $this->checkContentType(); 29 | 30 | return $next($request); 31 | } 32 | 33 | /** 34 | * Check GET input variaibles. 35 | * 36 | * @return void 37 | */ 38 | private function checkGetVars() 39 | { 40 | if (! $this->request->isMethod('get')) { 41 | return false; 42 | } 43 | 44 | $badRequestInput = array_except($this->request->all(), array_keys(config('jsonapi.allowed_get'))); 45 | 46 | if (request()->input('page')) { 47 | $badRequestInput = array_merge($badRequestInput, array_except(request()->input('page'), config('jsonapi.allowed_get.page'))); 48 | } 49 | 50 | if (empty($badRequestInput)) { 51 | return false; 52 | } 53 | 54 | $errors = []; 55 | 56 | foreach (array_keys($badRequestInput) as $field) { 57 | array_push($errors, [ 58 | //'code' => 0, 59 | 'source' => ['pointer' => $field], 60 | 'title' => trans('jsonapi::errors.invalid_get.title'), 61 | ]); 62 | } 63 | 64 | throw (new BadRequestException('invalid_get'))->withErrors($errors); 65 | } 66 | 67 | /** 68 | * Check Accept Header. 69 | * 70 | * @return void 71 | */ 72 | private function checkAccept() 73 | { 74 | preg_match('/application\/vnd\.api\.([\w\d\.]+)\+([\w]+)/', $this->request->header('Accept'), $matches); 75 | 76 | app('api')->setVersion(isset($matches[1]) ? $matches[1] : config('jsonapi.version')); 77 | 78 | if ($matches || $this->request->header('Accept') === config('jsonapi.accept') || ! config('jsonapi.strict')) { 79 | return; 80 | } 81 | 82 | throw (new NotAcceptableException('not-acceptable'))->withDetails(config('jsonapi.accept')); 83 | } 84 | 85 | /** 86 | * Check Content-Type Header. 87 | * 88 | * @return void 89 | */ 90 | private function checkContentType() 91 | { 92 | if ($this->request->header('Content-Type') === config('jsonapi.content_type') || ! config('jsonapi.strict')) { 93 | return; 94 | } 95 | 96 | throw (new UnsupportedMediaTypeException('unsupported-media-type'))->withDetails(config('jsonapi.content_type')); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Http/Responses/ApiResponse.php: -------------------------------------------------------------------------------- 1 | json($this->jsonapiData($results), $code, [ 21 | 'Content-Type' => config('jsonapi.content_type'), 22 | ], true); 23 | } 24 | 25 | /** 26 | * Render the output for the json api. 27 | * 28 | * @return array 29 | */ 30 | public function jsonapiData($data = []) 31 | { 32 | return array_merge($data, [ 33 | 'jsonapi' => [ 34 | 'version' => config('jsonapi.json_version', '1.0'), 35 | 'self' => app('api')->getVersion(), 36 | ], 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Providers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Askedio/laravel-Cruddy/3077f8b619b68b9d1a4873d56a0f415c207b46c0/src/Providers/.gitkeep -------------------------------------------------------------------------------- /src/Providers/GenericServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton( 18 | \Illuminate\Contracts\Debug\ExceptionHandler::class, 19 | \Askedio\Laravel5ApiController\Exceptions\Handler::class 20 | ); 21 | 22 | $this->app->singleton('api', function () { 23 | return new \Askedio\Laravel5ApiController\Helpers\Api(); 24 | }); 25 | 26 | $this->mergeConfigFrom( 27 | __DIR__.'/../config/jsonapi.php', 'jsonapi' 28 | ); 29 | } 30 | 31 | /** 32 | * Register routes, translations, views and publishers. 33 | * 34 | * @return void 35 | */ 36 | public function boot(Router $router) 37 | { 38 | $this->loadTranslationsFrom(__DIR__.'/../lang', 'jsonapi'); 39 | 40 | $this->publishes([ 41 | __DIR__.'/../lang' => resource_path('lang/vendor/jsonapi'), 42 | __DIR__.'/../config/jsonapi.php' => config_path('jsonapi.php'), 43 | ]); 44 | 45 | $router->middleware('jsonapi', \Askedio\Laravel5ApiController\Http\Middleware\JsonApiMiddleware::class); 46 | $router->middleware('jsonapi.auth.basic', \Askedio\Laravel5ApiController\Http\Middleware\Auth\JsonApiAuthBasicMiddleware::class); 47 | 48 | response()->macro('jsonapi', function ($code, $value) { 49 | $apiResponse = new \Askedio\Laravel5ApiController\Http\Responses\ApiResponse(); 50 | 51 | return $apiResponse->jsonapi($code, $value); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Traits/ControllerTrait.php: -------------------------------------------------------------------------------- 1 | version) && app('api')->getVersion() !== $this->version) { 17 | throw (new NotAcceptableException('not-acceptable'))->withDetails('/application/vnd.api.'.$this->version.'+json'); 18 | } 19 | 20 | $this->results = new ApiController($this); 21 | } 22 | 23 | public function index() 24 | { 25 | return $this->render([ 26 | 'success' => 200, 27 | 'error' => [ 28 | 'class' => \Symfony\Component\HttpKernel\Exception\HttpException::class, 29 | 'message' => 500, 30 | ], 31 | 'results' => $this->results->index(), 32 | ]); 33 | } 34 | 35 | public function store() 36 | { 37 | return $this->render([ 38 | 'success' => 200, 39 | 'error' => [ 40 | 'class' => \Symfony\Component\HttpKernel\Exception\HttpException::class, 41 | 'message' => 500, 42 | ], 43 | 'results' => $this->results->store(), 44 | ]); 45 | } 46 | 47 | public function show($idd) 48 | { 49 | return $this->render([ 50 | 'success' => 200, 51 | 'error' => [ 52 | 'class' => \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class, 53 | 'message' => trans('jsonapi::errors.not_found'), 54 | ], 55 | 'results' => $this->results->show($idd), 56 | ]); 57 | } 58 | 59 | public function update($idd) 60 | { 61 | return $this->render([ 62 | 'success' => 200, 63 | 'error' => [ 64 | 'class' => \Symfony\Component\HttpKernel\Exception\HttpException::class, 65 | 'message' => 500, 66 | ], 67 | 'results' => $this->results->update($idd), 68 | ]); 69 | } 70 | 71 | public function destroy($idd) 72 | { 73 | return $this->render([ 74 | 'success' => 200, 75 | 'error' => [ 76 | 'class' => \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class, 77 | 'message' => trans('jsonapi::errors.not_found'), 78 | ], 79 | 'data' => $this->results->show($idd), 80 | 'results' => $this->results->destroy($idd), 81 | ]); 82 | } 83 | 84 | private function render($data) 85 | { 86 | if ($data['results']) { 87 | return response()->jsonapi($data['success'], (new ApiTransformer())->transform(isset($data['data']) ? $data['data'] : $data['results'])); 88 | } 89 | 90 | throw new $data['error']['class']($data['error']['message']); 91 | } 92 | 93 | public function getAuth() 94 | { 95 | return isset($this->auth) ? $this->auth : false; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Traits/ModelTrait.php: -------------------------------------------------------------------------------- 1 | includes) ? $this->includes : []; 17 | } 18 | 19 | public function getObjects() 20 | { 21 | if (! $this->objects) { 22 | $this->objects = new ApiObjects($this); 23 | } 24 | 25 | return $this->objects; 26 | } 27 | 28 | /** 29 | * The validation rules assigned in model. 30 | * 31 | * @var string 32 | * 33 | * @return array 34 | */ 35 | public function getRule($rule) 36 | { 37 | return isset($this->rules[$rule]) ? $this->rules[$rule] : []; 38 | } 39 | 40 | /** 41 | * The id_field defined in the model. 42 | * 43 | * @return string 44 | */ 45 | public function getId() 46 | { 47 | return isset($this->primaryKey) ? $this->primaryKey : 'id'; 48 | } 49 | 50 | /** 51 | * Return if Model has searchable flag. 52 | * 53 | * @return bool 54 | */ 55 | public function isSearchable() 56 | { 57 | return isset($this->searchable); 58 | } 59 | 60 | /** 61 | * Set order/sort as per json spec. 62 | * TO-DO: Should go into the ApiValidation class so it can manage relational sorts, ie sort=-profiles.id,users.id. 63 | * 64 | * @param string $query 65 | * @param string $sort 66 | * 67 | * @return object 68 | */ 69 | public function scopesetSort($query, $sort) 70 | { 71 | if (empty($sort) || ! is_string($sort) || empty($sorted = explode(',', $sort))) { 72 | return $query; 73 | } 74 | 75 | $columns = $this->columns(); 76 | 77 | $errors = array_filter(array_diff(array_map(function ($string) { 78 | return ltrim($string, '-'); 79 | }, $sorted), $columns)); 80 | 81 | if (! empty($errors)) { 82 | throw (new BadRequestException('invalid_sort'))->withDetails([[$this->getTable(), implode(' ', $errors)]]); 83 | } 84 | 85 | array_map(function ($column) use ($query) { 86 | return $query->orderBy(ltrim($column, '-'), ('-' === $column[0]) ? 'DESC' : 'ASC'); 87 | }, $sorted); 88 | 89 | return $query; 90 | } 91 | 92 | /** 93 | * Filter results based on filter get variable and transform them if enabled. 94 | * 95 | * @return array 96 | */ 97 | public function scopefilterAndTransform() 98 | { 99 | $fields = app('api')->fields(); 100 | 101 | $key = $this->getTable(); 102 | 103 | $results = $this->isTransformable($this) ? $this->transform($this) : $this; 104 | 105 | if ($fields->has($key)) { 106 | $results = array_diff_key($results, array_flip(array_diff(array_keys($results), $fields->get($key)))); 107 | } 108 | 109 | return $results; 110 | } 111 | 112 | /** 113 | * List of columns related to this Model, Cached. 114 | * 115 | * @return array 116 | */ 117 | private $cols; 118 | 119 | public function columns() 120 | { 121 | if (! $this->cols) { 122 | $this->cols = DB::connection()->getSchemaBuilder()->getColumnListing($this->getTable()); 123 | } 124 | 125 | return $this->cols; 126 | } 127 | 128 | /** 129 | * Checks whether the object is transformable or not. 130 | * 131 | * @param $item 132 | * 133 | * @return bool 134 | */ 135 | private function isTransformable($item) 136 | { 137 | return is_object($item) && method_exists($item, 'transform'); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Traits/SearchableTrait.php: -------------------------------------------------------------------------------- 1 | scopeSearchRestricted($qry, $search, $threshold, $entireText); 39 | } 40 | 41 | public function scopeSearchRestricted(Builder $qry, $search, $threshold = null, $entireText = null) 42 | { 43 | $query = clone $qry; 44 | $query->select($this->getTable().'.*'); 45 | $this->makeJoins($query); 46 | 47 | $search = mb_strtolower(trim($search)); 48 | $words = explode(' ', $search); 49 | 50 | $selects = []; 51 | $this->search_bindings = []; 52 | $relevanceCount = 0; 53 | 54 | foreach ($this->getColumns() as $column => $relevance) { 55 | $relevanceCount += $relevance; 56 | $queries = $this->getSearchQueriesForColumn($column, $relevance, $words); 57 | 58 | if ($entireText) { 59 | $queries[] = $this->getSearchQuery($column, $relevance, [$search], 30, '', '%'); 60 | } 61 | 62 | foreach ($queries as $select) { 63 | $selects[] = $select; 64 | } 65 | } 66 | 67 | $this->addSelectsToQuery($query, $selects); 68 | 69 | // Default the threshold if no value was passed. 70 | if (is_null($threshold)) { 71 | $threshold = $relevanceCount / 4; 72 | } 73 | 74 | $this->filterQueryWithRelevance($query, $selects, $threshold); 75 | 76 | $this->makeGroupBy($query); 77 | 78 | $this->addBindingsToQuery($query, $this->search_bindings); 79 | 80 | $this->mergeQueries($query, $qry); 81 | 82 | return $qry; 83 | } 84 | 85 | /** 86 | * Returns database driver Ex: mysql, pgsql, sqlite. 87 | * 88 | * @return array 89 | */ 90 | protected function getDatabaseDriver() 91 | { 92 | $key = $this->connection ?: config('database.default'); 93 | 94 | return config('database.connections.'.$key.'.driver'); 95 | } 96 | 97 | /** 98 | * Returns the search columns. 99 | * 100 | * @return array 101 | */ 102 | protected function getColumns() 103 | { 104 | if (isset($this->searchable) && array_key_exists('columns', $this->searchable)) { 105 | return $this->searchable['columns']; 106 | } 107 | 108 | return DB::connection()->getSchemaBuilder()->getColumnListing($this->getTable()); 109 | } 110 | 111 | /** 112 | * Returns whether or not to keep duplicates. 113 | * 114 | * @return array 115 | */ 116 | protected function getGroupBy() 117 | { 118 | if (isset($this->searchable) && array_key_exists('groupBy', $this->searchable)) { 119 | return $this->searchable['groupBy']; 120 | } 121 | 122 | return false; 123 | } 124 | 125 | /** 126 | * Returns the tables that are to be joined. 127 | * 128 | * @return array 129 | */ 130 | protected function getJoins() 131 | { 132 | return array_get($this->searchable, 'joins', []); 133 | } 134 | 135 | /** 136 | * Adds the sql joins to the query. 137 | * 138 | * @param \Illuminate\Database\Eloquent\Builder $query 139 | */ 140 | protected function makeJoins(Builder $query) 141 | { 142 | foreach ($this->getJoins() as $table => $keys) { 143 | $query->leftJoin($table, function ($join) use ($keys) { 144 | $join->on($keys[0], '=', $keys[1]); 145 | if (array_key_exists(2, $keys) && array_key_exists(3, $keys)) { 146 | $join->where($keys[2], '=', $keys[3]); 147 | } 148 | }); 149 | } 150 | } 151 | 152 | /** 153 | * Makes the query not repeat the results. 154 | * 155 | * @param \Illuminate\Database\Eloquent\Builder $query 156 | */ 157 | protected function makeGroupBy(Builder $query) 158 | { 159 | if ($groupBy = $this->getGroupBy()) { 160 | $query->groupBy($groupBy); 161 | 162 | return $query; 163 | } 164 | 165 | $columns = $this->getTable().'.'.$this->primaryKey; 166 | 167 | $query->groupBy($columns); 168 | 169 | $joins = array_keys(($this->getJoins())); 170 | 171 | foreach (array_keys($this->getColumns()) as $column) { 172 | array_map(function ($join) use ($column, $query) { 173 | if (str_contains($column, $join)) { 174 | $query->groupBy($column); 175 | } 176 | }, $joins); 177 | } 178 | } 179 | 180 | /** 181 | * Puts all the select clauses to the main query. 182 | * 183 | * @param \Illuminate\Database\Eloquent\Builder $query 184 | * @param array $selects 185 | */ 186 | protected function addSelectsToQuery(Builder $query, array $selects) 187 | { 188 | $selects = new Expression('max('.implode(' + ', $selects).') as relevance'); 189 | $query->addSelect($selects); 190 | } 191 | 192 | /** 193 | * Adds the relevance filter to the query. 194 | * 195 | * @param \Illuminate\Database\Eloquent\Builder $query 196 | * @param array $selects 197 | * @param float $relevanceCount 198 | */ 199 | protected function filterQueryWithRelevance(Builder $query, array $selects, $relevanceCount) 200 | { 201 | $comparator = $this->getDatabaseDriver() !== 'mysql' ? implode(' + ', $selects) : 'relevance'; 202 | 203 | $relevanceCount = number_format($relevanceCount, 2, '.', ''); 204 | 205 | $query->havingRaw("$comparator > $relevanceCount"); 206 | $query->orderBy('relevance', 'desc'); 207 | 208 | // add bindings to postgres 209 | } 210 | 211 | /** 212 | * Returns the search queries for the specified column. 213 | * 214 | * @param \Illuminate\Database\Eloquent\Builder $query 215 | * @param string $column 216 | * @param float $relevance 217 | * @param array $words 218 | * 219 | * @return array 220 | */ 221 | protected function getSearchQueriesForColumn($column, $relevance, array $words) 222 | { 223 | $queries = []; 224 | 225 | $queries[] = $this->getSearchQuery($column, $relevance, $words, 15); 226 | $queries[] = $this->getSearchQuery($column, $relevance, $words, 5, '', '%'); 227 | $queries[] = $this->getSearchQuery($column, $relevance, $words, 1, '%', '%'); 228 | 229 | return $queries; 230 | } 231 | 232 | /** 233 | * Returns the sql string for the given parameters. 234 | * 235 | * @param \Illuminate\Database\Eloquent\Builder $query 236 | * @param string $column 237 | * @param string $relevance 238 | * @param array $words 239 | * @param string $compare 240 | * @param float $relevanceMultiplier 241 | * @param string $preWord 242 | * @param string $postWord 243 | * 244 | * @return string 245 | */ 246 | protected function getSearchQuery($column, $relevance, array $words, $relevanceMultiplier, $preWord = '', $postWord = '') 247 | { 248 | $likeComparator = $this->getDatabaseDriver() === 'pgsql' ? 'ILIKE' : 'LIKE'; 249 | $cases = []; 250 | 251 | foreach ($words as $word) { 252 | $cases[] = $this->getCaseCompare($column, $likeComparator, $relevance * $relevanceMultiplier); 253 | $this->search_bindings[] = $preWord.$word.$postWord; 254 | } 255 | 256 | return implode(' + ', $cases); 257 | } 258 | 259 | /** 260 | * Returns the comparison string. 261 | * 262 | * @param string $column 263 | * @param string $compare 264 | * @param float $relevance 265 | * 266 | * @return string 267 | */ 268 | protected function getCaseCompare($column, $compare, $relevance) 269 | { 270 | /* commented out for CI 271 | } 272 | */ 273 | $column = str_replace('.', '`.`', $column); 274 | $field = 'LOWER(`'.$column.'`) '.$compare.' ?'; 275 | 276 | return '(case when '.$field.' then '.$relevance.' else 0 end)'; 277 | } 278 | 279 | /** 280 | * Adds the bindings to the query. 281 | * 282 | * @param \Illuminate\Database\Eloquent\Builder $query 283 | * @param array $bindings 284 | */ 285 | protected function addBindingsToQuery(Builder $query, array $bindings) 286 | { 287 | $count = $this->getDatabaseDriver() !== 'mysql' ? 2 : 1; 288 | for ($i = 0; $i < $count; $i++) { 289 | foreach ($bindings as $binding) { 290 | $type = $i === 0 ? 'where' : 'having'; 291 | $query->addBinding($binding, $type); 292 | } 293 | } 294 | } 295 | 296 | /** 297 | * Merge our cloned query builder with the original one. 298 | * 299 | * @param \Illuminate\Database\Eloquent\Builder $clone 300 | * @param \Illuminate\Database\Eloquent\Builder $original 301 | */ 302 | protected function mergeQueries(Builder $clone, Builder $original) 303 | { 304 | $tableName = DB::connection($this->connection)->getTablePrefix().$this->getTable(); 305 | 306 | $original->from(DB::connection($this->connection)->raw("({$clone->toSql()}) as `{$tableName}`")); 307 | 308 | $original->mergeBindings($clone->getQuery()); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/Transformers/ApiTransformer.php: -------------------------------------------------------------------------------- 1 | object = $object; 28 | 29 | $results = $this->isPaginator() ? $this->transformPaginator() : $this->transformObject(); 30 | 31 | return (new KeysTransformer())->transform($results); 32 | } 33 | 34 | /** 35 | * Transform Pagination. 36 | * 37 | * @return array 38 | */ 39 | private function transformPaginator() 40 | { 41 | $results = array_map(function ($object) { 42 | return $this->transformation($object, false); 43 | }, $this->object->all()); 44 | 45 | return array_merge(['data' => $results], $this->getPaginationMeta()); 46 | } 47 | 48 | /** 49 | * Transform objects. 50 | * 51 | * @return transformation 52 | */ 53 | private function transformObject() 54 | { 55 | return $this->transformation($this->object, true); 56 | } 57 | 58 | /** 59 | * Build the transformed results. 60 | * 61 | * @param object $object 62 | * @param bool $single 63 | * 64 | * @return array 65 | */ 66 | private function transformation($object, $single) 67 | { 68 | $includes = $this->objectIncludes($object); 69 | 70 | $item = $single ? ['data' => $this->item($object)] : $this->item($object); 71 | $data = array_merge($item, ['relationships' => $this->relations($includes)]); 72 | 73 | return array_filter(array_merge( 74 | $data, 75 | ['included' => $includes], 76 | $single ? ['links' => ['self' => request()->url()]] : [] 77 | )); 78 | } 79 | 80 | /** 81 | * Build a list of includes for this object. 82 | * 83 | * @param object $object 84 | * 85 | * @return array 86 | */ 87 | private function objectIncludes($object) 88 | { 89 | $results = []; 90 | 91 | foreach (app('api')->includes() as $include) { 92 | if (is_object($object->$include)) { 93 | foreach ($object->$include as $included) { 94 | $results[] = $this->item($included); 95 | } 96 | } 97 | } 98 | 99 | return $results; 100 | } 101 | 102 | /** 103 | * Build json api style results per item. 104 | * 105 | * @param $object 106 | * 107 | * @return array 108 | */ 109 | private function item($object) 110 | { 111 | $pimaryId = $object->getId(); 112 | 113 | return [ 114 | 'type' => $object->getTable(), 115 | 'id' => $object->$pimaryId, 116 | 'attributes' => $object->filterAndTransform(), 117 | ]; 118 | } 119 | 120 | /** 121 | * Get relations for the included items. 122 | * 123 | * @param [type] $includes [description] 124 | * @param [type] $object [description] 125 | * 126 | * @return [type] [description] 127 | */ 128 | private function relations($includes) 129 | { 130 | return array_map(function ($inc) { 131 | return [$inc['type'] => ['data' => ['id' => $inc['attributes']['id'], 'type' => $inc['type']]]]; 132 | }, $includes); 133 | } 134 | 135 | /** 136 | * @param $object 137 | * 138 | * @return bool 139 | */ 140 | private function isPaginator() 141 | { 142 | return $this->object instanceof LengthAwarePaginator; 143 | } 144 | 145 | /** 146 | * Gets the pagination meta data. Assumes that a paginator 147 | * instance is passed \Illuminate\Pagination\LengthAwarePaginator. 148 | * 149 | * @param $paginator 150 | * 151 | * @return array 152 | */ 153 | private function getPaginationMeta() 154 | { 155 | $object = $this->object; 156 | 157 | return [ 158 | 'meta' => [ 159 | 'total' => $object->total(), 160 | 'currentPage' => $object->currentPage(), 161 | 'perPage' => $object->perPage(), 162 | 'hasMorePages' => $object->hasMorePages(), 163 | 'hasPages' => $object->hasPages(), 164 | ], 165 | 'links' => [ 166 | 'self' => $object->url($object->currentPage()), 167 | 'first' => $object->url(1), 168 | 'last' => $object->url($object->lastPage()), 169 | 'next' => $object->nextPageUrl(), 170 | 'prev' => $object->previousPageUrl(), 171 | ], 172 | ]; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Transformers/KeysTransformer.php: -------------------------------------------------------------------------------- 1 | ', 41 | '?', 42 | '@', 43 | '\\', 44 | '^', 45 | '`', 46 | '{', 47 | '|', 48 | '}', 49 | '~', 50 | ]; 51 | /** 52 | * @link http://jsonapi.org/format/#document-member-names-allowed-characters 53 | * 54 | * @var array 55 | */ 56 | protected $forbiddenFirstOrLast = [ 57 | '-', 58 | '_', 59 | ' ', 60 | ]; 61 | 62 | /** 63 | * Convert array indexes to json api spec indexes. 64 | * 65 | * @param array $array [description] 66 | * 67 | * @return array 68 | */ 69 | public function transform($array) 70 | { 71 | $results = []; 72 | foreach ($array as $key => $value) { 73 | $results[$this->convert($key)] = is_array($value) ? $this->transform($value) : $value; 74 | } 75 | 76 | return $results; 77 | } 78 | 79 | /** 80 | * Do the actual conversion. 81 | * 82 | * @param 83 | * 84 | * @return 85 | */ 86 | private function convert($key) 87 | { 88 | if (! is_string($key)) { 89 | return $key; 90 | } 91 | 92 | $firstLast = implode('', $this->forbiddenFirstOrLast); 93 | 94 | return str_replace($this->forbiddenCharacters, '', ltrim(rtrim($key, $firstLast), $firstLast)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/config/jsonapi.php: -------------------------------------------------------------------------------- 1 | env('JSONAPI_STRICT', false), 5 | 'version' => 'v1', 6 | 'json_version' => '1.0', 7 | 'url' => 'api/*', 8 | 'accept' => 'application/vnd.api+json', 9 | 'content_type' => 'application/vnd.api+json', 10 | 'allowed_get' => [ 11 | 'include' => '', 12 | 'fields' => '', 13 | 'page' => ['size', 'number'], 14 | 'sort' => '', 15 | 'search' => '', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /src/lang/en/errors.php: -------------------------------------------------------------------------------- 1 | 'Page not found', 6 | 7 | 'invalid-credentials' => [ 8 | 'title' => 'Unauthorized', 9 | 'detail' => 'Invalid credentials.', 10 | ], 11 | 12 | 'not-acceptable' => [ 13 | 'title' => 'Not Acceptable', 14 | 'detail' => 'Accept was not %s.', 15 | ], 16 | 17 | 'unsupported-media-type' => [ 18 | 'title' => 'Unsupported Media Type', 19 | 'detail' => 'Content-Type was not'.config('jsonapi.content-type'), 20 | ], 21 | 22 | 'invalid_sort' => [ 23 | 'title' => 'Invalid Query Parameter.', 24 | 'detail' => 'The resource `%s` does not have an `%s` sorting option.', 25 | 'source' => ['type' => 'parameter', 'value' => '%s.%s'], 26 | ], 27 | 28 | 'invalid_filter' => [ 29 | 'title' => 'Invalid Query Parameter.', 30 | 'detail' => 'The resource does not have an `%s` filter option.', 31 | 'source' => ['type' => 'parameter', 'value' => '%s'], 32 | ], 33 | 34 | 'invalid_include' => [ 35 | 'title' => 'Invalid Query Parameter', 36 | 'detail' => 'The resource does not have an `%s` relationship path.', 37 | 'source' => ['type' => 'parameter', 'value' => '%s'], 38 | ], 39 | 40 | 'invalid_get' => [ 41 | 'title' => 'Invalid Query Parameter', 42 | 'detail' => '%s is not an allowed query parameter.', 43 | 'source' => ['type' => 'parameter', 'value' => '%s'], 44 | ], 45 | 46 | 'invalid_attribute' => [ 47 | 'title' => 'Invalid Attribute', 48 | 'detail' => '%s', 49 | 'source' => ['type' => 'pointer', 'value' => '%s'], 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /tests/AcceptanceTestCase.php: -------------------------------------------------------------------------------- 1 | config('jsonapi.content_type'), 'Accept' => config('jsonapi.accept')], $headers)); 13 | 14 | return parent::json($method, $uri, $data, $headers); 15 | } 16 | 17 | /** 18 | * Create User Helpers. 19 | * 20 | * @return json 21 | */ 22 | public function createUser() 23 | { 24 | return $this->json('POST', '/api/user', [ 25 | 'data' => [ 26 | 'type' => 'users', 27 | 'attributes' => [ 28 | 'name' => 'Ember Hamster', 29 | 'email' => 'test@test.com', 30 | 'password' => bcrypt('password'), 31 | ], 32 | ], 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/AuthTest.php: -------------------------------------------------------------------------------- 1 | createUserRaw(); 12 | $this->json('GET', '/api/me/profile', [], ['Authorization' => 'Basic YWRtaW5AbG9jYWxob3N0LmNvbTpwYXNzd29yZA==']); 13 | $response = $this->response; 14 | $this->assertEquals(200, $response->getStatusCode()); 15 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 16 | $this->seeOrSaveJsonStructure($response); 17 | } 18 | 19 | public function testBadAuth() 20 | { 21 | $this->createUserRaw(); 22 | $this->json('GET', '/api/me/profile', [], ['Authorization' => 'Basic ZWRtaW5AbG9jYWxob3N0LmNvbTpwYXNzd29yZA==']); 23 | $response = $this->response; 24 | 25 | $this->assertEquals(401, $response->getStatusCode()); 26 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/CrudTest.php: -------------------------------------------------------------------------------- 1 | createUser(); 12 | $response = $results->response; 13 | $this->assertEquals(200, $response->getStatusCode()); 14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 15 | } 16 | 17 | public function testRead() 18 | { 19 | $this->createUser(); 20 | $this->json('GET', '/api/user/1'); 21 | $response = $this->response; 22 | $this->assertEquals(200, $response->getStatusCode()); 23 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 24 | $this->seeOrSaveJsonStructure($response); 25 | } 26 | 27 | public function testDelete() 28 | { 29 | $this->createUser(); 30 | 31 | $this->json('DELETE', '/api/user/1'); 32 | $response = $this->response; 33 | $this->assertEquals(200, $response->getStatusCode()); 34 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/HeadersTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/api/user/', [], ['Content-Type' => 'test']); 12 | $response = $this->response; 13 | $this->assertEquals(415, $response->getStatusCode()); 14 | } 15 | 16 | public function testBadContentAccept() 17 | { 18 | $this->json('GET', '/api/user/', [], ['Accept' => 'test']); 19 | $response = $this->response; 20 | $this->assertEquals(406, $response->getStatusCode()); 21 | } 22 | 23 | public function testVersionContentType() 24 | { 25 | $this->json('GET', '/api/user/', [], ['Accept' => 'application/vnd.api.v1+json']); 26 | $response = $this->response; 27 | $this->assertEquals(200, $response->getStatusCode()); 28 | } 29 | 30 | public function testBadVersion() 31 | { 32 | $this->json('GET', '/api/user/', [], ['Accept' => 'application/vnd.api.v2+json']); 33 | $response = $this->response; 34 | $this->assertEquals(406, $response->getStatusCode()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/IncludesTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/api/user/?include=badtest'); 12 | $response = $this->response; 13 | $this->assertEquals(400, $response->getStatusCode()); 14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 15 | } 16 | 17 | public function testInclude() 18 | { 19 | $this->createUserRaw(); 20 | $this->json('GET', '/api/user/?include=profiles'); 21 | $response = $this->response; 22 | $this->assertEquals(200, $response->getStatusCode()); 23 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 24 | } 25 | 26 | public function testListWithIncludeFields() 27 | { 28 | $this->createUserRaw(); 29 | 30 | $this->json('GET', '/api/user?fields[profiles]=id,phone'); 31 | $response = $this->response; 32 | $this->assertEquals(200, $response->getStatusCode()); 33 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/JsonTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/api/404'); 12 | $response = $this->response; 13 | $this->assertEquals(404, $response->getStatusCode()); 14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 15 | } 16 | 17 | public function testError404User() 18 | { 19 | $this->json('GET', '/api/user/404'); 20 | $response = $this->response; 21 | $this->assertEquals(404, $response->getStatusCode()); 22 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 23 | } 24 | 25 | public function testBadQueryVar() 26 | { 27 | $this->json('GET', '/api/user/?badtest'); 28 | $response = $this->response; 29 | $this->assertEquals(400, $response->getStatusCode()); 30 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 31 | } 32 | 33 | public function testNonApiException() 34 | { 35 | $this->json('GET', '/not-the-api'); 36 | $response = $this->response; 37 | $this->assertEquals(404, $response->getStatusCode()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/ListTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/api/user'); 12 | $response = $this->response; 13 | $this->assertEquals(200, $response->getStatusCode()); 14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 15 | $this->seeOrSaveJsonStructure(); 16 | } 17 | 18 | public function testSort() 19 | { 20 | $this->json('GET', '/api/user?sort=-id'); 21 | $response = $this->response; 22 | $this->assertEquals(200, $response->getStatusCode()); 23 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 24 | $this->seeOrSaveJsonStructure(); 25 | } 26 | 27 | public function testBadSort() 28 | { 29 | $this->json('GET', '/api/user?sort=-test'); 30 | $response = $this->response; 31 | $this->assertEquals(400, $response->getStatusCode()); 32 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 33 | } 34 | 35 | public function testListWithFields() 36 | { 37 | $this->createUser(); 38 | 39 | $this->json('GET', '/api/user?fields[users]=id,name'); 40 | $response = $this->response; 41 | $this->assertEquals(200, $response->getStatusCode()); 42 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 43 | } 44 | 45 | public function testListWithBadFields() 46 | { 47 | $this->createUser(); 48 | 49 | $this->json('GET', '/api/user?fields[users]=id,name,badtest'); 50 | $response = $this->response; 51 | $this->assertEquals(400, $response->getStatusCode()); 52 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 53 | } 54 | 55 | public function testListWithBadFieldName() 56 | { 57 | $this->createUser(); 58 | 59 | $this->json('GET', '/api/user?fields[badtest]=id,name,bad'); 60 | $response = $this->response; 61 | $this->assertEquals(400, $response->getStatusCode()); 62 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 63 | } 64 | 65 | public function testSearch() 66 | { 67 | $this->json('GET', '/api/user?search=test'); 68 | $response = $this->response; 69 | $this->assertEquals(200, $response->getStatusCode()); 70 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 71 | } 72 | 73 | public function testSearchEmpty() 74 | { 75 | $this->json('GET', '/api/user?search='); 76 | $response = $this->response; 77 | $this->assertEquals(200, $response->getStatusCode()); 78 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 79 | } 80 | 81 | public function testPagination() 82 | { 83 | $this->json('GET', '/api/user?page[size]=1&page[number]=1'); 84 | $response = $this->response; 85 | $this->assertEquals(200, $response->getStatusCode()); 86 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 87 | } 88 | 89 | public function testPaginationBadField() 90 | { 91 | $this->json('GET', '/api/user?page[badtest]=1&page[number]=1'); 92 | $response = $this->response; 93 | $this->assertEquals(400, $response->getStatusCode()); 94 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/PatchTest.php: -------------------------------------------------------------------------------- 1 | createUser(); 12 | $this->json('PATCH', '/api/user/1', [ 13 | 'data' => [ 14 | 'type' => 'users', 15 | 'attributes' => [ 16 | 'name' => 'testupdate', 17 | ], 18 | ], 19 | ]); 20 | $response = $this->response; 21 | $this->assertEquals(200, $response->getStatusCode()); 22 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 23 | $this->seeJson(['name' => 'testupdate']); 24 | } 25 | 26 | public function testBadPatchField() 27 | { 28 | $this->createUser(); 29 | $this->json('PATCH', '/api/user/1', [ 30 | 'test' => 'test', 31 | ]); 32 | 33 | $response = $this->response; 34 | $this->assertEquals(400, $response->getStatusCode()); 35 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 36 | } 37 | 38 | public function testPatchValidation() 39 | { 40 | $this->createUser(); 41 | $this->json('PATCH', '/api/user/1', [ 42 | 'data' => [ 43 | 'type' => 'users', 44 | 'attributes' => [ 45 | 'email' => 'notanemail', 46 | ], 47 | ], 48 | ]); 49 | 50 | $response = $this->response; 51 | $this->assertEquals(403, $response->getStatusCode()); 52 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 53 | } 54 | 55 | public function testPatch404() 56 | { 57 | $this->json('PATCH', '/api/user/404', [ 58 | 'data' => [ 59 | 'type' => 'users', 60 | 'attributes' => [ 61 | 'name' => 'Ember Hamster kpok', 62 | ], 63 | ], 64 | ]); 65 | 66 | $response = $this->response; 67 | $this->assertEquals(500, $response->getStatusCode()); 68 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/AcceptanceTests/PostTest.php: -------------------------------------------------------------------------------- 1 | json('POST', '/api/user', [ 12 | 'data' => [ 13 | 'type' => 'users', 14 | 'attributes' => [ 15 | 'badfield' => 'Ember Hamster kpok', 16 | ], 17 | ], 18 | ]); 19 | 20 | $response = $this->response; 21 | $this->assertEquals(400, $response->getStatusCode()); 22 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 23 | } 24 | 25 | public function testPostValidation() 26 | { 27 | $this->json('POST', '/api/user', [ 28 | 'data' => [ 29 | 'type' => 'users', 30 | 'attributes' => [ 31 | 'email' => 'test', 32 | ], 33 | ], 34 | ]); 35 | 36 | $response = $this->response; 37 | $this->assertEquals(403, $response->getStatusCode()); 38 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/App/Database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /tests/App/Database/Migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Askedio/laravel-Cruddy/3077f8b619b68b9d1a4873d56a0f415c207b46c0/tests/App/Database/Migrations/.gitkeep -------------------------------------------------------------------------------- /tests/App/Database/Migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | getSchemaBuilder()->create('users', function (Blueprint $table) { 16 | $table->increments('id'); 17 | $table->string('name'); 18 | $table->string('email')->unique(); 19 | $table->string('password', 60); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | DB::connection()->getSchemaBuilder()->dropIfExists('users'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/App/Database/Migrations/2016_03_09_190517_create_profiles_table.php: -------------------------------------------------------------------------------- 1 | getSchemaBuilder()->create('profiles', function (Blueprint $table) { 16 | $table->increments('id'); 17 | $table->integer('user_id'); 18 | $table->string('phone'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | DB::connection()->getSchemaBuilder()->dropIfExists('profiles'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/App/Database/Migrations/2016_03_09_223910_create_profiles_lookup_table.php: -------------------------------------------------------------------------------- 1 | getSchemaBuilder()->create('profiles_user', function (Blueprint $table) { 16 | $table->integer('user_id'); 17 | $table->integer('profiles_id'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | DB::connection()->getSchemaBuilder()->dropIfExists('profiles_user'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/App/Http/Controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | 'api', 'middleware' => ['api', 'jsonapi']], function () { 15 | Route::resource('user', 'Askedio\Tests\App\Http\Controllers\UserController'); 16 | Route::resource('profile', 'Askedio\Tests\App\Http\Controllers\ProfileController'); 17 | }); 18 | 19 | Route::group(['prefix' => 'api/me', 'middleware' => ['api', 'jsonapi', 'jsonapi.auth.basic']], function () { 20 | Route::resource('profile', 'Askedio\Tests\App\Http\Controllers\ProfileController'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/App/Profiles.php: -------------------------------------------------------------------------------- 1 | belongsTo('Askedio\Tests\App\User'); 18 | } 19 | 20 | /** 21 | * The attributes that are mass assignable. 22 | * 23 | * @var array 24 | */ 25 | protected $fillable = [ 26 | 'phone', 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /tests/App/ProfilesAlt.php: -------------------------------------------------------------------------------- 1 | belongsTo('Askedio\Tests\App\User'); 18 | } 19 | 20 | /** 21 | * The attributes that are mass assignable. 22 | * 23 | * @var array 24 | */ 25 | protected $fillable = [ 26 | 'phone', 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /tests/App/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | realpath(__DIR__.'/../Database/Migrations') => database_path('migrations'), 19 | ], 'migrations'); 20 | $this->publishes([ 21 | realpath(__DIR__.'/../Database/Seeds') => database_path('seeds'), 22 | ], 'seeds'); 23 | 24 | parent::boot($router); 25 | } 26 | 27 | /** 28 | * Define the routes for the application. 29 | * 30 | * @param \Illuminate\Routing\Router $router 31 | */ 32 | public function map(Router $router) 33 | { 34 | $router->group(['namespace' => $this->namespace], function ($router) { 35 | require dirname(__FILE__).'/../Http/routes.php'; 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/App/User.php: -------------------------------------------------------------------------------- 1 | [ 36 | 'email' => 'email|unique:users,email', 37 | ], 38 | 'create' => [ 39 | 'email' => 'email|required|unique:users,email', 40 | ], 41 | ]; 42 | 43 | protected $searchable = [ 44 | 'columns' => [ 45 | 'users.name' => 10, 46 | 'users.email' => 5, 47 | 'profiles.user_id' => 5, 48 | ], 49 | 'joins' => [ 50 | 'profiles' => ['users.id', 'profiles.user_id'], 51 | ], 52 | 'groupBy' => 'profiles.user_id', 53 | ]; 54 | 55 | protected $primaryKey = 'id'; 56 | 57 | public function transform(User $user) 58 | { 59 | return [ 60 | 'id' => $user->id, 61 | 'name' => $user->name, 62 | 'email' => $user->email, 63 | ]; 64 | } 65 | 66 | public function profiles() 67 | { 68 | return $this->hasMany('Askedio\Tests\App\Profiles'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/App/UserAlt.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'email' => 'email|unique:users,email', 39 | ], 40 | 'create' => [ 41 | 'email' => 'email|required|unique:users,email', 42 | ], 43 | ]; 44 | 45 | protected $searchable = [ 46 | 'columns' => [ 47 | 'users.name' => 10, 48 | 'users.email' => 5, 49 | 'profiles.phone' => 5, 50 | ], 51 | 'joins' => [ 52 | 'profiles' => ['users.id', 'profiles.user_id', 'users.id', 'profiles.user_id'], 53 | ], 54 | ]; 55 | 56 | protected $primaryKey = 'id'; 57 | 58 | public function transform(User $user) 59 | { 60 | return [ 61 | 'id' => $user->id, 62 | 'name' => $user->name, 63 | 'email' => $user->email, 64 | ]; 65 | } 66 | 67 | public function profiles() 68 | { 69 | return $this->hasMany('Askedio\Tests\App\Profiles'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | app['config']->set('database.default', 'sqlite'); 25 | $this->app['config']->set('database.connections.sqlite.database', ':memory:'); 26 | $this->app['config']->set('app.url', 'http://localhost/'); 27 | $this->app['config']->set('app.debug', false); 28 | $this->app['config']->set('app.key', env('APP_KEY', '1234567890123456')); 29 | $this->app['config']->set('app.cipher', 'AES-128-CBC'); 30 | $this->app['config']->set('auth.providers.users.model', \Askedio\Tests\App\User::class); 31 | 32 | $this->app->boot(); 33 | 34 | $this->migrate(); 35 | } 36 | 37 | /** 38 | * run package database migrations. 39 | */ 40 | public function migrate() 41 | { 42 | $fileSystem = new Filesystem(); 43 | $classFinder = new ClassFinder(); 44 | 45 | foreach ($fileSystem->files(__DIR__.'/App/Database/Migrations') as $file) { 46 | $fileSystem->requireOnce($file); 47 | $migrationClass = $classFinder->findClass($file); 48 | (new $migrationClass())->down(); 49 | (new $migrationClass())->up(); 50 | } 51 | } 52 | 53 | /** 54 | * Boots the application. 55 | * 56 | * @return \Illuminate\Foundation\Application 57 | */ 58 | public function createApplication() 59 | { 60 | /** @var $app \Illuminate\Foundation\Application */ 61 | $app = require __DIR__.'/../vendor/laravel/laravel/bootstrap/app.php'; 62 | 63 | $this->setUpHttpKernel($app); 64 | $app->register(\Askedio\Laravel5ApiController\Providers\GenericServiceProvider::class); 65 | $app->register(\Askedio\Tests\App\Providers\RouteServiceProvider::class); 66 | 67 | return $app; 68 | } 69 | 70 | /** 71 | * @return Router 72 | */ 73 | protected function getRouter() 74 | { 75 | $router = new Router(new Dispatcher()); 76 | 77 | return $router; 78 | } 79 | 80 | /** 81 | * @param \Illuminate\Foundation\Application $app 82 | */ 83 | private function setUpHttpKernel($app) 84 | { 85 | $app->instance('request', (new \Illuminate\Http\Request())->instance()); 86 | $app->make('Illuminate\Foundation\Http\Kernel', [$app, $this->getRouter()])->bootstrap(); 87 | } 88 | 89 | /** 90 | * Temporary. 91 | */ 92 | public function createUserRaw() 93 | { 94 | /* temporary since we dont have relational creation yet */ 95 | return (new User())->create([ 96 | 'name' => 'admin', 97 | 'email' => 'admin@localhost.com', 98 | 'password' => bcrypt('password'), 99 | ])->profiles()->saveMany([ 100 | new Profiles(['phone' => '123']), 101 | ]); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/IntegrationTestCase.php: -------------------------------------------------------------------------------- 1 | createUserRaw(); 13 | 14 | * 15 | * need some way to pass form/input data 16 | * $results = $this->api()->index(); 17 | */ 18 | 19 | // ... 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/IntegrationTests/SearchTest.php: -------------------------------------------------------------------------------- 1 | search('test')->with('profiles')->get(); 15 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $search); 16 | } 17 | 18 | public function testSearchGroupBy() 19 | { 20 | $search = (new User())->search('test')->with('profiles')->get(); 21 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $search); 22 | } 23 | 24 | public function testSearchNoColumnsDefined() 25 | { 26 | $search = (new Profiles())->search('test'); 27 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Builder', $search); 28 | } 29 | 30 | public function testSearchAltGroupBy() 31 | { 32 | $search = (new UserAlt())->search('test')->with('profiles')->get(); 33 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $search); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Main.json.postman_collection: -------------------------------------------------------------------------------- 1 | { 2 | "id": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 3 | "name": "Main", 4 | "description": "", 5 | "order": [ 6 | "a2751967-0b45-0868-cc4d-370658e70470", 7 | "d37258a7-8074-a30d-9926-84363d7578ea", 8 | "8ce49bc4-4b3d-4da2-0a38-624bbdb2a844", 9 | "b15ccc96-82d6-c838-ea73-034836395500", 10 | "3d5535b8-f059-8237-36f3-aca6f40d83e3", 11 | "46050c55-b832-69b3-2c60-93ebfd2f9b48", 12 | "e8755e27-4396-cf48-5d77-4e20d6b4906e", 13 | "847e81b7-2f7e-7196-598b-c7079b94e06d", 14 | "76a6a3d5-2697-367c-febe-f0c517857ff6" 15 | ], 16 | "folders": [ 17 | { 18 | "id": "6cafb725-c73c-ca4c-47af-c12ac1250db0", 19 | "name": "Errors", 20 | "description": "", 21 | "order": [ 22 | "7b8f4299-fb53-992e-b94d-1cd1c05f4528", 23 | "f04d11a1-3e9b-0213-ed77-6d8f7db91d38", 24 | "0db676e2-f8a8-3309-c305-9b386ca3c5ef" 25 | ], 26 | "owner": "391915", 27 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71" 28 | } 29 | ], 30 | "timestamp": 1457906270302, 31 | "owner": "391915", 32 | "remoteLink": "https://www.getpostman.com/collections/f67615f76d74506553d6", 33 | "public": false, 34 | "requests": [ 35 | { 36 | "id": "0db676e2-f8a8-3309-c305-9b386ca3c5ef", 37 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 38 | "url": "http://localhost:8000/api/admin/user?page[limiit]=1&page[number]=2", 39 | "preRequestScript": null, 40 | "pathVariables": {}, 41 | "method": "GET", 42 | "data": null, 43 | "dataMode": "params", 44 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\n\ntests[\"Status code is 400\"] = responseCode.code === 400;", 45 | "currentHelper": "normal", 46 | "helperAttributes": {}, 47 | "time": 1458236437329, 48 | "name": "List Pagination Bad Page Variable", 49 | "description": " ", 50 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 51 | "responses": [], 52 | "folder": "6cafb725-c73c-ca4c-47af-c12ac1250db0" 53 | }, 54 | { 55 | "id": "3d5535b8-f059-8237-36f3-aca6f40d83e3", 56 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 57 | "url": "http://localhost:8000/api/admin/user?include=profiles", 58 | "preRequestScript": "", 59 | "pathVariables": {}, 60 | "method": "GET", 61 | "data": [], 62 | "dataMode": "params", 63 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\ntests[\"Status code is 200\"] = responseCode.code === 200;\n\n", 64 | "currentHelper": "normal", 65 | "helperAttributes": {}, 66 | "time": 1457906683361, 67 | "name": "List", 68 | "description": "", 69 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 70 | "responses": [] 71 | }, 72 | { 73 | "id": "46050c55-b832-69b3-2c60-93ebfd2f9b48", 74 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 75 | "url": "http://localhost:8000/api/admin/user?search=hamster", 76 | "preRequestScript": null, 77 | "pathVariables": {}, 78 | "method": "GET", 79 | "data": null, 80 | "dataMode": "params", 81 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\n\ntests[\"Status code is 200\"] = responseCode.code === 200;", 82 | "currentHelper": "normal", 83 | "helperAttributes": {}, 84 | "time": 1458450215423, 85 | "name": "List Search", 86 | "description": " ", 87 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 88 | "responses": [] 89 | }, 90 | { 91 | "id": "76a6a3d5-2697-367c-febe-f0c517857ff6", 92 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 93 | "url": "http://localhost:8000/api/admin/user/1", 94 | "preRequestScript": null, 95 | "pathVariables": {}, 96 | "method": "DELETE", 97 | "data": null, 98 | "dataMode": "params", 99 | "version": 2, 100 | "tests": "tests[\"Content-Type is application/vnd.api+json\"] = postman.getResponseHeader(\"Content-Type\");", 101 | "currentHelper": "normal", 102 | "helperAttributes": {}, 103 | "time": 1458450685116, 104 | "name": "Delete User", 105 | "description": "", 106 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 107 | "responses": [] 108 | }, 109 | { 110 | "id": "7b8f4299-fb53-992e-b94d-1cd1c05f4528", 111 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 112 | "url": "http://localhost:8000/api/admin/user?fields[profiles]=id&include=profiles&sort=-id&adf", 113 | "preRequestScript": null, 114 | "pathVariables": {}, 115 | "method": "GET", 116 | "data": null, 117 | "dataMode": "params", 118 | "tests": "\nvar jsonData = JSON.parse(responseBody);\ntests[\"Your test name\"] = jsonData.errors.title = \"Invalid Query Parameter\"\n\ntests[\"Status code is 400\"] = responseCode.code === 400;", 119 | "currentHelper": "normal", 120 | "helperAttributes": {}, 121 | "time": 1458451657752, 122 | "name": "Bad Query Param", 123 | "description": "", 124 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 125 | "responses": [], 126 | "folder": "6cafb725-c73c-ca4c-47af-c12ac1250db0" 127 | }, 128 | { 129 | "id": "847e81b7-2f7e-7196-598b-c7079b94e06d", 130 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 131 | "url": "http://localhost:8000/api/admin/user?include=profiles", 132 | "preRequestScript": "", 133 | "pathVariables": {}, 134 | "method": "GET", 135 | "data": [], 136 | "dataMode": "params", 137 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\ntests[\"Status code is 200\"] = responseCode.code === 200;\n\n", 138 | "currentHelper": "normal", 139 | "helperAttributes": {}, 140 | "time": 1457906683361, 141 | "name": "List Pagination Sort", 142 | "description": "", 143 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 144 | "responses": [] 145 | }, 146 | { 147 | "id": "8ce49bc4-4b3d-4da2-0a38-624bbdb2a844", 148 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 149 | "url": "http://localhost:8000/api/admin/user/1", 150 | "preRequestScript": "", 151 | "pathVariables": {}, 152 | "method": "GET", 153 | "data": [], 154 | "dataMode": "params", 155 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\ntests[\"Status code is 200\"] = responseCode.code === 200;\n\n", 156 | "currentHelper": "normal", 157 | "helperAttributes": {}, 158 | "time": 1458451647615, 159 | "name": "Get User", 160 | "description": "", 161 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 162 | "responses": [] 163 | }, 164 | { 165 | "id": "a2751967-0b45-0868-cc4d-370658e70470", 166 | "headers": "Accept: application/vnd.api+json\nContent-Type: application/vnd.api+json\n", 167 | "url": "http://localhost:8000/api/admin/user", 168 | "preRequestScript": null, 169 | "pathVariables": {}, 170 | "method": "POST", 171 | "data": [], 172 | "dataMode": "raw", 173 | "tests": null, 174 | "currentHelper": "normal", 175 | "helperAttributes": {}, 176 | "time": 1458409064024, 177 | "name": "Create User", 178 | "description": "", 179 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 180 | "responses": [], 181 | "rawModeData": "{\r\n \"data\": {\r\n \"type\": \"users\",\r\n \"attributes\": {\r\n \"name\": \"Ember Hamster\",\r\n \"email\": \"test@test.com\"\r\n }\r\n }\r\n}" 182 | }, 183 | { 184 | "id": "b15ccc96-82d6-c838-ea73-034836395500", 185 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 186 | "url": "http://localhost:8000/api/admin/user?fields[profiles]=id&include=profiles&sort=-id", 187 | "pathVariables": {}, 188 | "preRequestScript": null, 189 | "method": "GET", 190 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 191 | "data": null, 192 | "dataMode": "params", 193 | "name": "Includes & Profiles", 194 | "description": "", 195 | "descriptionFormat": "html", 196 | "time": 1457907027561, 197 | "version": 2, 198 | "responses": [], 199 | "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\n\n", 200 | "currentHelper": "normal", 201 | "helperAttributes": {} 202 | }, 203 | { 204 | "id": "d37258a7-8074-a30d-9926-84363d7578ea", 205 | "headers": "Accept: application/vnd.api+json\nContent-Type: application/vnd.api+json\n", 206 | "url": "http://localhost:8000/api/admin/user/1", 207 | "preRequestScript": null, 208 | "pathVariables": {}, 209 | "method": "PATCH", 210 | "data": [], 211 | "dataMode": "raw", 212 | "tests": null, 213 | "currentHelper": "normal", 214 | "helperAttributes": {}, 215 | "time": 1458450227925, 216 | "name": "Edit User", 217 | "description": "", 218 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 219 | "responses": [], 220 | "rawModeData": "{\r\n \"data\": {\r\n \"type\": \"users\",\r\n \"attributes\": {\r\n \"name\": \"Blue Hamster\"\r\n }\r\n }\r\n}" 221 | }, 222 | { 223 | "id": "e8755e27-4396-cf48-5d77-4e20d6b4906e", 224 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 225 | "url": "http://localhost:8000/api/admin/user?page[size]=1&page[number]=1", 226 | "preRequestScript": null, 227 | "pathVariables": {}, 228 | "method": "GET", 229 | "data": null, 230 | "dataMode": "params", 231 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\n\ntests[\"Status code is 200\"] = responseCode.code === 200;", 232 | "currentHelper": "normal", 233 | "helperAttributes": {}, 234 | "time": 1458489164991, 235 | "name": "List Pagination", 236 | "description": " ", 237 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 238 | "responses": [] 239 | }, 240 | { 241 | "id": "f04d11a1-3e9b-0213-ed77-6d8f7db91d38", 242 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n", 243 | "url": "http://localhost:8000/api/admin/user/404", 244 | "preRequestScript": null, 245 | "pathVariables": {}, 246 | "method": "DELETE", 247 | "data": null, 248 | "dataMode": "params", 249 | "version": 2, 250 | "tests": "tests[\"Content-Type is application/vnd.api+json\"] = postman.getResponseHeader(\"Content-Type\");", 251 | "currentHelper": "normal", 252 | "helperAttributes": {}, 253 | "time": 1458450672722, 254 | "name": "Delete Bad User", 255 | "description": "", 256 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71", 257 | "responses": [], 258 | "folder": "6cafb725-c73c-ca4c-47af-c12ac1250db0" 259 | } 260 | ] 261 | } -------------------------------------------------------------------------------- /tests/SeeOrSaveJsonStructure.php: -------------------------------------------------------------------------------- 1 | setup(); 17 | 18 | $file = rtrim(env('RESPONSE_FOLDER'), '\\/').DIRECTORY_SEPARATOR.class_basename(debug_backtrace()[1]['class']).'-'.debug_backtrace()[1]['function'].'.json'; 19 | 20 | if (! env('SAVE_RESPONSES', false) && file_exists($file)) { 21 | $this->seeJsonStructure(json_decode(file_get_contents($file), true)); 22 | 23 | return $this; 24 | } 25 | 26 | file_put_contents($file, json_encode($this->getKeys($this->response->getContent()))); 27 | 28 | return $this; 29 | } 30 | 31 | /** 32 | * Create the response folder. 33 | * 34 | * @return void 35 | */ 36 | private function setup() 37 | { 38 | if (! is_dir(env('RESPONSE_FOLDER'))) { 39 | mkdir(env('RESPONSE_FOLDER'), 0600, true); 40 | } 41 | } 42 | 43 | /** 44 | * Return a array of keys. 45 | * 46 | * @param array $array 47 | * 48 | * @return array 49 | */ 50 | private function arrayKeys($array) 51 | { 52 | $results = []; 53 | foreach ($array as $key => $value) { 54 | if (is_array($value)) { 55 | $results = array_merge($results, [$key => array_merge(array_keys($value), $this->arrayKeys($value))]); 56 | } 57 | } 58 | 59 | return $results; 60 | } 61 | 62 | /** 63 | * Get keys from a json array. 64 | * 65 | * @param string $content 66 | * 67 | * @return arrayKeys 68 | */ 69 | private function getKeys($content) 70 | { 71 | return $this->arrayKeys(json_decode($content, true)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/UnitTestCase.php: -------------------------------------------------------------------------------- 1 | createUserRaw(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/UnitTests/ExceptionTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException(\Askedio\Laravel5ApiController\Exceptions\BadRequestException::class); 12 | throw new \Askedio\Laravel5ApiController\Exceptions\BadRequestException(); 13 | } 14 | 15 | public function testNoTemplateException() 16 | { 17 | $this->setExpectedException(\Askedio\Laravel5ApiController\Exceptions\BadRequestException::class); 18 | throw new \Askedio\Laravel5ApiController\Exceptions\BadRequestException('badtemplate'); 19 | } 20 | } 21 | --------------------------------------------------------------------------------