├── composer.json ├── readme.md ├── resources ├── css │ └── crudify.css ├── js │ └── crudify.js ├── stubs │ ├── generate │ │ ├── DummyClass.stub │ │ ├── DummyClassController.stub │ │ ├── DummyClassDatatable.stub │ │ ├── DummyClassFactory.stub │ │ ├── DummyClassRequest.stub │ │ ├── DummyClassSeeder.stub │ │ ├── DummyMigration.stub │ │ ├── navbar-link.stub │ │ ├── routes.stub │ │ └── views │ │ │ ├── actions.stub │ │ │ ├── create.stub │ │ │ ├── edit.stub │ │ │ ├── fields.stub │ │ │ ├── index.stub │ │ │ └── show.stub │ └── install │ │ ├── browser-sync.stub │ │ └── datatables-script.stub └── views │ ├── checkbox.blade.php │ ├── checkboxes.blade.php │ ├── file.blade.php │ ├── input.blade.php │ ├── radios.blade.php │ ├── select.blade.php │ └── textarea.blade.php └── src ├── Commands ├── GeneratesCrud.php └── InstallsCrudify.php ├── Components ├── Checkbox.php ├── Checkboxes.php ├── File.php ├── Input.php ├── Radios.php ├── Select.php └── Textarea.php ├── Http └── Datatable.php ├── Providers └── CrudifyServiceProvider.php ├── Seeders └── AdminUserSeeder.php └── Traits ├── FillsColumns.php ├── FormatsOptions.php ├── HasInputAttributes.php └── SerializesDates.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kejojedi/crudify", 3 | "description": "Laravel 7 CRUD app scaffolding & generator.", 4 | "homepage": "https://github.com/kejojedi/crudify", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Kevin Dion", 9 | "email": "kejojedi@gmail.com", 10 | "role": "Developer" 11 | } 12 | ], 13 | "require": { 14 | "barryvdh/laravel-ide-helper": "^2.6", 15 | "laravel/framework": "^7.0", 16 | "laravel/ui": "^2.0", 17 | "yajra/laravel-datatables-html": "^4.23", 18 | "yajra/laravel-datatables-oracle": "^9.9" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Kejojedi\\Crudify\\": "src" 23 | } 24 | }, 25 | "extra": { 26 | "laravel": { 27 | "providers": [ 28 | "Kejojedi\\Crudify\\Providers\\CrudifyServiceProvider" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Imgur](https://i.imgur.com/0LaKZQK.png) 2 | 3 | # Crudify 4 | 5 | Crudify is a Laravel 7 package which includes sensible CRUD app scaffolding and a generator to make your life easier. It automates initial CRUD app setup with the `crudify:install` command, and generates CRUD resource files for you with the `crudify:generate` command. It also includes form components to make creating forms a breeze. 6 | 7 | It is configured to work well with PHPStorm, Valet, and Laragon, among others. **This package requires Node.js to be installed in order to run `npm` commands.** 8 | 9 | Useful links: 10 | 11 | - Support: [GitHub Issues](https://github.com/kejojedi/crudify/issues) 12 | - Contribute: [GitHub Pulls](https://github.com/kejojedi/crudify/pulls) 13 | - Donate: [PayPal](https://www.paypal.com/paypalme2/kjjdion) 14 | 15 | ## Installation 16 | 17 | Install Laravel: 18 | 19 | laravel new app 20 | 21 | Configure `.env` file: 22 | 23 | APP_NAME=App 24 | APP_URL=http://app.test 25 | DB_DATABASE=app 26 | MAIL_USERNAME=mailtrap_username 27 | MAIL_PASSWORD=mailtrap_password 28 | MAIL_FROM_ADDRESS=info@app.test 29 | 30 | Require Crudify: 31 | 32 | composer require kejojedi/crudify 33 | 34 | Install Crudify: 35 | 36 | php artisan crudify:install 37 | 38 | Visit your app URL and login using: 39 | 40 | Email: admin@example.com 41 | Password: password 42 | 43 | The `AdminUserSeeder` call can be removed from your `DatabaseSeeder` any time. 44 | 45 | ## Generating CRUD 46 | 47 | Run `crudify:generate` for a new model: 48 | 49 | php artisan crudify:generate Model 50 | 51 | This will generate: 52 | 53 | - Controller 54 | - Datatable 55 | - Form Request 56 | - Model 57 | - Factory 58 | - Migration 59 | - Seeder 60 | - View Files 61 | - Navbar Link 62 | - Routes 63 | 64 | Don't forget to migrate after updating the new migration file. 65 | 66 | **Tip: use the `--force` in order to replace existing generated files e.g. `php artisan crudify:generate Model --force`** 67 | 68 | ## Datatables 69 | 70 | Crudify includes a wrapper for [yajra/laravel-datatables-html](https://github.com/yajra/laravel-datatables-html) to make building datatables nice and declarative. Generated model datatable classes are located in `app\Http\Datatables`. 71 | 72 | Declaring [columns](https://yajrabox.com/docs/laravel-datatables/master/html-builder-column-builder): 73 | 74 | protected function columns() 75 | { 76 | return [ 77 | Column::make('id'), 78 | Column::make('name'), 79 | Column::make('created_at'), 80 | Column::make('updated_at'), 81 | ]; 82 | } 83 | 84 | Different ways of defining default sort order: 85 | 86 | protected $order_by = 'id'; // sorts by id, ascending 87 | protected $order_by = ['created_at', 'desc']; // sorts by created_at, descending 88 | 89 | protected function orderBy() 90 | { 91 | return 'id'; // sorts by id, ascending 92 | } 93 | 94 | protected function orderBy() 95 | { 96 | return ['created_at', 'desc']; // sorts by created_at, descending 97 | } 98 | 99 | **Note: a users per-page entries & sorting preferences are saved per-table in their browser indefinitely, so this will only set the initial default order.** 100 | 101 | Example of adding methods to the datatables html builder: 102 | 103 | protected function htmlMethods(Builder &$html) 104 | { 105 | $html->ajax([ 106 | 'url' => route('users.index'), 107 | 'type' => 'GET', 108 | 'data' => 'function(d) { d.key = "value"; }', 109 | ]); 110 | } 111 | 112 | Example of adding methods to the datatables json builder: 113 | 114 | protected function jsonMethods(DataTableAbstract &$datatables) 115 | { 116 | $datatables->editColumn('name', function(User $user) { 117 | return 'Hi ' . $user->name . '!'; 118 | }); 119 | } 120 | 121 | **Tip: If you don't want a datatable to have an actions column, simply remove the `actions()` method entirely.** 122 | 123 | ## Form Components 124 | 125 | Crudify offers simple form components to make building forms fast & easy. See below for minimal and complete examples of each component. 126 | 127 | Input: 128 | 129 | 130 | 131 | 132 | Textarea: 133 | 134 | 135 | 136 | 137 | Select: 138 | 139 | 140 | 141 | 142 | **Note: if the options are an associative array, the keys are used as the labels and the values as the values. For sequential arrays, the values are used for both the labels and values.** 143 | 144 | File: 145 | 146 | 147 | 148 | 149 | Checkbox: 150 | 151 | 152 | 153 | 154 | **Note: checkbox attributes should have `boolean` migration columns.** 155 | 156 | Checkboxes: 157 | 158 | 159 | 160 | 161 | **Note: checkboxes attributes should be cast to `array` with `text` migration columns.** 162 | 163 | Radios: 164 | 165 | 166 | 167 | 168 | **Tip: you can determine if the fields are showing on the `create` or `edit` page by checking `isset($model)` (e.g. `isset($car)`). If a `$model` is set, it means the user is on the edit page.** 169 | 170 | ## Packages Used 171 | 172 | Composer packages: 173 | 174 | - [barryvdh/laravel-ide-helper](https://github.com/barryvdh/laravel-ide-helper) 175 | - [laravel/ui](https://github.com/laravel/ui) 176 | - [yajra/laravel-datatables-html](https://github.com/yajra/laravel-datatables-html) 177 | - [yajra/laravel-datatables-oracle](https://github.com/yajra/laravel-datatables) 178 | 179 | NPM packages: 180 | 181 | - [@fortawesome/fontawesome-free](https://www.npmjs.com/package/@fortawesome/fontawesome-free) 182 | - [browser-sync](https://www.npmjs.com/package/browser-sync) 183 | - [datatables.net-bs4](https://www.npmjs.com/package/datatables.net-bs4) 184 | - [datatables.net-responsive-bs4](https://www.npmjs.com/package/datatables.net-responsive-bs4) 185 | -------------------------------------------------------------------------------- /resources/css/crudify.css: -------------------------------------------------------------------------------- 1 | .card-body table.dataTable { 2 | margin-left: -1.25rem; 3 | margin-right: -1.25rem; 4 | width: calc(100% + 2.5rem); 5 | } 6 | 7 | table.dataTable tr th { 8 | white-space: nowrap; 9 | } 10 | 11 | div.dataTables_paginate { 12 | overflow-x: auto 13 | } 14 | div.dataTables_paginate .pagination { 15 | display: inline-table 16 | } 17 | div.dataTables_paginate .pagination li { 18 | display: table-cell 19 | } 20 | 21 | @media (min-width: 768px) { 22 | .card-body table.dataTable tr th:first-child, .card-body table.dataTable tr td:first-child { 23 | padding-left: 1.25rem; 24 | } 25 | .card-body table.dataTable tr th:last-child, .card-body table.dataTable tr td:last-child { 26 | padding-right: 1.25rem; 27 | } 28 | table.dataTable tr th, table.dataTable tr td { 29 | vertical-align: middle; 30 | } 31 | } 32 | 33 | @media (max-width: 767.98px) { 34 | table.dataTable > tbody > tr.child span.dtr-title:empty { 35 | display: none; 36 | } 37 | 38 | .col-form-label { 39 | padding-top: 0 !important; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /resources/js/crudify.js: -------------------------------------------------------------------------------- 1 | $.extend(true, $.fn.dataTable.defaults, { 2 | autoWidth: false, 3 | responsive: true, 4 | stateDuration: 0, 5 | stateSave: true, 6 | stateSaveParams: function (settings, data) { 7 | data.search.search = ''; 8 | data.start = 0; 9 | }, 10 | stateLoadCallback: function (settings, callback) { 11 | return JSON.parse(localStorage.getItem($(this).attr('id'))); 12 | }, 13 | stateSaveCallback: function (settings, data) { 14 | localStorage.setItem($(this).attr('id'), JSON.stringify(data)); 15 | } 16 | }); 17 | 18 | $(document).on('input', '.custom-file-input', function () { 19 | let files = []; 20 | 21 | for (let i = 0; i < $(this)[0].files.length; i++) { 22 | files.push($(this)[0].files[i].name); 23 | } 24 | 25 | $(this).next('.custom-file-label').html(files.join(', ')); 26 | }); 27 | -------------------------------------------------------------------------------- /resources/stubs/generate/DummyClass.stub: -------------------------------------------------------------------------------- 1 | middleware('auth'); 15 | } 16 | 17 | public function index(Request $request) 18 | { 19 | $query = DummyClass::query(); 20 | $datatables = DummyClassDatatable::make($query); 21 | 22 | return $request->ajax() 23 | ? $datatables->json() 24 | : view('DummyVars.index', $datatables->html()); 25 | } 26 | 27 | public function create() 28 | { 29 | return view('DummyVars.create'); 30 | } 31 | 32 | public function store(DummyClassRequest $request) 33 | { 34 | DummyClass::create($request->all()); 35 | 36 | return $request->input('submit') == 'reload' 37 | ? redirect()->route('DummyVars.create') 38 | : redirect()->route('DummyVars.index'); 39 | } 40 | 41 | public function show(DummyClass $DummyVar) 42 | { 43 | return view('DummyVars.show', compact('DummyVar')); 44 | } 45 | 46 | public function edit(DummyClass $DummyVar) 47 | { 48 | return view('DummyVars.edit', compact('DummyVar')); 49 | } 50 | 51 | public function update(DummyClassRequest $request, DummyClass $DummyVar) 52 | { 53 | $DummyVar->update($request->all()); 54 | 55 | return $request->input('submit') == 'reload' 56 | ? redirect()->route('DummyVars.edit', $DummyVar->id) 57 | : redirect()->route('DummyVars.index'); 58 | } 59 | 60 | /** @noinspection PhpUnhandledExceptionInspection */ 61 | public function destroy(DummyClass $DummyVar) 62 | { 63 | $DummyVar->delete(); 64 | 65 | return redirect()->route('DummyVars.index'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resources/stubs/generate/DummyClassDatatable.stub: -------------------------------------------------------------------------------- 1 | define(DummyClass::class, function (Faker $faker) { 9 | return [ 10 | 'name' => $faker->firstNameFemale, 11 | ]; 12 | }); 13 | -------------------------------------------------------------------------------- /resources/stubs/generate/DummyClassRequest.stub: -------------------------------------------------------------------------------- 1 | ['required', Rule::unique('DummyVars')->ignore($this->route('DummyVar'))], 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/stubs/generate/DummyClassSeeder.stub: -------------------------------------------------------------------------------- 1 | create(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /resources/stubs/generate/DummyMigration.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('name'); 14 | $table->timestamps(); 15 | }); 16 | } 17 | 18 | public function down() 19 | { 20 | Schema::dropIfExists('DummyVars'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/stubs/generate/navbar-link.stub: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/stubs/generate/routes.stub: -------------------------------------------------------------------------------- 1 | Route::resource('DummyVars', 'DummyClassController'); 2 | -------------------------------------------------------------------------------- /resources/stubs/generate/views/actions.stub: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 |
16 | @csrf 17 | @method('delete') 18 |
19 |
20 | -------------------------------------------------------------------------------- /resources/stubs/generate/views/create.stub: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', __('Create DummyTitle')) 4 | @section('content') 5 |
6 |

