Luezoid came up with a compact way to help one creating the APIs very fast & without much hassle. Using this package, one case easily create simple CRUD operations in Laravel framework in couple of minutes with just creating a few files & configuring the components. With a lot of efforts and deep analysis of day-to-day problems (we) developers faced in the past, we came up with a sub-framework of it's own kind to simplify & fasten up the REST APIs building process.
A few cool features of this package are:
- Simplest & fastest way to create CRUDs.
- Pre-built support to define table columns which are to be specifically excluded before creating/updating a record(in default CRUD).
- Pre-built Search & Filters ready to use with just configuring components.
- Pre-built Pagination & Ordering of records ready in a Standard communication format.
- Relationship's data in the APIs(GET) is just a config thing.
- Better way to correctly fire an event upon successful completion of an action.
- File uploads has never been so easy before. Upload to local filesystem or AWS S3 bucket on the go.
- Pre-built feature rich Service classes eg. EnvironmentService, RequestService, UtilityService, etc.
- Nested Related models can be queried with simple config based approach from the code components.
- On the go
SELECT
& Relation filters can be passed with JSON & object operator(->
) in query params to select particular columns from a table(and related objects defined in models) making the API's response with less garbage data instead of writing custom query every time a new endpoint is created. - Generic/Open Search over a set of related objects(tables) with simple Array based config.
Note: For a complete working example of all these feature with core package pre-configured is available on this repository luezoidtechnologies/laravel-core-base-repo-example.
We recommend using Composer to install this package. This package supports Laravel versions >= 5.x.
composer require "luezoid/laravel-core" # For latest version of Laravel (>=10.x)
composer require "luezoid/laravel-core:^9.0" # For Laravel version 9.x
composer require "luezoid/laravel-core:^8.0" # For Laravel version 8.x
composer require "luezoid/laravel-core:^7.0" # For Laravel version 7.x
composer require "luezoid/laravel-core:^6.0" # For Laravel version 6.x
composer require "luezoid/laravel-core:^5.0" # For Laravel version 5.x
Next, configure your app/Exceptions/Handler.php
and extend it with Luezoid\Laravelcore\Exceptions\Handler
. Sample file can be seen here.
Using this packages adds an extra entity between the Controller & Model paradigm in a normal MVC architecture. This extra entity we are referring here is called Repository. A Repository is a complete class where your whole business logic resides from getting the processed data from a Controller and saving it into database using Model(s). By using Repository as an intermediate between Controller & Model, we aim at maintaing clean code at Controller's end and making it a mediator which only receives data(from View, typically a REST route), validate it against the defined validation rules(if any, we uses Request class to define such rules), pre-process it(for eg. transform camelCased data from front-end into snake_case) & sending business cooked data back the View.
Let's start with creating a simple Minions CRUD.
We have sample migration for table minions
, model Minion
, controller MinionController
and repository MinionRepository
.
Add these files into your application & adjust the namespaces accordingly. Then create a Route resource as below in routes/api.php
and we are all ready:
Route::resource('minions', 'MinionController', ['parameters' => ['minions' => 'id']]);
Assuming your local server is running on port: 7872, try hitting REST endpoints as below:
-
POST /minions
curl -X POST \ http://localhost:7872/api/minions \ -H 'Content-Type: application/json' \ -H 'cache-control: no-cache' \ -d '{ "name": "Stuart", "totalEyes": 2, "favouriteSound": "Grrrrrrrrrrr", "hasHairs": true }'
-
PUT /minions/1
curl -X PUT \ http://localhost:7872/api/minions/1 \ -H 'Content-Type: application/json' \ -H 'cache-control: no-cache' \ -d '{ "name": "Stuart - The Pfff", "totalEyes": 2, "favouriteSound": "Grrrrrrrrrrr Pffffff", "hasHairs": false }'
-
DELETE /minions/1
curl -X DELETE \ http://localhost:7872/api/minions/1 \ -H 'cache-control: no-cache'
-
GET /minions
curl -X GET \ http://localhost:7872/api/minions \ -H 'cache-control: no-cache'
-
GET /minions/2
curl -X GET \ http://localhost:7872/api/minions/2 \ -H 'cache-control: no-cache'
Refer the Minon
model. We have the below public properties which can be used to define an array containing list of the columns to be specifically excluded for default POST & PUT requests of CRUD:
// To exclude the key(s) if present in request body for default POST CRUD routes eg. POST /minions
public $createExcept = [
'id'
];
// To exclude the key(s) if present in request body for default PUT CRUD routes eg. PUT /minions/1
public $updateExcept = [
'total_eyes',
'has_hairs'
];
The major advantage for using such config in the model in the first place is: to provide a clean & elegant way & to simply reduce the coding efforts to be done just before saving the whole data into table. Typical examples could be:
- You don't want to save a column value say is_email_verified if an attacker sends it the request body of POST /users; just add it into
$createExcept
. You need not to exclude this specifically in the codes/or request rules. - You don't want to update a column say username if an attacker sends it the request body of PUT /users/{id}; just add it into
$updateExcept
. You need not to exclude this specifically in the codes/or request rules.
You can simply search over the list of available column(s) in the table for all GET requests. Let's begin with examples:
-
General Searching
By default all the available columns in the tables are ready to be queried over GET request just by passing the key(s)-value(s) pair(s) in the query params. But to specifically mention it in the Model itsef, just define a public property
$searchable
which is an array containing the columns allowed to be searched. Let's say you want to search for all minions whose favourite sound is Pchhhh.curl -X GET \ 'http://localhost:7872/api/minions?favouriteSound=Pchhhh' \ -H 'cache-control: no-cache'
Response should contain all minions having Pchhhh string available in the column
favourite_sound
in the tableminions
.Searching is using
LIKE
operator as'%-SEARCH-STRING%'
. -
General Filtering
Similar to searching, you need to define public property
$filterable
which is again an array containing the columns allowed to be filtered.public $filterable = [ 'id', 'total_eyes', 'has_hairs' ];
Exact match will performed against these columns if present in the query params.
Example: To find all the minions having
totalEyes
equal to 1:curl -X GET \ 'http://localhost:7872/api/minions?totalEyes=1' \ -H 'cache-control: no-cache'
Filtering is using
=
operator as'total_eyes'=1
. -
Date Filters
Add the column which you want to be used for date filtering into
$filterable
property in the model class. Once done, you can now simply pass the query params from (AND/OR) to with the date(or datetime) values in standard MySQL format(Y-m-d H:i:s
). Example: We want to search for all the minions which are created after 2020-04-25 09:25:20:curl -X GET \ 'http://localhost:7872/api/minions?createdAt=2020-04-25%20%2009:25:20' \ -H 'cache-control: no-cache'
You can also pass the column name in the query params over which you want the date search to be applied for. Just pass the key dateFilterColumn with the column name you want to use date search on (but note that the $filterable property must have this column specified in order to make things work).
Notes:
- You may specify multiple key-value pairs in the query params & all the conditions will be queried with
AND
operators.- Pass all the variables in camelCasing & all will be transferred into snake_casing internally. You may configure this transformation turning off by overriding properties
$isCamelToSnake
&$isSnakeToCamel
and setting them tofalse
in ApiCotroller.
Did you notice the response of GET endpoint we just created above? Let's take a look in brief. Refer the response of GET /minions API.
{
"message": null,
"data": {
"items": [
{},
{},
...
],
"page": 1, // tells us about the current page
"total": 6, // tells us the total results available (matching all the query params for searching/filtering applied)
"pages": 1, // total pages in which the whole result set is distributed
"perpage": 15 // total results per page
},
"type": null
}
Pretty self explanatory, eh?
You can pass query param perpage=5
(to limit the per page size). Similarly, the page=2
will grab the results of page 2.
For ordering the result set by a particular column, just send query param key orderby
with the name of column & a separate key order
with value ASC
for ascending (or) DESC
for sorting in descending order. By default, results are sorted in descending order.
Paginating & Ordering the results has never been so easy before :)
Note: Any GET(index) route retrieving results from a Repository (eg.MinionRepository) extending
\Luezoid\Laravelcore\Repositories\EloquentBaseRepository::getAll()
is all ready with such pagination & ordering. Make sure to use this pre-built feature & save time for manually implementing these features for every endpoint & grab a pint of beer to chill.
Let's assume each Minion leads a mission operated by Gru i.e. there is one-to-one relationship exists between Minion & Missions. See the missions
table migrations(1,2) and Model Mission
. To retrieve the leading mission by each Minion in GET requests(index & show), just add the relationship name in the MinionController
properties as follows:
-
GET /minions
protected $indexWith = [ 'leading_mission' // name of the hasOne() relationship defined in the Minion model ];
-
GET /minions/2
protected $showWith = [ 'leading_mission' // name of the hasOne() relationship defined in the Minion model ];
That's it. Just a config thingy & you can see in the response each Minion object contains another object leadingMission which is an instance of Mission
model lead by respective Minion.
Note: For nested relationships, you can define them appending dot(.) operator eg.
employee.designations
.
Let's arrange an Event to get triggered to bring a Minion to Gru's lab whenever a new Mission
is created leading by the Minion. Create a POST route in routes/api.php
:
Route::post('missions', 'MissionController@createMission')->name('missions.store');
We need to have MissionController
, MissionRepository
, MissionCreateRequest
and MissionCreateJob
ready for this route to work.
Also we need to have an Event say BringMinionToLabEvent
ready to be triggered & the same to be configured into job MissionCreateJob
under property public $event = BringMinionToLabEvent::class;
Now try hitting the route POST /missions as follows:
curl -X POST \
http://localhost:7872/api/missions \
-H 'Content-Type: application/json' \
-H 'cache-control: no-cache' \
-d '{
"name": "Steal the Moon! Part - 4",
"description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.",
"minionId": 2
}'
You should be able to see a log entry under file storage/logs/laravel.log
which is the action we had set in the event BringMinionToLabEvent
.
With this package, file upload is just a config thing away.
Publish the file.php
configuration file to the config directory with below command:
php artisan vendor:publish --tag=luezoid-file-config
Configure the new type
representing a specific module eg. MINION_PROFILE_PICTURE as per your requirement, define the validation
(if any), valid_file_types
allowed, local_path
(for local filesystem), etc. A sample type
named EXAMPLE is added by default for reference.
Next, add the below code in AppServiceProvide
:
$this->app->bind(\Luezoid\Laravelcore\Contracts\IFile::class, function ($app) {
if (config('file.is_local')) {
return $app->make(\Luezoid\Laravelcore\Files\Services\LocalFileUploadService::class);
}
return $app->make(\Luezoid\Laravelcore\Files\Services\SaveFileToS3Service::class);
});
Next, create a route as below:
Route::post('files', '\Luezoid\Laravelcore\Http\Controllers\FileController@store')->name('files.store');
Now, you are all set to upload files.
curl -X POST \
http://localhost:7872/api/files \
-H 'cache-control: no-cache' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F type=EXAMPLE \
-F file=@/home/choxx/Desktop/a61113f5afc5ad52eb59f98ce293c266.jpg
The response body will have an id
, url
, and a few other fields representing the storage location & other details for the uploaded file. See sample here. This id
field you could use to store in your table as foreign key column & make hasOne()
relation with the records & use them as per need.
Moreover, in the config itself you could configure if local filesystem has to be used for uploads or on the go AWS S3 bucket. For using S3 bucket, you need to configure AWS credentials in .env
file:
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
Isn't is awesome? :)
This is one of the coolest feature of this package. Tired off writing query to shorten up the result set & applying filters over nested relation data? Apply such filters with simplicity with just sending simple query params. Let's see how:
-
Select Filters:
In the query params, a key-value pair can be passed with which you could simply restrict the number of columns to be present in the response for a particular model object & it's related model entities. The key to be sent is
selectFilters
& the value has to be minified JSON as explained below:k is keys, r is relation, cOnly is flag to set if only count is needed or the whole relational data. cOnly flag can be used in r relations nestedly.
{ "cOnly": false, "k": ["id", "name", "favouriteSound"], "r": { "missions": { "k": ["name", "description"] } } }
So, with the above JSON, a GET request like:
curl -X GET \ 'http://localhost:7872/api/minions?selectFilters={%22cOnly%22:false,%22k%22:[%22id%22,%22name%22,%22favouriteSound%22],%22r%22:{%22missions%22:{%22k%22:[%22name%22,%22description%22]}}}' \ -H 'cache-control: no-cache'
would return only the columns
id
,name
&favourite_sound
of tableminions
and columnsname
&description
of tablemissions
in the response. Rest redundant columns which are not needed are eleminated from the response causing substantial reduction in the size of overall response & speeding up the response time of APIs.Note: both the columns the 'local key' & the 'foreign key' must be present in the main & the related relation(s). This whole config can go as deeper as needed in the same way as the first relation goes.
-
Relation Filters:
Let's assume we want to find all the minions whose Leading Mission name is "Steal the Moon!". Using
Eloquent
queries we can retrieve such results as:$query = Minion::whereHas('missions', function ($q) { $q->where('name', 'Steal the Moon!'); });
Seems easy right? But wait, for this to be dynamic, you need to customarily pass the query param holding this
missions.name
column & manage yourself by writing custom logic to make such filtering work. But by using this package, you can simply do it with just sending the query params as we have seen in Searching & Filters. You just need to send the query params as relation-name->column-name={string-to-be-filtered}. See the below example:curl -X GET \ 'http://localhost:7872/api/minions?missions-%3Ename=Steal%20the%20Moon%21' \ -H 'cache-control: no-cache'
Notice that we have passed query param
missions-%3Ename=Steal%20the%20Moon%21
& hence reducing our effort of writing the above custom logic altogether.So, do we deserve a star rating? :)
Note: These Select & Relation filters can be combined together to reduce the response size as well as the size of bacck-end code. Make use of these two features combinely & make wonderful applications.
Need to send key searchKey
via query params along with value.
In repo, define config along with models & column names to be searched on.
Usage:
$searchConfig = [
'abc' => [
'model' => Abc::class,
'keys' => ['name', 'field']
],
'groups' => [
'model' => ProductGroup::class,
'keys' => ['name', 'code', 'hsn_code']
]
];
return $this->search($searchConfig, $params);
Laravel-core is released under the MIT License. See the bundled LICENSE for details.