├── LICENSE ├── README.md ├── composer.json ├── docs ├── .nojekyll ├── 1.x │ ├── .nojekyll │ ├── README.md │ ├── _coverpage.md │ ├── _sidebar.md │ ├── advanced_usage.md │ ├── appends.md │ ├── configuration.md │ ├── exception_handler.md │ ├── filtering.md │ ├── filters_old.md │ ├── including_relationships.md │ ├── index.html │ ├── installation.md │ ├── pagination.md │ ├── repository.md │ ├── scopes.md │ ├── selecting_fields.md │ └── sorting.md ├── README.md ├── _coverpage.md ├── _sidebar.md ├── advanced_usage.md ├── api_consumer.md ├── appends.md ├── configuration.md ├── exception_handler.md ├── filtering.md ├── filters_old.md ├── including_relationships.md ├── index.html ├── installation.md ├── pagination.md ├── repository.md ├── scopes.md ├── selecting_fields.md └── sorting.md └── src ├── Config └── larapi.php ├── Console ├── ComponentMakeCommand.php └── Stubs │ ├── controllers │ └── controller.stub │ ├── events │ ├── wascreated.stub │ ├── wasdeleted.stub │ └── wasupdated.stub │ ├── exceptions │ └── notfoundexception.stub │ ├── models │ └── model.stub │ ├── repositories │ └── repository.stub │ ├── requests │ ├── createrequest.stub │ └── updaterequest.stub │ └── services │ └── service.stub ├── Controllers └── LaravelController.php ├── Database ├── EloquentBuilderTrait.php └── Repository.php ├── Exceptions ├── ApiException.php └── LarapiException.php ├── ExceptionsFormatters ├── ExceptionFormatter.php └── UnprocessableEntityHttpExceptionFormatter.php ├── Facades └── ApiConsumer.php ├── Providers └── LarapiServiceProvider.php └── Routes └── ApiConsumerRouter.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 one2tek 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 | ### Introduction 2 | 3 | **Larapi** is a package thats offers you to do modern API development in Laravel with support for new versions of Laravel. 4 | 5 | **Larapi** comes included with... 6 | * Exception handler for APIs. 7 | * A Controller class that gives response, parse data for your endpoints. 8 | * A Repository class for requesting entities from your database. 9 | * Sorting. 10 | * Filtering. 11 | * Eager loading. 12 | * Pagination. 13 | * Selecting columns dynamically. 14 | * Selecting scopes dynamically. 15 | * Appends. 16 | * A class for making internal API requests. 17 | 18 | ### Documentation 19 | https://one2tek.github.io/larapi/ -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "one2tek/larapi", 3 | "description": "Modern API development in Laravel.", 4 | "homepage": "https://github.com/one2tek/larapi", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Gentrit Abazi", 9 | "email": "gentritabazi01@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "one2tek\\larapi\\": "src/" 15 | } 16 | }, 17 | "require": { 18 | "illuminate/support": "~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0" 19 | }, 20 | "config": { 21 | "sort-packages": true 22 | }, 23 | "extra": { 24 | "laravel": { 25 | "providers": [ 26 | "one2tek\\larapi\\Providers\\LarapiServiceProvider" 27 | ] 28 | } 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true 32 | } 33 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/one2tek/larapi/23780ac23136b771368b62ffb0ca4d5a4daa1ed6/docs/.nojekyll -------------------------------------------------------------------------------- /docs/1.x/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/one2tek/larapi/23780ac23136b771368b62ffb0ca4d5a4daa1ed6/docs/1.x/.nojekyll -------------------------------------------------------------------------------- /docs/1.x/README.md: -------------------------------------------------------------------------------- 1 | ## Larapi 2 | 3 | > Build fast API-s in Laravel. 4 | 5 | ## What it is 6 | 7 | Larapi is a package thats offers you to do modern API development in Laravel with support for new versions of Laravel. 8 | 9 | ## Features 10 | 11 | - Exception handler for APIs. 12 | - A Controller class that gives response, parse data for your endpoints. 13 | - A Repository class for requesting entities from your database. 14 | - Sorting. 15 | - Filtering. 16 | - Eager loading. 17 | - Pagination. 18 | - Selecting columns dynamically. 19 | - Selecting scopes dynamically. 20 | - Appends. 21 | - A class for making internal API requests. 22 | 23 | ## Authors 24 | 25 | - Gentrit Abazi (https://github.com/gentritabazi01) -------------------------------------------------------------------------------- /docs/1.x/_coverpage.md: -------------------------------------------------------------------------------- 1 | 2 | # Larapi 3 | 4 | > Build fast API-s in Laravel. 5 | 6 | [GitHub](https://github.com/one2tek/larapi) 7 | [Get Started](README.md) -------------------------------------------------------------------------------- /docs/1.x/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Getting started 2 | - [Quick start](README.md) 3 | - [Installation](installation.md) 4 | - [Configuration](configuration.md) 5 | 6 | - Selecting fields 7 | - [Quick start](selecting_fields.md?id=selecting-fields) 8 | - [Basic usage](selecting_fields.md?id=basic-usage) 9 | - [Selecting fields for included relations](selecting_fields.md?id=selecting-fields-for-included-relations) 10 | 11 | - Scopes 12 | - [Quick start](scopes.md?id=scopes) 13 | - [Usage](scopes.md?id=usage) 14 | - [Remove Global Scopes](scopes.md?id=remove-global-scopes) 15 | 16 | - Appends 17 | - [Quick start](appends.md?id=appends) 18 | - [Usage](appends.md?id=usage) 19 | 20 | - Including relationships 21 | - [Quick start](including_relationships.md?id=including-relationships) 22 | - [Basic usage](including_relationships.md?id=basic-usage) 23 | - [Load multiple](including_relationships.md?id=load-multiple) 24 | - [Load nested](including_relationships.md?id=load-nested) 25 | - [Counting related relations](including_relationships.md?id=counting-related-relations) 26 | - [Querying Relationship Existence](including_relationships.md?id=querying-relationship-existence) 27 | - [Querying Relationship Absence](including_relationships.md?id=querying-relationship-absence) 28 | - [Modes](including_relationships.md?id=modes) 29 | - [IDs mode](including_relationships.md?id=ids-mode) 30 | - [Sideload mode](including_relationships.md?id=sideload-mode) 31 | 32 | - Pagination 33 | - [Quick start](pagination.md?id=pagination) 34 | - [Usage](pagination.md?id=usage) 35 | 36 | - Sorting 37 | - [Quick start](sorting.md?id=sorting) 38 | - [Usage](sorting.md?id=usage) 39 | - [Sort multiple columns](sorting.md?id=sort-multiple-columns) 40 | 41 | - Filtering 42 | - [Quick start](filtering.md?id=filtering) 43 | - [Operators](filtering.md?id=operators) 44 | - [Build filter](filtering.md?id=build-filter) 45 | - [Example filters](filtering.md?id=example-filters) 46 | 47 | - Repository 48 | - [Quick start](repository.md?id=repository) 49 | - [Create repository](repository.md?id=create-repository) 50 | - [Default sort](repository.md?id=default-sort) 51 | - [Functions](repository.md?id=functions) 52 | 53 | - Exception Handler 54 | - [Quick start](exception_handler.md?id=exception-handler) 55 | - [Configure](exception_handler.md?id=configure) 56 | - [Formatters](exception_handler.md?id=formatters) 57 | 58 | - Advanced Usage 59 | - [Quick start](advanced_usage.md?id=avanced_usage) 60 | - [Custom Sort](advanced_usage.md?id=custom-sort) 61 | - [Custom Filter](advanced_usage.md?id=custom-filter) -------------------------------------------------------------------------------- /docs/1.x/advanced_usage.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | Learn how to use `Larapi` in Advanced usage. 4 | 5 | # Custom Sort 6 | 7 | You can create custom sort in your repository like this: 8 | 9 | ```php 10 | public function sortMyName($queryBuilder, $direction) 11 | { 12 | // 13 | } 14 | ``` 15 | 16 | # Custom Filter 17 | 18 | You can create custom filter in your repository like this: 19 | 20 | ```php 21 | public function filterName($queryBuilder, $method, $operator, $value, $clauseOperator, $or) 22 | { 23 | // 24 | } 25 | ``` -------------------------------------------------------------------------------- /docs/1.x/appends.md: -------------------------------------------------------------------------------- 1 | # Appends 2 | 3 | With Laravel you can add attributes that do not have a corresponding column in your database. 4 | 5 | # Usage 6 | 7 | The following example appends `isAdmin` attribute: 8 | 9 | ```console 10 | {base_url}/users?append[]=isAdmin 11 | ``` 12 | 13 | # Append multiple 14 | 15 | You can append multiple attributes separating them with a comma: 16 | 17 | ```console 18 | {base_url}/users?append[]=isAdmin,isDriver 19 | ``` -------------------------------------------------------------------------------- /docs/1.x/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | > [Check how to use in real application.](https://github.com/gentritabazi01/Clean-Laravel-Api) 4 | 5 | 1. Extends `one2tek\larapi\Controllers\LaravelController` in your base controller. 6 | 7 | ```php 8 | userRepository = $userRepository; 40 | } 41 | 42 | public function getAll() 43 | { 44 | $resourceOptions = $this—>parseResourceOptions(); 45 | 46 | $users = $this—>userRepository—>get($resourceOptions); 47 | 48 | return $this—>response($users); 49 | } 50 | } 51 | ``` 52 | 53 | 4. Example Repository for Users. 54 | 55 | ```php 56 | generateExceptionResponse(); 18 | } 19 | ``` 20 | 21 | # Formatters 22 | 23 | `Larapi` already comes with sensible formatters out of the box. In `config/larapi.php` is a section where the formatter priority is defined. 24 | 25 | ```php 26 | 'exceptions_formatters' => [ 27 | Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException::class => one2tek\larapi\ExceptionsFormatters\UnprocessableEntityHttpExceptionFormatter::class, 28 | Throwable::class => one2tek\larapi\ExceptionsFormatters\ExceptionFormatter::class 29 | ] 30 | ``` 31 | 32 | You can write custom formatters easily: 33 | 34 | ```php 35 | false, 48 | 'status' => self::STATUS_CODE, 49 | 'message' => self::MESSAGE 50 | ]; 51 | 52 | return response()->json($data, self::STATUS_CODE); 53 | } 54 | } 55 | ``` 56 | 57 | Now you just need to add it to `config/larapi.php` and all `NotFoundHttpExceptions` will be formatted using our custom formatter. -------------------------------------------------------------------------------- /docs/1.x/filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | Data filtering is very easy with at `Larapi` see the examples below. 4 | 5 | By default, all filters have to be explicitly allowed using `$whiteListFilter` property in specified Model. 6 | 7 | For more advanced use cases, [custom filter](advanced_usage?id=custom-filter) can be used. 8 | 9 | #### Operators 10 | 11 | Type | Description 12 | ---- | ----------- 13 | ct | String contains 14 | sw | Starts with 15 | ew | Ends with 16 | eq | Equals 17 | gt | Greater than 18 | gte| Greater than or equalTo 19 | lt | Lesser than 20 | lte | Lesser than or equalTo 21 | in | In array 22 | bt | Between 23 | 24 | #### Build filter 25 | 26 | The way a filter should be formed is: 27 | 28 | ```console 29 | {base_url}/users?filter[columnName][operator][not]=value 30 | ``` 31 | 32 | Another available parameter is `filterByOr`, `search` and `searchByOr`. 33 | 34 | * **columnName** - (Required) - Name of column you want to filter, for relationships use `dots`. 35 | * **operator** - (Optional | Default: `eq`) Type of operator you want to use. 36 | * **not** - (Optional | Default: `false`) Negate the filter (Accepted values: yes|true|1). 37 | 38 | #### Example filters 39 | 40 | Filter all users whose id start with `1000`. 41 | 42 | ```console 43 | {base_url}/users?filter[name][sw]=1000 44 | ``` 45 | 46 | Filter all books whose author is `Gentrit`. 47 | 48 | ```console 49 | {base_url}/users?filter[name]=author.name 50 | ``` 51 | 52 | Filter all users whose name start with `Gentrit` or ends with `Abazi`. 53 | 54 | ```console 55 | {base_url}/users?filterByOr[name][sw]=Gentrit&filterByOr[name][ew]=Abazi 56 | ``` 57 | 58 | [See other ways for filtering](filters_old.md) -------------------------------------------------------------------------------- /docs/1.x/filters_old.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | Filters should be defined as an array of filter groups. 4 | 5 | **Filter groups** 6 | 7 | Property | Value type | Description 8 | -------- | ---------- | ----------- 9 | or | boolean | Should the filters in this group be grouped by logical OR or AND operator 10 | filters | array | Array of filters (see syntax below) 11 | 12 | **Filters** 13 | 14 | Property | Value type | Description 15 | -------- | ---------- | ----------- 16 | column | string | The property of the model to filter by (can also be custom filter) 17 | value | mixed | The value to search for 18 | operator | string | The filter operator to use (see different types below) 19 | not | boolean | Negate the filter 20 | 21 | **Operators** 22 | 23 | Type | Description 24 | ---- | ----------- 25 | ct | String contains 26 | sw | Starts with 27 | ew | Ends with 28 | eq | Equals 29 | gt | Greater than 30 | gte| Greater than or equalTo 31 | lt | Lesser than 32 | lte | Lesser than or equalTo 33 | in | In array 34 | bt | Between 35 | 36 | #### Example filters 37 | 38 | Filter all users whose name start with “Gentrit” or ends with “Abazi”. 39 | 40 | ```SELECT * FROM `users` WHERE name LIKE "Gentrit%" OR name LIKE "%Abazi"``` 41 | 42 | ```json 43 | { 44 | "filter_groups": [ 45 | { 46 | "or": true, 47 | "filters": [ 48 | { 49 | "column": "name", 50 | "operator": "sw", 51 | "value": "Gentrit" 52 | }, 53 | { 54 | "column": "name", 55 | "operator": "ew", 56 | "value": "Abazi" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | Filter all users whose name start with “A” and which were born between years 1990 and 2000. 65 | 66 | ```SELECT * FROM `users` WHERE (name LIKE "A%") AND (`birth_year` >= 1990 and `birth_year` <= 2000)``` 67 | 68 | ```json 69 | { 70 | "filter_groups": [ 71 | { 72 | "filters": [ 73 | { 74 | "column": "name", 75 | "operator": "sw", 76 | "value": "A" 77 | } 78 | ] 79 | }, 80 | { 81 | "filters": [ 82 | { 83 | "column": "birth_year", 84 | "value": 1990, 85 | "operator": "gte" 86 | }, 87 | { 88 | "column": "birth_year", 89 | "value": 2000, 90 | "operator": "lte" 91 | } 92 | ] 93 | } 94 | ] 95 | } 96 | ``` 97 | 98 | Filter all books whose author is Gentrit. 99 | ```json 100 | { 101 | "filter_groups": [ 102 | { 103 | "filters": [ 104 | { 105 | "column": "author.name", 106 | "operator": "eq", 107 | "value": "Gentrit" 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ``` -------------------------------------------------------------------------------- /docs/1.x/including_relationships.md: -------------------------------------------------------------------------------- 1 | # Including relationships 2 | 3 | The `include` query parameter will load any Eloquent relation on the resulting models. 4 | 5 | # Basic usage 6 | 7 | The following query parameter will include the `logs` relation: 8 | 9 | ```console 10 | {base_url}/users?include=logs 11 | ``` 12 | 13 | Users will have all their their `logs` related models loaded. 14 | 15 | # Load multiple 16 | 17 | You can load multiple relationships by separating them with a semicolon: 18 | 19 | ```console 20 | {base_url}/users?include=logs;tasks 21 | ``` 22 | 23 | # Load nested 24 | 25 | You can load nested relationships using the dot `.` notation: 26 | 27 | ```console 28 | {base_url}/users?include=logs.causer 29 | ``` 30 | 31 | # Counting related relations 32 | 33 | If you want to count the number of results from a relationship without actually loading them you may use the `withCount` query parameter, which will place a {relation}_count column on your resulting models. 34 | 35 | ```console 36 | {base_url}/users?withCount[]=comments 37 | ``` 38 | 39 | # Querying Relationship Existence 40 | 41 | Imagine you want to retrieve all blog posts that have at least one comment. 42 | You can do this by passing `has` paramter in query: 43 | 44 | ```console 45 | {base_url}/posts?has[]=comments 46 | ``` 47 | 48 | Nested `has` statements may also be constructed using "dot" notation. For example, you may retrieve all posts that have at least one `comment` and `vote`: 49 | 50 | ```console 51 | {base_url}/posts?has[]=comments.votes 52 | ``` 53 | 54 | # Querying Relationship Absence 55 | 56 | When accessing the records for a model, you may wish to limit your results based on the absence of a relationship. For example, imagine you want to retrieve all blog posts that don't have any `comments`. To do so, you may pass `doesntHave` paramter in query: 57 | 58 | ```console 59 | {base_url}/posts?doesntHave[]=comments 60 | ``` 61 | 62 | # Modes 63 | 64 | With the `Larapi` package you have the opportunity to return the relations in `IDs` mode and `Sideload` mode. 65 | 66 | # IDs mode 67 | 68 | ```console 69 | {base_url}/books?modeIds[]=author 70 | ``` 71 | 72 | Will return a collection of Books eager loaded with the ID of their Author. 73 | 74 | # Sideload mode 75 | 76 | ```console 77 | {base_url}/books?modeSideload[]=author 78 | ``` 79 | 80 | Will return a collection of Books and a eager loaded collection of their Authors in the root scope. -------------------------------------------------------------------------------- /docs/1.x/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Larapi 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/1.x/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | You can install the package via composer: 4 | 5 | ```bash 6 | composer require one2tek/larapi 7 | ``` -------------------------------------------------------------------------------- /docs/1.x/pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | Two parameters are available: `limit` and `page`. limit will determine the number of records per page and page will determine the current page. 4 | 5 | # Usage 6 | 7 | ```console 8 | {base_url}/books?limit=10&page=3 9 | ``` 10 | 11 | Will return books number 30-40. -------------------------------------------------------------------------------- /docs/1.x/repository.md: -------------------------------------------------------------------------------- 1 | # Repository 2 | 3 | `Larapi` included Repository class for requesting entities from your database. 4 | 5 | # Create repository 6 | 7 | To create repository is easy you need just to define own model: 8 | 9 | ```php 10 | $value1, $column2 => $value2]`) 70 | 71 | ### getWhereIn (string $column, array $values, array $options = []) 72 | 73 | Get `User` rows where `$column` can be any of the values given by `$values` 74 | 75 | ### getLatest (array $options = []) 76 | 77 | Get the most recent `User` 78 | 79 | ### getLatestWhere (string $column, mixed $value, array $options = []) 80 | 81 | Get the most recent `User` where `$column=$value` 82 | 83 | ### delete ($id) 84 | 85 | Delete `User` rows by primary key 86 | 87 | ### deleteWhere ($column, $value) 88 | 89 | Delete `User` rows where `$column=$value` 90 | 91 | ### deleteWhereArray (array $clauses) 92 | 93 | Delete `User` rows by multiple where clauses (`[$column1 => $value1, $column2 => $value2]`) -------------------------------------------------------------------------------- /docs/1.x/scopes.md: -------------------------------------------------------------------------------- 1 | # Scopes 2 | 3 | Sometimes more advanced filtering options are necessary. This is where scope filters, callback filters and custom filters come in handy. 4 | 5 | # Usage 6 | 7 | The following query parameter will add the `popular` scope: 8 | 9 | ```console 10 | {base_url}/users?scope[]=popular 11 | ``` 12 | 13 | # Remove Global Scopes 14 | 15 | Global scopes allow you to add constraints to all queries for a given model. 16 | What if we want to remove it ? 17 | 18 | The following query parameter will remove the `delivered` global scope: 19 | 20 | ```console 21 | {base_url}/books?excludeGlobalScopes[]=delivered 22 | ``` 23 | 24 | # Select multiple 25 | 26 | You can select multiple scopes separating them with a comma: 27 | 28 | ```console 29 | {base_url}/users?scope[]=popular,famous 30 | ``` -------------------------------------------------------------------------------- /docs/1.x/selecting_fields.md: -------------------------------------------------------------------------------- 1 | # Selecting fields 2 | 3 | Sometimes you'll want to fetch only a couple fields to reduce the overall size of your SQL query. This can be done by specifying some fields in request by query parameter. 4 | 5 | # Basic usage 6 | 7 | The following example fetches only the users' id and name: 8 | 9 | ```console 10 | {base_url}/users?select=id,name 11 | ``` 12 | 13 | The SQL query will look like this: 14 | 15 | ```sql 16 | `SELECT "id", "name" FROM "users"` 17 | ``` 18 | 19 | # Selecting fields for included relations 20 | 21 | The following example fetches only the authors' id and name: 22 | 23 | ```console 24 | {base_url}/books?include=author:id,name. 25 | ``` -------------------------------------------------------------------------------- /docs/1.x/sorting.md: -------------------------------------------------------------------------------- 1 | # Sorting 2 | 3 | The `sortByAsc` and `sortByDesc` query parameters are used to determine by which property the results collection will be ordered. 4 | 5 | For more advanced use cases, [custom sorts](advanced_usage?id=custom-sort) can be used. 6 | 7 | # Usage 8 | 9 | The following query parameter `sortByAsc` will sort results by from the lowest value to the highest value: 10 | 11 | ```console 12 | {base_url}/books?sortByAsc=id 13 | ``` 14 | 15 | The following query parameter `sortByDesc` will sort results by from the highest value to the lowest value: 16 | 17 | ```console 18 | {base_url}/books?sortByDesc=id 19 | ``` 20 | 21 | # Sort multiple columns 22 | 23 | You can sort multiple columns separating them with a comma: 24 | 25 | ```console 26 | {base_url}/books?sortByDesc=id,name 27 | ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Larapi 2 | 3 | > Build fast API-s in Laravel. 4 | 5 | ## What it is 6 | 7 | Larapi is a package thats offers you to do modern API development in Laravel with support for new versions of Laravel. 8 | 9 | ## Features 10 | 11 | - Exception handler for APIs. 12 | - A Controller class that gives response, parse data for your endpoints. 13 | - A Repository class for requesting entities from your database. 14 | - Sorting. 15 | - Filtering. 16 | - Eager loading. 17 | - Pagination. 18 | - Selecting columns dynamically. 19 | - Selecting scopes dynamically. 20 | - Appends. 21 | - A class for making internal API requests. 22 | 23 | ## Authors 24 | 25 | - Gentrit Abazi (https://github.com/gentritabazi) 26 | 27 | ## Contributors 28 | 29 | - https://github.com/one2tek/larapi/graphs/contributors 30 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | 2 | # Larapi 3 | 4 | > Build fast API-s in Laravel. 5 | 6 | [GitHub](https://github.com/one2tek/larapi) 7 | [Get Started](README.md) -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Getting started 2 | - [Quick start](README.md) 3 | - [Installation](installation.md) 4 | - [Configuration](configuration.md) 5 | 6 | - Selecting fields 7 | - [Quick start](selecting_fields.md?id=selecting-fields) 8 | - [Basic usage](selecting_fields.md?id=basic-usage) 9 | - [Selecting fields for included relations](selecting_fields.md?id=selecting-fields-for-included-relations) 10 | 11 | - Scopes 12 | - [Quick start](scopes.md?id=scopes) 13 | - [Usage](scopes.md?id=usage) 14 | - [Remove Global Scopes](scopes.md?id=remove-global-scopes) 15 | 16 | - Appends 17 | - [Quick start](appends.md?id=appends) 18 | - [Usage](appends.md?id=usage) 19 | 20 | - Including relationships 21 | - [Quick start](including_relationships.md?id=including-relationships) 22 | - [Basic usage](including_relationships.md?id=basic-usage) 23 | - [Load multiple](including_relationships.md?id=load-multiple) 24 | - [Load nested](including_relationships.md?id=load-nested) 25 | - [Counting related relations](including_relationships.md?id=counting-related-relations) 26 | - [Querying Relationship Existence](including_relationships.md?id=querying-relationship-existence) 27 | - [Querying Relationship Absence](including_relationships.md?id=querying-relationship-absence) 28 | 29 | - Pagination 30 | - [Quick start](pagination.md?id=pagination) 31 | - [Usage](pagination.md?id=usage) 32 | 33 | - Sorting 34 | - [Quick start](sorting.md?id=sorting) 35 | - [Usage](sorting.md?id=usage) 36 | - [Sort multiple columns](sorting.md?id=sort-multiple-columns) 37 | 38 | - Filtering 39 | - [Quick start](filtering.md?id=filtering) 40 | - [Operators](filtering.md?id=operators) 41 | - [Build filter](filtering.md?id=build-filter) 42 | - [Example filters](filtering.md?id=example-filters) 43 | 44 | - Repository 45 | - [Quick start](repository.md?id=repository) 46 | - [Create repository](repository.md?id=create-repository) 47 | - [Default sort](repository.md?id=default-sort) 48 | - [Functions](repository.md?id=functions) 49 | 50 | - Exception Handler 51 | - [Quick start](exception_handler.md?id=exception-handler) 52 | - [Configure](exception_handler.md?id=configure) 53 | - [Formatters](exception_handler.md?id=formatters) 54 | 55 | - Api Consumer 56 | - [Quick start](api_consumer.md?api-consumer) 57 | - [Usage Example](api_consumer.md?usage-example) 58 | 59 | - Advanced Usage 60 | - [Quick start](advanced_usage.md?id=avanced_usage) 61 | - [Custom Sort](advanced_usage.md?id=custom-sort) 62 | - [Custom Filter](advanced_usage.md?id=custom-filter) 63 | - [Include Soft Deleted](advanced_usage.md?id=include-soft-deleted) -------------------------------------------------------------------------------- /docs/advanced_usage.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | Learn how to use `Larapi` in Advanced usage. 4 | 5 | # Custom Sort 6 | 7 | You can create custom sort in your repository like this: 8 | 9 | ```php 10 | public function sortMyName($queryBuilder, $direction) 11 | { 12 | // 13 | } 14 | ``` 15 | 16 | # Custom Filter 17 | 18 | You can create custom filter in your repository like this: 19 | 20 | ```php 21 | public function filterName($queryBuilder, $method, $operator, $value, $clauseOperator, $or) 22 | { 23 | // 24 | } 25 | ``` 26 | 27 | # Include Soft Deleted 28 | 29 | By laravel soft deleted models will automatically be excluded from query results. However, you may force soft deleted models to be included in a query's results by calling the `withTrashed` parameter on the url: 30 | 31 | ```console 32 | {base_url}/users?withTrashed=1 33 | ``` -------------------------------------------------------------------------------- /docs/api_consumer.md: -------------------------------------------------------------------------------- 1 | # Api Consumer 2 | 3 | `Api Consumer` is a small class for making internal API requests. 4 | 5 | # Usage Example 6 | 7 | ```php 8 | private $apiConsumer; 9 | 10 | public function __construct() 11 | { 12 | $this->apiConsumer = app()->make('apiconsumer'); 13 | } 14 | 15 | public function index() 16 | { 17 | return $this->apiConsumer->post('/oauth/token', []); 18 | } 19 | ``` -------------------------------------------------------------------------------- /docs/appends.md: -------------------------------------------------------------------------------- 1 | # Appends 2 | 3 | With Laravel you can add attributes that do not have a corresponding column in your database. 4 | 5 | # Usage 6 | 7 | The following example appends `isAdmin` attribute: 8 | 9 | ```console 10 | {base_url}/users?append=isAdmin 11 | ``` 12 | 13 | # Append multiple 14 | 15 | You can append multiple attributes separating them with a comma: 16 | 17 | ```console 18 | {base_url}/users?append=isAdmin,isDriver 19 | ``` -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | > [Check how to use in real application.](https://github.com/gentritabazi01/Clean-Laravel-Api) 4 | 5 | 1. Extends `one2tek\larapi\Controllers\LaravelController` in your base controller. 6 | 7 | ```php 8 | userRepository = $userRepository; 40 | } 41 | 42 | public function getAll() 43 | { 44 | $resourceOptions = $this—>parseResourceOptions(); 45 | 46 | $users = $this—>userRepository—>get($resourceOptions); 47 | 48 | return $this—>response($users); 49 | } 50 | } 51 | ``` 52 | 53 | 4. Example Repository for Users. 54 | 55 | ```php 56 | generateExceptionResponse(); 18 | } 19 | ``` 20 | 21 | # Formatters 22 | 23 | `Larapi` already comes with sensible formatters out of the box. In `config/larapi.php` is a section where the formatter priority is defined. 24 | 25 | ```php 26 | 'exceptions_formatters' => [ 27 | Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException::class => one2tek\larapi\ExceptionsFormatters\UnprocessableEntityHttpExceptionFormatter::class, 28 | Throwable::class => one2tek\larapi\ExceptionsFormatters\ExceptionFormatter::class 29 | ] 30 | ``` 31 | 32 | You can write custom formatters easily: 33 | 34 | ```php 35 | false, 48 | 'status' => self::STATUS_CODE, 49 | 'message' => self::MESSAGE 50 | ]; 51 | 52 | return response()->json($data, self::STATUS_CODE); 53 | } 54 | } 55 | ``` 56 | 57 | Now you just need to add it to `config/larapi.php` and all `NotFoundHttpExceptions` will be formatted using our custom formatter. -------------------------------------------------------------------------------- /docs/filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | Data filtering is very easy with at `Larapi` see the examples below. 4 | 5 | By default, all filters have to be explicitly allowed using `$whiteListFilter` property in specified Model. 6 | 7 | List of all valid syntax for $whiteListFilter: 8 | 9 | ```php 10 | public static $whiteListFilter = ['*']; 11 | public static $whiteListFilter = ['id', 'title', 'author']; 12 | public static $whiteListFilter = ['id', 'title', 'author.*']; 13 | ``` 14 | 15 | If the filter is `['*']` then all properties and sub-properties can be used for filtering. 16 | 17 | If the filter is a list of model properties then only the selected properties can be filtered. 18 | 19 | If some of the filter are a relationship then only the `$whiteListFilter` properties of the sub-property's model can be filtered. 20 | 21 | If some of the filter contains a `.*` the all sub-properties of the relationship model can be filtered. 22 | 23 | For more advanced use cases, [custom filter](advanced_usage?id=custom-filter) can be used. 24 | 25 | #### Operators 26 | 27 | Type | Description 28 | ---- | ----------- 29 | ct | String contains 30 | sw | Starts with 31 | ew | Ends with 32 | eq | Equals 33 | gt | Greater than 34 | gte| Greater than or equalTo 35 | lt | Lesser than 36 | lte | Lesser than or equalTo 37 | in | In array 38 | bt | Between 39 | 40 | #### Build filter 41 | 42 | The way a filter should be formed is: 43 | 44 | ```console 45 | {base_url}/users?filter[columnName][operator][not]=value 46 | ``` 47 | 48 | Another available parameter is `filterByOr`, `search` and `searchByOr`. 49 | 50 | * **columnName** - (Required) - Name of column you want to filter, for relationships use `dots`. 51 | * **operator** - (Optional | Default: `eq`) Type of operator you want to use. 52 | * **not** - (Optional | Default: `false`) Negate the filter (Accepted values: not|yes|true|1). 53 | 54 | #### Example filters 55 | 56 | Filter all users whose id start with `1000`. 57 | 58 | ```console 59 | {base_url}/users?filter[name][sw]=1000 60 | ``` 61 | 62 | Filter all books whose author is `Gentrit`. 63 | 64 | ```console 65 | {base_url}/users?filter[author.name]=Gentrit 66 | ``` 67 | 68 | Filter all users whose name start with `Gentrit` or ends with `Abazi`. 69 | 70 | ```console 71 | {base_url}/users?filterByOr[name][sw]=Gentrit&filterByOr[name][ew]=Abazi 72 | ``` 73 | 74 | [See other ways for filtering](filters_old.md) -------------------------------------------------------------------------------- /docs/filters_old.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | Filters should be defined as an array of filter groups. 4 | 5 | **Filter groups** 6 | 7 | Property | Value type | Description 8 | -------- | ---------- | ----------- 9 | or | boolean | Should the filters in this group be grouped by logical OR or AND operator 10 | filters | array | Array of filters (see syntax below) 11 | 12 | **Filters** 13 | 14 | Property | Value type | Description 15 | -------- | ---------- | ----------- 16 | column | string | The property of the model to filter by (can also be custom filter) 17 | value | mixed | The value to search for 18 | operator | string | The filter operator to use (see different types below) 19 | not | boolean | Negate the filter 20 | 21 | **Operators** 22 | 23 | Type | Description 24 | ---- | ----------- 25 | ct | String contains 26 | sw | Starts with 27 | ew | Ends with 28 | eq | Equals 29 | gt | Greater than 30 | gte| Greater than or equalTo 31 | lt | Lesser than 32 | lte | Lesser than or equalTo 33 | in | In array 34 | bt | Between 35 | 36 | #### Example filters 37 | 38 | Filter all users whose name start with “Gentrit” or ends with “Abazi”. 39 | 40 | ```SELECT * FROM `users` WHERE name LIKE "Gentrit%" OR name LIKE "%Abazi"``` 41 | 42 | ```json 43 | { 44 | "filter_groups": [ 45 | { 46 | "or": true, 47 | "filters": [ 48 | { 49 | "column": "name", 50 | "operator": "sw", 51 | "value": "Gentrit" 52 | }, 53 | { 54 | "column": "name", 55 | "operator": "ew", 56 | "value": "Abazi" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | Filter all users whose name start with “A” and which were born between years 1990 and 2000. 65 | 66 | ```SELECT * FROM `users` WHERE (name LIKE "A%") AND (`birth_year` >= 1990 and `birth_year` <= 2000)``` 67 | 68 | ```json 69 | { 70 | "filter_groups": [ 71 | { 72 | "filters": [ 73 | { 74 | "column": "name", 75 | "operator": "sw", 76 | "value": "A" 77 | } 78 | ] 79 | }, 80 | { 81 | "filters": [ 82 | { 83 | "column": "birth_year", 84 | "value": 1990, 85 | "operator": "gte" 86 | }, 87 | { 88 | "column": "birth_year", 89 | "value": 2000, 90 | "operator": "lte" 91 | } 92 | ] 93 | } 94 | ] 95 | } 96 | ``` 97 | 98 | Filter all books whose author is Gentrit. 99 | ```json 100 | { 101 | "filter_groups": [ 102 | { 103 | "filters": [ 104 | { 105 | "column": "author.name", 106 | "operator": "eq", 107 | "value": "Gentrit" 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ``` -------------------------------------------------------------------------------- /docs/including_relationships.md: -------------------------------------------------------------------------------- 1 | # Including relationships 2 | 3 | The `include` query parameter will load any Eloquent relation on the resulting models. 4 | 5 | # Basic usage 6 | 7 | The following query parameter will include the `logs` relation: 8 | 9 | ```console 10 | {base_url}/users?include=logs 11 | ``` 12 | 13 | Users will have all their their `logs` related models loaded. 14 | 15 | # Load multiple 16 | 17 | You can load multiple relationships by separating them with a semicolon: 18 | 19 | ```console 20 | {base_url}/users?include=logs;tasks 21 | ``` 22 | 23 | # Load nested 24 | 25 | You can load nested relationships using the dot `.` notation: 26 | 27 | ```console 28 | {base_url}/users?include=logs.causer 29 | ``` 30 | 31 | # Counting related relations 32 | 33 | If you want to count the number of results from a relationship without actually loading them you may use the `withCount` query parameter, which will place a {relation}_count column on your resulting models. 34 | 35 | ```console 36 | {base_url}/users?withCount=comments 37 | ``` 38 | 39 | # Querying Relationship Existence 40 | 41 | Imagine you want to retrieve all blog posts that have at least one comment. 42 | You can do this by passing `has` paramter in query: 43 | 44 | ```console 45 | {base_url}/posts?has=comments 46 | ``` 47 | 48 | Nested `has` statements may also be constructed using "dot" notation. For example, you may retrieve all posts that have at least one `comment` and `vote`: 49 | 50 | ```console 51 | {base_url}/posts?has=comments.votes 52 | ``` 53 | 54 | # Querying Relationship Absence 55 | 56 | When accessing the records for a model, you may wish to limit your results based on the absence of a relationship. For example, imagine you want to retrieve all blog posts that don't have any `comments`. To do so, you may pass `doesntHave` paramter in query: 57 | 58 | ```console 59 | {base_url}/posts?doesntHave=comments 60 | ``` -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Larapi 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | You can install the package via composer: 4 | 5 | ```bash 6 | composer require one2tek/larapi 7 | ``` -------------------------------------------------------------------------------- /docs/pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | Two parameters are available: `limit` and `page`. limit will determine the number of records per page and page will determine the current page. 4 | 5 | # Usage 6 | 7 | ```console 8 | {base_url}/books?limit=10&page=3 9 | ``` 10 | 11 | Will return books number 30-40. -------------------------------------------------------------------------------- /docs/repository.md: -------------------------------------------------------------------------------- 1 | # Repository 2 | 3 | `Larapi` included Repository class for requesting entities from your database. 4 | 5 | # Create repository 6 | 7 | To create repository is easy you need just to define own model: 8 | 9 | ```php 10 | $value1, $column2 => $value2]`) 69 | 70 | ### getWhereIn (string $column, array $values, array $options = []) 71 | 72 | Get `User` rows where `$column` can be any of the values given by `$values` 73 | 74 | ### getLatest (array $options = []) 75 | 76 | Get the most recent `User` 77 | 78 | ### getLatestWhere (string $column, mixed $value, array $options = []) 79 | 80 | Get the most recent `User` where `$column=$value` 81 | 82 | ### delete ($id) 83 | 84 | Delete `User` rows by primary key 85 | 86 | ### deleteWhere ($column, $value) 87 | 88 | Delete `User` rows where `$column=$value` 89 | 90 | ### deleteWhereArray (array $clauses) 91 | 92 | Delete `User` rows by multiple where clauses (`[$column1 => $value1, $column2 => $value2]`) -------------------------------------------------------------------------------- /docs/scopes.md: -------------------------------------------------------------------------------- 1 | # Scopes 2 | 3 | Sometimes more advanced filtering options are necessary. This is where scope filters, callback filters and custom filters come in handy. 4 | 5 | # Usage 6 | 7 | The following query parameter will add the `popular` scope: 8 | 9 | ```console 10 | {base_url}/users?scope=popular 11 | ``` 12 | 13 | # Remove Global Scopes 14 | 15 | Global scopes allow you to add constraints to all queries for a given model. 16 | What if we want to remove it ? 17 | 18 | The following query parameter will remove the `delivered` global scope: 19 | 20 | ```console 21 | {base_url}/books?excludeGlobalScopes=delivered 22 | ``` 23 | 24 | # Select multiple 25 | 26 | You can select multiple scopes separating them with a comma: 27 | 28 | ```console 29 | {base_url}/users?scope=popular,famous 30 | ``` -------------------------------------------------------------------------------- /docs/selecting_fields.md: -------------------------------------------------------------------------------- 1 | # Selecting fields 2 | 3 | Sometimes you'll want to fetch only a couple fields to reduce the overall size of your SQL query. This can be done by specifying some fields in request by query parameter. 4 | 5 | # Basic usage 6 | 7 | The following example fetches only the users' id and name: 8 | 9 | ```console 10 | {base_url}/users?select=id,name 11 | ``` 12 | 13 | The SQL query will look like this: 14 | 15 | ```sql 16 | `SELECT "id", "name" FROM "users"` 17 | ``` 18 | 19 | # Selecting fields for included relations 20 | 21 | The following example fetches only the authors' id and name: 22 | 23 | ```console 24 | {base_url}/books?include=author:id,name. 25 | ``` -------------------------------------------------------------------------------- /docs/sorting.md: -------------------------------------------------------------------------------- 1 | # Sorting 2 | 3 | The `sortByAsc` and `sortByDesc` query parameters are used to determine by which property the results collection will be ordered. 4 | 5 | For more advanced use cases, [custom sorts](advanced_usage?id=custom-sort) can be used. 6 | 7 | # Usage 8 | 9 | The following query parameter `sortByAsc` will sort results by from the lowest value to the highest value: 10 | 11 | ```console 12 | {base_url}/books?sortByAsc=id 13 | ``` 14 | 15 | The following query parameter `sortByDesc` will sort results by from the highest value to the lowest value: 16 | 17 | ```console 18 | {base_url}/books?sortByDesc=id 19 | ``` 20 | 21 | # Sort multiple columns 22 | 23 | You can sort multiple columns separating them with a comma: 24 | 25 | ```console 26 | {base_url}/books?sortByDesc=id,name 27 | ``` 28 | 29 | # Order by random 30 | 31 | You can also sort data randomly: 32 | 33 | ```console 34 | {base_url}/books?orderByRandom=1 35 | ``` -------------------------------------------------------------------------------- /src/Config/larapi.php: -------------------------------------------------------------------------------- 1 | 'api', 6 | 7 | 'exceptions_formatters' => [ 8 | Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException::class => one2tek\larapi\ExceptionsFormatters\UnprocessableEntityHttpExceptionFormatter::class, 9 | Throwable::class => one2tek\larapi\ExceptionsFormatters\ExceptionFormatter::class 10 | ] 11 | 12 | ]; 13 | -------------------------------------------------------------------------------- /src/Console/ComponentMakeCommand.php: -------------------------------------------------------------------------------- 1 | [ 39 | 'model' 40 | ], 41 | 'Controllers' => [ 42 | 'controller' 43 | ], 44 | 'Services' => [ 45 | 'service' 46 | ], 47 | 'Repositories' => [ 48 | 'repository' 49 | ], 50 | 'Events' => [ 51 | 'WasCreated', 52 | 'WasUpdated', 53 | 'WasDeleted' 54 | ], 55 | 'Exceptions' => [ 56 | 'NotFoundException' 57 | ], 58 | 'Requests' => [ 59 | 'CreateRequest', 60 | 'UpdateRequest' 61 | ] 62 | ]; 63 | 64 | /** 65 | * Create a new command instance. 66 | * 67 | * @param Filesystem $files 68 | * @param Composer $composer 69 | */ 70 | public function __construct(Filesystem $files) 71 | { 72 | parent::__construct(); 73 | 74 | $this->files = $files; 75 | } 76 | 77 | /** 78 | * Execute the console command. 79 | * 80 | * @return mixed 81 | */ 82 | public function fire() 83 | { 84 | $this->makeDirectory(base_path(). '/'. $this->getModulesFolder(). '/'. $this->argument('parent')); 85 | 86 | foreach ($this->fileTypes as $key => $fileType) { 87 | $this->makeSubDirectories(base_path(). '/'. $this->getModulesFolder(). '/'. $this->argument('parent'), $key); 88 | 89 | foreach ($fileType as $file) { 90 | $this->makeFile($key, $file); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Make file. 97 | */ 98 | protected function makeFile($dir, $fileName) 99 | { 100 | $stubFile = strtolower(rtrim($dir. '/'. $fileName, 's')); 101 | $name = $this->argument('name'); 102 | $myFileName = $fileName; 103 | if ($myFileName == 'model') { 104 | $myFileName = ''; 105 | } 106 | $filePath = base_path(). '/'. $this->getModulesFolder(). '/'. $this->argument('parent'). '/' . $dir. '/'. $name. ucfirst($myFileName). '.php'; 107 | 108 | if (!$this->files->exists($filePath)) { 109 | $class = $this->buildFile($name, $stubFile, $dir. '/'. $fileName, $dir, $fileName); 110 | $this->files->put($filePath, $class); 111 | } 112 | } 113 | 114 | /** 115 | * Build the directory for the class if necessary. 116 | * 117 | * @param string $path 118 | * 119 | * @return string 120 | */ 121 | protected function makeDirectory($path) 122 | { 123 | if (!$this->files->isDirectory($path)) { 124 | $this->files->makeDirectory($path, 0777, true, true); 125 | } 126 | } 127 | 128 | /** 129 | * Build the directory for the class if necessary. 130 | * 131 | * @param string $path 132 | * 133 | * @return string 134 | */ 135 | protected function makeSubDirectories($path, $fileType) 136 | { 137 | if ($this->files->isDirectory($path)) { 138 | $this->makeDirectory($path.'/'.$fileType.'/'); 139 | } 140 | } 141 | 142 | /** 143 | * Build file. 144 | */ 145 | protected function buildFile($name, $type, $fileName, $dir, $shortFileName) 146 | { 147 | $stub = $this->files->get($this->getStub($type)); 148 | return $this->replaceNamespace($stub, $fileName, $dir)->replaceClass($stub, $name, $fileName, $shortFileName); 149 | } 150 | 151 | /** 152 | * Replace the class name for the given stub. 153 | * 154 | * @param string $stub 155 | * @param string $name 156 | * 157 | * @return string 158 | */ 159 | protected function replaceClass($stub, $name, $fileName, $shortFileName) 160 | { 161 | $class = str_replace($this->getNamespace($name). '\\', '', $name); 162 | 163 | $stub = str_replace('DummyVariable', $class, $stub); 164 | $stub = str_replace('dummyVariable', lcfirst($class), $stub); 165 | $stub = str_replace('dummyvariable', strtolower($class), $stub); 166 | $stub = str_replace('Singular_Dummy_Variable', Str::singular(strtolower($class)), $stub); 167 | $stub = str_replace('Plural_Dummy_Variable', Str::plural(strtolower($class)), $stub); 168 | $stub = str_replace('DummyName', ucfirst($name), $stub); 169 | 170 | return str_replace('DummyClass', $this->argument('name'). ucfirst($shortFileName), $stub); 171 | } 172 | 173 | /** 174 | * Get the stub file for the generator. 175 | * 176 | * @return string 177 | */ 178 | protected function getStub($type) 179 | { 180 | return __DIR__. '/Stubs/'. $type. '.stub'; 181 | } 182 | 183 | /** 184 | * Replace the namespace for the given stub. 185 | * 186 | * @param string $stub 187 | * @param string $name 188 | * 189 | * @return $this 190 | */ 191 | protected function replaceNamespace(&$stub, $fileName, $dir) 192 | { 193 | $stub = str_replace('DummyNamespace', 'Api\\'. $this->argument('parent'). '\\'. $dir, $stub); 194 | $stub = str_replace('DummyPath', 'Api\\'.$this->argument('parent'), $stub); 195 | 196 | return $this; 197 | } 198 | 199 | /** 200 | * Get the full namespace for a given class, without the class name. 201 | * 202 | * @param string $name 203 | * 204 | * @return string 205 | */ 206 | protected function getNamespace($name) 207 | { 208 | return trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); 209 | } 210 | 211 | /** 212 | * Execute the console command. 213 | * 214 | * @return mixed 215 | */ 216 | public function handle() 217 | { 218 | $this->fire(); 219 | 220 | $routePath = $this->argument('name'); 221 | $routePath = strtolower($routePath); 222 | $routePath = Str::plural($routePath); 223 | $controllerName = $this->argument('name'); 224 | $controllerName = ucfirst($controllerName); 225 | 226 | $this->info( 227 | "Routes: ". PHP_EOL. 228 | "$". "router->get('/". $routePath. "', '". $controllerName. "Controller@getAll');". PHP_EOL. 229 | "$". "router->get('/". $routePath. "/{id}', '". $controllerName. "Controller@getById');". PHP_EOL. 230 | "$". "router->post('/". $routePath. "', '". $controllerName. "Controller@create');". PHP_EOL. 231 | "$". "router->put('/". $routePath. "/{id}', '". $controllerName. "Controller@update');". PHP_EOL. 232 | "$". "router->delete('/". $routePath. "/{id}', '". $controllerName. "Controller@delete');". PHP_EOL 233 | ); 234 | } 235 | 236 | private function getModulesFolder() 237 | { 238 | return config('larapi.modules_folder'); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Console/Stubs/controllers/controller.stub: -------------------------------------------------------------------------------- 1 | dummyVariableService = $dummyVariableService; 18 | } 19 | 20 | public function getAll() 21 | { 22 | $resourceOptions = $this->parseResourceOptions(); 23 | 24 | $sendData = $this->dummyVariableService->getAll($resourceOptions); 25 | 26 | return $this->response($sendData); 27 | } 28 | 29 | public function getById($dummyVariableId) 30 | { 31 | $resourceOptions = $this->parseResourceOptions(); 32 | 33 | $sendData['dummyvariable'] = $this->dummyVariableService->getById($dummyVariableId, $resourceOptions); 34 | 35 | return $this->response($sendData); 36 | } 37 | 38 | public function create(DummyVariableCreateRequest $request) 39 | { 40 | $data = $request->validated(); 41 | 42 | $sendData['dummyvariable'] = $this->dummyVariableService->create($data); 43 | 44 | return $this->response($sendData, 201); 45 | } 46 | 47 | public function update($dummyVariableId, DummyVariableUpdateRequest $request) 48 | { 49 | $data = $request->validated(); 50 | 51 | $sendData['dummyvariable'] = $this->dummyVariableService->update($dummyVariableId, $data); 52 | 53 | return $this->response($sendData); 54 | } 55 | 56 | public function delete($dummyVariableId) 57 | { 58 | $this->dummyVariableService->delete($dummyVariableId); 59 | 60 | return $this->response(null, 204); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Console/Stubs/events/wascreated.stub: -------------------------------------------------------------------------------- 1 | dummyVariable = $dummyVariable; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Console/Stubs/events/wasdeleted.stub: -------------------------------------------------------------------------------- 1 | dummyVariable = $dummyVariable; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Console/Stubs/events/wasupdated.stub: -------------------------------------------------------------------------------- 1 | dummyVariable = $dummyVariable; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Console/Stubs/exceptions/notfoundexception.stub: -------------------------------------------------------------------------------- 1 | getModel(); 18 | 19 | $dummyVariable->fill($data); 20 | $dummyVariable->save(); 21 | 22 | return $dummyVariable; 23 | } 24 | 25 | public function update(DummyVariable $dummyVariable, array $data) 26 | { 27 | $dummyVariable->fill($data); 28 | 29 | $dummyVariable->save(); 30 | 31 | return $dummyVariable; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Console/Stubs/requests/createrequest.stub: -------------------------------------------------------------------------------- 1 | 'required', 13 | 'field2' => 'required' 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Console/Stubs/requests/updaterequest.stub: -------------------------------------------------------------------------------- 1 | 'nullable', 13 | 'field2' => 'nullable' 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Console/Stubs/services/service.stub: -------------------------------------------------------------------------------- 1 | dummyVariableRepository = $dummyVariableRepository; 22 | $this->dispatcher = $dispatcher; 23 | } 24 | 25 | public function getAll($options = []) 26 | { 27 | return $this->dummyVariableRepository->getWithCount($options); 28 | } 29 | 30 | public function getById($dummyVariableId, array $options = []) 31 | { 32 | $dummyVariable = $this->getRequestedDummyVariable($dummyVariableId, $options); 33 | 34 | return $dummyVariable; 35 | } 36 | 37 | public function create($data) 38 | { 39 | $dummyVariable = $this->dummyVariableRepository->create($data); 40 | 41 | $this->dispatcher->dispatch(new DummyVariableWasCreated($dummyVariable)); 42 | 43 | return $dummyVariable; 44 | } 45 | 46 | public function update($dummyVariableId, array $data) 47 | { 48 | $dummyVariable = $this->getRequestedDummyVariable($dummyVariableId); 49 | 50 | $this->dummyVariableRepository->update($dummyVariable, $data); 51 | 52 | $this->dispatcher->dispatch(new DummyVariableWasUpdated($dummyVariable)); 53 | 54 | return $dummyVariable; 55 | } 56 | 57 | public function delete($dummyVariableId) 58 | { 59 | $dummyVariable = $this->getRequestedDummyVariable($dummyVariableId); 60 | 61 | $this->dummyVariableRepository->delete($dummyVariableId); 62 | 63 | $this->dispatcher->dispatch(new DummyVariableWasDeleted($dummyVariable)); 64 | } 65 | 66 | public function getRequestedDummyVariable($dummyVariableId, $options = []) 67 | { 68 | $dummyVariable = $this->dummyVariableRepository->getById($dummyVariableId, $options); 69 | 70 | if (is_null($dummyVariable)) { 71 | throw new DummyVariableNotFoundException; 72 | } 73 | 74 | return $dummyVariable; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Controllers/LaravelController.php: -------------------------------------------------------------------------------- 1 | toArray(); 33 | } 34 | 35 | return new JsonResponse($data, $statusCode, $headers); 36 | } 37 | 38 | /** 39 | * Parse sort by asc. 40 | * 41 | * @param string $sortByAsc 42 | * @return array 43 | */ 44 | protected function parseSortByAsc($sortByAsc) 45 | { 46 | if (is_null($sortByAsc)) { 47 | return []; 48 | } 49 | 50 | return explode(',', $sortByAsc); 51 | } 52 | 53 | /** 54 | * Parse sort by desc. 55 | * 56 | * @param string $sortByDesc 57 | * @return array 58 | */ 59 | protected function parseSortByDesc($sortByDesc) 60 | { 61 | if (is_null($sortByDesc)) { 62 | return []; 63 | } 64 | 65 | return explode(',', $sortByDesc); 66 | } 67 | 68 | /** 69 | * Parse selects. 70 | * 71 | * @param string $selects 72 | * @return array 73 | */ 74 | protected function parseSelects($selects) 75 | { 76 | if (is_null($selects)) { 77 | return []; 78 | } 79 | 80 | return explode(',', $selects); 81 | } 82 | 83 | /** 84 | * Parse appends. 85 | * 86 | * @param string $appends 87 | * @return array 88 | */ 89 | protected function parseAppends($appends) 90 | { 91 | if (is_null($appends)) { 92 | return []; 93 | } 94 | 95 | return explode(',', $appends); 96 | } 97 | 98 | /** 99 | * Parse includes. 100 | * 101 | * @param string $includes 102 | * @return array 103 | */ 104 | protected function parseIncludes($includes) 105 | { 106 | if (is_null($includes)) { 107 | return []; 108 | } 109 | 110 | return explode(';', $includes); 111 | } 112 | 113 | /** 114 | * Parse with counts into resource. 115 | * 116 | * @param string $withCounts 117 | * @return array 118 | */ 119 | protected function parseWithCounts($withCounts) 120 | { 121 | if (is_null($withCounts)) { 122 | return []; 123 | } 124 | 125 | return explode(',', $withCounts); 126 | } 127 | 128 | /** 129 | * Parse has into resource. 130 | * 131 | * @param string $has 132 | * @return array 133 | */ 134 | protected function parseHas($has) 135 | { 136 | if (is_null($has)) { 137 | return []; 138 | } 139 | 140 | return explode(',', $has); 141 | } 142 | 143 | /** 144 | * Parse doesnt have into resource. 145 | * 146 | * @param string $has 147 | * @return array 148 | */ 149 | protected function parseDoesntHave($doesntHave) 150 | { 151 | if (is_null($doesntHave)) { 152 | return []; 153 | } 154 | 155 | return explode(',', $doesntHave); 156 | } 157 | 158 | /** 159 | * Parse exclude global scopes into resource. 160 | * 161 | * @param string $excludeGlobalScopes 162 | * @return array 163 | */ 164 | protected function parseExcludeGlobalScopes($excludeGlobalScopes) 165 | { 166 | if (is_null($excludeGlobalScopes)) { 167 | return []; 168 | } 169 | 170 | return explode(',', $excludeGlobalScopes); 171 | } 172 | 173 | /** 174 | * Parse scopes into resource. 175 | * 176 | * @param string $scopes 177 | * @return array 178 | */ 179 | protected function parseScopes($scopes) 180 | { 181 | if (is_null($scopes)) { 182 | return []; 183 | } 184 | 185 | return explode(',', $scopes); 186 | } 187 | 188 | /** 189 | * Parse order by random. 190 | * 191 | * @param string $value 192 | * @return bool 193 | */ 194 | protected function parseOrderByRandom($value) 195 | { 196 | $value = $value ? filter_var($value, FILTER_VALIDATE_BOOLEAN) : false; 197 | 198 | return $value; 199 | } 200 | 201 | /** 202 | * Parse with trashed. 203 | * 204 | * @param string $value 205 | * @return bool 206 | */ 207 | protected function parseWithTrashed($value) 208 | { 209 | $value = $value ? filter_var($value, FILTER_VALIDATE_BOOLEAN) : false; 210 | 211 | return $value; 212 | } 213 | 214 | /** 215 | * Parse filters. 216 | * 217 | * @param array $filter 218 | * @param bool $or 219 | * @return array 220 | */ 221 | protected function parseFilters(array $filters, $or = false, $defaultOperator = 'eq') 222 | { 223 | if (!count($filters)) { 224 | return []; 225 | } 226 | 227 | $parsedFilters = []; 228 | $allowedOperators = [ 229 | 'ct', 230 | 'sw', 231 | 'ew', 232 | 'eq', 233 | 'gt', 234 | 'gte', 235 | 'lte', 236 | 'lt', 237 | 'in', 238 | 'bt', 239 | ]; 240 | 241 | foreach ($filters as $column => $part) { 242 | $arrayCountValues = is_array($part) ? count($part, COUNT_RECURSIVE) : 0; 243 | $operator = $defaultOperator; 244 | $not = false; 245 | $value = (is_array($part)) ? null : $part; 246 | if (is_array($part)) { 247 | $operator = key($part) ?? $defaultOperator; 248 | } 249 | 250 | if ($arrayCountValues > 2) { 251 | throw new LarapiException('Filter is not well formed.'); 252 | } 253 | 254 | if (!in_array($operator, $allowedOperators)) { 255 | throw new LarapiException('Operator '. $operator. ' is not supported.'); 256 | } 257 | 258 | if (is_array($part)) { 259 | $not = ($arrayCountValues == 2) ? key($part[$operator]) : false; 260 | $not = $not === 'not' ? true : $not; 261 | $not = $not ? filter_var($not, FILTER_VALIDATE_BOOLEAN) : false; 262 | 263 | $value = $part[(string)key($part)]; 264 | $value = is_array($value) ? array_shift($value) : $value; 265 | $value = (Str::contains($value, ',')) ? explode(',', $value) : $value; 266 | } 267 | 268 | $parsedFilters[] = [ 269 | 'column' => $column, 270 | 'operator' => $operator, 271 | 'not' => $not, 272 | 'value' => $value 273 | ]; 274 | } 275 | 276 | return [ 277 | [ 278 | 'filters' => $parsedFilters, 279 | 'or' => $or 280 | ] 281 | ]; 282 | } 283 | 284 | /** 285 | * Parse filter group strings into filters. 286 | * 287 | * @param array $filter_groups 288 | * @return array 289 | */ 290 | protected function parseFilterGroups(array $filter_groups) 291 | { 292 | $return = []; 293 | 294 | $keysNeeded = ['column', 'operator', 'value']; 295 | foreach ($filter_groups as $group) { 296 | if (!array_key_exists('filters', $group)) { 297 | throw new LarapiException('Filter group does not have the \'filters\' key.'); 298 | } 299 | $filters = array_map(function ($filter) use ($keysNeeded) { 300 | if (count(array_intersect_key(array_flip($keysNeeded), $filter)) != count($keysNeeded)) { 301 | throw new LarapiException('You need to pass column, operator and value in filters.'); 302 | } 303 | 304 | if (($filter['operator'] == 'in') && (!is_array($filter['value']))) { 305 | throw new LarapiException('You need to make value as array because you are using \'in\' operator.'); 306 | } 307 | 308 | if (($filter['operator'] == 'bt') && (!is_array($filter['value']))) { 309 | throw new LarapiException('You need to make value as array because you are using \'bt\' operator.'); 310 | } 311 | 312 | if (!isset($filter['not'])) { 313 | $filter['not'] = false; 314 | } 315 | return $filter; 316 | }, $group['filters']); 317 | 318 | $return[] = [ 319 | 'filters' => $filters, 320 | 'or' => isset($group['or']) ? $group['or'] : false 321 | ]; 322 | } 323 | 324 | return $return; 325 | } 326 | 327 | /** 328 | * Parse GET parameters into resource options. 329 | * 330 | * @return array 331 | */ 332 | protected function parseResourceOptions($request = null) 333 | { 334 | if ($request === null) { 335 | $request = request(); 336 | } 337 | 338 | $this->defaults = array_merge([ 339 | 'selects' => null, 340 | 'select' => null, 341 | 'includes' => null, 342 | 'include' => null, 343 | 'withCount' => null, 344 | 'has' => null, 345 | 'doesntHave' => null, 346 | 'excludeGlobalScopes' => null, 347 | 'scope' => null, 348 | 'limit' => null, 349 | 'page' => null, 350 | 'filter_groups' => [], 351 | 'filterByAnd' => [], 352 | 'filterByOr' => [], 353 | 'searchByAnd' => [], 354 | 'searchByOr' => [], 355 | 'append' => null, 356 | 'sortByDesc' => null, 357 | 'sortByAsc' => null, 358 | 'orderByRandom' => false, 359 | 'withTrashed' => false 360 | ], $this->defaults); 361 | 362 | $selects = $this->parseSelects($request->get('selects', $this->defaults['selects'])); 363 | $select = $this->parseSelects($request->get('select', $this->defaults['select'])); 364 | $includes = $this->parseIncludes($request->get('includes', $this->defaults['includes'])); 365 | $include = $this->parseIncludes($request->get('include', $this->defaults['include'])); 366 | $withCount = $this->parseWithCounts($request->get('withCount', $this->defaults['withCount'])); 367 | $has = $this->parseHas($request->get('has', $this->defaults['has'])); 368 | $doesntHave = $this->parseDoesntHave($request->get('doesntHave', $this->defaults['doesntHave'])); 369 | $excludeGlobalScopes = $this->parseExcludeGlobalScopes($request->get('excludeGlobalScopes', $this->defaults['excludeGlobalScopes'])); 370 | $scope = $this->parseScopes($request->get('scope', $this->defaults['scope'])); 371 | $limit = $request->get('limit', $this->defaults['limit']); 372 | $page = $request->get('page', $this->defaults['page']); 373 | $filter_groups = $this->parseFilterGroups($request->get('filter_groups', $this->defaults['filter_groups'])); 374 | $filterByAnd = $this->parseFilters($request->get('filter', $this->defaults['filterByAnd'])); 375 | $filterByOr = $this->parseFilters($request->get('filterByOr', $this->defaults['filterByOr']), true); 376 | $searchByAnd = $this->parseFilters($request->get('search', $this->defaults['searchByAnd']), false, 'ct'); 377 | $searchByOr = $this->parseFilters($request->get('searchByOr', $this->defaults['searchByOr']), true, 'ct'); 378 | $append = $this->parseAppends($request->get('append', $this->defaults['append'])); 379 | $sortByDesc = $this->parseSortByDesc($request->get('sortByDesc', $this->defaults['sortByDesc'])); 380 | $sortByAsc = $this->parseSortByAsc($request->get('sortByAsc', $this->defaults['sortByAsc'])); 381 | $orderByRandom = $this->parseOrderByRandom($request->get('orderByRandom', $this->defaults['orderByRandom'])); 382 | $withTrashed = $this->parseWithTrashed($request->get('withTrashed', $this->defaults['withTrashed'])); 383 | 384 | $data = [ 385 | 'select' => $select, 386 | 'selects' => $selects, 387 | 'includes' => $includes, 388 | 'include' => $include, 389 | 'withCount' => $withCount, 390 | 'has' => $has, 391 | 'doesntHave' => $doesntHave, 392 | 'excludeGlobalScopes' => $excludeGlobalScopes, 393 | 'scope' => $scope, 394 | 'limit' => $limit, 395 | 'page' => $page, 396 | 'filter_groups' => $filter_groups, 397 | 'filterByAnd' => $filterByAnd, 398 | 'filterByOr' => $filterByOr, 399 | 'searchByAnd' => $searchByAnd, 400 | 'searchByOr' => $searchByOr, 401 | 'append' => $append, 402 | 'sortByDesc' => $sortByDesc, 403 | 'sortByAsc' => $sortByAsc, 404 | 'orderByRandom' => $orderByRandom, 405 | 'withTrashed' => $withTrashed 406 | ]; 407 | 408 | $this->validateResourceOptions($data); 409 | 410 | return $data; 411 | } 412 | 413 | /** 414 | * Validate resource options. 415 | */ 416 | private function validateResourceOptions(array $data) 417 | { 418 | if ($data['page'] !== null && $data['limit'] === null) { 419 | throw new LarapiException('Cannot use page option without limit option.'); 420 | } 421 | 422 | if (!is_null($data['page'])) { 423 | if (!is_int((int)$data['page'])) { 424 | throw new LarapiException('Page need to be int.'); 425 | } 426 | 427 | if ($data['page'] == 0) { 428 | throw new LarapiException('Page need to start from 1.'); 429 | } 430 | } 431 | 432 | if (!is_null($data['limit'])) { 433 | if (!is_int((int)$data['limit'])) { 434 | throw new LarapiException('Limit need to be int.'); 435 | } 436 | } 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/Database/EloquentBuilderTrait.php: -------------------------------------------------------------------------------- 1 | applySelects($queryBuilder, $selects); 21 | } 22 | 23 | if (isset($select) && $select) { 24 | $this->applySelects($queryBuilder, $select); 25 | } 26 | 27 | if (isset($includes)) { 28 | $this->applyWith($queryBuilder, $includes); 29 | } 30 | 31 | if (isset($include)) { 32 | $this->applyWith($queryBuilder, $include); 33 | } 34 | 35 | if (isset($withCount)) { 36 | $this->applyWithCount($queryBuilder, $withCount); 37 | } 38 | 39 | if (isset($has)) { 40 | $this->applyHas($queryBuilder, $has); 41 | } 42 | 43 | if (isset($doesntHave)) { 44 | $this->applyDoesntHave($queryBuilder, $doesntHave); 45 | } 46 | 47 | if (isset($excludeGlobalScopes)) { 48 | $this->applyWithouGlobalScopes($queryBuilder, $excludeGlobalScopes); 49 | } 50 | 51 | if (isset($filter_groups)) { 52 | $this->applyFilterGroups($queryBuilder, $filter_groups); 53 | } 54 | 55 | if (isset($filterByAnd)) { 56 | $this->applyFilterGroups($queryBuilder, $filterByAnd); 57 | } 58 | 59 | if (isset($filterByOr)) { 60 | $this->applyFilterGroups($queryBuilder, $filterByOr); 61 | } 62 | 63 | if (isset($searchByAnd)) { 64 | $this->applyFilterGroups($queryBuilder, $searchByAnd); 65 | } 66 | 67 | if (isset($searchByOr)) { 68 | $this->applyFilterGroups($queryBuilder, $searchByOr); 69 | } 70 | 71 | if (isset($limit)) { 72 | $queryBuilder->limit($limit); 73 | } 74 | 75 | if (isset($page)) { 76 | $queryBuilder->offset(($page > 0 ? $page - 1 : 0) * $limit); 77 | } 78 | 79 | if (isset($sortByAsc)) { 80 | $this->applySortByAsc($queryBuilder, $sortByAsc); 81 | } 82 | 83 | if (isset($sortByDesc)) { 84 | $this->applySortByDesc($queryBuilder, $sortByDesc); 85 | } 86 | 87 | if (isset($orderByRandom) && $orderByRandom) { 88 | $this->applyOrderByRandom($queryBuilder, $orderByRandom); 89 | } 90 | 91 | if( (isset($withTrashed) && $withTrashed)) { 92 | $this->applyWithTrashed($queryBuilder); 93 | } 94 | 95 | return $queryBuilder; 96 | } 97 | 98 | protected function applyFilterGroups(Builder $queryBuilder, array $filterGroups = []) 99 | { 100 | foreach ($filterGroups as $groups) { 101 | $or = $groups['or']; 102 | $filters = $groups['filters']; 103 | 104 | $queryBuilder->where(function (Builder $query) use ($filters, $or) { 105 | foreach ($filters as $filter) { 106 | $this->applyFilter($query, $filter, $or); 107 | } 108 | }); 109 | } 110 | } 111 | 112 | protected function applySortByAsc(Builder $queryBuilder, array $sortByAsc = []) 113 | { 114 | foreach ($sortByAsc as $sortByAscKey) { 115 | $customSortMethod = $this->hasCustomMethod('sort', $sortByAscKey); 116 | if ($customSortMethod) { 117 | call_user_func([$this, $customSortMethod], $queryBuilder, 'ASC'); 118 | } else { 119 | $queryBuilder->orderBy($sortByAscKey); 120 | } 121 | } 122 | } 123 | 124 | protected function applySortByDesc(Builder $queryBuilder, array $sortByDesc = []) 125 | { 126 | foreach ($sortByDesc as $sortByDescKey) { 127 | $customSortMethod = $this->hasCustomMethod('sort', $sortByDescKey); 128 | if ($customSortMethod) { 129 | call_user_func([$this, $customSortMethod], $queryBuilder, 'DESC'); 130 | } else { 131 | $queryBuilder->orderByDesc($sortByDescKey); 132 | } 133 | } 134 | } 135 | 136 | protected function applySelects(Builder $queryBuilder, array $fields = []) 137 | { 138 | $queryBuilder->select($fields); 139 | } 140 | 141 | protected function applyWith(Builder $queryBuilder, array $withs = []) 142 | { 143 | $queryBuilder->with($withs); 144 | } 145 | 146 | protected function applyWithCount(Builder $queryBuilder, array $withCount = []) 147 | { 148 | $queryBuilder->withCount($withCount); 149 | } 150 | 151 | protected function applyOrderByRandom(Builder $queryBuilder, bool $orderByRaw) 152 | { 153 | $queryBuilder->orderByRaw('RAND()'); 154 | } 155 | 156 | protected function applyWithTrashed(Builder $queryBuilder) 157 | { 158 | $queryBuilder->withTrashed(); 159 | } 160 | 161 | protected function applyHas(Builder $queryBuilder, array $relations = []) 162 | { 163 | foreach ($relations as $relation) { 164 | $queryBuilder->has($relation); 165 | } 166 | } 167 | 168 | protected function applyDoesntHave(Builder $queryBuilder, array $relations = []) 169 | { 170 | foreach ($relations as $relation) { 171 | $queryBuilder->doesntHave($relation); 172 | } 173 | } 174 | 175 | protected function applyWithouGlobalScopes(Builder $queryBuilder, array $excludeGlobalScopes = []) 176 | { 177 | $queryBuilder->withoutGlobalScopes($excludeGlobalScopes); 178 | } 179 | 180 | protected function applyFilter(Builder $queryBuilder, array $filter, $or = false) 181 | { 182 | $column = $filter['column']; 183 | $method = 'where'; 184 | $operator = $filter['operator'] ?? 'eq'; 185 | $value = $filter['value']; 186 | $not = $filter['not'] ?? false; 187 | $wantsRelationship = stripos($column, '.'); 188 | $clauseOperator = true; 189 | $lastColumn = explode('.', $column); 190 | $lastColumn = end($lastColumn); 191 | $relationName = str_replace('.'. $lastColumn, '', $column); 192 | $filterRawJoinColumns = isset($this->filterRawJoinColumns) ? $this->filterRawJoinColumns : []; 193 | 194 | $this->checkFilterColumn($column, get_class($queryBuilder->getModel())); 195 | 196 | // Check operator. 197 | switch ($operator) { 198 | // String contains 199 | case 'ct': 200 | $operator = $not ? 'NOT LIKE' : 'LIKE'; 201 | $value = "%$value%"; 202 | break; 203 | 204 | // Starts with 205 | case 'sw': 206 | $operator = $not ? 'NOT LIKE' : 'LIKE'; 207 | $value = "$value%"; 208 | break; 209 | 210 | // Ends with 211 | case 'ew': 212 | $operator = $not ? 'NOT LIKE' : 'LIKE'; 213 | $value = "%$value"; 214 | break; 215 | 216 | // Equals 217 | case 'eq': 218 | $operator = $not ? '!=' : '='; 219 | break; 220 | 221 | // Greater than 222 | case 'gt': 223 | $operator = $not ? '<' : '>'; 224 | break; 225 | 226 | // Greater than or equalTo 227 | case 'gte': 228 | $operator = $not ? '<' : '>='; 229 | break; 230 | 231 | // Lesser than or equalTo 232 | case 'lte': 233 | $operator = $not ? '>' : '<='; 234 | break; 235 | 236 | // Lesser than 237 | case 'lt': 238 | $operator = $not ? '>' : '<'; 239 | break; 240 | 241 | // In array 242 | case 'in': 243 | $method = $not ? 'whereNotIn' : 'whereIn'; 244 | $clauseOperator = false; 245 | break; 246 | 247 | // Between 248 | case 'bt': 249 | $method = $not ? 'whereNotBetween' : 'whereBetween'; 250 | $clauseOperator = false; 251 | break; 252 | } 253 | 254 | // Support or operator. 255 | if ($or == true) { 256 | $method = 'or'. $method; 257 | } 258 | 259 | // Custom filter. 260 | $customFilterMethod = $this->hasCustomMethod('filter', $column); 261 | if ($customFilterMethod) { 262 | return call_user_func_array([$this, 'filter'. $column], array($queryBuilder, $method, $operator, $value, $clauseOperator, $or)); 263 | } 264 | 265 | // Finally apply filter. 266 | if ($wantsRelationship && !in_array($column, $filterRawJoinColumns)) { 267 | // Remove or operator support. 268 | $method = str_replace('or', '', $method); 269 | 270 | $queryFunction = function ($q) use ($lastColumn, $operator, $value, $method, $clauseOperator) { 271 | if ($clauseOperator == false) { 272 | $q->$method($lastColumn, $value); 273 | } else { 274 | $q->$method($lastColumn, $operator, $value); 275 | } 276 | }; 277 | 278 | if ($or == true) { 279 | $queryBuilder->orWhereHas($relationName, $queryFunction); 280 | } else { 281 | $queryBuilder->whereHas($relationName, $queryFunction); 282 | }; 283 | } else { 284 | if ($clauseOperator == false) { 285 | $queryBuilder->$method($column, $value); 286 | } else { 287 | $queryBuilder->$method($column, $operator, $value); 288 | } 289 | } 290 | } 291 | 292 | private function checkFilterColumn(String $column, String $baseClassName, array $overrideWhiteListFilter = null) 293 | { 294 | if (empty($column) || empty($baseClassName)) { 295 | return; 296 | } 297 | 298 | // Retrieve the whiteListFilter 299 | $whiteListFilter = $overrideWhiteListFilter ?? ((array)(get_class_vars($baseClassName)['whiteListFilter']) ?? []); 300 | 301 | // Check if the whitelist filter is a star 302 | if (in_array('*', $whiteListFilter)) { 303 | if (count($whiteListFilter) > 1) { 304 | throw new LarapiException('Oops! If you use "*" for the whiteListFilter, you cannot specify another column on ' . $baseClassName . ' class.'); 305 | } 306 | return; 307 | } 308 | 309 | // Check if full column can filered. 310 | if (in_array($column, $whiteListFilter)) { 311 | return; 312 | } 313 | 314 | $parts = explode('.', $column); 315 | $firstPart = $parts[0]; 316 | 317 | $simpleColumnCheckInListFilter = in_array($firstPart, $whiteListFilter); 318 | $complexColumnCheckInListFilter = in_array($firstPart . '.*', $whiteListFilter); 319 | 320 | // Check if splitted column can filered. 321 | if (!$simpleColumnCheckInListFilter && !$complexColumnCheckInListFilter) { 322 | throw new LarapiException('Oops! You cannot filter column ' . $column . ' on ' . $baseClassName . ' class.'); 323 | } 324 | 325 | // Get next part and next class 326 | $nextColums = join('.', array_slice($parts, 1)); 327 | $baseClass = new $baseClassName(); 328 | $nextClass = method_exists($baseClass, $firstPart) ? get_class($baseClass->$firstPart()->getRelated()) : ''; 329 | 330 | // If the whiteListFilter contains a column with a star we want to bypass the check for the next part 331 | $nextOverrideWhiteListFilter = $complexColumnCheckInListFilter ? ['*'] : null; 332 | 333 | // Recursive call to check sub parts 334 | $this->checkFilterColumn($nextColums, $nextClass, $nextOverrideWhiteListFilter); 335 | } 336 | 337 | private function hasCustomMethod($type, $key) 338 | { 339 | $methodName = sprintf('%s%s', $type, Str::studly($key)); 340 | if (method_exists($this, $methodName)) { 341 | return $methodName; 342 | } 343 | 344 | return false; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/Database/Repository.php: -------------------------------------------------------------------------------- 1 | model = $this->getModel(); 25 | } 26 | 27 | /** 28 | * Get all resources. 29 | * 30 | * @param array $options 31 | * 32 | * @return Collection 33 | */ 34 | public function get(array $options = []) 35 | { 36 | $query = $this->createBaseBuilder($options); 37 | 38 | $data = $query->get(); 39 | 40 | $this->appendAttributes($data, $options); 41 | 42 | return $data; 43 | } 44 | 45 | /** 46 | * Get all resources with count. 47 | * 48 | * @param array $options 49 | * 50 | * @return array 51 | */ 52 | public function getWithCount(array $options = []) 53 | { 54 | $query = $this->createBaseBuilder($options); 55 | 56 | $totalData = $this->countRows($query); 57 | $allRows = $query->get(); 58 | 59 | $this->appendAttributes($allRows, $options); 60 | 61 | return ['total_data' => $totalData, 'rows' => $allRows]; 62 | } 63 | 64 | /** 65 | * Get a resource by its primary key. 66 | * 67 | * @param mixed $id 68 | * @param array $options 69 | * 70 | * @return Collection 71 | */ 72 | public function getById($id, array $options = []) 73 | { 74 | $query = $this->createBaseBuilder($options); 75 | 76 | $query = $query->find($id); 77 | 78 | $this->appendAttributes($query, $options); 79 | 80 | return $query; 81 | } 82 | 83 | /** 84 | * Append attributes. 85 | * 86 | * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $query 87 | */ 88 | public function appendAttributes($query, $options = []) 89 | { 90 | if (is_null($query)) { 91 | return; 92 | } 93 | 94 | if (!isset($options['append'])) { 95 | return; 96 | } 97 | 98 | foreach ($options['append'] as $append) { 99 | $appends = explode('.', $append); 100 | $lastAppend = count($appends) - 1; 101 | $appends[$lastAppend] = Str::snake($appends[$lastAppend]); 102 | 103 | if (count($appends) == 1) { 104 | $query = $query->append($appends[0]); 105 | continue; 106 | } 107 | 108 | if (!is_a($query, 'Illuminate\Database\Eloquent\Collection')) { 109 | if (count($appends) == 2) { 110 | $relation1 = $appends[0]; 111 | $relation1InstanceOf = get_class($query->$relation1()); 112 | $attributeName = $appends[1]; 113 | 114 | if ($relation1InstanceOf == 'Illuminate\Database\Eloquent\Relations\HasOne') { 115 | $query->$relation1->setAppends([$attributeName]); 116 | } 117 | 118 | if ($relation1InstanceOf == 'Illuminate\Database\Eloquent\Relations\HasMany') { 119 | $query->$relation1->each->setAppends([$attributeName]); 120 | } 121 | } 122 | 123 | if (count($appends) == 3) { 124 | $relation1 = $appends[0]; 125 | $relation1InstanceOf = get_class($query->$relation1()); 126 | $relation1Exists = $query->$relation1()->exists(); 127 | $relation2 = $appends[1]; 128 | if ($relation1Exists) { 129 | $relation2InstanceOf = get_class($query->$relation1->$relation2()); 130 | $attributeName = $appends[2]; 131 | 132 | if ($relation1InstanceOf == 'Illuminate\Database\Eloquent\Relations\HasOne') { 133 | if ($relation2InstanceOf == 'Illuminate\Database\Eloquent\Relations\BelongsTo') { 134 | $query->$relation1->$relation2->setAppends([$attributeName]); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Get all resources ordered by recentness. 145 | * 146 | * @param array $options 147 | * 148 | * @return Collection 149 | */ 150 | public function getRecent(array $options = []) 151 | { 152 | $query = $this->createBaseBuilder($options); 153 | 154 | $query->orderBy($this->getCreatedAtColumn(), 'DESC'); 155 | 156 | $data = $query->get(); 157 | 158 | $this->appendAttributes($data, $options); 159 | 160 | return $data; 161 | } 162 | 163 | /** 164 | * Get all resources by a where clause ordered by recentness. 165 | * 166 | * @param string $column 167 | * @param mixed $value 168 | * @param array $options 169 | * 170 | * @return Collection 171 | */ 172 | public function getRecentWhere($column, $value, array $options = []) 173 | { 174 | $query = $this->createBaseBuilder($options); 175 | 176 | $query->where($column, $value); 177 | 178 | $query->orderBy($this->getCreatedAtColumn(), 'DESC'); 179 | 180 | $data = $query->get(); 181 | 182 | $this->appendAttributes($data, $options); 183 | 184 | return $data; 185 | } 186 | 187 | /** 188 | * Get latest resource. 189 | * 190 | * @param array $options 191 | * 192 | * @return Collection 193 | */ 194 | public function getLatest(array $options = []) 195 | { 196 | $query = $this->createBaseBuilder($options); 197 | 198 | $query->orderBy($this->getCreatedAtColumn(), 'DESC'); 199 | 200 | $data = $query->first(); 201 | 202 | $this->appendAttributes($data, $options); 203 | 204 | return $data; 205 | } 206 | 207 | /** 208 | * Get latest resource by a where clause. 209 | * 210 | * @param string $column 211 | * @param mixed $value 212 | * @param array $options 213 | * 214 | * @return Collection 215 | */ 216 | public function getLatestWhere($column, $value, array $options = []) 217 | { 218 | $query = $this->createBaseBuilder($options); 219 | 220 | $query->where($column, $value); 221 | 222 | $query->orderBy($this->getCreatedAtColumn(), 'DESC'); 223 | 224 | $data = $query->get(); 225 | 226 | $this->appendAttributes($data, $options); 227 | 228 | return $data; 229 | } 230 | 231 | /** 232 | * Get resources by a where clause. 233 | * 234 | * @param string $column 235 | * @param mixed $value 236 | * @param array $options 237 | * 238 | * @return Collection 239 | */ 240 | public function getWhere($column, $value, array $options = []) 241 | { 242 | $query = $this->createBaseBuilder($options); 243 | 244 | $query->where($column, $value); 245 | 246 | $data = $query->get(); 247 | 248 | $this->appendAttributes($data, $options); 249 | 250 | return $data; 251 | } 252 | 253 | /** 254 | * Get resources by multiple where clauses. 255 | * 256 | * @param array $clauses 257 | * @param array $options 258 | * 259 | * @return Collection 260 | */ 261 | public function getWhereArray(array $clauses, array $options = []) 262 | { 263 | $query = $this->createBaseBuilder($options); 264 | 265 | $this->applyWhereArray($query, $clauses); 266 | 267 | $data = $query->get(); 268 | 269 | $this->appendAttributes($data, $options); 270 | 271 | return $data; 272 | } 273 | 274 | /** 275 | * Get resources where a column value exists in array. 276 | * 277 | * @param string $column 278 | * @param array $values 279 | * @param array $options 280 | * 281 | * @return Collection 282 | */ 283 | public function getWhereIn($column, array $values, array $options = []) 284 | { 285 | $query = $this->createBaseBuilder($options); 286 | 287 | $query->whereIn($column, $values); 288 | 289 | $data = $query->get(); 290 | 291 | $this->appendAttributes($data, $options); 292 | 293 | return $data; 294 | } 295 | 296 | /** 297 | * Delete a resource by its primary key. 298 | * 299 | * @param mixed $id 300 | * 301 | * @return void 302 | */ 303 | public function delete($id) 304 | { 305 | $query = $this->createQueryBuilder(); 306 | 307 | $query->where($this->getPrimaryKey($query), $id); 308 | $query->delete(); 309 | } 310 | 311 | /** 312 | * Delete resources by a where clause. 313 | * 314 | * @param string $column 315 | * @param mixed $value 316 | * 317 | * @return void 318 | */ 319 | public function deleteWhere($column, $value) 320 | { 321 | $query = $this->createQueryBuilder(); 322 | 323 | $query->where($column, $value); 324 | $query->delete(); 325 | } 326 | 327 | /** 328 | * Delete resources by multiple where clauses. 329 | * 330 | * @param array $clauses 331 | * 332 | * @return void 333 | */ 334 | public function deleteWhereArray(array $clauses) 335 | { 336 | $query = $this->createQueryBuilder(); 337 | 338 | $this->applyWhereArray($query, $clauses); 339 | $query->delete(); 340 | } 341 | 342 | /** 343 | * Creates a new query builder with options set. 344 | * 345 | * @param array $options 346 | * 347 | * @return Builder 348 | */ 349 | protected function createBaseBuilder(array $options = []) 350 | { 351 | $query = $this->createQueryBuilder(); 352 | 353 | if (!empty($options['scope'])) { 354 | foreach ($options['scope'] as $scope) { 355 | $query = $query->$scope(); 356 | } 357 | } 358 | 359 | $this->applyResourceOptions($query, $options); 360 | 361 | if (empty($options['sortByDesc']) && empty($options['sortByAsc'])) { 362 | $this->defaultSort($query, $options); 363 | } 364 | 365 | return $query; 366 | } 367 | 368 | /** 369 | * Creates a new query builder. 370 | * 371 | * @return Builder 372 | */ 373 | protected function createQueryBuilder() 374 | { 375 | return $this->model->newQuery(); 376 | } 377 | 378 | /** 379 | * Get primary key name of the underlying model. 380 | * 381 | * @param Builder $query 382 | * 383 | * @return string 384 | */ 385 | protected function getPrimaryKey($query) 386 | { 387 | return $query->getModel()->getKeyName(); 388 | } 389 | 390 | /** 391 | * Order query by the specified sorting property. 392 | * 393 | * @param Builder $query 394 | * @param array $options 395 | * 396 | * @return void 397 | */ 398 | protected function defaultSort($query, array $options = []) 399 | { 400 | if (isset($this->sortProperty)) { 401 | $query->orderBy($this->sortProperty, $this->sortDirection); 402 | } 403 | } 404 | 405 | /** 406 | * Get the name of the "created at" column. 407 | * 408 | * @return string 409 | */ 410 | protected function getCreatedAtColumn() 411 | { 412 | $model = $this->model; 413 | return ($model::CREATED_AT) ? $model::CREATED_AT : 'created_at'; 414 | } 415 | 416 | protected function applyWhereArray($query, array $clauses) 417 | { 418 | foreach ($clauses as $key => $value) { 419 | preg_match('/NOT\:(.+)/', $key, $matches); 420 | 421 | $not = false; 422 | if (isset($matches[1])) { 423 | $not = true; 424 | $key = $matches[1]; 425 | } 426 | 427 | if (is_array($value)) { 428 | if (!$not) { 429 | $query->whereIn($key, $value); 430 | } else { 431 | $query->whereNotIn($key, $value); 432 | } 433 | } elseif (is_null($value)) { 434 | if (!$not) { 435 | $query->whereNull($key); 436 | } else { 437 | $query->whereNotNull($key); 438 | } 439 | } else { 440 | if (!$not) { 441 | $query->where($key, $value); 442 | } else { 443 | $query->where($key, '!=', $value); 444 | } 445 | } 446 | } 447 | } 448 | 449 | protected function countRows($query) 450 | { 451 | $totalQuery = clone $query; 452 | 453 | return $totalQuery->offset(0)->limit(PHP_INT_MAX)->count(); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/Exceptions/ApiException.php: -------------------------------------------------------------------------------- 1 | request = $request; 13 | $this->e = $e; 14 | } 15 | 16 | public function generateExceptionResponse() 17 | { 18 | $formatters = config('larapi.exceptions_formatters'); 19 | 20 | foreach ($formatters as $exceptionType => $formatter) { 21 | if (!($this->e instanceof $exceptionType)) { 22 | continue; 23 | } 24 | 25 | if (!class_exists($formatter)) { 26 | continue; 27 | } 28 | 29 | $formatterInstance = new $formatter(); 30 | return $formatterInstance->format($this->request, $this->e); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Exceptions/LarapiException.php: -------------------------------------------------------------------------------- 1 | getStatusCode(); 22 | } 23 | 24 | $message = $this->getMessage($statusCode, $e); 25 | 26 | $data = [ 27 | 'success' => false, 28 | 'status' => $statusCode, 29 | 'message' => $message 30 | ]; 31 | 32 | if (config('app.debug')) { 33 | $data['exception'] = (string) $e; 34 | $data['line'] = $e->getLine(); 35 | $data['file'] = $e->getFile(); 36 | } 37 | 38 | return response()->json($data, $statusCode); 39 | } 40 | 41 | public function getMessage($statusCode, $e) 42 | { 43 | switch ($statusCode) { 44 | case 401: 45 | $message = strlen($e->getMessage()) ? $e->getMessage() : self::DEFAULT_401_MESSAGE; 46 | break; 47 | case 403: 48 | $message = strlen($e->getMessage()) ? $e->getMessage() : self::DEFAULT_403_MESSAGE; 49 | break; 50 | case 404: 51 | $message = strlen($e->getMessage()) ? $e->getMessage() : self::DEFAULT_404_MESSAGE; 52 | break; 53 | case 405: 54 | $message = strlen($e->getMessage()) ? $e->getMessage() : self::DEFAULT_405_MESSAGE; 55 | break; 56 | case 500: 57 | $message = (app()->environment('production')) ? self::DEFAULT_500_MESSAGE : $e->getMessage(); 58 | break; 59 | case 503: 60 | $message = self::DEFAULT_503_MESSAGE; 61 | break; 62 | default: 63 | $message = $e->getMessage(); 64 | break; 65 | } 66 | 67 | return $message; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ExceptionsFormatters/UnprocessableEntityHttpExceptionFormatter.php: -------------------------------------------------------------------------------- 1 | false, 14 | 'status' => self::STATUS_CODE, 15 | 'message' => self::MESSAGE 16 | ]; 17 | 18 | // Laravel validation errors will return JSON string 19 | $decoded = json_decode($e->getMessage(), true); 20 | 21 | // Message was not valid JSON 22 | // This occurs when we throw UnprocessableEntityHttpExceptions 23 | if (json_last_error() !== JSON_ERROR_NONE) { 24 | // Mimick the structure of Laravel validation errors 25 | $decoded = [[$e->getMessage()]]; 26 | } 27 | 28 | // Laravel errors are formatted as {"field": [/*errors as strings*/]} 29 | $data['errors'] = array_reduce($decoded, function ($carry, $item) use ($e) { 30 | return array_merge($carry, array_map(function ($current) use ($e) { 31 | return [ 32 | 'title' => 'Validation error.', 33 | 'detail' => $current 34 | ]; 35 | }, $item)); 36 | }, []); 37 | 38 | return response()->json($data, self::STATUS_CODE); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Facades/ApiConsumer.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 19 | __DIR__. '../../Config/larapi.php', 20 | 'larapi' 21 | ); 22 | } 23 | 24 | /** 25 | * Bootstrap the application services. 26 | * 27 | * @return void 28 | */ 29 | public function boot() 30 | { 31 | $this->publishes([ 32 | __DIR__. '/../Config/larapi.php' => config_path('larapi.php'), 33 | ]); 34 | 35 | if ($this->app->runningInConsole()) { 36 | $this->commands([ 37 | ComponentMakeCommand::class 38 | ]); 39 | } 40 | 41 | $this->app->singleton('apiconsumer', function () { 42 | $app = app(); 43 | 44 | return new ApiConsumerRouter($app, $app['request'], $app['router']); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Routes/ApiConsumerRouter.php: -------------------------------------------------------------------------------- 1 | app = $app; 22 | $this->request = $request; 23 | $this->router = $router; 24 | } 25 | 26 | public function get() 27 | { 28 | return $this->quickCall('GET', func_get_args()); 29 | } 30 | 31 | public function post() 32 | { 33 | return $this->quickCall('POST', func_get_args()); 34 | } 35 | 36 | public function put() 37 | { 38 | return $this->quickCall('PUT', func_get_args()); 39 | } 40 | 41 | public function delete() 42 | { 43 | return $this->quickCall('DELETE', func_get_args()); 44 | } 45 | 46 | public function batchRequest(array $requests) 47 | { 48 | foreach ($requests as $i => $request) { 49 | $requests[$i] = call_user_func_array([$this, 'singleRequest'], $request); 50 | } 51 | 52 | return $requests; 53 | } 54 | 55 | public function quickCall($method, array $args) 56 | { 57 | array_unshift($args, $method); 58 | return call_user_func_array([$this, "singleRequest"], $args); 59 | } 60 | 61 | public function singleRequest($method, $uri, array $data = [], array $headers = [], $content = null) 62 | { 63 | // Save the current request so we can reset the router back to it 64 | // after we've completed our internal request. 65 | $currentRequest = $this->request->instance()->duplicate(); 66 | 67 | $headers = $this->overrideHeaders($currentRequest->server->getHeaders(), $headers); 68 | 69 | if ($this->disableMiddleware) { 70 | $this->app->instance('middleware.disable', true); 71 | } 72 | 73 | $response = $this->request($method, $uri, $data, $headers, $content); 74 | 75 | if ($this->disableMiddleware) { 76 | $this->app->instance('middleware.disable', false); 77 | } 78 | 79 | // Once the request has completed we reset the currentRequest of the router 80 | // to match the original request. 81 | $this->request->instance()->initialize( 82 | $currentRequest->query->all(), 83 | $currentRequest->request->all(), 84 | $currentRequest->attributes->all(), 85 | $currentRequest->cookies->all(), 86 | $currentRequest->files->all(), 87 | $currentRequest->server->all(), 88 | $currentRequest->content 89 | ); 90 | 91 | return $response; 92 | } 93 | 94 | private function overrideHeaders(array $default, array $headers) 95 | { 96 | $headers = $this->transformHeadersToUppercaseUnderscoreType($headers); 97 | return array_merge($default, $headers); 98 | } 99 | 100 | public function enableMiddleware() 101 | { 102 | $this->disableMiddleware = false; 103 | } 104 | 105 | public function disableMiddleware() 106 | { 107 | $this->disableMiddleware = true; 108 | } 109 | 110 | private function request($method, $uri, array $data = [], array $headers = [], $content = null) 111 | { 112 | // Create a new request object for the internal request 113 | $request = $this->createRequest($method, $uri, $data, $headers, $content); 114 | 115 | // Handle the request in the kernel and prepare a response 116 | $response = $this->router->prepareResponse($request, $this->app->handle($request)); 117 | 118 | return $response; 119 | } 120 | 121 | private function createRequest($method, $uri, array $data = [], array $headers = [], $content = null) 122 | { 123 | $server = $this->transformHeadersToServerVariables($headers); 124 | 125 | return $this->request->create($uri, $method, $data, [], [], $server, $content); 126 | } 127 | 128 | private function transformHeadersToUppercaseUnderscoreType($headers) 129 | { 130 | $transformed = []; 131 | 132 | foreach ($headers as $headerType => $headerValue) { 133 | $headerType = strtoupper(str_replace('-', '_', $headerType)); 134 | 135 | $transformed[$headerType] = $headerValue; 136 | } 137 | 138 | return $transformed; 139 | } 140 | 141 | private function transformHeadersToServerVariables($headers) 142 | { 143 | $server = []; 144 | 145 | foreach ($headers as $headerType => $headerValue) { 146 | $headerType = 'HTTP_' . $headerType; 147 | 148 | $server[$headerType] = $headerValue; 149 | } 150 | 151 | return $server; 152 | } 153 | } 154 | --------------------------------------------------------------------------------