macropay-solutions / laravel-crud-wizard-free
Free library for laravel/lumen crud operations including url query language
Requires
- php: ^8.0
- ext-json: *
- maatwebsite/excel: ^3.1
- nesbot/carbon: ^2.62
- psr/log: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- illuminate/database: ^8.83
- illuminate/http: ^8.83
- illuminate/pagination: ^8.83
- illuminate/routing: ^8.83
- illuminate/validation: ^8.83
- phpunit/phpunit: ^8.5
- symfony/http-foundation: ^5.0
Suggests
- ext-pdo_mysql: *
- barryvdh/laravel-ide-helper: *
- laravel/framework: >= 8.0
- laravel/lumen-framework: >= 8.0
- macropay-solutions/laravel-crud-wizard-demo: dev-production
This package is not auto-updated.
Last update: 2024-12-19 07:20:13 UTC
README
This is a stripped down version from the paid version Laravel crud wizard (Tested on laravel/lumen 8, laravel 9, laravel 10 and it should work also on laravel 11).
Url query language lib for RESTful CRUD (micro) services using lumen/laravel 8-9-10-11
This is not just another CRUD lib!
It has built in filtering capabilities that can be used for listing but also for mass deleting, so it could be called a CRUFD (create, read, update, filter and delete) lib instead.
I. Install
II. Start using it
III. Crud routes
III.1. Create resource
III.2. Get resource
III.3. List filtered resource
III.4. Update resource (or create)
III.5. Delete resource
I. Install
composer install macropay-solutions/laravel-crud-wizard-free
II. Start using it
Register \MacropaySolutions\LaravelCrudWizard\Providers\CrudProvider \MacropaySolutions\LaravelCrudWizard\Http\Middleware\UnescapedJsonMiddleware::class in lumen or laravel.
Create a constant in your code
class DbCrudMap { public const MODEL_FQN_TO_CONTROLLER_MAP = [ BaseModelChild::class => ResourceControllerTraitIncludedChild::class, ... ]; }
Extend:
- BaseModel (it needs datetime NOT timestamp for created_at and updated_at columns)
- BaseResourceService
Create a Controller that uses ResourceControllerTrait and call $this->init() from its __construct.
For model properties autocomplete:
-
extend BaseModelAttributes following the same FQN structure as the parent's:
\App\Models\ChildBaseModel paired with \App\Models\Attributes\ChildBaseModelAttributes \App\Models\Folder\ChildBaseModel paired with \App\Models\Folder\Attributes\ChildBaseModelAttributes
-
add in its class dock block using @property all the models properties/attributes/columns
-
add in the model's class dock block @property ChildBaseModelAttributes $a and @mixin ChildBaseModelAttributes
-
use $model->a-> instead of $model->
-
BaseModelFrozenAttributes can be also extended on the same logic and used for model read only situations - DTO without setters (Reflection or Closure binding usage will retrieve/set private stdClass not Model - but the model can be retrieved from DB by its primary key that is readable in this frozen model):
#OperationModel example for BaseModelFrozenAttributes public function getFrozen(): OperationFrozenAttributes { return parent::getFrozen(); // this is needed for autocompletion and will include also the loaded relations // or return new OperationFrozenAttributes((clone $this)->forceFill($this->toArray())); // or just attributes without loaded relations return new OperationFrozenAttributes((clone $this)->forceFill($this->attributesToArray())); }
#OperationService example for BaseModelAttributes and BaseModelFrozenAttributes public function someFunction(): void { // BaseModelAttributes echo $this->model-a->value; // has autocomplete - will print for example 1 echo $this->model-a->value = 10; // has autocomplete - will print 10 echo $this->model->value; // has autocomplete - will print 10 // BaseModelFrozenAttributes $dto = $this->model->getFrozen(); echo $dto->client_id; // has autocomplete - will print for example 1 $dto->client_id = 4; // Exception: Dynamic properties are forbidden. if (isset($dto->client)) { /** @var ClientFrozenAttributes $client */ // $client will be an stdClass that has autocomplete like a ClientFrozenAttributes $client = $dto->client; echo $client->name; // has autocomplete - will print for example 'name' $client->name = 'text'; // NO Exception echo $client->name; // will print 'text' // $client changes can happen, but they will not be persisted in the $dto ($client is a stdClass clone) echo $dto->client->name; // will print 'name' } foreach (($dto->products ?? []) as $k => $product) { /** @var ProductFrozenAttributes $product */ // $product will be an stdClass that has autocompletes like a ProductFrozenAttributes echo $product->value; // has autocomplete - will print for example 1 $product->value = 2; // NO Exception echo $product->value; // will print 2 // $product changes can happen, but they will not be persisted in the $dto ($product is a stdClass clone) echo $dto->products[$k]->name; // will print 1 } }
Add this new resource to the above map.
Register the crud routes in your application using (for example in Laravel)
try { foreach ( ResourceHelper::getResourceNameToControllerFQNMap(DbCrudMap::MODEL_FQN_TO_CONTROLLER_MAP) as $resource => $controller ) { Route::get('/' . $resource, [$controller, 'list'])->name('apiinfo.list_' . $resource); Route::post('/' . $resource, [$controller, 'create'])->name('apiinfo.create_' . $resource); Route::put('/' . $resource . '/{identifier}', [$controller, 'update'])->name('apiinfo.update_' . $resource); Route::get('/' . $resource . '/{identifier}', [$controller, 'get'])->name('apiinfo.get_' . $resource); Route::delete('/' . $resource . '/{identifier}', [$controller, 'delete'])->name('apiinfo.delete_' . $resource); // Route::get('/' . $resource . '/{identifier}/{relation}', [$controller, 'listRelation']); // paid version only } } catch (Throwable $e) { \Illuminate\Support\Facades\Log::error($e->getMessage()); }
for example for lumen:
try { foreach ( ResourceHelper::getResourceNameToControllerFQNMap( DbCrudMap::MODEL_FQN_TO_CONTROLLER_MAP ) as $resource => $controllerFqn ) { $controllerFqnExploded = \explode('\\', $controllerFqn); $controller = \end($controllerFqnExploded); //$router->get('/' . $resource . '/{identifier}/{relation}', [ // 'as' => $resource . '.listRelation', // 'uses' => $controller . '@listRelation', //]); // paid version only $router->get('/' . $resource, [ 'as' => $resource . '.list', 'uses' => $controller . '@list', ]); $router->post('/' . $resource, [ 'as' => $resource . '.create', 'uses' => $controller . '@create', ]); $router->put('/' . $resource . '/{identifier}', [ 'as' => $resource . '.update', 'uses' => $controller . '@update', ]); $router->get('/' . $resource . '/{identifier}', [ 'as' => $resource . '.get', 'uses' => $controller . '@get', ]); $router->delete('/' . $resource . '/{identifier}', [ 'as' => $resource . '.delete', 'uses' => $controller . '@delete', ]); } } catch (Throwable $e) { \Illuminate\Support\Facades\Log::error($e->getMessage()); }
Set LIVE_MODE=false in your .env file for non prod environments.
See also Laravel crud wizard demo
III. Crud routes
The identifier can be a primary key or a combination of primary keys with _ between them if the resource has a combined primary key!!!
see \MacropaySolutions\LaravelCrudWizard\Models\BaseModel::COMPOSITE_PK_SEPARATOR
III.1 Create resource
POST /{resource}
headers:
Authorization: Bearer ... // if needed. not coded in this lib
Accept: application/json
ContentType: application/json
body:
{
"column_name":"value",
...
}
Json Response:
201:
{
"column_name":"value",
...
}
400:
{
"message": "The given data was invalid.", // or other message
"errors": {
"column_name1": [
"The column name 1 field is required."
],
"column_name_2": [
"The column name 2 field is required."
],
...
}
}
The above "errors" are optional and appear only for validation errors while "message" will always be present.
III.2 Get resource
GET /info/{resource}/{identifier}?withRelations[]=has_manyRelation&withRelations[]=has_oneRelation&withRelationsCount[]=has_manyRelation&withRelationsExistence[]=has_manyRelation
headers:
Authorization: Bearer ... // if needed. not coded in this lib
Accept: application/json
Json Response:
200:
{
"identifier":"value",
"column_name":"value",
...
"index_required_on_filtering": [
"column_name_1",
"column_name2"
],
"has_oneRelation":{...},
"has_manyRelation":[
{
"id": ...,
"name": "...",
"pivot": {
"key1": 25,
"key2": 5
}
}
],
"has_manyRelation_count": 0,
"has_manyRelation_exist": false
}
400:
{
"message": ...
}
The identifier can be composed by multiple identifiers for pivot resources that have composite primary key. Example:/table1-table2-pivot/3_10
The relations will be retrieved as well when required. The relation keys CAN'T be used for filtering!!!
index_required_on_filtering
key CAN'T be used for filtering.
pivot
is optional and appears only on relations that are tied via a pivot.
III.3 List filtered resource
GET /{resource}?page=1&limit=10&column=2&sort[0][by]=updated_at&sort[0][dir]=ASC&withRelations[]=has_manyRelation&withRelations[]=has_oneRelation&withRelationsCount[]=has_manyRelation&withRelationsExistence[]=has_manyRelation // advanced filters and aggregations are available only in the paid version
GET /{resource}/{identifier}/{relation}?... // available only in paid version
headers:
Authorization: Bearer ... // if needed. not coded in this lib
Accept: application/json or application/xls
Json Response:
200:
{
"index_required_on_filtering": [
"column_name1",
"column_name2"
],
"current_page": 1, // not present when cursor is present in request
"data": [
{
"identifier":"value",
"column_name":"value",
...,
"has_oneRelation":{...},
"has_manyRelation":[
{
"id": ...,
"name": "...",
"pivot": {
"key1": 25,
"key2": 5
}
}
],
"has_manyRelation_count": 0,
"has_manyRelation_exist": false
}
],
"from": 1, // not present when cursor is present in request
"last_page": 1, // not present when cursor is present in request or when simplePaginate is true in controller or present in request
"per_page": 10,
"to": 1, // not present when cursor is present in request
"total": 1, // not present when cursor is present in request or simplePaginate is true in controller or present in request
"has_more_pages": bool,
"cursor": "..." // present only when cursor is present in request
}
and for application/xls: binary with contents from data
The reserved words / parameters that will be used as query params are:
page,
limit,
simplePaginate
cursor,
sort,
withRelations,
withRelationsCount,
withRelationsExistence,
Defaults:
page=1;
limit=10;
simplePaginate is false by default and only its presence is checked in request, not its value
cursor is not defined
sort[][dir]=DESC
Obs.
index_required_on_filtering key CAN'T be used for filtering.
use ?cursor= for cursor pagination and ?simplePaginate=1 for simplePaginate. Use none of them for length aware paginator.
if \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class is used use ?cursor=1 instead of emtpy string
sort works also on aggregated colums for relation count and existence
withRelations which uses with function does not load morphable relations in laravel. BaseResourceService::addRelationsToExistingModel can be used for those or loadMorph.
III.4 Update resource (or create)
PUT /{resource}/{identifier}
headers:
Authorization: Bearer ... // if needed. not coded in this lib
Accept: application/json
ContentType: application/json
body:
{
"column_name":"value",
...
}
Json Response:
200 | 201:
{
// all resource's fields
}
400:
{
"message": "The given data was invalid.", // or other message
"errors": {
"column_name": [
"The column name field is invalid."
],
...
}
}
The above "errors" are optional and appear only for validation errors while "message" will always be present.
The identifier can be composed by multiple identifiers for pivot resources that have composite primary key (and empty string primary key in their model). Example:/resources/3_10
Update is not available on some resources.
UpdateOrCreate is available on resources that have their model defined with incrementing = false ONLY if the request contains all the keys from the primary key (found also in function getPrimaryKeyFilter).
Update will validate only dirty columns, not all sent columns, meaning the update can be made with all columns of the resource instead of just the changed ones.
III.5 Delete resource
DELETE /{resource}/{identifier}
headers:
Authorization: Bearer ... // if needed. not coded in this lib
Json Response:
204:
[]
400:
{
"message": ...
}
Delete is not available by default.