@yield('title')

7 | 8 |
9 | @csrf 10 | 11 |
12 | @include('DummyVars.fields') 13 | 14 | 18 |
19 |
20 |
21 | @endsection 22 | -------------------------------------------------------------------------------- /resources/stubs/generate/views/edit.stub: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', __('Edit DummyTitle')) 4 | @section('content') 5 |
6 |
7 |
8 |

@yield('title')

9 |
10 |
11 | @include('DummyVars.actions') 12 |
13 |
14 | 15 |
16 | @csrf 17 | @method('patch') 18 | 19 |
20 | @include('DummyVars.fields') 21 | 22 | 26 |
27 |
28 |
29 | @endsection 30 | -------------------------------------------------------------------------------- /resources/stubs/generate/views/fields.stub: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /resources/stubs/generate/views/index.stub: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', __('DummyTitles')) 4 | @section('content') 5 |
6 |
7 |
8 |

@yield('title')

9 |
10 | 13 |
14 | 15 |
16 |
17 | {!! $html->table() !!} 18 | {!! $html->scripts() !!} 19 |
20 |
21 |
22 | @endsection 23 | -------------------------------------------------------------------------------- /resources/stubs/generate/views/show.stub: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', __('DummyTitle')) 4 | @section('content') 5 |
6 |
7 |
8 |

