├── composer.json ├── readme.md └── src ├── config └── vueApi.php ├── generate.php ├── templates ├── controller.blade.php ├── model.blade.php ├── vue-list.blade.php └── vue-single.blade.php └── vueApiServiceProvider.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lummy/laravel-vue-api-crud-generator", 3 | "description": "Creates a basic skeleton for a CRUD app in both Laravel & Vue.js single file components.", 4 | "authors": [ 5 | { 6 | "name": "Aaron Lumsden", 7 | "email": "aaronlumsden@me.com" 8 | } 9 | ], 10 | "license": "MIT", 11 | "require": {}, 12 | "autoload": { 13 | "psr-4": { 14 | "lummy\\": "src/" 15 | } 16 | }, 17 | "extra": { 18 | "laravel": { 19 | "providers": [ 20 | "lummy\\vueApi\\vueApiServiceProvider" 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Laravel Vue API Crud Generator 2 | 3 | #### Overview 4 | A Laravel package that lets you generate boilerplate code for a Vue.js/Laravel app. Simply enter the name of a database table and based on that it will create: 5 | 6 | - A Laravel model 7 | - A Laravel controller (with get, list, create, update, delete as well as validation based on a chosen DB table) 8 | - Laravel routes (get, list, create, update, delete) 9 | - 2 Vue.js single file components to create, update, list, delete and show (using Vform & axios) 10 | 11 | This package aims to speed up the process of communicating between backend (Laravel) and frontend (Vue.js). 12 | 13 | ### Installation 14 | `composer require lummy/laravel-vue-api-crud-generator` 15 | 16 | 17 | ## Usage 18 | 19 | Firstly you should create a new migration in the same way that you usually would. For example if creating a posts table use the command 20 | 21 | `php artisan make:migration create_posts_table` 22 | 23 | Then in your migration file add your fields as usual 24 | 25 | ``` 26 | Schema::create('posts', function (Blueprint $table) { 27 | $table->id(); 28 | $table->string('title',200); 29 | $table->text('content')->nullable(); 30 | $table->timestamps(); 31 | }); 32 | ``` 33 | 34 | Then run the migrate command to create the posts table 35 | 36 | `php artisan migrate` 37 | 38 | Once you have done that you just need to run one `vueapi` command. Add the name of your table to the end of the command so in this case it's posts. 39 | 40 | `php artisan vueapi:generate posts` 41 | 42 | This will then generate all the files mentioned above. 43 | 44 | Once you have run this command, using the `posts` example above, it will create the following boilerplate files: 45 | 46 | ### Routes 47 | 48 | Based on a `posts` DB table it will produce these routes 49 | 50 | ``` 51 | Route::get('posts', 'PostsController@list'); 52 | Route::get('posts/{id}', 'PostsController@get'); 53 | Route::post('posts', 'PostsController@create'); 54 | Route::put('posts/{id}', 'PostsController@update'); 55 | Route::delete('posts/{id}', 'PostsController@delete'); 56 | 57 | ``` 58 | 59 | ### Controller 60 | 61 | Based on a `posts` DB table it will produce this controller 62 | 63 | ``` 64 | validate([ 83 | 'title' => 'required |max:200 ', 84 | 'content' => 'required ', 85 | 'meta_description' => 'required |max:160 ', 86 | ],[ 87 | 'title.required' => 'title is a required field.', 88 | 'title.max' => 'title can only be 200 characters.', 89 | 'content.required' => 'content is a required field.', 90 | 'meta_description.required' => 'meta_description is a required field.', 91 | 'meta_description.max' => 'meta_description can only be 160 characters.', 92 | ]); 93 | 94 | $posts = Posts::create($request->all()); 95 | return $posts; 96 | } 97 | 98 | public function update(Request $request, $id){ 99 | 100 | $validatedData = $request->validate([ 101 | 'title' => 'required |max:200 ', 102 | 'content' => 'required ', 103 | 'meta_description' => 'required |max:160 ', 104 | ],[ 105 | 'title.required' => 'title is a required field.', 106 | 'title.max' => 'title can only be 200 characters.', 107 | 'content.required' => 'content is a required field.', 108 | 'meta_description.required' => 'meta_description is a required field.', 109 | 'meta_description.max' => 'meta_description can only be 160 characters.', 110 | ]); 111 | 112 | $posts = Posts::findOrFail($id); 113 | $input = $request->all(); 114 | $posts->fill($input)->save(); 115 | return $posts; 116 | } 117 | 118 | public function delete(Request $request, $id){ 119 | $posts = Posts::findOrFail($id); 120 | $posts->delete(); 121 | } 122 | } 123 | ?> 124 | 125 | ``` 126 | ### Model 127 | 128 | Based on a `posts` DB table it will produce this model 129 | 130 | ``` 131 | 157 | 158 | ``` 159 | 160 | ### Vue (List template) 161 | 162 | Based on a `posts` DB table it will produce this Vue.js list single file component (Posts-list.vue) 163 | 164 | ``` 165 | 234 | 235 | 284 | 285 | 340 | ``` 341 | 342 | ### Vue (Single template) 343 | 344 | Based on a `posts` DB table it will produce this Vue.js single file component (Posts-single.vue) 345 | 346 | ``` 347 | 392 | 393 | 449 | 450 | 498 | ``` 499 | 500 | ## Configuration 501 | 502 | Here are the configuration settings with their default values. 503 | 504 | ``` 505 | base_path('app'), 508 | 'controller_dir' => base_path('app/Http/Controllers'), 509 | 'vue_files_dir' => base_path('resources/views/vue'), 510 | 'routes_dir' => base_path('routes'), 511 | 'routes_file' => 'api.php' 512 | ]; 513 | ?> 514 | ``` 515 | To copy the config file to your working Laravel project enter the following artisan command 516 | 517 | `php artisan vendor:publish --provider="lummy\vueApi\vueApiServiceProvider" --tag="config"` 518 | 519 | ##### model_dir 520 | Specifies the location where the generated model files should be stored 521 | 522 | #### controller_dir 523 | 524 | Specifies the location where the generated controller files should be stored 525 | 526 | #### vue_files_dir 527 | 528 | Specifies the location where the Vue single file templates should be stored 529 | 530 | #### vue_url_prefix 531 | Specifies what prefix should be added to the URL in your view files. The default is `/api` ie `/api/posts` 532 | 533 | #### routes_dir 534 | Specifies the location of the routes directory 535 | 536 | #### routes_file 537 | Specifies the name of the routes file 538 | 539 | ### Customising the templates 540 | 541 | If you use another frontend framework such as React or you want to adjust the structure of the templates then you can customise the templates by publishing them to your working Laravel project 542 | 543 | `php artisan vendor:publish --provider="lummy\vueApi\vueApiServiceProvider" --tag="templates"`` 544 | 545 | They will then appear in 546 | 547 | `\resources\views\vendor\vueApi` 548 | 549 | 550 | ### Variables in the templates 551 | 552 | Each template file passes a data array with the following fields 553 | 554 | ##### $data['singular'] 555 | The singular name for the DB table eg Post 556 | 557 | ##### $data['plural'] 558 | The plural name for the DB table eg Posts 559 | 560 | ##### $data['singular_lower'] 561 | The singular name for the DB table (lowercase) eg post 562 | 563 | ##### $data['plural_lower'] 564 | The plural name for the DB table eg (lowercase) eg posts 565 | 566 | ##### $data['fields'] 567 | An array of the fields that are part of the model. 568 | 569 | - name (the field name) 570 | - type (the mysql varchar, int etc) 571 | - simplified_type (text, textarea, number) 572 | - required (is the field required) 573 | - max (the maximum number of characters) 574 | 575 | ### Other things to note 576 | 577 | I have only tested this on Laravel MYSQL driver so I'm not sure if it will work on other databases. 578 | 579 | In Vue.js files the routes are presumed to be: using the posts example. You can easily configure these from the templates generated 580 | 581 | /posts (Posts-list.vue) 582 | /posts/{id} (Posts-single.vue) 583 | 584 | Please feel free to contact me with any feedback or suggestions https://github.com/aarondo 585 | 586 | 587 | 588 | 589 | 590 | -------------------------------------------------------------------------------- /src/config/vueApi.php: -------------------------------------------------------------------------------- 1 | base_path('app'), 4 | 'controller_dir' => base_path('app/Http/Controllers'), 5 | 'vue_files_dir' => base_path('resources/views/vue'), 6 | 'vue_url_prefix' => '/api', 7 | 'routes_dir' => base_path('routes'), 8 | 'routes_file' => 'api.php' 9 | ]; 10 | ?> -------------------------------------------------------------------------------- /src/generate.php: -------------------------------------------------------------------------------- 1 | config('vueApi.model_dir')]); 42 | 43 | // Check if file already exists. If it does ask if we want to overwrite 44 | if ($client->exists($data['singular'])) { 45 | if (!$this->confirm($data['singular'].' model already exists. Would you like to overwrite this model?')){ 46 | return false; 47 | } 48 | } 49 | 50 | // Create the file 51 | $modelTemplate = view::make('vueApi::model',['data' => $data])->render(); 52 | $modelTemplate = ""; 53 | $client->put($data['plural'].'.php', $modelTemplate ); 54 | 55 | return; 56 | 57 | } 58 | 59 | 60 | 61 | public function createController($data){ 62 | 63 | $client = Storage::createLocalDriver(['root' => config('vueApi.controller_dir')]); 64 | 65 | // Check if file already exists. If it does ask if we want to overwrite 66 | if ($client->exists($data['plural'].'Controller.php')) { 67 | if (!$this->confirm($data['plural'].'Controller.php already exists. Would you like to overwrite this controller?')){ 68 | return false; 69 | } 70 | } 71 | 72 | 73 | // Create the file 74 | $controllerTemplate = view::make('vueApi::controller',['data' => $data])->render(); 75 | $controllerTemplate = ""; 76 | $client->put($data['plural'].'Controller.php', $controllerTemplate ); 77 | 78 | return; 79 | 80 | } 81 | 82 | 83 | public function createVueListTemplate($data){ 84 | 85 | $client = Storage::createLocalDriver(['root' => config('vueApi.vue_files_dir')]); 86 | 87 | // Check if file already exists. If it does ask if we want to overwrite 88 | if ($client->exists($data['plural'].'-list.vue')) { 89 | if (!$this->confirm($data['plural'].'-list.vue already exists. Would you like to overwrite this component?')) { 90 | return false; 91 | } 92 | } 93 | 94 | // Create the file 95 | $vueTemplate = view::make('vueApi::vue-list',['data' => $data])->render(); 96 | $client->put($data['plural'].'-list.vue', $vueTemplate ); 97 | 98 | return; 99 | 100 | } 101 | 102 | public function createVueSingleTemplate($data){ 103 | 104 | $client = Storage::createLocalDriver(['root' => config('vueApi.vue_files_dir')]); 105 | 106 | // Check if file already exists. If it does ask if we want to overwrite 107 | if ($client->exists($data['plural'].'-single.vue')) { 108 | if (!$this->confirm($data['plural'].'-single.vue already exists. Would you like to overwrite this component?')) { 109 | return false; 110 | } 111 | } 112 | 113 | // Create the file 114 | $vueTemplate = view::make('vueApi::vue-single',['data' =>$data])->render(); 115 | $client->put($data['plural'].'-single.vue', $vueTemplate ); 116 | 117 | return; 118 | 119 | 120 | } 121 | 122 | public function createRoutes($data){ 123 | 124 | $client = Storage::createLocalDriver(['root' => config('vueApi.routes_dir')]); 125 | 126 | $routes = "\nRoute::get('".$data['plural_lower']."', '".$data['plural']."Controller@list');\n"; 127 | $routes .= "Route::get('".$data['plural_lower']."/{id}', '".$data['plural']."Controller@get');\n"; 128 | $routes .= "Route::post('".$data['plural_lower']."', '".$data['plural']."Controller@create');\n"; 129 | $routes .= "Route::put('".$data['plural_lower']."/{id}', '".$data['plural']."Controller@update');\n"; 130 | $routes .= "Route::delete('".$data['plural_lower']."/{id}', '".$data['plural']."Controller@delete');\n"; 131 | 132 | if ($client->exists(config('vueApi.routes_file'))) { 133 | $routeFile = $client->get('/'.config('vueApi.routes_file')); 134 | $appendedRoutes = $routeFile.$routes; 135 | $client->put(config('vueApi.routes_file'), $appendedRoutes); 136 | } else { 137 | $routeFile = $client->get('/'.config('vueApi.routes_file')); 138 | $client->put(config('vueApi.routes_file'), $routes); 139 | } 140 | 141 | 142 | 143 | 144 | } 145 | 146 | public function getFieldsData($singular, $plural){ 147 | 148 | $data = DB::select('DESCRIBE '.strtolower($plural)); 149 | 150 | 151 | $fieldsArray = array(); 152 | $i = 0; 153 | foreach ($data as $key) { 154 | 155 | // Extract if its required 156 | $required = ($key->Null == 'NO') ? true : false ; 157 | 158 | //Extract the field type 159 | $type = $typeArr = explode("(", $key->Type, 2)[0]; 160 | 161 | //extract the number for the max attribute 162 | preg_match_all('!\d+!', $key->Type, $matches); 163 | 164 | // Setup simplified type arrays 165 | $stringArray = ['char','varchar','tinystring']; 166 | $textAreaArray = ['text','mediumtext','longtext']; 167 | $simplifiedType = 'number'; 168 | 169 | if (in_array($type, $stringArray)) { 170 | $simplifiedType = 'text'; 171 | } else if(in_array($type, $textAreaArray)){ 172 | $simplifiedType = 'textarea'; 173 | } 174 | 175 | $fieldsArray[$i]['name'] = $key->Field; 176 | $fieldsArray[$i]['type'] = $type; 177 | $fieldsArray[$i]['simplified_type'] = $simplifiedType; 178 | $fieldsArray[$i]['required'] = $required; 179 | $fieldsArray[$i]['max'] = (isset($matches[0][0])) ? (int)$matches[0][0] : false; 180 | 181 | $i++; 182 | }; 183 | 184 | return array( 185 | 'singular' => $singular, 186 | 'plural' => $plural, 187 | 'singular_lower' => strtolower($singular), 188 | 'plural_lower' => strtolower($plural), 189 | 'fields' => $fieldsArray 190 | ); 191 | 192 | 193 | 194 | } 195 | 196 | public function createFormData($plural){ 197 | 198 | $validatorArray = array(); 199 | $vform = ''; 200 | $fieldsArray = array(); 201 | 202 | $data = DB::select('DESCRIBE '.strtolower($plural)); 203 | 204 | foreach ($data as $key) { 205 | 206 | 207 | 208 | $vform .= "\t\t\t\t\t
\n"; 209 | $vform .= "\t\t\t\t\t\t\n"; 210 | 211 | $thisValidations = array(); 212 | ($key->Null == 'NO') ? '' : array_push($thisValidations,'required'); 213 | 214 | preg_match_all('!\d+!', $key->Type, $matches); 215 | 216 | 217 | 218 | $lengthValue = (isset($matches[0][0])) ? 'max:'.array_push($thisValidations,'max:'.$matches[0][0]) : ''; 219 | 220 | $inputLength = (isset($matches[0][0])) ? "maxlength='".$matches[0][0]."'" : ''; 221 | 222 | $fieldsArray[$key->Field] = ''; 223 | 224 | 225 | if ($thisValidations && $key->Field !== 'id' && $key->Field !== 'created_at' && $key->Field !== 'updated_at') { 226 | $validatorArray[0][$key->Field] = implode('|',$thisValidations); 227 | 228 | if ($key->Null == 'YES') { 229 | $validatorArray[1][$key->Field.'.required'] = 'Please ensure you have filled in '.$key->Field; 230 | } 231 | } 232 | 233 | $numericArray = ['tinyiny','smallint','mediumint','int','bigint','decimal','float','double','bit']; 234 | 235 | $typeArr = explode("(", $key->Type, 2); 236 | $type = $typeArr[0]; 237 | 238 | if (in_array($type,$numericArray)) { 239 | $vform.="\t\t\t\t\t\t\n"; 240 | } 241 | 242 | 243 | $stringArray = ['char','varchar','tinystring']; 244 | 245 | 246 | if (in_array($type,$stringArray)) { 247 | $vform.="\t\t\t\t\t\t\n"; 248 | } 249 | 250 | $textAreaArray = ['text','mediumtext','longtext']; 251 | 252 | 253 | if (in_array($type,$textAreaArray)) { 254 | 255 | $vform.="\t\t\t\t\t\t\n"; 256 | } 257 | 258 | $dateArray = ['date','timestamp','date','datetime','year']; 259 | 260 | if (in_array($type,$dateArray)) { 261 | $vform.="\t\t\t\t\t\t\n"; 262 | } 263 | 264 | $enumArray = ['enum']; 265 | 266 | if ($type == 'enum') { 267 | $values = str_replace("'",'',str_replace(')','',str_replace('enum(','',$key->Type))); 268 | $valuesAr = explode(',',$values); 269 | $vform.="\t\t\t\t\t\t\n"; 275 | } 276 | 277 | if ($thisValidations) { 278 | $vform.="\t\t\t\t\t\t\n"; 279 | } 280 | 281 | 282 | $vform .= "\t\t\t\t\t
\n\n"; 283 | 284 | } 285 | 286 | 287 | 288 | 289 | //$validatorArray 290 | //$vform 291 | //$fieldsArray 292 | 293 | return ['htmlForm'=>$vform,'validator' => $validatorArray,'fields'=>$fieldsArray]; 294 | 295 | 296 | } 297 | 298 | 299 | 300 | 301 | /** 302 | * Execute the console command. 303 | * 304 | * @return mixed 305 | */ 306 | public function handle() 307 | { 308 | 309 | 310 | 311 | $singular = strtolower(Str::camel($this->argument('model'))); 312 | $singular = Ucfirst(Str::singular($singular)); 313 | $plural = Ucfirst(Str::plural($singular)); 314 | 315 | $data = $this->getFieldsData($singular, $plural); 316 | 317 | 318 | $this->createRoutes($data); 319 | $this->createModel($data); 320 | $this->createController($data); 321 | $this->createVueListTemplate($data); 322 | $this->createVueSingleTemplate($data); 323 | 324 | return $this->info('Created '.$singular.'Controller.php, '.$singular.'.vue and the routes in '.config('vueApi.routes_file')); 325 | 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/templates/controller.blade.php: -------------------------------------------------------------------------------- 1 | namespace App\Http\Controllers; 2 | 3 | use Illuminate\Http\Request; 4 | use App\{{ $data['plural'] }}; 5 | 6 | class {{ $data['plural'] }}Controller extends Controller 7 | { 8 | public function get(Request $request, $id){ 9 | return {{ $data['plural'] }}::findOrFail($id); 10 | } 11 | 12 | public function list(Request $request){ 13 | return {{ $data['plural'] }}::get(); 14 | } 15 | 16 | public function create(Request $request){ 17 | 18 | $validatedData = $request->validate([ 19 | @foreach($data['fields'] as $field) 20 | @if($field['required'] && $field['name'] !== 'id') 21 | '{{ $field['name'] }}' => 'required @if($field['max'])|max:{{$field['max']}} @endif', 22 | @endif 23 | @endforeach 24 | ],[ 25 | @foreach($data['fields'] as $field) 26 | @if($field['required'] && $field['name'] !== 'id') 27 | '{{ $field['name'] }}.required' => '{{ $field['name'] }} is a required field.', 28 | @if($field['max']) 29 | '{{ $field['name'] }}.max' => '{{ $field['name'] }} can only be {{$field['max']}} characters.', 30 | @endif 31 | @endif 32 | @endforeach 33 | ]); 34 | 35 | ${{ $data['plural_lower'] }} = {{ $data['plural'] }}::create($request->all()); 36 | return ${{ $data['plural_lower'] }}; 37 | } 38 | 39 | public function update(Request $request, $id){ 40 | 41 | $validatedData = $request->validate([ 42 | @foreach($data['fields'] as $field) 43 | @if($field['required'] && $field['name'] !== 'id') 44 | '{{ $field['name'] }}' => 'required @if($field['max'])|max:{{$field['max']}} @endif', 45 | @endif 46 | @endforeach 47 | ],[ 48 | @foreach($data['fields'] as $field) 49 | @if($field['required'] && $field['name'] !== 'id') 50 | '{{ $field['name'] }}.required' => '{{ $field['name'] }} is a required field.', 51 | @if($field['max']) 52 | '{{ $field['name'] }}.max' => '{{ $field['name'] }} can only be {{$field['max']}} characters.', 53 | @endif 54 | @endif 55 | @endforeach 56 | ]); 57 | 58 | ${{ $data['plural_lower'] }} = {{ $data['plural'] }}::findOrFail($id); 59 | $input = $request->all(); 60 | ${{ $data['plural_lower'] }}->fill($input)->save(); 61 | return ${{ $data['plural_lower'] }}; 62 | } 63 | 64 | public function delete(Request $request, $id){ 65 | ${{$data['plural_lower']}} = {{$data['plural']}}::findOrFail($id); 66 | ${{$data['plural_lower']}}->delete(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/templates/model.blade.php: -------------------------------------------------------------------------------- 1 | namespace App; 2 | 3 | use Illuminate\Database\Eloquent\Model; 4 | 5 | class {{ $data['plural'] }} extends Model 6 | { 7 | 8 | /** 9 | * The attributes that should be hidden for arrays. 10 | * 11 | * @var array 12 | */ 13 | protected $guarded = [ 14 | 'id' 15 | ]; 16 | 17 | /** 18 | * The attributes that should be cast to native types. 19 | * 20 | * @var array 21 | */ 22 | protected $casts = [ 23 | '' 24 | ]; 25 | } -------------------------------------------------------------------------------- /src/templates/vue-list.blade.php: -------------------------------------------------------------------------------- 1 | 68 | 69 | 115 | 116 | -------------------------------------------------------------------------------- /src/templates/vue-single.blade.php: -------------------------------------------------------------------------------- 1 | 44 | 45 | 97 | 98 | -------------------------------------------------------------------------------- /src/vueApiServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 23 | __DIR__ . '/config/vueApi.php', 'vueApi' 24 | ); 25 | 26 | $this->commands($this->commands); 27 | } 28 | 29 | /** 30 | * Bootstrap services. 31 | * 32 | * @return void 33 | */ 34 | public function boot() 35 | { 36 | 37 | 38 | $this->loadViewsFrom(__DIR__.'/templates', 'vueApi'); 39 | 40 | $this->publishes([ 41 | __DIR__ . '/config/vueApi.php' => config_path('vueApi.php'), 42 | ], 'config'); 43 | 44 | 45 | 46 | $this->publishes([ 47 | __DIR__.'/templates' => resource_path('views/vendor/vueApi'), 48 | ],'templates'); 49 | 50 | } 51 | 52 | 53 | 54 | } 55 | --------------------------------------------------------------------------------