macropay-solutions/laravel-crud-wizard-free

Free library for laravel/lumen crud operations including url query language

3.4.9 2024-12-19 07:05 UTC

README

Total Downloads Latest Stable Version License

laravel-lumen-crud-wizard-logo

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.

Demo page for listing

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.