@yield('title')

9 |
10 |
11 | @include('DummyVars.actions') 12 |
13 |
14 | 15 |
16 |
17 | @foreach($DummyVar->toArray() as $attribute => $value) 18 |
19 |
20 |
21 | {{ Str::title(str_replace('_', ' ', $attribute)) }} 22 |
23 |
24 | @if(is_array($value)) 25 |
{{ json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
26 | @else 27 | {{ $value ?? __('N/A') }} 28 | @endif 29 |
30 |
31 |
32 | @endforeach 33 |
34 |
35 |
36 | @endsection 37 | -------------------------------------------------------------------------------- /resources/stubs/install/browser-sync.stub: -------------------------------------------------------------------------------- 1 | mix.browserSync({ 2 | proxy: 'DummyDomain', 3 | snippetOptions: { 4 | whitelist: ['*'], 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /resources/stubs/install/datatables-script.stub: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', function() { 2 | (function(window,$){window.LaravelDataTables=window.LaravelDataTables||{};window.LaravelDataTables["%1$s"]=$("#%1$s").DataTable(%2$s);})(window,jQuery); 3 | }); 4 | -------------------------------------------------------------------------------- /resources/views/checkbox.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @if($label) 4 | 5 | @endif 6 |
7 |
8 | 9 | 16 | 17 |
18 | 19 | @error($name) {{ $message }} @enderror 20 | @if($hint) {{ $hint }} @endif 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /resources/views/checkboxes.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 | @foreach($options as $option_label => $option_value) 8 |
9 | 16 | 17 |
18 | @endforeach 19 | 20 | @error($name) {{ $message }} @enderror 21 | @if($hint) {{ $hint }} @endif 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /resources/views/file.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 11 | 12 | @error($name) {{ $message }} @enderror 13 | @if($hint) {{ $hint }} @endif 14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /resources/views/input.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 12 | @error($name) {{ $message }} @enderror 13 | @if($hint) {{ $hint }} @endif 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /resources/views/radios.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 | @foreach($options as $option_label => $option_value) 8 |
9 | 16 | 17 |
18 | @endforeach 19 | 20 | @error($name) {{ $message }} @enderror 21 | @if($hint) {{ $hint }} @endif 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /resources/views/select.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 16 | @error($name) {{ $message }} @enderror 17 | @if($hint) {{ $hint }} @endif 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /resources/views/textarea.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 11 | @error($name) {{ $message }} @enderror 12 | @if($hint) {{ $hint }} @endif 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/Commands/GeneratesCrud.php: -------------------------------------------------------------------------------- 1 | setReplaces(); 19 | $this->createPhpFiles(); 20 | $this->createViewFiles(); 21 | $this->insertNavLink(); 22 | $this->insertRoutes(); 23 | 24 | Artisan::call('ide-helper:generate', [], $this->getOutput()); 25 | 26 | $this->info('CRUD generation complete for: ' . $this->argument('model')); 27 | $this->warn("Don't forget to migrate after updating the new migration file."); 28 | } 29 | 30 | private function setReplaces() 31 | { 32 | $title = trim(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $this->argument('model'))); 33 | 34 | $this->replaces = [ 35 | 'DummyTitles' => $titles = Str::plural($title), 36 | 'DummyTitle' => $title, 37 | 'DummyClasses' => str_replace(' ', '', $titles), 38 | 'DummyClass' => $this->argument('model'), 39 | 'DummyVars' => $vars = Str::snake($titles), 40 | 'DummyVar' => Str::snake($title), 41 | 'DummyMigration' => date('Y_m_d_') . '000000_create_' . $vars . '_table', 42 | ]; 43 | } 44 | 45 | private function replace($contents) 46 | { 47 | foreach ($this->replaces as $search => $replace) { 48 | $contents = str_replace($search, $replace, $contents); 49 | } 50 | 51 | return $contents; 52 | } 53 | 54 | private function createPhpFiles() 55 | { 56 | File::ensureDirectoryExists(app_path('Http/Datatables')); 57 | File::ensureDirectoryExists(app_path('Http/Requests')); 58 | 59 | $files = [ 60 | 'DummyClass' => app_path(), 61 | 'DummyClassController' => app_path('Http/Controllers'), 62 | 'DummyClassDatatable' => app_path('Http/Datatables'), 63 | 'DummyClassRequest' => app_path('Http/Requests'), 64 | 'DummyClassFactory' => database_path('factories'), 65 | 'DummyClassSeeder' => database_path('seeds'), 66 | 'DummyMigration' => database_path('migrations'), 67 | ]; 68 | 69 | foreach ($files as $stub => $path) { 70 | $stub_contents = file_get_contents(__DIR__ . '/../../resources/stubs/generate/' . $stub . '.stub'); 71 | $new_file = $path . '/' . $this->replace($stub) . '.php'; 72 | 73 | $this->createFile($new_file, $stub_contents); 74 | } 75 | } 76 | 77 | private function createViewFiles() 78 | { 79 | $view_path = resource_path('views/' . $this->replaces['DummyVars']); 80 | File::ensureDirectoryExists($view_path); 81 | 82 | foreach (File::allFiles(__DIR__ . '/../../resources/stubs/generate/views') as $stub) { 83 | $stub_contents = $this->replace($stub->getContents()); 84 | $new_file = $view_path . '/' . str_replace('.stub', '.blade.php', $stub->getBasename()); 85 | 86 | $this->createFile($new_file, $stub_contents); 87 | } 88 | } 89 | 90 | private function createFile($new_file, $stub_contents) 91 | { 92 | if (!file_exists($new_file) || $this->option('force')) { 93 | file_put_contents($new_file, $this->replace($stub_contents)); 94 | 95 | $this->line('Created new file: ' . $new_file); 96 | } 97 | else { 98 | $this->info('File already exists: ' . $new_file); 99 | } 100 | } 101 | 102 | private function insertNavLink() 103 | { 104 | $stub_contents = $this->replace(rtrim(file_get_contents(__DIR__ . '/../../resources/stubs/generate/navbar-link.stub'))); 105 | $nav_file = resource_path('views/layouts/app.blade.php'); 106 | 107 | if (file_exists($nav_file)) { 108 | $nav_contents = file_get_contents($nav_file); 109 | $nav_hook = '