Development
This is a work in progress section, please feel free to both contribute and make suggestions about new topics.
Topics:
- Enso conventions
- Building CRUD files
- Overwriting functionality
- Creating new themes
Recommended
No matter the topic, please also go through the related packages' documentation for a more in-depth understanding of the available functionality.
Enso conventions
Named routes
Enso exclusively uses named routes so it is important to set names for the routes you may add since the named routes are tied in with the permission system.
Routes and permissions
Enso is a SPA, and generally, SPAs handle routing for their web pages and use a API for fetching and persisting data.
Therefore, we use two types of routes:
- front end routes, found on the
resources/js/routes
path - back end routes, present in the
routes/api.php
file
We strive to have the 2 sets of routes aligned as much as possible as it makes things clearer, but there will be differences.
For example, the most of the *.index
routes are present only on the front end.
What is important is that all routes, front end or back end, have corresponding permissions as these are used also for authorization in both layers.
Back end permission authorization
On the back end, we have a Gate defined method that can be used to check for authorization on a given route.
$user->can('access-route', 'administration.users.store')
Front end permission authorization
On the front end, a helper is available that can be used for authorization on a given route. Don't forget to inject it first.
inject: ['canAccess']
...
v-if="canAccess('administration.users.store')">
Invocable controllers
We're currently using invocable controllers for all our actions. While this is not a must and this choice is not tied to other core functionality, keep in mind that if using the CLI for resource files generation, the generated files WILL follow this convention.
Fat models, slim controllers
In time, we've transitioned to this style as it matches our beliefs and is also a style recommended by Taylor Otwell. You may read more here
Building CRUD structures
No matter how complex the application ends up, you'll still need CRUD pages.
Because of the modularity of Enso, when creating such pages, there are several packages involved, each with their own templates, controllers and routes.
In order to simplify and streamline the process of adding CRUD pages, we've created the CLI package and command.
You can use the cli command to create:
- models
- the common Enso permissions
- menus
- the required files, such as:
- database migrations
- controllers
- back-end routes
- request validators
- form templates and builders
- table templates and builders
- front-end pages
- front-end routes
Note that these are meant to provide you with a starting point, as you'll still need to customize and tweak some of the resulted files.
Customizing
If you already created some of the files, such as a model and a migration, you can configure the CLI to skip building them.
Starting clean
If you're just starting to use the CLI,
it is recommended to do so on a clean project state, after you've configured/initialized git,
and you've committed your other changes, as once the files are generated,
you'll be able to use git status
to see a list of changes.
Using the CLI
To get started, open a terminal window in the root of your project and type:
php artisan enso:cli
You'll be greeted with a list of options:
Create a new Laravel Enso Structure
Current configuration status:
Model ✗
Permission Group ✗
Permissions ✗
Menu ✗
Files ✗
Package ✗
Choose element to configure:
[0] Model
[1] Permission Group
[2] Permissions
[3] Menu
[4] Files
[5] Package
[6] Generate
[7] Toggle Validation
[8] Exit
>
You can tell that the options are yet to be configured. It's best to follow the order in which they are given.
Jumping ahead
If you jump ahead or miss some of the required options, you'll receive a warning.
For each option, you'll be first asked to confirm that you want to configure it.
Making changes
Once you've configured an option, before generating the files, you can go back and re-configure any of the options.
Autocomplete
When making changes to an already configured option, you can
type the first character and have the cli
autocomplete the previously set value
so you can advance faster through the options.
Configuring the model
After confirming you want to configure the model, you'll want to input its name.
If you enter a non namespaced name, by convention, the model will be placed
in your projectRoot/App
folder.
Model configuration:
name =>
Configure Model (yes/no) [no]:
> y
name:
> Car
Current configuration status:
Model ✓
Permission Group ✗
Permissions ✗
Menu ✗
Files ✗
Package ✗
You may also input a namespaced name, in which case, the model will be placed in the proper folder.
Namespace
If you are inputting a namespaced model, please type the full namespace, including App
.
Model configuration:
name => Car
Configure Model (yes/no) [no]:
> y
name:
> App\Vehicles\Motorized\Car
Configuring the permission group
Permission groups result out of the names of permissions, so for instance,
both vehicles.motorized.cars.create
and vehicles.motorized.cars.update
belong to the
vehicles.motorized.cars
permission group.
At this step, the permission group is requested because it will be used when building the structure migration, the routes and more, based on your choices.
When your project requirements are simple, you may use a flat structure for the group otherwise go with a nested one.
Also, if you're unsure when choosing the routes/permissions/group naming, it's a good idea to follow the classes' namespace structure.
By convention use the plural name for resources.
Permission Group configuration:
name =>
Configure Permission Group (yes/no) [no]:
> y
name:
> vehicles.motorized.cars
Current configuration status:
Model ✓
Permission Group ✓
Permissions ✗
Menu ✗
Files ✗
Package ✗
Configuring the permissions
Simply choose the desired permissions from the list:
index
will generate:- front end route, used for navigating to the index page, where you'll see the list of resources in a data table
create
, will generate:- front end route, used to display the creation form for your resource.
- back end route, used to fetch the form data
store
will generate:- back end route, used by the form to persist a new resource
show
will generate:- front end route, used to display the show page
- back end route, used to fetch a model
edit
will generate:- front end route, used to display the edit form for an existing resource.
- back end route, used to fetch the form data
update
will generate:- back end route, used by the form to update an existing resource
destroy
will generate:- back end route, used by the form's and tables's delete actions to delete a resource
initTable
will generate:- back end route, used for the initialization of the index page's table
tableData
will generate:- back end route, used for fetching the data for the index page's table
exportExcel
will generate:- back end route, used for exporting the information for the the index page's table
options
will generate:- back end route, utilized by a server-side Select component for fetching a list of options for the resource
Permissions configuration:
index => ✗
create => ✗
store => ✗
show => ✗
edit => ✗
update => ✗
destroy => ✗
initTable => ✗
tableData => ✗
exportExcel => ✗
options => ✗
Configure Permissions (yes/no) [no]:
> y
index (yes/no) [no]:
> y
create (yes/no) [no]:
> y
store (yes/no) [no]:
> y
show (yes/no) [no]:
> y
edit (yes/no) [no]:
> y
update (yes/no) [no]:
> y
destroy (yes/no) [no]:
> y
initTable (yes/no) [no]:
> y
tableData (yes/no) [no]:
> y
exportExcel (yes/no) [no]:
> y
options (yes/no) [no]:
> y
Current configuration status:
Model ✓
Permission Group ✓
Permissions ✓
Menu ✗
Files ✗
Package ✗
Front end helper
On the front end, a route
helper is available that can be used to build the proper URL out of
the route name. You may search the Enso pages for examples.
Configuring the menu
The menu will need a few attributes:
the name is a string and needs to be unique at its level
the icon must be a font awesome icon and needs to be imported in your project
the parent menu is the name of the parent menu. If given, the parent menu must already be present in the database.
If you don't provide a parent menu, the new menu will be added at the root level.Dot notation
When specifying the parent menu you can input the desired menu with its entire, dot separated hierarchy, for example
Vehicles.Motorized
the route shall be the named route that is utilized when the user clicks on the menu, in the front-end. This is usually a route that ends with
.index
. You may only specify the final segment of the route, as the permission group is used when building the whole route for the menu. You can also notice that by default, the route is index.Permissions
If a user does not have access to the given route, that menu will not be visible.
the order index is used to sort menus of the same level and should be an integer
the has_children flag is used to mark a menu as a parent.
Parent menus cannot have a route while all other menus must have a route.
By clicking on a parent menu, you will expand and reveal its children.
Menu configuration:
name =>
icon =>
parentMenu =>
route => index
order_index => 999
has_children => ✗
Configure Menu (yes/no) [no]:
> y
name:
> Cars
icon:
> car
parentMenu:
> Motorized
route:
> index
order_index:
> 100
has_children (yes/no) [no]:
> n
Current configuration status:
Model ✓
Permission Group ✓
Permissions ✓
Menu ✓
Files ✗
Package ✗
Configuring the files
Once everything else is configured, you may choose what files you want to have generated for you.
Note that the options are interdependent, so, for instance, if you choose the
routes
option, the generated routes will match the permissions you selected at the 3rd step.
Simply choose the desired files from the list:
- model, generates:
- model class
- model table migration
- structure, generates:
- structure migration
- routes, generates:
- front end routes
- back end routes
- views, generates:
- front end pages
- form, generates:
- controllers, request validator, form builder and template
- table, generates:
- controllers, table builder and template
- options, generates:
- controller
Files configuration:
model => ✗
migration => ✗
routes => ✗
views => ✗
form => ✗
table => ✗
options => ✗
Configure Files (yes/no) [no]:
> y
model (yes/no) [no]:
> y
migration (yes/no) [no]:
> y
routes (yes/no) [no]:
> y
views (yes/no) [no]:
> y
form (yes/no) [no]:
> y
table (yes/no) [no]:
> y
options (yes/no) [no]:
> y
Current configuration status:
Model ✓
Permission Group ✓
Permissions ✓
Menu ✓
Files ✓
Package ✗
Package
Most of the time you'll probably want to generate a structure in the local project, and you don't need to modify anything else.
However, if you want to build a package, you may access the package menu and setup the following options:
- name
- the name of the package
- is used for the creation of the package's folder
- vendor
- the name of the vendor
- is used for the creation of the vendor's folder
- config
- if chosen, will create a config file for the package
- providers
- if chose, will create package providers
When creating a package, don't forget to properly align the model namespace with the vendor and package name.
Package configuration:
name =>
vendor => laravel-enso
config => ✗
providers => ✗
Configure Package (yes/no) [no]:
> y
name:
> foo
vendor:
> laravel-enso
config (yes/no) [no]:
> y
providers (yes/no) [no]:
> y
Generating the files
Once your options are configured, you may generate the corresponding files.
While files are generated for you in their proper locations,
the back end routes are printed in the terminal and you should copy them into your
routes/api.php
files.
Choose element to configure:
[0] Model
[1] Permission Group
[2] Permissions
[3] Menu
[4] Files
[5] Package
[6] Generate
[7] Toggle Validation
[8] Exit
> 6
Copy and paste the following code into your api.php routes file:
Route::namespace('Vehicles\Motorized\Cars')
->prefix('vehicles/motorized/cars')->as('vehicles.motorized.cars.')
->group(function () {
Route::get('', 'Index')->name('index');
Route::get('create', 'Create')->name('create');
Route::post('', 'Store')->name('store');
Route::get('{car}/edit', 'Edit')->name('edit');
Route::patch('{car}', 'Update')->name('update');
Route::delete('{car}', 'Destroy')->name('destroy');
Route::get('initTable', 'InitTable')->name('initTable');
Route::get('tableData', 'TableData')->name('tableData');
Route::get('exportExcel', 'ExportExcel')->name('exportExcel');
Route::get('options', 'Options')->name('options');
Route::get('{car}', 'Show')->name('show');
});
The new structure is created, you can start playing
If you've setup git for the project, you may use git status
to see a list of
new files and folders:
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/Forms/
app/Http/Controllers/Vehicles/
app/Http/Requests/
app/Tables/
app/Vehicles/
database/migrations/2019_05_27_134825_create_cars_table.php
database/migrations/2019_05_27_134825_create_structure_for_cars.php
resources/js/pages/vehicles/
resources/js/routes/vehicles.js
resources/js/routes/vehicles/
no changes added to commit (use "git add" and/or "git commit -a")
Next steps
Below you'll find examples of customizing the generated files, considering the most complete scenario where we're creating the entire structure, with all the files locally within the project.
The table migration
Since most likely, the options you chose also involve the creation of migrations, customize your model migration.
class CreateCarsTable extends Migration
{
public function up()
{
Schema::create('cars', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name')->unique();
$table->text('description')->nullable();
$table->unsignedTinyInteger('make');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('cars');
}
}
After customizing the migration you may run
php artisan migrate
The menu icon
Import the required icon in the resources/js/app.js
file:
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCar } from '@fortawesome/free-solid-svg-icons';
library.add(faCar);
If you haven't done so already, build the front-end and/or start the HMR process.
CD into the client
directory and run:
yarn serve
To make a production build, run:
yarn build
After refreshing the page you should be able to see the new menu and navigate to the index page, where you'll notice an empty table, as there are no models added to the database yet.
The model class
class Car extends Model
{
protected $fillable = [
'name', 'description', 'make'
];
}
The request validation
By default, the CLI will generate one request validators and use it for both the store and the update actions.
class ValidateCarRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|max:255',
'description' => 'nullable|max:255',
'make' => 'required|integer',
];
}
}
This is a basic request validators; note that we're not checking for the name to be unique.
In cases where you need different validation rules for the two actions, it makes sense to use 2 validation classes:
class ValidateCarStore extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => ['required', 'max:255', $this->unique()],
'description' => 'nullable|max:255',
'make' => 'required|integer',
];
}
protected function unique()
{
return Rule::unique('cars', 'name');
}
}
class ValidateCarUpdate extends ValidateCarStore
{
protected function unique()
{
return parent::unique()
->ignore($this->route('car')->id);
}
}
If using different validators than the one generated by the CLI, don't forget to update the controllers so that they use the new validation classes:
App\Http\Controllers\Vehicles\Motorized\Cars\Store
App\Http\Controllers\Vehicles\Motorized\Cars\Update
.
Using an Enum for make (optional)
In the cars
migration we defined the make
column as a tiny integer so we could demonstrate
using an Enso Enum for storing car makes.
namespace App\Enums;
use LaravelEnso\Helpers\app\Classes\Enum;
class CarMakes extends Enum
{
const AUDI = 1;
const BMW = 2;
}
You may read more about enums in their documentation but in short they are a construct which integrates with tables, forms, selects that allows for a better representation of type values (and more).
The table builder
class CarTable extends Table
{
protected $templatePath = __DIR__.'/../../../Templates/Vehicles/Motorized/cars.json';
public function query()
{
return Car::selectRaw('
cars.id as "dtRowId",
cars.name,
cars.description,
cars.make
');
}
}
Note that the dtRowId
identifier is required for all tables.
The table template
{
"name": "Car",
"routePrefix": "vehicles.motorized.cars",
"crtNo": true,
"buttons": [
"excel",
"create",
"show",
"edit",
"destroy"
],
"columns": [
{
"label": "Name",
"name": "name",
"data": "cars.name",
"meta": [
"searchable",
"sortable"
]
},
{
"label": "Description",
"name": "description",
"data": "cars.description",
"meta": [
"searchable",
"sortable"
]
},
{
"label": "Make",
"name": "make",
"data": "cars.make",
"enum": "App\\Enums\\CarMakes",
"meta": [
"sortable"
]
}
]
}
Note that while integer values are store in the DB for the car make, we're using the Enum to present them as human friendly values.
The form template
{
"routePrefix": "vehicles.motorized.cars",
"sections": [
{
"columns": 2,
"fields": [
{
"label": "Name",
"name": "name",
"value": null,
"meta": {
"custom": false,
"type": "input",
"content": "text",
"disabled": false
}
},
{
"label": "Make",
"name": "make",
"value": null,
"meta": {
"options": "App\\Enums\\CarMakes",
"type": "select",
"disabled": false
}
}
]
},
{
"columns": 1,
"fields": [
{
"label": "Description",
"name": "description",
"value": null,
"meta": {
"custom": false,
"type": "textarea",
"content": "text",
"disabled": false,
"rows": 2
}
}
]
}
]
}
You can now use the Create
button in the index page's table to add a new car, then update it, delete it, etc.
Overwriting functionality
Dependency injection
One of the cleanest ways of customizing core logic is by extending classes, overwriting the required attributes and methods and then using dependency injection to obtain the desired implementation.
This means that when a customization is needed, you can extend the class you want to customize and then bind your local implementation to the core class, in your application service provider, therefore having the container use the local implementation instead.
Overwriting Controllers \ Validators:
use App\Http\Requests\People\ValidatePersonStore;
use App\Http\Requests\People\ValidatePersonUpdate;
use LaravelEnso\People\app\Http\Requests\ValidatePersonStore as EnsoPersonStoreValidator;
use LaravelEnso\People\app\Http\Requests\ValidatePersonUpdate as EnsoPersonUpdateValidator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public $bindings = [
EnsoPersonStoreValidator::class => ValidatePersonStore::class,
EnsoPersonUpdateValidator::class => ValidatePersonUpdate::class,
];
public function boot() {...}
public function register() {...}
...
Overwriting Models :
Local Model
namespace App\Models;
use LaravelEnso\Permissions\app\Models\Permission as EnsoPermission;
class Permission extends EnsoPermission
{
public function localRelation()
{
return this->belongsToMany(LocalRelation::class);
}
}
Local AppServiceProvider
use LaravelEnso\Permissions\app\Models\Permission as EnsoPermission;
use App\Models\Permission;
class AppServiceProvider extends ServiceProvider
{
public $bindings = [
EnsoPermission::class => Permission::class
];
...
Changing back end logic
If the modifications you require are more extensive and cannot be resolved via using dependency injection, the other option is to overwrite the required routes, and point to your local implementation/controllers.
Changing front end pages
When you need to customize any of the front end pages supplied with Enso, you generally have two options:
- use patch-package to make a patch to the package that contains the page(s) to be modified
- create your local version of the page(s) and overwrite the front end routes so that they point to your page(s)
Breaking the build
Once you start messing with the core Enso functionality it is possible that the changes you're making could break parts of the various Enso core functionality.
In order to be aware of this, you may run the Enso tests locally, by typing into the terminal:
phpunit
If the tests fail, you may use the results to identify the issue. When opening issues on github, if the tests are failing, please let us know as that might speed up the troubleshooting.
Themes
If you want to use a different theme instead of, or thoroughly customize the themes supplied with Enso,
you should use as example the default themes, available on the client/node_modules/@enso-ui/themes
path.
You may create a patch using patch-package
for the themes
package and customize them to your heart's desire.
Adding static assets
If there are local static assets such as image resources which you may require
within the project, you can create a client/src/images
folder and place them there.
Afterwards, you should add a configuration object to the CopyPlugin
section
from vue.config.js
so that the images are copied on build.
Linting & Aliases
You may have noticed that there are two .eslintrc.js
files that come with Enso, by default:
- in the project root
- in the
client
folder
The .eslintrc.js
found in the root folder is used out of the box by most code editors/IDEs
and that's the reason for its existence. Thus, its configuration is somewhat optional
because in the worst case, your editor will report errors but the build will complete.
The .eslintrc.js
found in the client
folder is used by VueCLI (vue-cli-service)
during the linting & build phases. Thus, it is essential that it is configured
as intended, because otherwise you'll get errors reported and the build may not complete.
Whenever we declare aliases for packages or paths, it is ideal to configure/set the aliases in:
client/vue.config.js
, for the buildclient/eslintrc.js
, for the automatic lintingeslintrc.js
, for your editor's linting
When you look into the files, you'll find examples out of the box.
← Usage Under the Hood →