├── .gitignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── SECURITY.md
├── composer.json
├── phpunit.xml
├── setup.sh
├── src
├── ApiController.php
├── ApiModel.php
├── ApiResponse.php
├── Exceptions
│ ├── ApiException.php
│ ├── ErrorCodes.php
│ ├── Parse
│ │ ├── FilterNotFoundException.php
│ │ ├── InvalidFilterDefinitionException.php
│ │ ├── InvalidLimitException.php
│ │ ├── InvalidOrderingDefinitionException.php
│ │ ├── MaxLimitException.php
│ │ ├── NotAllowedToFilterOnThisFieldException.php
│ │ └── UnknownFieldException.php
│ ├── RelatedResourceNotFoundException.php
│ ├── ResourceNotFoundException.php
│ ├── UnauthenticationException.php
│ ├── UnauthorizedException.php
│ └── ValidationException.php
├── ExtendedRelations
│ └── BelongsToMany.php
├── Facades
│ └── ApiRoute.php
├── Handlers
│ └── ApiExceptionHandler.php
├── Middleware
│ └── ApiMiddleware.php
├── Providers
│ └── ApiServiceProvider.php
├── RequestParser.php
├── Routing
│ ├── ApiResourceRegistrar.php
│ ├── ApiRouter.php
│ └── ApiUrlGenerator.php
└── api.php
└── tests
├── Controllers
├── CommentController.php
├── PostController.php
└── UserController.php
├── DummyUserTest.php
├── Factories
└── ModelFactory.php
├── Models
├── DummyComment.php
├── DummyPhone.php
├── DummyPost.php
└── DummyUser.php
├── PaginationTest.php
└── TestCase.php
/.gitignore:
--------------------------------------------------------------------------------
1 | laravel/
2 | .idea/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.6
5 | - 7.0
6 |
7 | sudo: false
8 |
9 | cache:
10 | directories:
11 | - laravel
12 | install:
13 | # specify the laravel service providers to insert
14 | - export PACKAGE_PROVIDER="
15 | Froiden\\\\RestAPI\\\\Providers\\\\ApiServiceProvider::class"
16 | - export FACADES="
17 | 'ApiRoute' => Froiden\\\\RestAPI\\\\Facades\\\\ApiRoute::class"
18 | - export DB_HOST="localhost"
19 | - export DB_DATABASE="homestead_test"
20 | - export DB_USERNAME="root"
21 | - export DB_PASSWORD=""
22 | #specify the package to test
23 | - export PACKAGE_NAME=froiden/laravel-rest-api
24 | #run the setup script
25 | - curl -s https://raw.githubusercontent.com/froiden/laravel-rest-api/master/setup.sh | bash
26 | - mysql -e 'create database homestead_test;'
27 |
28 | services: mysql
29 |
30 | script:
31 | - cd laravel/laravel-rest-api
32 | - ../vendor/bin/phpunit
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Froiden
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 Rest API
2 |
3 | []()
4 | [](https://packagist.org/packages/froiden/laravel-rest-api)
5 |
6 | This package provides a powerful Rest API functionality for your Laravel project, with minimum code required for you to write.
7 |
8 | ## Documentation
9 | For full documentation please refer to [plugin's wiki](https://github.com/Froiden/laravel-rest-api/wiki).
10 |
11 | ## Contribution
12 | This package is in its very early phase. We would love your feedback, issue reports and contributions. Many things are missing, like, tests, many bugs are there and many new features need to be implemented. So, if you find this useful, please contribute.
13 |
14 | ## 💰 Sponsor
15 | I fell in love with open-source in 2012 and there has been no looking back since! You can read more about me [Froiden][https://froiden.com].
16 |
17 | - ☕ How about we get to know each other over coffee? Buy me a cup for just [**$5**][buymeacoffee]
18 |
19 |
20 |
21 | [paypal]: https://paypal.com/froiden
22 | [buymeacoffee]: https://www.buymeacoffee.com/froiden
23 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "froiden/laravel-rest-api",
3 | "description": "Powerful RestAPI plugin for Laravel framework",
4 | "type": "library",
5 | "require": {
6 | "laravel/framework": "5.6.*|5.7.*|5.8.*|6.*|7.*|8.*|9.*|10.*|^11.0|^12.0"
7 | },
8 | "license": "MIT",
9 | "autoload": {
10 | "psr-4": {
11 | "Froiden\\RestAPI\\": "src",
12 | "Froiden\\RestAPI\\Tests\\": "tests"
13 | }
14 | },
15 | "authors": [
16 | {
17 | "name": "Shashank Jain",
18 | "email": "shashank@froiden.com"
19 | },
20 | {
21 | "name": "Ajay Kumar Choudhary",
22 | "email": "ajay@froiden.com"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 | ./app
20 |
21 | ./app/Http/routes.php
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ ! -f "laravel/composer.json" ] ; then
4 | rm -rf laravel
5 | git clone https://github.com/laravel/laravel
6 | cd laravel || exit
7 | # git checkout 5.2
8 | composer install --no-interaction
9 | cp .env.example .env
10 | php artisan key:generate
11 |
12 | if [[ -v PACKAGE_PROVIDER ]]; then
13 | echo "$(awk '/'\''providers'\''[^\n]*?\[/ { print; print "'$(sed -e 's/\s*//g' <<<${PACKAGE_PROVIDER})',"; next }1' config/app.php)" > config/app.php
14 | fi
15 |
16 | if [[ -v FACADES ]]; then
17 | echo "$(awk '/'\''aliases'\''[^\n]*?\[/ { print; print "'$(sed -e 's/\s*//g' <<<${FACADES})',"; next }1' config/app.php)" > config/app.php
18 | fi
19 |
20 | sed -i "s|'strict' => true|'strict' => false|g" ./config/database.php
21 |
22 | php -r "
23 | \$arr = json_decode(file_get_contents(\"composer.json\"), true);
24 | \$arr[\"autoload\"][\"psr-4\"][\"Froiden\\\\RestAPI\\\\\"] = \"laravel-rest-api/src\";
25 | \$arr[\"autoload\"][\"psr-4\"][\"Froiden\\\\RestAPI\\\\Tests\\\\\"] = \"laravel-rest-api/tests\";
26 | file_put_contents(\"composer.json\", json_encode(\$arr));
27 | "
28 | else
29 | cd laravel || exit
30 | fi
31 |
32 | rm -rf laravel-rest-api
33 | git clone https://github.com/Froiden/laravel-rest-api
34 | git checkout master
35 | composer du
36 | cd .. || exit
--------------------------------------------------------------------------------
/src/ApiController.php:
--------------------------------------------------------------------------------
1 | processingStartTime = microtime(true);
127 |
128 | if ($this->model) {
129 | // Only if model is defined. Otherwise, this is a normal controller
130 | $this->primaryKey = call_user_func([new $this->model(), "getKeyName"]);
131 | $this->table = call_user_func([new $this->model(), "getTable"]);
132 | }
133 |
134 | if (config('app.debug') == true) {
135 | \DB::enableQueryLog();
136 | }
137 | }
138 |
139 | /**
140 | * Process index page request
141 | *
142 | * @return mixed
143 | */
144 | public function index()
145 | {
146 | $this->validate();
147 |
148 | $results = $this->parseRequest()
149 | ->addIncludes()
150 | ->addFilters()
151 | ->addOrdering()
152 | ->addPaging()
153 | ->modify()
154 | ->getResults()
155 | ->toArray();
156 |
157 | $meta = $this->getMetaData();
158 |
159 | return ApiResponse::make(null, $results, $meta);
160 | }
161 |
162 | /**
163 | * Process the show request
164 | *
165 | * @return mixed
166 | */
167 | public function show(...$args)
168 | {
169 | // We need to do this in order to support multiple parameter resource routes. For example,
170 | // if we map route /user/{user}/comments/{comment} to a controller, Laravel will pass `user`
171 | // as first argument and `comment` as last argument. So, id object that we want to fetch
172 | // is the last argument.
173 | $id = last(func_get_args());
174 |
175 | $this->validate();
176 |
177 | $results = $this->parseRequest()
178 | ->addIncludes()
179 | ->addKeyConstraint($id)
180 | ->modify()
181 | ->getResults(true)
182 | ->first()
183 | ->toArray();
184 |
185 | $meta = $this->getMetaData(true);
186 |
187 | return ApiResponse::make(null, $results, $meta);
188 | }
189 |
190 | public function store()
191 | {
192 | \DB::beginTransaction();
193 |
194 | $this->validate();
195 |
196 | // Create new object
197 | /** @var ApiModel $object */
198 | $object = new $this->model();
199 | $object->fill(request()->all());
200 |
201 | // Run hook if exists
202 | if(method_exists($this, 'storing')) {
203 | $object = call_user_func([$this, 'storing'], $object);
204 | }
205 |
206 | $object->save();
207 |
208 | $meta = $this->getMetaData(true);
209 |
210 | \DB::commit();
211 |
212 | if(method_exists($this, 'stored')) {
213 | call_user_func([$this, 'stored'], $object);
214 | }
215 |
216 | return ApiResponse::make("Resource created successfully", [ "id" => $object->id ], $meta);
217 | }
218 |
219 | public function update(...$args)
220 | {
221 | \DB::beginTransaction();
222 |
223 | $id = last(func_get_args());
224 |
225 | $this->validate();
226 |
227 | // Get object for update
228 | $this->query = call_user_func($this->model . "::query");
229 | $this->modify();
230 |
231 | /** @var ApiModel $object */
232 | $object = $this->query->find($id);
233 |
234 | if (!$object) {
235 | throw new ResourceNotFoundException();
236 | }
237 |
238 | $object->fill(request()->all());
239 |
240 | if(method_exists($this, 'updating')) {
241 | $object = call_user_func([$this, 'updating'], $object);
242 | }
243 |
244 | $object->save();
245 |
246 | $meta = $this->getMetaData(true);
247 |
248 | \DB::commit();
249 |
250 | if(method_exists($this, 'updated')) {
251 | call_user_func([$this, 'updated'], $object);
252 | }
253 |
254 | return ApiResponse::make("Resource updated successfully", [ "id" => $object->id ], $meta);
255 | }
256 |
257 | public function destroy(...$args)
258 | {
259 | \DB::beginTransaction();
260 |
261 | $id = last(func_get_args());
262 |
263 | $this->validate();
264 |
265 | // Get object for update
266 | $this->query = call_user_func($this->model . "::query");
267 | $this->modify();
268 |
269 | /** @var Model $object */
270 | $object = $this->query->find($id);
271 |
272 | if (!$object) {
273 | throw new ResourceNotFoundException();
274 | }
275 |
276 | if(method_exists($this, 'destroying')) {
277 | $object = call_user_func([$this, 'destroying'], $object);
278 | }
279 |
280 | $object->delete();
281 |
282 | $meta = $this->getMetaData(true);
283 |
284 | \DB::commit();
285 |
286 | if(method_exists($this, 'destroyed')) {
287 | call_user_func([$this, 'destroyed'], $object);
288 | }
289 |
290 | return ApiResponse::make("Resource deleted successfully", null, $meta);
291 | }
292 |
293 | public function relation($id, $relation)
294 | {
295 | $this->validate();
296 |
297 | // To show relations, we just make a new fields parameter, which requests
298 | // only object id, and the relation and get the results like normal index request
299 |
300 | $fields = "id," . $relation . ".limit(" . ((request()->limit) ? request()->limit : $this->defaultLimit) .
301 | ")" . ((request()->offset) ? ".offset(" . request()->offset . ")": "" )
302 | . ((request()->fields) ? "{" .request()->fields . "}" : "");
303 |
304 | request()->fields = $fields;
305 |
306 | $results = $this->parseRequest()
307 | ->addIncludes()
308 | ->addKeyConstraint($id)
309 | ->modify()
310 | ->getResults(true)
311 | ->first()
312 | ->toArray();
313 |
314 | $data = $results[$relation];
315 |
316 | $meta = $this->getMetaData(true);
317 |
318 | return ApiResponse::make(null, $data, $meta);
319 |
320 | }
321 |
322 | protected function parseRequest()
323 | {
324 | $this->parser = new RequestParser($this->model);
325 |
326 | return $this;
327 | }
328 |
329 | protected function validate()
330 | {
331 |
332 | if ($this->isIndex()) {
333 | $requestClass = $this->indexRequest;
334 | }
335 | else if ($this->isShow()) {
336 | $requestClass = $this->showRequest;
337 | }
338 | else if ($this->isUpdate()) {
339 | $requestClass = $this->updateRequest;
340 | }
341 | else if ($this->isDelete()) {
342 | $requestClass = $this->deleteRequest;
343 | }
344 | else if ($this->isStore()) {
345 | $requestClass = $this->storeRequest;
346 | }
347 | else if ($this->isRelation()) {
348 | $requestClass = $this->indexRequest;
349 | }
350 | else {
351 | $requestClass = null;
352 | }
353 |
354 | if ($requestClass) {
355 | // We just make the class, its validation is called automatically
356 | app()->make($requestClass);
357 | }
358 | }
359 |
360 | /**
361 | * Looks for relations in the requested fields and adds with query for them
362 | *
363 | * @return $this current controller object for chain method calling
364 | */
365 | protected function addIncludes()
366 | {
367 |
368 | $relations = $this->parser->getRelations();
369 |
370 | if (!empty($relations)) {
371 | $includes = [];
372 |
373 | foreach ($relations as $key => $relation) {
374 | $includes[$key] = function (Relation $q) use ($relation, $key) {
375 |
376 | $relations = $this->parser->getRelations();
377 |
378 | $tableName = $q->getRelated()->getTable();
379 | $primaryKey = $q->getRelated()->getKeyName();
380 |
381 | if ($relation["userSpecifiedFields"]) {
382 | // Prefix table name so that we do not get ambiguous column errors
383 | $fields = $relation["fields"];
384 | }
385 | else {
386 | // Add default fields, if no fields specified
387 | $related = $q->getRelated();
388 |
389 | $fields = call_user_func(get_class($related) . "::getDefaultFields");
390 | $fields = array_merge($fields, $relation["fields"]);
391 |
392 | $relations[$key]["fields"] = $fields;
393 | }
394 |
395 | // Remove appends from select
396 | $appends = call_user_func(get_class($q->getRelated()) . "::getAppendFields");
397 | $relations[$key]["appends"] = $appends;
398 |
399 | if (!in_array($primaryKey, $fields)) {
400 | $fields[] = $primaryKey;
401 | }
402 |
403 | $fields = array_map(function($name) use($tableName) {
404 | return $tableName . "." . $name;
405 | }, array_diff($fields, $appends));
406 |
407 | if ($q instanceof BelongsToMany) {
408 | // Because laravel loads all the related models of relations in many-to-many
409 | // together, limit and offset do not work. So, we have to complicate things
410 | // to make them work
411 | $innerQuery = $q->getQuery();
412 | $innerQuery->select($fields);
413 | $innerQuery->selectRaw("@currcount := IF(@currvalue = " . $q->getQualifiedForeignPivotKeyName() . ", @currcount + 1, 1) AS rank");
414 | $innerQuery->selectRaw("@currvalue := " . $q->getQualifiedForeignPivotKeyName() . " AS whatever");
415 | $innerQuery->orderBy($q->getQualifiedForeignPivotKeyName(), ($relation["order"] == "chronological") ? "ASC" : "DESC");
416 |
417 | // Inner Join causes issues when a relation for parent does not exist.
418 | // So, we change it to right join for this query
419 | $innerQuery->getQuery()->joins[0]->type = "right";
420 |
421 | $outerQuery = $q->newPivotStatement();
422 | $outerQuery->from(\DB::raw("(". $innerQuery->toSql() . ") as `$tableName`"))
423 | ->mergeBindings($innerQuery->getQuery());
424 |
425 | $q->select($fields)
426 | ->join(\DB::raw("(" . $outerQuery->toSql() . ") as `outer_query`"), function ($join) use($q) {
427 | $join->on("outer_query." . $q->getRelatedKeyName(), "=", $q->getQualifiedRelatedPivotKeyName ());
428 | $join->on("outer_query.whatever", "=", $q->getQualifiedForeignPivotKeyName());
429 | })
430 | ->setBindings(array_merge($q->getQuery()->getBindings(), $outerQuery->getBindings()))
431 | ->where("rank", "<=", $relation["limit"] + $relation["offset"])
432 | ->where("rank", ">", $relation["offset"]);
433 | }
434 | else {
435 | // We need to select foreign key so that Laravel can match to which records these
436 | // need to be attached
437 | if ($q instanceof BelongsTo) {
438 | $fields[] = $q->getOwnerKeyName();
439 |
440 | if (strpos($key, ".") !== false) {
441 | $parts = explode(".", $key);
442 | array_pop($parts);
443 |
444 | $relation["limit"] = $relations[implode(".", $parts)]["limit"];
445 | }
446 | }
447 | else if ($q instanceof HasOne) {
448 | $fields[] = $q->getQualifiedForeignKeyName();
449 |
450 | // This will be used to hide this foreign key field
451 | // in the processAppends function later
452 | $relations[$key]["foreign"] = $q->getQualifiedForeignKeyName();
453 | }
454 | else if ($q instanceof HasMany) {
455 | $fields[] = $q->getQualifiedForeignKeyName();
456 | $relations[$key]["foreign"] = $q->getQualifiedForeignKeyName();
457 |
458 | $q->orderBy($primaryKey, ($relation["order"] == "chronological") ? "ASC" : "DESC");
459 | }
460 |
461 | $q->select($fields);
462 |
463 | $q->take($relation["limit"]);
464 |
465 | if ($relation["offset"] !== 0) {
466 | $q->skip($relation["offset"]);
467 | }
468 | }
469 |
470 | $this->parser->setRelations($relations);
471 | };
472 | }
473 |
474 | $this->query = call_user_func($this->model."::with", $includes);
475 | }
476 | else {
477 | $this->query = call_user_func($this->model."::query");
478 | }
479 |
480 | return $this;
481 | }
482 |
483 | /**
484 | * Add requested filters. Filters are defined similar to normal SQL queries like
485 | * (name eq "Milk" or name eq "Eggs") and price lt 2.55
486 | * The string should be enclosed in double quotes
487 | * @return $this
488 | * @throws NotAllowedToFilterOnThisFieldException
489 | */
490 | protected function addFilters()
491 | {
492 | if ($this->parser->getFilters()) {
493 |
494 | $this->query->whereRaw($this->parser->getFilters());
495 | }
496 |
497 | return $this;
498 | }
499 |
500 | /**
501 | * Add sorting to the query. Sorting is similar to SQL queries
502 | *
503 | * @return $this
504 | */
505 | protected function addOrdering()
506 | {
507 | if ($this->parser->getOrder()) {
508 | $this->query->orderByRaw($this->parser->getOrder());
509 | }
510 |
511 | return $this;
512 | }
513 |
514 | /**
515 | * Adds paging limit and offset to SQL query
516 | *
517 | * @return $this
518 | */
519 | protected function addPaging()
520 | {
521 | $limit = $this->parser->getLimit();
522 | $offset = $this->parser->getOffset();
523 |
524 | if ($offset <= 0) {
525 | $skip = 0;
526 | }
527 | else {
528 | $skip = $offset;
529 | }
530 |
531 | $this->query->skip($skip);
532 |
533 | $this->query->take($limit);
534 |
535 | return $this;
536 | }
537 |
538 | protected function addKeyConstraint($id)
539 | {
540 | // Add equality constraint
541 | $this->query->where($this->table . "." . ($this->primaryKey), "=", $id);
542 |
543 | return $this;
544 | }
545 |
546 | /**
547 | * Runs query and fetches results
548 | *
549 | * @param bool $single
550 | * @return Collection
551 | * @throws ResourceNotFoundException
552 | */
553 | protected function getResults($single = false)
554 | {
555 | $customAttributes = call_user_func($this->model."::getAppendFields");
556 |
557 | // Laravel's $appends adds attributes always to the output. With this method,
558 | // we can specify which attributes are to be included
559 | $appends = [];
560 |
561 | $fields = $this->parser->getFields();
562 |
563 | foreach ($fields as $key => $field) {
564 | if (in_array($field, $customAttributes)) {
565 | $appends[] = $field;
566 | unset($fields[$key]);
567 | }
568 | else {
569 | // Add table name to fields to prevent ambiguous column issues
570 | $fields[$key] = $this->table . "." . $field;
571 | }
572 | }
573 |
574 | $this->parser->setFields($fields);
575 |
576 | if (!$single) {
577 | /** @var Collection $results */
578 | $results = $this->query->select($fields)->get();
579 | }
580 | else {
581 | /** @var Collection $results */
582 | $results = $this->query->select($fields)->skip(0)->take(1)->get();
583 |
584 | if ($results->count() == 0) {
585 | throw new ResourceNotFoundException();
586 | }
587 | }
588 |
589 | foreach($results as $result) {
590 | $result->setAppends($appends);
591 | }
592 |
593 | $this->processAppends($results);
594 |
595 | $this->results = $results;
596 |
597 | return $results;
598 | }
599 |
600 | private function processAppends($models, $parent = null)
601 | {
602 | if (! ($models instanceof Collection)) {
603 | return $models;
604 | }
605 | else if ($models->count() == 0) {
606 | return $models;
607 | }
608 |
609 | // Attribute at $key is a relation
610 | $first = $models->first();
611 | $attributeKeys = array_keys($first->getRelations());
612 | $relations = $this->parser->getRelations();
613 |
614 | foreach ($attributeKeys as $key) {
615 | $relationName = ($parent === null) ? $key : $parent . "." . $key;
616 |
617 | if (isset($relations[$relationName])) {
618 |
619 | $appends = $relations[$relationName]["appends"];
620 | $appends = array_intersect($appends, $relations[$relationName]["fields"]);
621 |
622 | if (isset($relations[$relationName]["foreign"])) {
623 | $foreign = explode(".", $relations[$relationName]["foreign"])[1];
624 | }
625 | else {
626 | $foreign = null;
627 | }
628 |
629 | foreach ($models as $model) {
630 | if ($model->$key instanceof Collection) {
631 | $model->{$key}->each(function ($item, $key) use($appends, $foreign) {
632 | $item->setAppends($appends);
633 |
634 | // Hide the foreign key fields
635 | if (!empty($foreign)) {
636 | $item->makeHidden($foreign);
637 | }
638 | });
639 |
640 | $this->processAppends($model->$key, $key);
641 | }
642 | else if (!empty($model->$key)) {
643 | $model->$key->setAppends($appends);
644 |
645 | if (!empty($foreign)) {
646 | $model->$key->makeHidden($foreign);
647 | }
648 |
649 | $this->processAppends(collect($model->$key), $key);
650 | }
651 | }
652 | }
653 | }
654 | }
655 |
656 | /**
657 | * Builds metadata - paging, links, time to complete request, etc
658 | *
659 | * @return array
660 | */
661 | protected function getMetaData($single = false)
662 | {
663 | if (!$single) {
664 | $meta = [
665 | "paging" => [
666 | "links" => [
667 |
668 | ]
669 | ]
670 | ];
671 | $limit = $this->parser->getLimit();
672 | $pageOffset = $this->parser->getOffset();
673 |
674 | $current = $pageOffset;
675 |
676 | // Remove offset because setting offset does not return
677 | // result. As, there is single result in count query,
678 | // and setting offset will not return that record
679 | $offset = $this->query->getQuery()->offset;
680 |
681 | $this->query->offset(0);
682 |
683 | $totalRecords = $this->query->count($this->table . "." . $this->primaryKey);
684 |
685 | $this->query->offset($offset);
686 |
687 | $meta["paging"]["total"] = $totalRecords;
688 |
689 | if (($current + $limit) < $meta["paging"]["total"]) {
690 | $meta["paging"]["links"]["next"] = $this->getNextLink();
691 | }
692 |
693 | if ($current >= $limit) {
694 | $meta["paging"]["links"]["previous"] = $this->getPreviousLink();
695 | }
696 | }
697 |
698 | $meta["time"] = round(microtime(true) - $this->processingStartTime, 3);
699 |
700 | if (config('app.debug') == true) {
701 | $log = \DB::getQueryLog();
702 | \DB::disableQueryLog();
703 |
704 | $meta["queries"] = count($log);
705 | $meta["queries_list"] = $log;
706 | }
707 |
708 | return $meta;
709 | }
710 |
711 | protected function getPreviousLink()
712 | {
713 | $offset = $this->parser->getOffset();
714 | $limit = $this->parser->getLimit();
715 |
716 | $queryString = ((request()->fields) ? "&fields=" . urlencode(request()->fields) : "") .
717 | ((request()->filters) ? "&filters=" . urlencode(request()->filters) : "") .
718 | ((request()->order) ? "&fields=" . urlencode(request()->order) : "");
719 |
720 | $queryString .= "&offset=" . ($offset - $limit);
721 |
722 | return request()->url() . "?" . trim($queryString, "&");
723 | }
724 |
725 | protected function getNextLink()
726 | {
727 | $offset = $this->parser->getOffset();
728 | $limit = $this->parser->getLimit();
729 |
730 | $queryString = ((request()->fields) ? "&fields=" . urlencode(request()->fields) : "") .
731 | ((request()->filters) ? "&filters=" . urlencode(request()->filters) : "") .
732 | ((request()->order) ? "&fields=" . urlencode(request()->order) : "");
733 |
734 | $queryString .= "&offset=" . ($offset + $limit);
735 |
736 | return request()->url() . "?" . trim($queryString, "&");
737 | }
738 |
739 | /**
740 | * Checks if current request is index request
741 | * @return bool
742 | */
743 | protected function isIndex()
744 | {
745 | return in_array("index", explode(".", request()->route()->getName()));
746 | }
747 |
748 | /**
749 | * Checks if current request is create request
750 | * @return bool
751 | */
752 | protected function isCreate()
753 | {
754 | return in_array("create", explode(".", request()->route()->getName()));
755 | }
756 |
757 | /**
758 | * Checks if current request is show request
759 | * @return bool
760 | */
761 | protected function isShow()
762 | {
763 | return in_array("show", explode(".", request()->route()->getName()));
764 | }
765 |
766 | /**
767 | * Checks if current request is update request
768 | * @return bool
769 | */
770 | protected function isUpdate()
771 | {
772 | return in_array("update", explode(".", request()->route()->getName()));
773 | }
774 |
775 | /**
776 | * Checks if current request is delete request
777 | * @return bool
778 | */
779 | protected function isDelete()
780 | {
781 | return in_array("destroy", explode(".", request()->route()->getName()));
782 | }
783 |
784 | /**
785 | * Checks if current request is store request
786 | * @return bool
787 | */
788 | protected function isStore()
789 | {
790 | return in_array("store", explode(".", request()->route()->getName()));
791 | }
792 |
793 | /**
794 | * Checks if current request is relation request
795 | * @return bool
796 | */
797 | protected function isRelation()
798 | {
799 | return in_array("relation", explode(".", request()->route()->getName()));
800 | }
801 |
802 | /**
803 | * Calls the modifyRequestType methods to modify query just before execution
804 | * @return $this
805 | */
806 | private function modify()
807 | {
808 | if ($this->isIndex()) {
809 | $this->query = $this->modifyIndex($this->query);
810 | }
811 | else if ($this->isShow()) {
812 | $this->query = $this->modifyShow($this->query);
813 | }
814 | else if ($this->isDelete()) {
815 | $this->query = $this->modifyDelete($this->query);
816 | }
817 | else if ($this->isUpdate()) {
818 | $this->query = $this->modifyUpdate($this->query);
819 | }
820 |
821 | return $this;
822 | }
823 |
824 | /**
825 | * Modify the query for show request
826 | * @param $query
827 | * @return mixed
828 | */
829 | protected function modifyShow($query)
830 | {
831 | return $query;
832 | }
833 |
834 | /**
835 | * Modify the query for update request
836 | * @param $query
837 | * @return mixed
838 | */
839 | protected function modifyUpdate($query)
840 | {
841 | return $query;
842 | }
843 |
844 | /**
845 | * Modify the query for delete request
846 | * @param $query
847 | * @return mixed
848 | */
849 | protected function modifyDelete($query)
850 | {
851 | return $query;
852 | }
853 |
854 | /**
855 | * Modify the query for index request
856 | * @param $query
857 | * @return mixed
858 | */
859 | protected function modifyIndex($query)
860 | {
861 | return $query;
862 | }
863 |
864 | protected function getQuery() {
865 | return $this->query;
866 | }
867 |
868 | protected function setQuery($query) {
869 | $this->query = $query;
870 | }
871 |
872 | //endregion
873 | }
874 |
--------------------------------------------------------------------------------
/src/ApiModel.php:
--------------------------------------------------------------------------------
1 | table;
68 | }
69 |
70 | /**
71 | * Date fields in this model
72 | *
73 | * @return array
74 | */
75 | public static function getDateFields()
76 | {
77 | return (new static)->dates;
78 | }
79 |
80 | /**
81 | * List of custom fields (attributes) that are appended by default
82 | * ($appends array)
83 | *
84 | * @return array
85 | */
86 | public static function getAppendFields()
87 | {
88 | return (new static)->appends;
89 | }
90 |
91 | /**
92 | * List of fields to display by default ($defaults array)
93 | *
94 | * @return array
95 | */
96 | public static function getDefaultFields()
97 | {
98 | return (new static)->default;
99 | }
100 |
101 | /**
102 | * Return the $relationKeys array
103 | *
104 | * @return mixed
105 | */
106 | public static function getRelationKeyFields()
107 | {
108 | return (new static)->relationKeys;
109 | }
110 |
111 | /**
112 | * Returns list of fields on which filter is allowed to be applied
113 | *
114 | * @return array
115 | */
116 | public static function getFilterableFields()
117 | {
118 | return (new static)->filterable;
119 | }
120 |
121 | /**
122 | * Checks if given relation exists on the model
123 | *
124 | * @param $relation
125 | * @return bool
126 | */
127 | public static function relationExists($relation)
128 | {
129 | // Check if relation name in modal is in camel case or not
130 | if (config("api.relation_case", 'snakecase') === 'camelcase') {
131 | return (method_exists(new static(), $relation) ?? false) || (method_exists(new static(), Str::camel($relation)) ?? false);
132 | }
133 | return method_exists(new static(), $relation);
134 | }
135 |
136 | //endregion
137 |
138 | /**
139 | * Prepare a date for array / JSON serialization. Override base method in Model to suite our needs
140 | *
141 | * @param \DateTime $date
142 | * @return string
143 | */
144 | protected function serializeDate(\DateTimeInterface $date)
145 | {
146 | return $date->format("c");
147 | }
148 |
149 | /**
150 | * Return a timestamp as DateTime object.
151 | *
152 | * @param mixed $value
153 | * @return \Carbon\Carbon
154 | */
155 | protected function asDateTime($value)
156 | {
157 | // If this value is already a Carbon instance, we shall just return it as is.
158 | // This prevents us having to re-instantiate a Carbon instance when we know
159 | // it already is one, which wouldn't be fulfilled by the DateTime check.
160 | if ($value instanceof Carbon) {
161 | return $value;
162 | }
163 |
164 | // If the value is already a DateTime instance, we will just skip the rest of
165 | // these checks since they will be a waste of time, and hinder performance
166 | // when checking the field. We will just return the DateTime right away.
167 | if ($value instanceof DateTimeInterface) {
168 | return new Carbon(
169 | $value->format('Y-m-d H:i:s.u'), $value->getTimeZone()
170 | );
171 | }
172 |
173 | // If this value is an integer, we will assume it is a UNIX timestamp's value
174 | // and format a Carbon object from this timestamp. This allows flexibility
175 | // when defining your date fields as they might be UNIX timestamps here.
176 | if (is_numeric($value)) {
177 | return Carbon::createFromTimestamp($value);
178 | }
179 |
180 | // If the value is in simply year, month, day format, we will instantiate the
181 | // Carbon instances from that format. Again, this provides for simple date
182 | // fields on the database, while still supporting Carbonized conversion.
183 | if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value)) {
184 | return Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
185 | }
186 |
187 | // Parse ISO 8061 date
188 | if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\\+(\d{2}):(\d{2})$/', $value)) {
189 | return Carbon::createFromFormat('Y-m-d\TH:i:s+P', $value);
190 | }
191 | elseif (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2}T(\d{2}):(\d{2}):(\d{2})\\.(\d{1,3})Z)$/', $value)) {
192 | return Carbon::createFromFormat('Y-m-d\TH:i:s.uZ', $value);
193 | }
194 |
195 | // Finally, we will just assume this date is in the format used by default on
196 | // the database connection and use that format to create the Carbon object
197 | // that is returned back out to the developers after we convert it here.
198 | return Carbon::createFromFormat($this->getDateFormat(), $value);
199 | }
200 |
201 | /**
202 | * Eagerly load the relationship on a set of models.
203 | *
204 | * @param array $models
205 | * @param string $name
206 | * @param \Closure $constraints
207 | * @return array
208 | */
209 | protected function loadRelation(array $models, $name, Closure $constraints)
210 | {
211 | // First we will "back up" the existing where conditions on the query so we can
212 | // add our eager constraints. Then we will merge the wheres that were on the
213 | // query back to it in order that any where conditions might be specified.
214 | $relation = $this->getRelation($name);
215 |
216 | $relation->addEagerConstraints($models);
217 |
218 | call_user_func($constraints, $relation);
219 |
220 | $models = $relation->initRelation($models, $name);
221 |
222 | // Once we have the results, we just match those back up to their parent models
223 | // using the relationship instance. Then we just return the finished arrays
224 | // of models which have been eagerly hydrated and are readied for return.
225 | $results = $relation->getEager();
226 |
227 | return $relation->match($models, $results, $name);
228 | }
229 |
230 | /**
231 | * Fill the model with an array of attributes.
232 | *
233 | * @param array $attributes
234 | * @param bool $relations If the attributes also contain relations
235 | * @return Model
236 | */
237 | public function fill(array $attributes = [])
238 | {
239 | $this->raw = $attributes;
240 |
241 | $excludes = config("api.excludes");
242 |
243 | foreach ($attributes as $key => $attribute) {
244 | // Guarded attributes should be removed
245 | if (in_array($key, $excludes)) {
246 | unset($attributes[$key]);
247 | }
248 | else if (method_exists($this, $key) && ((is_array($attribute) || is_null($attribute)))) {
249 | // Its a relation
250 | $this->relationAttributes[$key] = $attribute;
251 |
252 | // For belongs to relation, while filling, we need to set relation key.
253 | $relation = call_user_func([$this, $key]);
254 |
255 | if ($relation instanceof BelongsTo) {
256 | $primaryKey = $relation->getRelated()->getKeyName();
257 |
258 | if ($attribute !== null) {
259 | // If key value is not set in request, we create new object
260 | if (!isset($attribute[$primaryKey])) {
261 | throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
262 | }
263 | else {
264 | $model = $relation->getRelated()->find($attribute[$primaryKey]);
265 |
266 | if (!$model) {
267 | // Resource not found
268 | throw new ResourceNotFoundException();
269 | }
270 | }
271 | }
272 |
273 | $relationKey = $relation->getForeignKeyName();
274 |
275 | $this->setAttribute($relationKey, ($attribute === null) ? null : $model->getKey());
276 | }
277 |
278 | unset($attributes[$key]);
279 | }
280 | }
281 |
282 | return parent::fill($attributes);
283 | }
284 |
285 | public function save(array $options = [])
286 | {
287 | // Belongs to relation needs to be set before, because we need the parent's Id
288 | foreach ($this->relationAttributes as $key => $relationAttribute) {
289 | /** @var Relation $relation */
290 | $relation = call_user_func([$this, $key]);
291 |
292 | if ($relation instanceof BelongsTo) {
293 | $primaryKey = $relation->getRelated()->getKeyName();
294 |
295 | if ($relationAttribute !== null) {
296 | // If key value is not set in request, we create new object
297 | if (!isset($relationAttribute[$primaryKey])) {
298 | throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
299 | }
300 | else {
301 | $model = $relation->getRelated()->find($relationAttribute[$primaryKey]);
302 |
303 | if (!$model) {
304 | // Resource not found
305 | throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
306 | }
307 | }
308 | }
309 |
310 | $relationKey = $relation->getForeignKeyName();
311 |
312 | $this->setAttribute($relationKey, ($relationAttribute === null) ? null : $model->getKey());
313 |
314 | unset($this->relationAttributes[$key]);
315 | }
316 | }
317 |
318 | parent::save($options);
319 |
320 | // Fill all other relations
321 | foreach ($this->relationAttributes as $key => $relationAttribute) {
322 | /** @var Relation $relation */
323 | $relation = call_user_func([$this, $key]);
324 | $primaryKey = $relation->getRelated()->getKeyName();
325 |
326 | if ($relation instanceof HasOne || $relation instanceof HasMany) {
327 |
328 | if ($relation instanceof HasOne) {
329 | $relationAttribute = [$relationAttribute];
330 | }
331 |
332 | $relationKey = explode(".", $relation->getQualifiedParentKeyName())[1];
333 |
334 | foreach ($relationAttribute as $val) {
335 | if ($val !== null) {
336 | if (!isset($val[$primaryKey])) {
337 | throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
338 | }
339 | else {
340 | /** @var Model $model */
341 | $model = $relation->getRelated()->find($val[$primaryKey]);
342 |
343 | if (!$model) {
344 | // Resource not found
345 | throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
346 | }
347 |
348 | // Only update relation key to attach $model to $this object
349 | $model->{$relationKey} = $this->getKey();
350 | $model->save();
351 | }
352 | }
353 | }
354 | }
355 |
356 | else if ($relation instanceof BelongsToMany) {
357 | $relatedIds = [];
358 |
359 | // Value is an array of related models
360 | foreach ($relationAttribute as $val) {
361 | if ($val !== null) {
362 | if (!isset($val[$primaryKey])) {
363 | throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
364 | }
365 | else {
366 | /** @var Model $model */
367 | $model = $relation->getRelated()->find($val[$primaryKey]);
368 |
369 | if (!$model) {
370 | // Resource not found
371 | throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
372 | }
373 | }
374 | }
375 |
376 | if ($val !== null) {
377 | if(isset($val['pivot'])) {
378 | // We have additional fields other than primary key
379 | // that need to be saved to pivot table
380 | /*
381 | [
382 | {
383 | "id": 12, // Primary key
384 | "pivot": {
385 | "count": 8 // Pivot table column
386 | }
387 | }
388 | ]
389 | */
390 | $relatedIds[$model->getKey()] = $val['pivot'];
391 | }
392 | else {
393 | // We just have ids
394 | $relatedIds[] = $model->getKey();
395 | }
396 | }
397 | }
398 |
399 | $relation->sync($relatedIds);
400 | }
401 | }
402 | }
403 | }
404 |
--------------------------------------------------------------------------------
/src/ApiResponse.php:
--------------------------------------------------------------------------------
1 | message;
32 | }
33 |
34 | /**
35 | * Set response message
36 | *
37 | * @param string $message
38 | */
39 | public function setMessage($message)
40 | {
41 | $this->message = $message;
42 | }
43 |
44 | /**
45 | * Get response data
46 | *
47 | * @return array
48 | */
49 | public function getData()
50 | {
51 | return $this->data;
52 | }
53 |
54 | /**
55 | * Set response data
56 | *
57 | * @param array $data
58 | */
59 | public function setData($data)
60 | {
61 | $this->data = $data;
62 | }
63 |
64 | /**
65 | * Make new success response
66 | * @param string $message
67 | * @param array $data
68 | * @return \Response
69 | */
70 | public static function make($message = null, $data = null, $meta = null)
71 | {
72 | $response = [];
73 |
74 | if (!empty($message)) {
75 | $response["message"] = $message;
76 | }
77 |
78 | if ($data !== null && is_array($data)){
79 | $response["data"] = $data;
80 | }
81 |
82 | if ($meta !== null && is_array($meta)){
83 | $response["meta"] = $meta;
84 | }
85 |
86 | $returnResponse = \Response::make($response);
87 |
88 | return $returnResponse;
89 | }
90 |
91 | /**
92 | * Handle api exception an return proper error response
93 | * @param ApiException $exception
94 | * @return \Illuminate\Http\Response
95 | * @throws ApiException
96 | */
97 | public static function exception(ApiException $exception)
98 | {
99 | $returnResponse = \Response::make($exception->jsonSerialize());
100 |
101 | $returnResponse->setStatusCode($exception->getStatusCode());
102 |
103 | return $returnResponse;
104 | }
105 | }
--------------------------------------------------------------------------------
/src/Exceptions/ApiException.php:
--------------------------------------------------------------------------------
1 | statusCode = $statusCode;
34 | }
35 |
36 | if ($code !== null) {
37 | $this->code = $code;
38 | }
39 |
40 | if ($innerError !== null) {
41 | $this->innerError = $innerError;
42 | }
43 |
44 | if (!empty($details)) {
45 | $this->details = $details;
46 | }
47 |
48 | if ($message == null) {
49 | parent::__construct($this->message, $this->code, $previous);
50 | }
51 | else {
52 | parent::__construct($message, $this->code, $previous);
53 | }
54 | }
55 |
56 | public function __toString()
57 | {
58 | return "ApiException (#{$this->getCode()}): {$this->getMessage()}";
59 | }
60 |
61 | /**
62 | * Return the status code the response should be sent with
63 | *
64 | * @return int
65 | */
66 | public function getStatusCode()
67 | {
68 | return $this->statusCode;
69 | }
70 |
71 | /**
72 | * Convert the exception to its JSON representation.
73 | *
74 | * @param int $options
75 | * @return string
76 | */
77 | public function toJson($options = 0)
78 | {
79 | return json_encode($this->jsonSerialize(), $options);
80 | }
81 |
82 | /**
83 | * Specify data which should be serialized to JSON
84 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
85 | * @return mixed data which can be serialized by json_encode,
86 | * which is a value of any type other than a resource.
87 | */
88 | public function jsonSerialize()
89 | {
90 | $jsonArray = [
91 | "message" => $this->getMessage(),
92 | "error" => [
93 | "message" => $this->getMessage(),
94 | "code" => $this->getCode()
95 | ]
96 | ];
97 |
98 | if (isset($this->details)) {
99 | $jsonArray["error"]["details"] = $this->details;
100 | }
101 |
102 | if (isset($this->innerError)) {
103 | $jsonArray["error"]["innererror"] = [
104 | "code" => $this->innerError
105 | ];
106 | }
107 |
108 | return $jsonArray;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Exceptions/ErrorCodes.php:
--------------------------------------------------------------------------------
1 | details = $errors;
26 | }
27 | }
--------------------------------------------------------------------------------
/src/ExtendedRelations/BelongsToMany.php:
--------------------------------------------------------------------------------
1 | relatedKey ?: 'id';
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Facades/ApiRoute.php:
--------------------------------------------------------------------------------
1 | is($prefix . '/*')) {
33 |
34 | // When the user is not authenticated or logged show this message with status code 401
35 | if ($e instanceof AuthenticationException) {
36 | return ApiResponse::exception(new UnauthenticationException());
37 | }
38 |
39 | if ($e instanceof HttpResponseException || $e instanceof \Illuminate\Validation\ValidationException) {
40 | if ($e->status == 403) {
41 | return ApiResponse::exception(new UnauthorizedException());
42 | }
43 | return ApiResponse::exception(new ValidationException($e->errors()));
44 | }
45 |
46 | if ($e instanceof NotFoundHttpException) {
47 | return ApiResponse::exception(new ApiException('This api endpoint does not exist', null, 404, 404, 2005, [
48 | 'url' => request()->url()
49 | ]));
50 | }
51 |
52 | if ($e instanceof ModelNotFoundException) {
53 | return ApiResponse::exception(new ApiException('Requested resource not found', null, 404, 404, null, [
54 | 'url' => request()->url()
55 | ]));
56 | }
57 |
58 | if ($e instanceof ApiException) {
59 | return ApiResponse::exception($e);
60 | }
61 |
62 | if ($e instanceof QueryException) {
63 | if ($e->getCode() == "422") {
64 | preg_match("/Unknown column \\'([^']+)\\'/", $e->getMessage(), $result);
65 |
66 | if (!isset($result[1])) {
67 | return ApiResponse::exception(new UnknownFieldException(null, $e));
68 | }
69 |
70 | $parts = explode(".", $result[1]);
71 |
72 | $field = count($parts) > 1 ? $parts[1] : $result;
73 |
74 | return ApiResponse::exception(new UnknownFieldException("Field '" . $field . "' does not exist", $e));
75 |
76 | }
77 |
78 | }
79 | // When Debug is on move show error here
80 | $message = null;
81 |
82 | if($debug){
83 | $response['trace'] = $e->getTrace();
84 | $response['code'] = $e->getCode();
85 | $message = $e->getMessage();
86 | }
87 |
88 | return ApiResponse::exception(new ApiException($message, null, 500, 500, null, $response));
89 | }
90 |
91 | return parent::render($request, $e);
92 | }
93 |
94 | }
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/Middleware/ApiMiddleware.php:
--------------------------------------------------------------------------------
1 | getStatusCode() == 403 && ($response->getContent() == "Forbidden" || Str::contains($response->getContent(), ['HttpException', 'authorized']))) {
20 | $response = ApiResponse::exception(new UnauthorizedException());
21 | }
22 |
23 | if (config("api.cors") && !$response instanceof StreamedResponse) {
24 | $response->header('Access-Control-Allow-Origin', '*')
25 | ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
26 | ->header('Access-Control-Allow-Headers', implode(',', config('api.cors_headers')));
27 | }
28 |
29 |
30 | return $response;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Providers/ApiServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
21 | __DIR__.'/../api.php' => config_path("api.php"),
22 | ]);
23 | }
24 |
25 | /**
26 | * Register the service provider.
27 | *
28 | * @return void
29 | */
30 | public function register()
31 | {
32 | $this->registerRouter();
33 | $this->registerExceptionHandler();
34 |
35 | $this->mergeConfigFrom(
36 | __DIR__.'/../api.php', 'api'
37 | );
38 | }
39 |
40 | public function registerRouter()
41 | {
42 | $this->app->singleton(
43 | ApiRouter::class,
44 | function ($app) {
45 | return new ApiRouter($app->make(Dispatcher::class), $app->make(Container::class));
46 | }
47 | );
48 |
49 | $this->app->singleton(
50 | ApiResourceRegistrar::class,
51 | function ($app) {
52 | return new ApiResourceRegistrar($app->make(ApiRouter::class));
53 | }
54 | );
55 | }
56 |
57 | public function registerExceptionHandler()
58 | {
59 | $this->app->singleton(
60 | \Illuminate\Contracts\Debug\ExceptionHandler::class,
61 | ApiExceptionHandler::class
62 | );
63 | }
64 | }
--------------------------------------------------------------------------------
/src/RequestParser.php:
--------------------------------------------------------------------------------
1 | [^{}]+)|(?&curly))*\\})?+)/";
21 |
22 | /**
23 | * Extracts fields parts
24 | */
25 | const FIELD_PARTS_REGEX = "/([^{.]+)(.limit\\(([0-9]+)\\)|.offset\\(([0-9]+)\\)|.order\\(([A-Za-z_]+)\\))*(\\{((?>[^{}]+)|(?R))*\\})?/i";
26 |
27 | /**
28 | * Checks if filters are correctly specified
29 | */
30 | const FILTER_REGEX = "/(\\((?:[\\s]*(?:and|or)?[\\s]*[\\w\\.]+[\\s]+(?:eq|ne|gt|ge|lt|le|lk)[\\s]+(?:\\\"(?:[^\\\"\\\\]|\\\\.)*\\\"|\\d+(,\\d+)*(\\.\\d+(e\\d+)?)?|null)[\\s]*|(?R))*\\))/i";
31 |
32 | /**
33 | * Extracts filter parts
34 | */
35 | const FILTER_PARTS_REGEX = "/([\\w\\.]+)[\\s]+(?:eq|ne|gt|ge|lt|le|lk)[\\s]+(?:\"(?:[^\"\\\\]|\\\\.)*\"|\\d+(?:,\\d+)*(?:\\.\\d+(?:e\\d+)?)?|null)/i";
36 |
37 | /**
38 | * Checks if ordering is specified correctly
39 | */
40 | const ORDER_FILTER = "/[\\s]*([\\w\\.]+)(?:[\\s](?!,))*(asc|desc|)/i";
41 |
42 | /**
43 | * Full class reference to model this controller represents
44 | *
45 | * @var string
46 | */
47 | protected $model = null;
48 |
49 | /**
50 | * Table name corresponding to the model this controller is handling
51 | *
52 | * @var string
53 | */
54 | private $table = null;
55 |
56 | /**
57 | * Primary key of the model
58 | *
59 | * @var string
60 | */
61 | private $primaryKey = null;
62 |
63 | /**
64 | * Fields to be returned in response. This does not include relations
65 | *
66 | * @var array
67 | */
68 | private $fields = [];
69 |
70 | /**
71 | * Relations to be included in the response
72 | *
73 | * @var array
74 | */
75 | private $relations = [];
76 |
77 | /**
78 | * Number of results requested per page
79 | *
80 | * @var int
81 | */
82 | private $limit = 10;
83 |
84 | /**
85 | * Offset from where fetching should start
86 | *
87 | * @var int
88 | */
89 | private $offset = 0;
90 |
91 | /**
92 | * Ordering string
93 | *
94 | * @var int
95 | */
96 | private $order = null;
97 |
98 | /**
99 | * Filters to be applied
100 | *
101 | * @var string
102 | */
103 | private $filters = null;
104 |
105 | /**
106 | * Attributes passed in request
107 | *
108 | * @var array
109 | */
110 | private $attributes = [];
111 |
112 | public function __construct($model)
113 | {
114 | $this->model = $model;
115 | $this->primaryKey = call_user_func([new $this->model(), "getKeyName"]);
116 |
117 | $this->parseRequest();
118 | }
119 |
120 | /**
121 | * @return array
122 | */
123 | public function getFields()
124 | {
125 | return $this->fields;
126 | }
127 |
128 | /**
129 | * @param array $fields
130 | */
131 | public function setFields($fields)
132 | {
133 | $this->fields = $fields;
134 | }
135 |
136 | /**
137 | * @return array
138 | */
139 | public function getRelations()
140 | {
141 | return $this->relations;
142 | }
143 |
144 | /**
145 | * @param array $relations
146 | */
147 | public function setRelations($relations)
148 | {
149 | $this->relations = $relations;
150 | }
151 |
152 | /**
153 | * @return int
154 | */
155 | public function getLimit()
156 | {
157 | return $this->limit;
158 | }
159 |
160 | /**
161 | * @return int
162 | */
163 | public function getOffset()
164 | {
165 | return $this->offset;
166 | }
167 |
168 | /**
169 | * @return int
170 | */
171 | public function getOrder()
172 | {
173 | return $this->order;
174 | }
175 |
176 | /**
177 | * @return string
178 | */
179 | public function getFilters()
180 | {
181 | return $this->filters;
182 | }
183 |
184 | /**
185 | * @return array
186 | */
187 | public function getAttributes()
188 | {
189 | return $this->attributes;
190 | }
191 |
192 | /**
193 | * Parse request and fill the parameters
194 | * @return $this current controller object for chain method calling
195 | * @throws InvalidFilterDefinitionException
196 | * @throws InvalidOrderingDefinitionException
197 | * @throws MaxLimitException
198 | */
199 | protected function parseRequest()
200 | {
201 | if (request()->limit) {
202 | if (request()->limit <= 0) {
203 | throw new InvalidLimitException();
204 | }
205 | else if (request()->limit > config("api.maxLimit")) {
206 | throw new MaxLimitException();
207 | }
208 | else {
209 | $this->limit = request()->limit;
210 | }
211 | }
212 | else {
213 | $this->limit = config("api.defaultLimit");
214 | }
215 |
216 | if (request()->offset) {
217 | $this->offset = request()->offset;
218 | }
219 | else {
220 | $this->offset = 0;
221 | }
222 |
223 | $this->extractFields();
224 | $this->extractFilters();
225 | $this->extractOrdering();
226 | $this->loadTableName();
227 |
228 | $this->attributes = request()->all();
229 |
230 | return $this;
231 | }
232 |
233 | protected function extractFields()
234 | {
235 | if (request()->fields) {
236 | $this->parseFields(request()->fields);
237 | }
238 | else {
239 | // Else, by default, we only return default set of visible fields
240 | $fields = call_user_func($this->model."::getDefaultFields");
241 |
242 | // We parse the default fields in same way as above so that, if
243 | // relations are included in default fields, they also get included
244 | $this->parseFields(implode(",", $fields));
245 | }
246 |
247 | if (!in_array($this->primaryKey, $this->fields)) {
248 | $this->fields[] = $this->primaryKey;
249 | }
250 | }
251 |
252 | protected function extractFilters()
253 | {
254 | if (request()->filters) {
255 | $filters = "(" . request()->filters . ")";
256 |
257 | if (preg_match(RequestParser::FILTER_REGEX, $filters) === 1) {
258 |
259 | preg_match_all(RequestParser::FILTER_PARTS_REGEX, $filters, $parts);
260 |
261 | $filterable = call_user_func($this->model . "::getFilterableFields");
262 |
263 | foreach ($parts[1] as $column) {
264 | if (!in_array($column, $filterable)) {
265 | throw new NotAllowedToFilterOnThisFieldException("Applying filter on field \"" . $column . "\" is not allowed");
266 | }
267 | }
268 |
269 | // Convert filter name to sql `column` format
270 | $where = preg_replace(
271 | [
272 | "/([\\w]+)\\.([\\w]+)[\\s]+(eq|ne|gt|ge|lt|le|lk)/i",
273 | "/([\\w]+)[\\s]+(eq|ne|gt|ge|lt|le|lk)/i",
274 | ],
275 | [
276 | "`$1`.`$2` $3",
277 | "`$1` $2",
278 | ],
279 | $filters
280 | );
281 |
282 | // convert eq null to is null and ne null to is not null
283 | $where = preg_replace(
284 | [
285 | "/ne[\\s]+null/i",
286 | "/eq[\\s]+null/i"
287 | ],
288 | [
289 | "is not null",
290 | "is null"
291 | ],
292 | $where
293 | );
294 |
295 | // Replace operators
296 | $where = preg_replace(
297 | [
298 | "/[\\s]+eq[\\s]+/i",
299 | "/[\\s]+ne[\\s]+/i",
300 | "/[\\s]+gt[\\s]+/i",
301 | "/[\\s]+ge[\\s]+/i",
302 | "/[\\s]+lt[\\s]+/i",
303 | "/[\\s]+le[\\s]+/i",
304 | "/[\\s]+lk[\\s]+/i"
305 | ],
306 | [
307 | " = ",
308 | " != ",
309 | " > ",
310 | " >= ",
311 | " < ",
312 | " <= ",
313 | " LIKE "
314 | ],
315 | $where
316 | );
317 |
318 | $this->filters = $where;
319 | }
320 | else {
321 | throw new InvalidFilterDefinitionException();
322 | }
323 | }
324 | }
325 |
326 | protected function extractOrdering()
327 | {
328 | if (request()->order) {
329 | if (preg_match(RequestParser::ORDER_FILTER, request()->order) === 1) {
330 | $order = request()->order;
331 |
332 |
333 | // eg : user.name asc, year desc, age,month
334 | $order = preg_replace(
335 | [
336 | "/[\\s]*([\\w]+)\\.([\\w]+)(?:[\\s](?!,))*(asc|desc|)/",
337 | "/[\\s]*([\\w`\\.]+)(?:[\\s](?!,))*(asc|desc|)/",
338 | ],
339 | [
340 | "$1`.`$2 $3", // Result: user`.`name asc, year desc, age,month
341 | "`$1` $2", // Result: `user`.`name` asc, `year` desc, `age`,`month`
342 | ],
343 | $order
344 | );
345 |
346 | $this->order = $order;
347 | }
348 | else {
349 | throw new InvalidOrderingDefinitionException();
350 | }
351 | }
352 | }
353 |
354 | /**
355 | * Recursively parses fields to extract limit, ordering and their own fields
356 | * and adds width relations
357 | *
358 | * @param $fields
359 | */
360 | private function parseFields($fields)
361 | {
362 | // If fields parameter is set, parse it using regex
363 | preg_match_all(static::FIELDS_REGEX, $fields, $matches);
364 |
365 | if (!empty($matches[0])) {
366 | foreach ($matches[0] as $match) {
367 |
368 | preg_match_all(static::FIELD_PARTS_REGEX, $match, $parts);
369 |
370 | $fieldName = $parts[1][0];
371 |
372 | if (Str::contains($fieldName, ":") || call_user_func($this->model . "::relationExists", $fieldName)) {
373 | // If field name has a colon, we assume its a relations
374 | // OR
375 | // If method with field name exists in the class, we assume its a relation
376 | // This is default laravel behavior
377 |
378 | $limit = ($parts[3][0] == "") ? config("api.defaultLimit") : $parts[3][0];
379 | $offset = ($parts[4][0] == "") ? 0 : $parts[4][0];
380 | $order = ($parts[5][0] == "chronological") ? "chronological" : "reverse_chronological";
381 |
382 | if (!empty($parts[7][0])) {
383 | $subFields = explode(",", $parts[7][0]);
384 | // This indicates if user specified fields for relation or not
385 | $userSpecifiedFields = true;
386 | }
387 | else {
388 | $subFields = [];
389 | $userSpecifiedFields = false;
390 | }
391 |
392 | $fieldName = str_replace(":", ".", $fieldName);
393 |
394 | // Check if relation name in modal is in camel case then convert relation name in camel case
395 | if(config("api.relation_case", 'snakecase') === 'camelcase'){
396 | $fieldName = Str::camel($fieldName);
397 | }
398 |
399 | if (!isset($this->relations[$fieldName])) {
400 | $this->relations[$fieldName] = [
401 | "limit" => $limit,
402 | "offset" => $offset,
403 | "order" => $order,
404 | "fields" => $subFields,
405 | "userSpecifiedFields" => $userSpecifiedFields
406 | ];
407 | }
408 | else {
409 | $this->relations[$fieldName]["limit"] = $limit;
410 | $this->relations[$fieldName]["offset"] = $offset;
411 | $this->relations[$fieldName]["order"] = $order;
412 | $this->relations[$fieldName]["fields"] = array_merge($this->relations[$fieldName]["fields"], $subFields);
413 | }
414 |
415 | // We also need to add the relation's foreign key field to select. If we don't,
416 | // relations always return null
417 |
418 | if (Str::contains($fieldName, ".")) {
419 |
420 | $relationNameParts = explode('.', $fieldName);
421 | $model = $this->model;
422 |
423 | $relation = null;
424 |
425 | foreach ($relationNameParts as $rp) {
426 | $relation = call_user_func([ new $model(), $rp]);
427 | $model = $relation->getRelated();
428 | }
429 |
430 | // Its a multi level relations
431 | $fieldParts = explode(".", $fieldName);
432 |
433 | if ($relation instanceof BelongsTo) {
434 | $singular = $relation->getForeignKeyName();
435 | }
436 | else if ($relation instanceof HasOne || $relation instanceof HasMany) {
437 | $singular = $relation->getForeignKeyName();
438 | }
439 |
440 | // Unset last element of array
441 | unset($fieldParts[count($fieldParts) - 1]);
442 |
443 | $parent = implode(".", $fieldParts);
444 |
445 | if ($relation instanceof HasOne || $relation instanceof HasMany) {
446 | // For hasMany and HasOne, the foreign key is in current relation table, not in parent
447 | $this->relations[$fieldName]["fields"][] = $singular;
448 | }
449 | else {
450 | // The parent might already been set because we cannot rely on order
451 | // in which user sends relations in request
452 | if (!isset($this->relations[$parent])) {
453 | $this->relations[$parent] = [
454 | "limit" => config("api.defaultLimit"),
455 | "offset" => 0,
456 | "order" => "chronological",
457 | "fields" => isset($singular) ? [$singular] : [],
458 | "userSpecifiedFields" => true
459 | ];
460 | }
461 | else {
462 | if (isset($singular)) {
463 | $this->relations[$parent]["fields"][] = $singular;
464 | }
465 | }
466 | }
467 |
468 | if ($relation instanceof BelongsTo) {
469 | $this->relations[$fieldName]["limit"] = max($this->relations[$fieldName]["limit"], $this->relations[$parent]["limit"]);
470 | }
471 | else if ($relation instanceof HasMany) {
472 | $this->relations[$fieldName]["limit"] = $this->relations[$fieldName]["limit"] * $this->relations[$parent]["limit"];
473 | }
474 | }
475 | else {
476 |
477 | $relation = call_user_func([new $this->model(), $fieldName]);
478 |
479 | if ($relation instanceof HasOne) {
480 | $keyField = explode(".", $relation->getQualifiedParentKeyName())[1];
481 | }
482 | else if ($relation instanceof BelongsTo) {
483 | $keyField = explode(".", $relation->getQualifiedForeignKeyName())[1];
484 | }
485 |
486 | if (isset($keyField) && !in_array($keyField, $this->fields)) {
487 | $this->fields[] = $keyField;
488 | }
489 |
490 | if ($relation instanceof BelongsTo) {
491 | $this->relations[$fieldName]["limit"] = max($this->relations[$fieldName]["limit"], $this->limit);
492 | }
493 | else if ($relation instanceof HasMany) {
494 | // Commented out for third level hasmany limit
495 | // $this->relations[$fieldName]["limit"] = $this->relations[$fieldName]["limit"] * $this->limit;
496 | }
497 | }
498 |
499 | }
500 | else {
501 | // Else, its a normal field
502 | $this->fields[] = $fieldName;
503 | }
504 | }
505 | }
506 | }
507 |
508 | /**
509 | * Load table name into the $table property
510 | */
511 | private function loadTableName()
512 | {
513 | $this->table = call_user_func($this->model."::getTableName");
514 | }
515 |
516 | }
517 |
--------------------------------------------------------------------------------
/src/Routing/ApiResourceRegistrar.php:
--------------------------------------------------------------------------------
1 | router = $router;
25 | }
26 |
27 | /**
28 | * Route a resource to a controller.
29 | *
30 | * @param string $name
31 | * @param string $controller
32 | * @param array $options
33 | * @return void
34 | */
35 | public function register($name, $controller, array $options = [])
36 | {
37 | if (isset($options['parameters']) && ! isset($this->parameters)) {
38 | $this->parameters = $options['parameters'];
39 | }
40 |
41 | // If the resource name contains a slash, we will assume the developer wishes to
42 | // register these resource routes with a prefix so we will set that up out of
43 | // the box so they don't have to mess with it. Otherwise, we will continue.
44 | if (Str::contains($name, '/')) {
45 | $this->prefixedResource($name, $controller, $options);
46 |
47 | return;
48 | }
49 |
50 | // We need to extract the base resource from the resource name. Nested resources
51 | // are supported in the framework, but we need to know what name to use for a
52 | // place-holder on the route parameters, which should be the base resources.
53 | $base = $this->getResourceWildcard(last(explode('.', $name)));
54 |
55 | $defaults = $this->resourceDefaults;
56 |
57 | foreach ($this->getResourceMethods($defaults, $options) as $m) {
58 | $this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options);
59 | }
60 | }
61 |
62 | /**
63 | * Add the relation get method for a resourceful route.
64 | *
65 | * @param string $name
66 | * @param string $base
67 | * @param string $controller
68 | * @param array $options
69 | * @return \Illuminate\Routing\Route
70 | */
71 | protected function addResourceRelation($name, $base, $controller, $options)
72 | {
73 | $uri = $this->getResourceUri($name).'/{'.$base.'}'."/{relation}";
74 |
75 | $action = $this->getResourceAction($name, $controller, 'relation', $options);
76 |
77 | return $this->router->get($uri, $action);
78 | }
79 | }
--------------------------------------------------------------------------------
/src/Routing/ApiRouter.php:
--------------------------------------------------------------------------------
1 | container && $this->container->bound('Froiden\RestAPI\Routing\ApiResourceRegistrar')) {
29 | $registrar = $this->container->make('Froiden\RestAPI\Routing\ApiResourceRegistrar');
30 | }
31 | else {
32 | $registrar = new ResourceRegistrar($this);
33 | }
34 |
35 | $registrar->register($name, $controller, $options);
36 | }
37 |
38 | public function version($versions, Closure $callback)
39 | {
40 | if (is_string($versions))
41 | {
42 | $versions = [$versions];
43 | }
44 |
45 | $this->versions = $versions;
46 |
47 | call_user_func($callback, $this);
48 | }
49 |
50 | /**
51 | * Add a route to the underlying route collection.
52 | *
53 | * @param array|string $methods
54 | * @param string $uri
55 | * @param \Closure|array|string|null $action
56 | * @return \Illuminate\Routing\Route
57 | */
58 | public function addRoute($methods, $uri, $action)
59 | {
60 | // We do not keep routes in ApiRouter. Whenever a route is added,
61 | // we add it to Laravel's primary route collection
62 | $routes = app("router")->getRoutes();
63 | $prefix = config("api.prefix");
64 |
65 | if (empty($this->versions)) {
66 | if (($default = config("api.default_version")) !== null) {
67 | $versions = [$default];
68 | }
69 | else {
70 | $versions = [null];
71 | }
72 |
73 | }
74 | else {
75 | $versions = $this->versions;
76 | }
77 |
78 |
79 | // Add version prefix
80 | foreach ($versions as $version) {
81 | // Add ApiMiddleware to all routes
82 | $route = $this->createRoute($methods, $uri, $action);
83 | $route->middleware(ApiMiddleware::class);
84 |
85 | if ($version !== null) {
86 | $route->prefix($version);
87 | $route->name("." . $version);
88 | }
89 |
90 | if (!empty($prefix)) {
91 | $route->prefix($prefix);
92 | }
93 |
94 | // $routes->add($route);
95 |
96 | // Options route
97 | // $route = $this->createRoute(['OPTIONS'], $uri, ['uses' => '\Froiden\RestAPI\Routing\ApiRouter@returnRoute']);
98 |
99 | // $route->middleware(ApiMiddleware::class);
100 |
101 | // if ($version !== null) {
102 | // $route->prefix($version);
103 | // $route->name("." . $version);
104 | // }
105 |
106 | // if (!empty($prefix)) {
107 | // $route->prefix($prefix);
108 | // }
109 |
110 | $routes->add($route);
111 | }
112 |
113 | app("router")->setRoutes($routes);
114 | }
115 | public function returnRoute()
116 | {
117 | return [];
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Routing/ApiUrlGenerator.php:
--------------------------------------------------------------------------------
1 | 10,
9 |
10 | /**
11 | * Maximum number of records to return in single request. This limit is used
12 | * when user enters large number in limit parameter of the request
13 | */
14 | 'maxLimit' => 1000,
15 |
16 | /*
17 | * Add allow cross origin headers. It is recommended by APIs to allow cross origin
18 | * requests. But, you can disable it.
19 | */
20 | 'cors' => true,
21 |
22 | /**
23 | * Which headers are allowed in CORS requests
24 | */
25 | 'cors_headers' => ['Authorization', 'Content-Type'],
26 |
27 | /**
28 | * List of fields that should not be considered while saving a model
29 | */
30 | 'excludes' => ['_token'],
31 |
32 | /**
33 | * Prefix for all the routes
34 | */
35 | 'prefix' => 'api',
36 |
37 | /**
38 | * Default version for the API. Set null to disable versions
39 | */
40 | 'default_version' => 'v1',
41 |
42 | /**
43 | * Relation method name case snakecase|camelcase default it is snakecase
44 | */
45 | 'relation_case' => 'snakecase'
46 | ];
47 |
--------------------------------------------------------------------------------
/tests/Controllers/CommentController.php:
--------------------------------------------------------------------------------
1 | call('GET', '/dummyUser');
21 |
22 | $this->assertEquals(200, $response->status());
23 | }
24 |
25 | public function testUserIndexWithFields()
26 | {
27 | $response = $this->call('GET', '/dummyUser',
28 | [
29 | 'fields' => "id,name,email,age",
30 | ]);
31 |
32 | $this->assertEquals(200, $response->status());
33 | }
34 |
35 | public function testOneToOneRelationWithFieldsParameter()
36 | {
37 |
38 | $response = $this->call('GET', '/dummyUser',
39 | [
40 | 'fields' => "id,name,email,phone",
41 | ]);
42 | $responseContent = json_decode($response->getContent(), true);
43 | $this->assertNotNull($responseContent["data"]["0"]["phone"]);
44 | $this->assertEquals(200, $response->status());
45 | }
46 |
47 | public function testOneToManyRelationWithFieldsParameter()
48 | {
49 | // Get Data With Related Post
50 | $response = $this->call('GET', '/dummyUser',
51 | [
52 | 'fields' => "id,name,email,posts",
53 | ]);
54 | $responseContent = json_decode($response->getContent(), true);
55 | $this->assertNotEmpty($responseContent["data"]["0"]["posts"]);
56 | $this->assertEquals(200, $response->status());
57 |
58 | // Get Data With User Comments on Post
59 | $response = $this->call('GET', '/dummyUser',
60 | [
61 | 'fields' => "id,name,email,comments",
62 | ]);
63 | $responseContent = json_decode($response->getContent(), true);
64 | $this->assertNotEmpty($responseContent["data"]["0"]["comments"]);
65 | $this->assertEquals(200, $response->status());
66 |
67 | }
68 |
69 | public function testUserIndexWithFilters()
70 | {
71 | $createFactory = \Illuminate\Database\Eloquent\Factory::construct(\Faker\Factory::create(),
72 | base_path() . '/laravel-rest-api/tests/Factories');
73 |
74 | $userId = $createFactory->of(\Froiden\RestAPI\Tests\Models\DummyUser::class)->create();
75 |
76 | // Use "filters" to modify The result
77 | $response = $this->call('GET', '/dummyUser',
78 | [
79 | 'filters' => 'age lt 7',
80 | ]);
81 | $this->assertEquals(200, $response->status());
82 |
83 | // With 'lk' operator
84 | $response = $this->call('GET', '/dummyUser',
85 | [
86 | 'fields' => "id,name",
87 | 'filters' => 'name lk "%'.$userId->name.'%"',
88 | ]);
89 | $this->assertEquals(200, $response->status());
90 | }
91 |
92 | public function testUserIndexWithLimit()
93 | {
94 | // Use "Limit" to get required number of result
95 | $response = $this->call('GET', '/dummyUser',
96 | [
97 | 'limit' => '5',
98 | ]);
99 |
100 | $this->assertEquals(200, $response->status());
101 | }
102 |
103 | public function testUserIndexWithsOrderParameter()
104 | {
105 | // Define order of result
106 | $response = $this->call('GET', '/dummyUser',
107 | [
108 | 'order' => "id desc",
109 | ]);
110 |
111 | $this->assertEquals(200, $response->status());
112 |
113 | $response = $this->call('GET', '/dummyUser',
114 | [
115 | 'order' => "id asc",
116 | ]);
117 |
118 | $this->assertEquals(200, $response->status());
119 |
120 | }
121 |
122 | public function testUserShowFunction()
123 | {
124 | $user = \Froiden\RestAPI\Tests\Models\DummyUser::all()->random();
125 | $response = $this->call('GET', '/dummyUser/'.$user->id);
126 |
127 | $this->assertEquals(200, $response->status());
128 | }
129 |
130 | public function testShowCommentsByUserRelationsEndpoint()
131 | {
132 | $user = \Froiden\RestAPI\Tests\Models\DummyUser::all()->random();
133 |
134 | $post = \Froiden\RestAPI\Tests\Models\DummyPost::all()->random();
135 |
136 | $createFactory = \Illuminate\Database\Eloquent\Factory::construct(\Faker\Factory::create(),
137 | base_path() . '/laravel-rest-api/tests/Factories');
138 |
139 | $comment = $createFactory->of(\Froiden\RestAPI\Tests\Models\DummyComment::class)->create([
140 | 'comment' => "Dummy Comments",
141 | 'user_id' => $user->id,
142 | 'post_id' => $post->id
143 | ]);
144 | $response = $this->call('GET', '/dummyUser/'.$user->id.'/comments');
145 |
146 | $responseContent = json_decode($response->getContent(), true);
147 |
148 | $this->assertNotEmpty($responseContent["data"]);
149 |
150 | $this->assertEquals(200, $response->status());
151 | }
152 |
153 | public function testShowPostsByUserRelationsEndpoint()
154 | {
155 | //region Insert Dummy Data
156 | $user = \Froiden\RestAPI\Tests\Models\DummyUser::all()->random();
157 |
158 | $createFactory = \Illuminate\Database\Eloquent\Factory::construct(\Faker\Factory::create(),
159 | base_path() . '/laravel-rest-api/tests/Factories');
160 |
161 | $createFactory->of(\Froiden\RestAPI\Tests\Models\DummyPost::class)->create([
162 | 'post' => "dummy POst",
163 | 'user_id' => $user->id,
164 | ]);
165 |
166 | //endregion
167 |
168 | $response = $this->call('GET', '/dummyUser/'.$user->id.'/posts');
169 |
170 | $responseContent = json_decode($response->getContent(), true);
171 |
172 | $this->assertNotEmpty($responseContent["data"]);
173 |
174 | $this->assertEquals(200, $response->status());
175 | }
176 |
177 | public function testUserStore()
178 | {
179 | $response = $this->call('POST', '/dummyUser',
180 | [
181 | 'name' => "Dummy User",
182 | 'email' => "dummy@test.com",
183 | 'age' => 25
184 | ]);
185 | $this->assertEquals(200, $response->status());
186 |
187 | }
188 |
189 | public function testUserUpdate()
190 | {
191 | $user = \Froiden\RestAPI\Tests\Models\DummyUser::all()->random();
192 |
193 | $response = $this->call('PUT', '/dummyUser/'.$user->id,
194 | [
195 | 'name' => "Dummy1 User",
196 | 'email' => "dummy2@test.com",
197 | 'age' => 25,
198 | ]);
199 | $this->assertEquals(200, $response->status());
200 | }
201 |
202 | /**
203 | * Test User Delete Function.
204 | *
205 | * @return void
206 | */
207 | public function testUserDelete()
208 | {
209 | $user = \Froiden\RestAPI\Tests\Models\DummyUser::all()->random();
210 |
211 | $response = $this->call('DELETE', '/dummyUser/'.$user->id);
212 |
213 | $this->assertEquals(200, $response->status());
214 | }
215 |
216 | }
217 |
--------------------------------------------------------------------------------
/tests/Factories/ModelFactory.php:
--------------------------------------------------------------------------------
1 | define(
4 | \Froiden\RestAPI\Tests\Models\DummyUser::class,
5 | function(Faker\Generator $faker){
6 | return [
7 | 'name' => $faker->name,
8 | 'email' => $faker->email,
9 | 'age' => $faker->randomDigitNotNull,
10 |
11 | ];
12 | }
13 | );
14 |
15 | $factory->define(
16 | \Froiden\RestAPI\Tests\Models\DummyPhone::class,
17 | function(Faker\Generator $faker){
18 |
19 | return [
20 | 'name' => $faker->name,
21 | 'modal_no' => $faker->swiftBicNumber,
22 | 'user_id' => \Froiden\RestAPI\Tests\Models\DummyUser::all()->random()->id,
23 | ];
24 | }
25 | );
26 |
27 | $factory->define(\Froiden\RestAPI\Tests\Models\DummyPost::class,
28 | function(Faker\Generator $faker)
29 | {
30 | $createFactory = \Illuminate\Database\Eloquent\Factory::construct(\Faker\Factory::create(),
31 | base_path() . '/laravel-rest-api/tests/Factories');
32 | return [
33 | 'post' => $faker->company,
34 | 'user_id' => \Froiden\RestAPI\Tests\Models\DummyUser::all()->random()->id,
35 | ];
36 | }
37 | );
38 |
39 | $factory->define(\Froiden\RestAPI\Tests\Models\DummyComment::class,
40 | function(Faker\Generator $faker)
41 | {
42 | $createFactory = \Illuminate\Database\Eloquent\Factory::construct(\Faker\Factory::create(),
43 | base_path() . '/laravel-rest-api/tests/Factories');
44 | return [
45 | 'comment' => $faker->text,
46 | 'user_id' => \Froiden\RestAPI\Tests\Models\DummyUser::all()->random()->id,
47 | 'post_id' => \Froiden\RestAPI\Tests\Models\DummyPost::all()->random()->id,
48 | ];
49 | }
50 | );
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/tests/Models/DummyComment.php:
--------------------------------------------------------------------------------
1 | belongsTo('Froiden\RestAPI\Tests\Models\DummyPost');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Models/DummyPhone.php:
--------------------------------------------------------------------------------
1 | hasMany('Froiden\RestAPI\Tests\Models\DummyComment');
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Models/DummyUser.php:
--------------------------------------------------------------------------------
1 | hasOne('Froiden\RestAPI\Tests\Models\DummyPhone', 'user_id', 'id');
39 | }
40 |
41 | /**
42 | * The posts that belong to the user.
43 | */
44 | public function posts()
45 | {
46 | return $this->hasMany('Froiden\RestAPI\Tests\Models\DummyPost', 'user_id', 'id');
47 | }
48 |
49 | /**
50 | * The comments that belong to the user.
51 | */
52 | public function comments()
53 | {
54 | return $this->hasMany('Froiden\RestAPI\Tests\Models\DummyComment', 'user_id', 'id');
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/PaginationTest.php:
--------------------------------------------------------------------------------
1 |
2 | call('GET', '/dummyUser',
21 | [
22 | 'order' => 'id asc',
23 | 'offset' => '5',
24 | 'limit' => '2'
25 | ]);
26 | $this->assertEquals(200, $response->status());
27 |
28 | // Pagination set offset = "1" or limit ="1"
29 | $response = $this->call('GET', '/dummyUser',
30 | [
31 | 'order' => 'id asc',
32 | 'offset' => '1',
33 | 'limit' => '1'
34 | ]);
35 | $this->assertEquals(200, $response->status());
36 |
37 | // Pagination set offset = "5" or limit ="3"
38 | $response = $this->call('GET', '/dummyUser',
39 | [
40 | 'order' => 'id asc',
41 | 'offset' => '5',
42 | 'limit' => '-2'
43 | ]);
44 | $this->assertNotEquals(200, $response->status());
45 | }
46 | }
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | createTables();
41 | $this->seedDummyData();
42 |
43 | $this->app[ApiRouter::class]->resource('/dummyUser', UserController::class);
44 | $this->app[ApiRouter::class]->resource('/dummyPost', PostController::class);
45 | $this->app[ApiRouter::class]->resource('/dummyComment', CommentController::class);
46 | }
47 |
48 | /**
49 | * Creates the application.
50 | *
51 | * @return \Illuminate\Foundation\Application
52 | */
53 |
54 | public function createApplication()
55 | {
56 | $app = require __DIR__.'/../../bootstrap/app.php';
57 |
58 | $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
59 |
60 | return $app;
61 | }
62 |
63 | /**
64 | * This is the description for the function below.
65 | *
66 | * Insert dummy data into tables
67 | *
68 | * @return void
69 | */
70 | public function seedDummyData()
71 | {
72 | $factory = \Illuminate\Database\Eloquent\Factory::construct(\Faker\Factory::create(),
73 | base_path() . '/laravel-rest-api/tests/Factories');
74 | \DB::beginTransaction();
75 |
76 | for($i = 0; $i < 10; $i++)
77 | {
78 | $user = $factory->of(DummyUser::class)->create();
79 | $factory->of(DummyPhone::class)->create(
80 | [
81 | 'user_id' => $user->id
82 | ]
83 | );
84 |
85 | $post = $factory->of(DummyPost::class)->create(
86 | [
87 | 'user_id' => $user->id,
88 | ]
89 | );
90 |
91 | $factory->of(DummyComment::class)->create(
92 | [
93 | 'post_id' => $post->id,
94 | 'user_id' => $user->id,
95 | ]
96 | );
97 |
98 | }
99 |
100 | \DB::commit();
101 |
102 | }
103 |
104 | /**
105 | * This is the description for the function below.
106 | *
107 | * Create a tables
108 | *
109 | * @return void
110 | */
111 | public function createTables()
112 | {
113 | Schema::dropIfExists('dummy_comments');
114 | Schema::dropIfExists('dummy_posts');
115 | Schema::dropIfExists('dummy_phones');
116 | Schema::dropIfExists('dummy_users');
117 |
118 | Schema::create('dummy_users', function (Blueprint $table) {
119 | $table->increments('id');
120 | $table->string('name');
121 | $table->string('email', 100)->unique();
122 | $table->integer('age');
123 | $table->timestamps();
124 | });
125 |
126 | Schema::create('dummy_phones', function (Blueprint $table) {
127 | $table->increments('id');
128 | $table->string('name');
129 | $table->string('modal_no');
130 | $table->unsignedInteger('user_id');
131 | $table->foreign('user_id')->references('id')->on('dummy_users')
132 | ->onUpdate('CASCADE')
133 | ->onDelete('CASCADE');
134 | $table->timestamps();
135 | });
136 |
137 | Schema::create('dummy_posts', function (Blueprint $table) {
138 | $table->increments('id');
139 | $table->string('post');
140 | $table->unsignedInteger('user_id');
141 | $table->foreign('user_id')->references('id')->on('dummy_users')
142 | ->onUpdate('CASCADE')
143 | ->onDelete('CASCADE');
144 | $table->timestamps();
145 | });
146 |
147 | Schema::create('dummy_comments', function (Blueprint $table) {
148 | $table->increments('id');
149 | $table->string('comment');
150 | $table->unsignedInteger('user_id');
151 | $table->foreign('user_id')->references('id')->on('dummy_users')
152 | ->onUpdate('CASCADE')
153 | ->onDelete('CASCADE');
154 | $table->unsignedInteger('post_id');
155 | $table->foreign('post_id')->references('id')->on('dummy_posts')
156 | ->onUpdate('CASCADE')
157 | ->onDelete('CASCADE');
158 | $table->timestamps();
159 | });
160 | }
161 |
162 | }
163 |
164 |
--------------------------------------------------------------------------------