├── .github └── FUNDING.yml ├── .gitignore ├── Config └── laravel_auto_crud.php ├── LICENSE ├── README.md ├── composer.json ├── images ├── command.png ├── laravel-auto-crud.png └── resources_views.png ├── phpunit.xml ├── src ├── Builders │ ├── BaseBuilder.php │ ├── ControllerBuilder.php │ ├── DocumentationBuilders │ │ ├── CURLBuilder.php │ │ ├── PostmanBuilder.php │ │ └── SwaggerAPIBuilder.php │ ├── EnumBuilder.php │ ├── RepositoryBuilder.php │ ├── RequestBuilder.php │ ├── ResourceBuilder.php │ ├── RouteBuilder.php │ ├── ServiceBuilder.php │ ├── SpatieDataBuilder.php │ └── ViewBuilder.php ├── Console │ └── Commands │ │ └── GenerateAutoCrudCommand.php ├── LaravelAutoCrudServiceProvider.php ├── Services │ ├── CRUDGenerator.php │ ├── DatabaseValidatorService.php │ ├── DocumentationGenerator.php │ ├── FileService.php │ ├── HelperService.php │ ├── ModelService.php │ └── TableColumnsService.php ├── Stubs │ ├── api.controller.stub │ ├── api_repository.controller.stub │ ├── api_repository_spatie_data.controller.stub │ ├── api_spatie_data.controller.stub │ ├── enum.stub │ ├── repository.stub │ ├── request.stub │ ├── resource.stub │ ├── service.stub │ ├── spatie_data.stub │ ├── web.controller.stub │ ├── web_repository.controller.stub │ ├── web_repository_spatie_data.controller.stub │ └── web_spatie_data.controller.stub ├── Traits │ └── TableColumnsTrait.php └── Transformers │ ├── EnumTransformer.php │ └── SpatieDataTransformer.php └── tests ├── Models └── User.php ├── Pest.php ├── TestCase.php └── Unit ├── DatabaseValidatorServiceTest.php ├── EnumTransformerTest.php ├── FileServiceTest.php ├── HelperServiceTest.php ├── LaravelAutoCrudServiceProviderTest.php ├── ModelServiceTest.php ├── SpatieDataTransformerTest.php └── TableColumnsServiceTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mrmarchone] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: mrmarchone 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea 3 | composer.lock 4 | .phpunit.cache 5 | -------------------------------------------------------------------------------- /Config/laravel_auto_crud.php: -------------------------------------------------------------------------------- 1 | 'app/Models/', 15 | ]; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Abdelrahman Muhammed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Auto CRUD Generator 2 | 3 | ![Laravel Auto CRUD](images/laravel-auto-crud.png) 4 | 5 | ![Total Downloads](https://img.shields.io/packagist/dt/mrmarchone/laravel-auto-crud) 6 | ![Monthly Downloads](https://img.shields.io/packagist/dm/mrmarchone/laravel-auto-crud) 7 | ![Daily Downloads](https://img.shields.io/packagist/dd/mrmarchone/laravel-auto-crud) 8 | ![Packagist Stars](https://img.shields.io/packagist/stars/mrmarchone/laravel-auto-crud) 9 | 10 | Laravel Auto CRUD Generator is a package that simplifies CRUD (Create, Read, Update, Delete) operations for your Laravel application. With a single command, you can generate all necessary files and logic for a selected model, reducing development time and effort. 11 | 12 | [Watch the Video on YouTube](https://www.youtube.com/watch?v=6IqRc3OgUIM) 13 | 14 | ## Features 15 | - Automatically detects models in the app/Models folder. 16 | - Provides an interactive CLI to select a model. 17 | - Generates controller, request validation, routes, views, and more. 18 | - Follows Laravel's best practices for clean and maintainable code. 19 | 20 | ## Installation 21 | 22 | You can install the package via Composer: 23 | 24 | ```bash 25 | composer require mrmarchone/laravel-auto-crud --dev 26 | ``` 27 | 28 | ## Publish Configuration 29 | 30 | You can publish the configuration file via: 31 | 32 | ```bash 33 | php artisan vendor:publish --provider="Mrmarchone\LaravelAutoCrud\LaravelAutoCrudServiceProvider" --tag="auto-crud-config" 34 | ``` 35 | 36 | 37 | ## Usage 38 | 39 | To generate CRUD operations for a model, use the following Artisan command: 40 | 41 | ```bash 42 | php artisan auto-crud:generate -h 43 | ``` 44 | 45 | ```textmate 46 | Description: 47 | A command to create auto CRUD for your models. 48 | 49 | Usage: 50 | auto-crud:generate [options] 51 | 52 | Options: 53 | -A, --all Force generate all possible files without overwrite option. 54 | -M, --model[=MODEL] Select one or more of your models. (multiple values allowed) 55 | -T, --type[=TYPE] Select api, web or both. (multiple values allowed) 56 | -R, --repository Working with repository design pattern. 57 | -O, --overwrite Overwrite the files if already exists. 58 | -P, --pattern[=PATTERN] Supports Spatie-Data Pattern. [default: "normal"] 59 | -C, --curl Generate CURL Requests for API. 60 | -S, --swagger-api Generate Swagger API json for API. 61 | -h, --help Display help for the given command. When no command is given display help for the list command 62 | --silent Do not output any message 63 | -q, --quiet Only errors are displayed. All other output is suppressed 64 | -V, --version Display this application version 65 | --ansi|--no-ansi Force (or disable --no-ansi) ANSI output 66 | -n, --no-interaction Do not ask any interactive question 67 | --env[=ENV] The environment the command should run under 68 | -FA, --force-all Force generate all possible files with overwrite option. 69 | -MP, --model-path[=MODEL-PATH] Set models path. 70 | -PM, --postman Generate Postman Collection for API. 71 | -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 72 | ``` 73 | 74 | > ⚠️ **Warning** 75 | > Take care when you create a models outside app directory, because command will generate the folder structure in all folders like Controllers, Resources, Requests, etc. 76 | > Example (Models Path) => AnotherModels/Models , this will generate files like this one app/Http/Controllers/AnotherModels/Models/ModelController.php 77 | 78 | ### Example: 79 | 80 | ```bash 81 | php artisan auto-crud:generate --model-path=app/AnotherModels --model=User --model=Manager --overwrite --type=api --repository --pattern=spatie-data --curl --postman 82 | ``` 83 | 84 | ![Views](images/command.png) 85 | 86 | This will generate: 87 | - API Controller: 88 | ```php 89 | paginate(10)); 105 | } 106 | 107 | public function store(UserRequest $request): UserResource|\Illuminate\Http\JsonResponse 108 | { 109 | try { 110 | $user = User::create($request->validated()); 111 | return new UserResource($user); 112 | } catch (\Exception $exception) { 113 | report($exception); 114 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 115 | } 116 | } 117 | 118 | public function show(User $user): UserResource 119 | { 120 | return UserResource::make($user); 121 | } 122 | 123 | public function update(UserRequest $request, User $user): UserResource|\Illuminate\Http\JsonResponse 124 | { 125 | try { 126 | $user->update($request->validated()); 127 | return new UserResource($user); 128 | } catch (\Exception $exception) { 129 | report($exception); 130 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 131 | } 132 | } 133 | 134 | public function destroy(User $user): \Illuminate\Http\JsonResponse 135 | { 136 | try { 137 | $user->delete(); 138 | return response()->json(['message' => 'Deleted successfully'], Response::HTTP_NO_CONTENT); 139 | } catch (\Exception $exception) { 140 | report($exception); 141 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 142 | } 143 | } 144 | } 145 | ``` 146 | - API Controller with Spatie Data (if applicable): 147 | ```php 148 | paginate(10)); 162 | } 163 | 164 | public function store(UserData $data): UserData|\Illuminate\Http\JsonResponse 165 | { 166 | try { 167 | $user = User::create($data->all()); 168 | return new UserData($user); 169 | } catch (\Exception $exception) { 170 | report($exception); 171 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 172 | } 173 | } 174 | 175 | public function show(User $user): UserData 176 | { 177 | return UserData::from($user); 178 | } 179 | 180 | public function update(UserData $data, User $user): UserData|\Illuminate\Http\JsonResponse 181 | { 182 | try { 183 | $user->update($data->all()); 184 | return UserData::from($user); 185 | } catch (\Exception $exception) { 186 | report($exception); 187 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 188 | } 189 | } 190 | 191 | public function destroy(User $user): \Illuminate\Http\JsonResponse 192 | { 193 | try { 194 | $user->delete(); 195 | return response()->json(['message' => 'Deleted successfully'], Response::HTTP_OK); 196 | } catch (\Exception $exception) { 197 | report($exception); 198 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 199 | } 200 | } 201 | } 202 | ``` 203 | 204 | - API Controller with Service (if applicable): 205 | ```php 206 | userService = $userService; 233 | } 234 | 235 | public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection 236 | { 237 | return UserResource::collection($this->userService->getAll()); 238 | } 239 | 240 | public function store(UserRequest $request): UserResource|\Illuminate\Http\JsonResponse 241 | { 242 | try { 243 | return new UserResource($this->userService->save($request->validated())); 244 | } catch (\Exception $exception) { 245 | report($exception); 246 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 247 | } 248 | } 249 | 250 | public function show(int $id): UserResource 251 | { 252 | return UserResource::make($this->userService->getById($id)); 253 | } 254 | 255 | public function update(UserRequest $request, int $id): UserResource|\Illuminate\Http\JsonResponse 256 | { 257 | try { 258 | return new UserResource($this->userService->update($request->validated(), $id)); 259 | } catch (\Exception $exception) { 260 | report($exception); 261 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 262 | } 263 | } 264 | 265 | public function destroy(int $id): \Illuminate\Http\JsonResponse 266 | { 267 | try { 268 | $this->userService->deleteById($id); 269 | return response()->json(['message' => 'Deleted successfully'], Response::HTTP_OK); 270 | } catch (\Exception $exception) { 271 | report($exception); 272 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 273 | } 274 | } 275 | } 276 | ``` 277 | 278 | - Web Controller: 279 | ```php 280 | paginate(10); 293 | return view('users.index', compact('users')); 294 | } 295 | 296 | public function create(): \Illuminate\Contracts\View\View 297 | { 298 | return view('users.create'); 299 | } 300 | 301 | public function store(UserRequest $request): \Illuminate\Http\RedirectResponse 302 | { 303 | User::create($request->validated()); 304 | return redirect()->route('users.index')->with('success', 'Created successfully'); 305 | } 306 | 307 | public function show(User $user): \Illuminate\Contracts\View\View 308 | { 309 | return view('users.show', compact('user')); 310 | } 311 | 312 | public function edit(User $user): \Illuminate\Contracts\View\View 313 | { 314 | return view('users.edit', compact('user')); 315 | } 316 | 317 | public function update(UserRequest $request, User $user): \Illuminate\Http\RedirectResponse 318 | { 319 | $user->update($request->validated()); 320 | return redirect()->route('users.index')->with('success', 'Updated successfully'); 321 | } 322 | 323 | public function destroy(User $user): \Illuminate\Http\RedirectResponse 324 | { 325 | $user->delete(); 326 | return redirect()->route('users.index')->with('success', 'Deleted successfully'); 327 | } 328 | } 329 | ``` 330 | - Web Controller with Spatie Data (if applicable): 331 | ```php 332 | paginate(10)); 345 | return view('users.index', compact('users')); 346 | } 347 | 348 | public function create(): \Illuminate\Contracts\View\View 349 | { 350 | return view('users.create'); 351 | } 352 | 353 | public function store(UserData $data): \Illuminate\Http\RedirectResponse 354 | { 355 | User::create($data->all()); 356 | return redirect()->route('users.index')->with('success', 'Created successfully'); 357 | } 358 | 359 | public function show(User $user): \Illuminate\Contracts\View\View 360 | { 361 | return view('users.show', compact('user')); 362 | } 363 | 364 | public function edit(User $user): \Illuminate\Contracts\View\View 365 | { 366 | return view('users.edit', compact('user')); 367 | } 368 | 369 | public function update(UserData $data, User $user): \Illuminate\Http\RedirectResponse 370 | { 371 | $user->update($data->all()); 372 | return redirect()->route('users.index')->with('success', 'Updated successfully'); 373 | } 374 | 375 | public function destroy(User $user): \Illuminate\Http\RedirectResponse 376 | { 377 | $user->delete(); 378 | return redirect()->route('users.index')->with('success', 'Deleted successfully'); 379 | } 380 | } 381 | ``` 382 | 383 | - Web Controller with Service (if applicable): 384 | ```php 385 | userService = $userService; 409 | } 410 | 411 | public function index(): \Illuminate\Contracts\View\View 412 | { 413 | $users = $this->userService->getAll(); 414 | return view('users.index', compact('users')); 415 | } 416 | 417 | public function create(): \Illuminate\Contracts\View\View 418 | { 419 | return view('users.create'); 420 | } 421 | 422 | public function store(UserRequest $request): \Illuminate\Http\RedirectResponse 423 | { 424 | $this->userService->save($request->validated()); 425 | return redirect()->route('users.index')->with('success', 'Created successfully'); 426 | } 427 | 428 | public function show(int $id): \Illuminate\Contracts\View\View 429 | { 430 | $user = $this->userService->getById($id); 431 | return view('users.show', compact('user')); 432 | } 433 | 434 | public function edit(int $id): \Illuminate\Contracts\View\View 435 | { 436 | $user = $this->userService->getById($id); 437 | return view('users.edit', compact('user')); 438 | } 439 | 440 | public function update(UserRequest $request, int $id): \Illuminate\Http\RedirectResponse 441 | { 442 | $this->userService->update($request->validated(), $id); 443 | return redirect()->route('users.index')->with('success', 'Updated successfully'); 444 | } 445 | 446 | public function destroy(int $id): \Illuminate\Http\RedirectResponse 447 | { 448 | $this->userService->deleteById($id); 449 | return redirect()->route('users.index')->with('success', 'Deleted successfully'); 450 | } 451 | } 452 | ``` 453 | 454 | - Request: 455 | ```php 456 | 'required|string|max:255', 473 | 'email' => 'required|string|max:255|unique:users,email', 474 | 'email_verified_at' => 'nullable|date', 475 | 'password' => 'required|string|max:255', 476 | 'remember_token' => 'nullable|string|max:100', 477 | ]; 478 | } 479 | } 480 | ``` 481 | 482 | - Resource: 483 | ```php 484 | $this->id, 497 | 'name' => $this->name, 498 | 'email' => $this->email, 499 | 'email_verified_at' => $this->email_verified_at, 500 | 'password' => $this->password, 501 | 'remember_token' => $this->remember_token, 502 | 'created_at' => $this->created_at, 503 | 'updated_at' => $this->updated_at, 504 | ]; 505 | } 506 | } 507 | ``` 508 | 509 | - API Routes: 510 | ```php 511 | 532 |

Create users

533 |
534 | @csrf 535 |
536 | 537 | 538 | @error("name") 539 |

{{$message}}

540 | @enderror 541 |
542 |
543 | 544 | 545 | @error("email") 546 |

{{$message}}

547 | @enderror 548 |
549 |
550 | 551 | 552 | @error("email_verified_at") 553 |

{{$message}}

554 | @enderror 555 |
556 |
557 | 558 | 559 | @error("password") 560 |

{{$message}}

561 | @enderror 562 |
563 |
564 | 565 | 566 | @error("remember_token") 567 |

{{$message}}

568 | @enderror 569 |
570 | 571 | 572 |
573 | 574 | ``` 575 | - Edit: 576 | ```bladehtml 577 |
578 |

Edit user

579 |
580 | @csrf 581 | @method("PATCH") 582 |
583 | 584 | 585 | @error("name") 586 |

{{$message}}

587 | @enderror 588 |
589 |
590 | 591 | 592 | @error("email") 593 |

{{$message}}

594 | @enderror 595 |
596 |
597 | 598 | 600 | @error("email_verified_at") 601 |

{{$message}}

602 | @enderror 603 |
604 |
605 | 606 | 607 | @error("password") 608 |

{{$message}}

609 | @enderror 610 |
611 |
612 | 613 | 615 | @error("remember_token") 616 |

{{$message}}

617 | @enderror 618 |
619 | 620 | 621 |
622 |
623 | ``` 624 | - Index: 625 | ```bladehtml 626 |
627 |

users List

628 | Create users 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | @foreach ($users as $item) 641 | 642 | 643 | 644 | 645 | 646 | 647 | 657 | 658 | @endforeach 659 | 660 |
nameemailemail_verified_atpasswordremember_token
{{$item->name}}{{$item->email}}{{$item->email_verified_at}}{{$item->password}}{{$item->remember_token}} 648 | Edit 649 |
650 | @csrf 651 | @method('DELETE') 652 | 655 |
656 |
661 |
662 | ``` 663 | - View: 664 | ```bladehtml 665 |
666 |

user Details

667 |

name: {{ $user ->name }}

668 |

email: {{ $user ->email }}

669 |

email_verified_at: {{ $user ->email_verified_at }}

670 |

password: {{ $user ->password }}

671 |

remember_token: {{ $user ->remember_token }}

672 | 673 |
674 | ``` 675 | 676 | - CURL (if applicable): 677 | - You will find it in the laravel-auto-crud folder under the name curl.txt. 678 | ```bash 679 | =====================User===================== 680 | curl --location 'http://127.0.0.1:8000/api/users' \ 681 | --header 'Accept: application/json' \ 682 | --header 'Content-Type: application/json' \ 683 | --request POST \ 684 | --data '{ 685 | "name": "value", 686 | "email": "value", 687 | "email_verified_at": "value", 688 | "password": "value", 689 | "remember_token": "value" 690 | }' 691 | 692 | curl --location 'http://127.0.0.1:8000/api/users/:id' \ 693 | --header 'Accept: application/json' \ 694 | --header 'Content-Type: application/json' \ 695 | --request PATCH \ 696 | --data '{ 697 | "name": "value", 698 | "email": "value", 699 | "email_verified_at": "value", 700 | "password": "value", 701 | "remember_token": "value" 702 | }' 703 | 704 | curl --location 'http://127.0.0.1:8000/api/users/:id' \ 705 | --header 'Accept: application/json' \ 706 | --header 'Content-Type: application/json' \ 707 | --request DELETE 708 | 709 | curl --location 'http://127.0.0.1:8000/api/users' \ 710 | --header 'Accept: application/json' \ 711 | --header 'Content-Type: application/json' \ 712 | --request GET 713 | 714 | curl --location 'http://127.0.0.1:8000/api/users/:id' \ 715 | --header 'Accept: application/json' \ 716 | --header 'Content-Type: application/json' \ 717 | --request GET 718 | 719 | =====================User===================== 720 | ``` 721 | - Postman Collection (if applicable): 722 | - You will find it in the laravel-auto-crud folder under the name postman.json. 723 | - Swagger API V3 Collection (if applicable): 724 | - You will find it in the laravel-auto-crud folder under the name swagger-api.json. 725 | - Repository (if applicable): 726 | ```php 727 | user = $user; 747 | } 748 | 749 | /** 750 | * Get all user. 751 | * 752 | * @return User $user 753 | */ 754 | public function all() 755 | { 756 | return $this->user->get(); 757 | } 758 | 759 | /** 760 | * Get user by id 761 | * 762 | * @param $id 763 | * @return mixed 764 | */ 765 | public function getById(int $id) 766 | { 767 | return $this->user->find($id); 768 | } 769 | 770 | /** 771 | * Save User 772 | * 773 | * @param $data 774 | * @return User 775 | */ 776 | public function save(array $data) 777 | { 778 | return User::create($data); 779 | } 780 | 781 | /** 782 | * Update User 783 | * 784 | * @param $data 785 | * @return User 786 | */ 787 | public function update(array $data, int $id) 788 | { 789 | $user = $this->user->find($id); 790 | $user->update($data); 791 | return $user; 792 | } 793 | 794 | /** 795 | * Delete User 796 | * 797 | * @param $data 798 | * @return User 799 | */ 800 | public function delete(int $id) 801 | { 802 | $user = $this->user->find($id); 803 | $user->delete(); 804 | return $user; 805 | } 806 | } 807 | ``` 808 | 809 | - Service (if applicable): 810 | ```php 811 | userRepository = $userRepository; 835 | } 836 | 837 | /** 838 | * Get all userRepository. 839 | * 840 | * @return String 841 | */ 842 | public function getAll() 843 | { 844 | return $this->userRepository->all(); 845 | } 846 | 847 | /** 848 | * Get userRepository by id. 849 | * 850 | * @param $id 851 | * @return String 852 | */ 853 | public function getById(int $id) 854 | { 855 | return $this->userRepository->getById($id); 856 | } 857 | 858 | /** 859 | * Validate userRepository data. 860 | * Store to DB if there are no errors. 861 | * 862 | * @param array $data 863 | * @return String 864 | */ 865 | public function save(array $data) 866 | { 867 | return $this->userRepository->save($data); 868 | } 869 | 870 | /** 871 | * Update userRepository data 872 | * Store to DB if there are no errors. 873 | * 874 | * @param array $data 875 | * @return String 876 | */ 877 | public function update(array $data, int $id) 878 | { 879 | DB::beginTransaction(); 880 | try { 881 | $userRepository = $this->userRepository->update($data, $id); 882 | DB::commit(); 883 | return $userRepository; 884 | } catch (Exception $e) { 885 | DB::rollBack(); 886 | report($e); 887 | throw new InvalidArgumentException('Unable to update post data'); 888 | } 889 | } 890 | 891 | /** 892 | * Delete userRepository by id. 893 | * 894 | * @param $id 895 | * @return String 896 | */ 897 | public function deleteById(int $id) 898 | { 899 | DB::beginTransaction(); 900 | try { 901 | $userRepository = $this->userRepository->delete($id); 902 | DB::commit(); 903 | return $userRepository; 904 | } catch (Exception $e) { 905 | DB::rollBack(); 906 | report($e); 907 | throw new InvalidArgumentException('Unable to delete post data'); 908 | } 909 | } 910 | 911 | } 912 | ``` 913 | - Spatie Data (if applicable): 914 | ```php 915 | =8.1", 7 | "illuminate/support": "^10.0|^11.0|^12.0", 8 | "laravel/prompts": "*", 9 | "spatie/laravel-data": "*" 10 | }, 11 | "license": "MIT", 12 | "autoload": { 13 | "psr-4": { 14 | "Mrmarchone\\LaravelAutoCrud\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Tests\\": "tests/" 20 | } 21 | }, 22 | "authors": [ 23 | { 24 | "name": "Abdelrahman Muhammed", 25 | "email": "mrmarchone@gmail.com" 26 | } 27 | ], 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "Mrmarchone\\LaravelAutoCrud\\LaravelAutoCrudServiceProvider" 32 | ] 33 | } 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true, 37 | "require-dev": { 38 | "laravel/pint": "*", 39 | "pestphp/pest": "^3.7", 40 | "orchestra/testbench": "^9.9", 41 | "mockery/mockery": "*" 42 | }, 43 | "config": { 44 | "allow-plugins": { 45 | "pestphp/pest-plugin": true 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /images/command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmarchone/laravel-auto-crud/a5fd7f0d1b950f5a7171dccb3e9955376786b8fe/images/command.png -------------------------------------------------------------------------------- /images/laravel-auto-crud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmarchone/laravel-auto-crud/a5fd7f0d1b950f5a7171dccb3e9955376786b8fe/images/laravel-auto-crud.png -------------------------------------------------------------------------------- /images/resources_views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmarchone/laravel-auto-crud/a5fd7f0d1b950f5a7171dccb3e9955376786b8fe/images/resources_views.png -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | ./tests/Unit 16 | 17 | 18 | ./tests/Feature 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | src/ 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Builders/BaseBuilder.php: -------------------------------------------------------------------------------- 1 | fileService = new FileService; 16 | } 17 | 18 | protected function getFullModelNamespace(array $modelData): string 19 | { 20 | return $modelData['namespace'] ? $modelData['namespace'].'\\'.$modelData['modelName'] : $modelData['modelName']; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Builders/ControllerBuilder.php: -------------------------------------------------------------------------------- 1 | fileService->createFromStub($modelData, 'api.controller', 'Http/Controllers/API', 'Controller', $overwrite, function ($modelData) use ($resource, $request) { 15 | $model = $this->getFullModelNamespace($modelData); 16 | $resourceName = explode('\\', $resource); 17 | $requestName = explode('\\', $request); 18 | 19 | return [ 20 | '{{ requestNamespace }}' => $request, 21 | '{{ resourceNamespace }}' => $resource, 22 | '{{ modelNamespace }}' => $model, 23 | '{{ resource }}' => end($resourceName), 24 | '{{ request }}' => end($requestName), 25 | '{{ model }}' => $modelData['modelName'], 26 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 27 | ]; 28 | }); 29 | } 30 | 31 | public function createAPISpatieData(array $modelData, string $spatieData, bool $overwrite = false): string 32 | { 33 | return $this->fileService->createFromStub($modelData, 'api_spatie_data.controller', 'Http/Controllers/API', 'Controller', $overwrite, function ($modelData) use ($spatieData) { 34 | $model = $this->getFullModelNamespace($modelData); 35 | $spatieDataName = explode('\\', $spatieData); 36 | 37 | return [ 38 | '{{ spatieDataNamespace }}' => $spatieData, 39 | '{{ modelNamespace }}' => $model, 40 | '{{ spatieData }}' => end($spatieDataName), 41 | '{{ model }}' => $modelData['modelName'], 42 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 43 | ]; 44 | }); 45 | } 46 | 47 | public function createAPIRepository(array $modelData, string $resource, string $request, string $service, bool $overwrite = false): string 48 | { 49 | return $this->fileService->createFromStub($modelData, 'api_repository.controller', 'Http/Controllers/API', 'Controller', $overwrite, function ($modelData) use ($resource, $request, $service) { 50 | $resourceName = explode('\\', $resource); 51 | $requestName = explode('\\', $request); 52 | $serviceName = explode('\\', $service); 53 | 54 | return [ 55 | '{{ requestNamespace }}' => $request, 56 | '{{ resourceNamespace }}' => $resource, 57 | '{{ resource }}' => end($resourceName), 58 | '{{ request }}' => end($requestName), 59 | '{{ serviceNamespace }}' => $service, 60 | '{{ service }}' => end($serviceName), 61 | '{{ serviceVariable }}' => lcfirst(end($serviceName)), 62 | ]; 63 | }); 64 | } 65 | 66 | public function createAPIRepositorySpatieData(array $modelData, string $spatieData, string $service, bool $overwrite = false): string 67 | { 68 | return $this->fileService->createFromStub($modelData, 'api_repository_spatie_data.controller', 'Http/Controllers/API', 'Controller', $overwrite, function ($modelData) use ($spatieData, $service) { 69 | $spatieDataName = explode('\\', $spatieData); 70 | $serviceName = explode('\\', $service); 71 | 72 | return [ 73 | '{{ spatieDataNamespace }}' => $spatieData, 74 | '{{ spatieData }}' => end($spatieDataName), 75 | '{{ serviceNamespace }}' => $service, 76 | '{{ service }}' => end($serviceName), 77 | '{{ serviceVariable }}' => lcfirst(end($serviceName)), 78 | ]; 79 | }); 80 | } 81 | 82 | public function createWeb(array $modelData, string $request, bool $overwrite = false): string 83 | { 84 | return $this->fileService->createFromStub($modelData, 'web.controller', 'Http/Controllers', 'Controller', $overwrite, function ($modelData) use ($request) { 85 | $model = $this->getFullModelNamespace($modelData); 86 | $requestName = explode('\\', $request); 87 | 88 | return [ 89 | '{{ requestNamespace }}' => $request, 90 | '{{ modelNamespace }}' => $model, 91 | '{{ request }}' => end($requestName), 92 | '{{ model }}' => $modelData['modelName'], 93 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 94 | '{{ viewPath }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 95 | '{{ modelPlural }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 96 | '{{ routeName }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 97 | ]; 98 | }); 99 | } 100 | 101 | public function createWebRepository(array $modelData, string $request, string $service, bool $overwrite = false): string 102 | { 103 | return $this->fileService->createFromStub($modelData, 'web_repository.controller', 'Http/Controllers', 'Controller', $overwrite, function ($modelData) use ($service, $request) { 104 | $model = $this->getFullModelNamespace($modelData); 105 | $serviceName = explode('\\', $service); 106 | $requestName = explode('\\', $request); 107 | 108 | return [ 109 | '{{ requestNamespace }}' => $request, 110 | '{{ request }}' => end($requestName), 111 | '{{ serviceNamespace }}' => $service, 112 | '{{ service }}' => end($serviceName), 113 | '{{ serviceVariable }}' => lcfirst(end($serviceName)), 114 | '{{ modelNamespace }}' => $model, 115 | '{{ model }}' => $modelData['modelName'], 116 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 117 | '{{ viewPath }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 118 | '{{ modelPlural }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 119 | '{{ routeName }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 120 | ]; 121 | }); 122 | } 123 | 124 | public function createWebSpatieData(array $modelData, string $spatieData, bool $overwrite = false): string 125 | { 126 | return $this->fileService->createFromStub($modelData, 'web_spatie_data.controller', 'Http/Controllers', 'Controller', $overwrite, function ($modelData) use ($spatieData) { 127 | $model = $this->getFullModelNamespace($modelData); 128 | $spatieDataName = explode('\\', $spatieData); 129 | 130 | return [ 131 | '{{ spatieDataNamespace }}' => $spatieData, 132 | '{{ modelNamespace }}' => $model, 133 | '{{ spatieData }}' => end($spatieDataName), 134 | '{{ model }}' => $modelData['modelName'], 135 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 136 | '{{ viewPath }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 137 | '{{ modelPlural }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 138 | '{{ routeName }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 139 | ]; 140 | }); 141 | } 142 | 143 | public function createWebRepositorySpatieData(array $modelData, string $spatieData, string $service, bool $overwrite = false): string 144 | { 145 | return $this->fileService->createFromStub($modelData, 'web_repository_spatie_data.controller', 'Http/Controllers', 'Controller', $overwrite, function ($modelData) use ($service, $spatieData) { 146 | $model = $this->getFullModelNamespace($modelData); 147 | $serviceName = explode('\\', $service); 148 | $spatieDataName = explode('\\', $spatieData); 149 | 150 | return [ 151 | '{{ spatieDataNamespace }}' => $spatieData, 152 | '{{ spatieData }}' => end($spatieDataName), 153 | '{{ serviceNamespace }}' => $service, 154 | '{{ service }}' => end($serviceName), 155 | '{{ serviceVariable }}' => lcfirst(end($serviceName)), 156 | '{{ modelNamespace }}' => $model, 157 | '{{ model }}' => $modelData['modelName'], 158 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 159 | '{{ viewPath }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 160 | '{{ modelPlural }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 161 | '{{ routeName }}' => HelperService::toSnakeCase(Str::plural($modelData['modelName'])), 162 | ]; 163 | }); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Builders/DocumentationBuilders/CURLBuilder.php: -------------------------------------------------------------------------------- 1 | modelService = new ModelService; 22 | $this->tableColumnsService = new TableColumnsService; 23 | } 24 | 25 | public function create(array $modelData, bool $overwrite = false): void 26 | { 27 | $laravelAutoCrudPath = base_path('laravel-auto-crud'); 28 | if (! file_exists($laravelAutoCrudPath)) { 29 | mkdir($laravelAutoCrudPath, 0755, true); 30 | } 31 | 32 | $routeBase = sprintf( 33 | config('app.url').'/api/%s', 34 | HelperService::toSnakeCase(Str::plural($modelData['modelName'])) 35 | ); 36 | $endpoints = [ 37 | ['POST', '', $this->getCurlData($modelData)], 38 | ['PATCH', '/:id', $this->getCurlData($modelData)], 39 | ['DELETE', '/:id'], 40 | ['GET', ''], 41 | ['GET', '/:id'], 42 | ]; 43 | $overwrite = $overwrite ? 0 : FILE_APPEND; 44 | file_put_contents($laravelAutoCrudPath.'/curl.txt', "====================={$modelData['modelName']}====================="."\n", $overwrite); 45 | foreach ($endpoints as $endpoint) { 46 | [$method, $path] = $endpoint; 47 | $data = $endpoint[2] ?? []; 48 | $curlCommand = $this->generateCurlCommand($method, $routeBase.$path, $data); 49 | file_put_contents($laravelAutoCrudPath.'/curl.txt', $curlCommand."\n\n", $overwrite); 50 | } 51 | file_put_contents($laravelAutoCrudPath.'/curl.txt', "====================={$modelData['modelName']}====================="."\n", $overwrite); 52 | 53 | info("Updated: $laravelAutoCrudPath/curl.txt"); 54 | } 55 | 56 | private function generateCurlCommand(string $method, string $url, array $data = []): string 57 | { 58 | $method = strtoupper($method); 59 | // Base cURL command 60 | $curlCommand = "curl --location '{$url}' \\\n"; 61 | $curlCommand .= "--header 'Accept: application/json' \\\n"; 62 | $curlCommand .= "--header 'Content-Type: application/json' \\\n"; 63 | // Attach data for methods that require a request body 64 | if (in_array($method, ['POST', 'PATCH'])) { 65 | $jsonData = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 66 | $curlCommand .= "--request {$method} \\\n"; 67 | $curlCommand .= "--data '".$jsonData."'"; 68 | } else { 69 | $curlCommand .= "--request {$method}"; 70 | } 71 | 72 | return $curlCommand; 73 | } 74 | 75 | private function getCurlData(array $modelData): array 76 | { 77 | $columns = $this->getAvailableColumns($modelData); 78 | $data = []; 79 | 80 | foreach ($columns as $column) { 81 | $columnName = $column['name']; 82 | $data[$columnName] = 'value'; 83 | } 84 | 85 | return $data; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Builders/DocumentationBuilders/PostmanBuilder.php: -------------------------------------------------------------------------------- 1 | modelService = new ModelService; 22 | $this->tableColumnsService = new TableColumnsService; 23 | } 24 | 25 | public function create(array $modelData, bool $overwrite = false): void 26 | { 27 | $laravelAutoCrudPath = base_path('laravel-auto-crud'); 28 | if (! file_exists($laravelAutoCrudPath)) { 29 | mkdir($laravelAutoCrudPath, 0755, true); 30 | } 31 | 32 | $oldItems = []; 33 | 34 | if (! $overwrite) { 35 | if (file_exists($laravelAutoCrudPath.'/postman.json')) { 36 | $fileContents = file_get_contents($laravelAutoCrudPath.'/postman.json'); 37 | $fileContents = json_decode($fileContents, true); 38 | $oldItems = $fileContents['item'] ?? []; 39 | } 40 | } 41 | 42 | $oldModels = array_values(array_column($oldItems, 'name')); 43 | 44 | if (count($oldModels)) { 45 | $oldModels = array_combine(range(1, count($oldModels)), $oldModels); 46 | } 47 | 48 | $model = HelperService::toSnakeCase(Str::plural($modelData['modelName'])); 49 | 50 | $routeBase = sprintf( 51 | config('app.url').'/api/%s', 52 | $model 53 | ); 54 | 55 | $parsedUrl = parse_url($routeBase); 56 | 57 | $items = [ 58 | 'name' => ucfirst($model), 59 | ]; 60 | 61 | if (! in_array($items['name'], $oldModels)) { 62 | $data = $this->getColumnsData($modelData); 63 | 64 | $endpoints = [ 65 | ['POST', '', $data, 'Create '.$model], 66 | ['PATCH', '/:id', $data, 'Update '.$model], 67 | ['DELETE', '/:id', [], 'Delete '.$model], 68 | ['GET', '', [], 'Get '.$model], 69 | ['GET', '/:id', [], 'Get single '.$model], 70 | ]; 71 | 72 | foreach ($endpoints as $endpoint) { 73 | [$method, $path] = $endpoint; 74 | $data = $endpoint[2] ?? []; 75 | $items['item'][] = [ 76 | 'name' => $endpoint[3], 77 | 'request' => [ 78 | 'method' => $method, 79 | 'header' => [ 80 | [ 81 | 'key' => 'Accept', 82 | 'value' => 'application/json', 83 | 'type' => 'text', 84 | ], 85 | [ 86 | 'key' => 'Content-Type', 87 | 'value' => 'application/json', 88 | 'type' => 'text', 89 | ], 90 | ], 91 | 'body' => [ 92 | 'mode' => 'raw', 93 | 'raw' => json_encode($data, JSON_PRETTY_PRINT), 94 | 'options' => [ 95 | 'raw' => [ 96 | 'language' => 'json', 97 | ], 98 | ], 99 | ], 100 | 'url' => [ 101 | 'raw' => $routeBase.$path, 102 | 'protocol' => $parsedUrl['scheme'], 103 | 'host' => explode('.', $parsedUrl['host']), 104 | 'port' => $parsedUrl['port'] ?? 80, 105 | 'path' => array_merge(explode('/', substr($parsedUrl['path'], 1)), ! empty($path) ? [substr($path, 1)] : []), 106 | 'variable' => ! empty($path) ? [ 107 | [ 108 | 'key' => 'id', 109 | 'value' => '1', 110 | ], 111 | ] : [], 112 | ], 113 | ], 114 | 'response' => [ 115 | ], 116 | ]; 117 | } 118 | 119 | $oldItems[] = $items; 120 | 121 | $newData = json_encode($this->buildPostmanObject(count($oldItems) ? $oldItems : [$items]), JSON_PRETTY_PRINT); 122 | file_put_contents($laravelAutoCrudPath.'/postman.json', $newData); 123 | } 124 | 125 | info("Updated: $laravelAutoCrudPath/postman.json"); 126 | } 127 | 128 | private function getColumnsData(array $modelData): array 129 | { 130 | $columns = $this->getAvailableColumns($modelData); 131 | $data = []; 132 | 133 | foreach ($columns as $column) { 134 | $columnName = $column['name']; 135 | $data[$columnName] = 'value'; 136 | } 137 | 138 | return $data; 139 | } 140 | 141 | private function buildPostmanObject(array $data): array 142 | { 143 | $appName = config('app.name'); 144 | 145 | return [ 146 | 'info' => [ 147 | 'name' => "Laravel Auto Crud ($appName)", 148 | 'schema' => 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json', 149 | ], 150 | 'item' => [ 151 | ...$data, 152 | ], 153 | ]; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Builders/DocumentationBuilders/SwaggerAPIBuilder.php: -------------------------------------------------------------------------------- 1 | modelService = new ModelService; 22 | $this->tableColumnsService = new TableColumnsService; 23 | } 24 | 25 | public function create(array $modelData, bool $overwrite = false): void 26 | { 27 | $laravelAutoCrudPath = base_path('laravel-auto-crud'); 28 | if (! file_exists($laravelAutoCrudPath)) { 29 | mkdir($laravelAutoCrudPath, 0755, true); 30 | } 31 | 32 | $oldSchemas = $oldPaths = []; 33 | 34 | if (! $overwrite) { 35 | if (file_exists($laravelAutoCrudPath.'/swagger-api.json')) { 36 | $fileContents = file_get_contents($laravelAutoCrudPath.'/swagger-api.json'); 37 | $fileContents = json_decode($fileContents, true); 38 | $oldPaths = $fileContents['paths'] ?? []; 39 | $oldSchemas = $fileContents['components']['schemas'] ?? []; 40 | } 41 | } 42 | 43 | $oldPathsKeys = array_keys($oldPaths); 44 | $oldSchemasKeys = array_keys($oldSchemas); 45 | 46 | $model = HelperService::toSnakeCase(Str::plural($modelData['modelName'])); 47 | 48 | $routeBase = config('app.url').'/api'; 49 | 50 | $items = $schemas = []; 51 | 52 | $data = $this->getColumnsData($modelData); 53 | 54 | $endpoints = [ 55 | ['POST', '', $data, 'Create '.$model], 56 | ['PATCH', '/{id}', $data, 'Update '.$model], 57 | ['DELETE', '/{id}', [], 'Delete '.$model], 58 | ['GET', '', [], 'Get '.$model], 59 | ['GET', '/{id}', [], 'Get single '.$model], 60 | ]; 61 | 62 | $ucFirstModel = ucfirst($model); 63 | 64 | foreach ($endpoints as $endpoint) { 65 | [$method, $parameter] = $endpoint; 66 | $path = '/'.$model.$parameter; 67 | if (in_array($path, $oldPathsKeys)) { 68 | continue; 69 | } 70 | $description = $endpoint[3] ?? ''; 71 | $requestBody = $endpoint[2] ?? []; 72 | $items[$path][strtolower($method)] = [ 73 | 'summary' => $description, 74 | 'operationId' => HelperService::toSnakeCase($description), 75 | 'tags' => [$ucFirstModel], 76 | 'parameters' => ! empty($parameter) ? [ 77 | [ 78 | 'name' => 'id', 79 | 'in' => 'path', 80 | 'required' => true, 81 | 'schema' => [ 82 | 'type' => 'integer', 83 | ], 84 | ]] : [], 85 | 'responses' => [ 86 | '200' => [ 87 | 'description' => 'Successful response', 88 | 'content' => [ 89 | 'application/json' => [ 90 | 'schema' => [ 91 | 'type' => 'array', 92 | 'items' => [ 93 | '$ref' => '#/components/schemas/'.$ucFirstModel, 94 | ], 95 | ], 96 | ], 97 | ], 98 | ], 99 | ], 100 | ]; 101 | if (count($requestBody)) { 102 | $items[$path][strtolower($method)]['requestBody'] = [ 103 | 'required' => true, 104 | 'content' => [ 105 | 'application/json' => [ 106 | 'schema' => [ 107 | '$ref' => '#/components/schemas/'.$ucFirstModel, 108 | ], 109 | ], 110 | ], 111 | ]; 112 | } 113 | } 114 | if (! in_array($ucFirstModel, $oldSchemasKeys)) { 115 | $schemas[$ucFirstModel] = [ 116 | 'type' => 'object', 117 | ]; 118 | 119 | if (count($data)) { 120 | $schemas[$ucFirstModel]['properties'] = collect($data)->map(fn ($item) => collect($item)->filter(fn ($value, $key) => $key !== 'is_required'))->toArray(); 121 | $schemas[$ucFirstModel]['required'] = collect($data)->where('is_required', true)->keys()->toArray(); 122 | } 123 | } 124 | 125 | if (count($items)) { 126 | $oldPaths = array_merge($oldPaths, $items); 127 | } 128 | 129 | if (count($schemas)) { 130 | $oldSchemas = array_merge($oldSchemas, $schemas); 131 | } 132 | 133 | $newData = json_encode($this->buildSwaggerObject($routeBase, $oldPaths, $oldSchemas), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 134 | 135 | file_put_contents($laravelAutoCrudPath.'/swagger-api.json', $newData); 136 | 137 | info("Updated: $laravelAutoCrudPath/swagger-api.json"); 138 | } 139 | 140 | private function getColumnsData(array $modelData): array 141 | { 142 | $columns = $this->getAvailableColumns($modelData); 143 | $data = []; 144 | $is_enum = false; 145 | foreach ($columns as $column) { 146 | $columnName = $column['name']; 147 | switch ($column['type']) { 148 | case 'varchar': 149 | case 'char': 150 | case 'text': 151 | case 'tinytext': 152 | case 'mediumtext': 153 | case 'longtext': 154 | $columnType = 'string'; 155 | $value = 'Example Value'; 156 | break; 157 | 158 | case 'int': 159 | case 'tinyint': 160 | case 'smallint': 161 | case 'mediumint': 162 | case 'bigint': 163 | $columnType = 'integer'; 164 | $value = 1; 165 | break; 166 | 167 | case 'decimal': 168 | case 'numeric': 169 | case 'float': 170 | case 'double': 171 | $columnType = 'number'; 172 | $value = 1.1; 173 | break; 174 | 175 | case 'boolean': 176 | case 'bit': 177 | case 'tinyint(1)': 178 | $columnType = 'boolean'; 179 | $value = true; 180 | break; 181 | 182 | case 'date': 183 | case 'datetime': 184 | case 'timestamp': 185 | $columnType = 'string'; 186 | $format = 'date-time'; 187 | $value = '2025-01-01 00:00:00'; 188 | break; 189 | 190 | case 'time': 191 | $columnType = 'string'; 192 | $format = 'time'; 193 | $value = '00:00:00'; 194 | break; 195 | 196 | case 'json': 197 | case 'enum': 198 | case 'set': 199 | $columnType = 'string'; 200 | $value = count($column['allowed_values']) ? $column['allowed_values'] : ['Value 1', 'Value 2']; 201 | $is_enum = true; 202 | $format = null; 203 | break; 204 | 205 | case 'blob': 206 | case 'binary': 207 | case 'varbinary': 208 | case 'tinyblob': 209 | case 'mediumblob': 210 | case 'longblob': 211 | $columnType = 'string'; 212 | $format = 'binary'; 213 | $value = 'Value'; 214 | break; 215 | 216 | default: 217 | $columnType = 'string'; 218 | $value = 'Value'; 219 | } 220 | 221 | $enumData = $is_enum ? ['enum' => $value] : ['example' => $value]; 222 | 223 | $data[$columnName] = ['type' => $columnType, ...$enumData, 'is_required' => ! $column['is_nullable']]; 224 | if (isset($format)) { 225 | $data[$columnName]['format'] = $format; 226 | } 227 | } 228 | 229 | return $data; 230 | } 231 | 232 | private function buildSwaggerObject(string $url, array $data, array $schemas = []): array 233 | { 234 | $appName = config('app.name'); 235 | 236 | return [ 237 | 'openapi' => '3.0.0', 238 | 'info' => [ 239 | 'title' => "Laravel Auto Crud ($appName)", 240 | 'version' => '1.0.0', 241 | 'description' => 'This is an automatic generated Swagger JSON file with full CRUD operations', 242 | ], 243 | 'servers' => [ 244 | [ 245 | 'url' => $url, 246 | 'description' => 'Server', 247 | ], 248 | ], 249 | 'paths' => $data, 250 | 'components' => [ 251 | 'schemas' => $schemas, 252 | ], 253 | ]; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Builders/EnumBuilder.php: -------------------------------------------------------------------------------- 1 | fileService->createFromStub($modelData, 'enum', 'Enums', 'Enum', $overwrite, function ($modelData) use ($values) { 14 | return [ 15 | '{{ data }}' => EnumTransformer::convertDataToString($values), 16 | ]; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Builders/RepositoryBuilder.php: -------------------------------------------------------------------------------- 1 | fileService->createFromStub($modelData, 'repository', 'Repositories', 'Repository', $overwrite, function ($modelData) { 12 | $model = $this->getFullModelNamespace($modelData); 13 | 14 | return [ 15 | '{{ modelNamespace }}' => $model, 16 | '{{ model }}' => $modelData['modelName'], 17 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 18 | ]; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Builders/RequestBuilder.php: -------------------------------------------------------------------------------- 1 | tableColumnsService = new TableColumnsService; 20 | $this->modelService = new ModelService; 21 | } 22 | 23 | public function create(array $modelData, bool $overwrite = false): string 24 | { 25 | return $this->fileService->createFromStub($modelData, 'request', 'Http/Requests', 'Request', $overwrite, function ($modelData) { 26 | return ['{{ data }}' => HelperService::formatArrayToPhpSyntax($this->getRequestData($modelData))]; 27 | }); 28 | } 29 | 30 | private function getRequestData(array $modelData): array 31 | { 32 | $columns = $this->getAvailableColumns($modelData); 33 | 34 | $validationRules = []; 35 | 36 | foreach ($columns as $column) { 37 | $rules = []; 38 | $columnType = $column['type']; 39 | $maxLength = $column['max_length']; 40 | $isUnique = $column['is_unique']; 41 | $allowedValues = $column['allowed_values']; 42 | // Handle column types 43 | switch ($columnType) { 44 | case 'string': 45 | case 'char': 46 | case 'varchar': 47 | $rules[] = 'string'; 48 | if ($maxLength) { 49 | $rules[] = 'max:'.$maxLength; 50 | } 51 | break; 52 | 53 | case 'integer': 54 | case 'int': 55 | case 'bigint': 56 | case 'smallint': 57 | case 'tinyint': 58 | $rules[] = 'integer'; 59 | if (str_contains($columnType, 'unsigned')) { 60 | $rules[] = 'min:0'; 61 | } 62 | break; 63 | 64 | case 'boolean': 65 | $rules[] = 'boolean'; 66 | break; 67 | 68 | case 'date': 69 | case 'datetime': 70 | case 'timestamp': 71 | $rules[] = 'date'; 72 | break; 73 | 74 | case 'text': 75 | case 'longtext': 76 | case 'mediumtext': 77 | $rules[] = 'string'; 78 | break; 79 | 80 | case 'decimal': 81 | case 'float': 82 | case 'double': 83 | $rules[] = 'numeric'; 84 | break; 85 | 86 | case 'enum': 87 | if (! empty($allowedValues)) { 88 | $rules[] = 'in:'.implode(',', $allowedValues); 89 | } 90 | break; 91 | 92 | case 'json': 93 | $rules[] = 'json'; 94 | break; 95 | 96 | case 'binary': 97 | case 'blob': 98 | $rules[] = 'string'; // Handle binary data as string for simplicity 99 | break; 100 | 101 | default: 102 | $rules[] = 'string'; // Default fallback 103 | break; 104 | } 105 | 106 | $columnName = $column['name']; 107 | 108 | // Handle unique columns 109 | if ($isUnique) { 110 | $rules[] = 'unique:'.$column['table'].','.$columnName; 111 | } 112 | 113 | // Add rules to the validation array 114 | $validationRules[$columnName] = implode('|', $rules); 115 | } 116 | 117 | return $validationRules; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Builders/ResourceBuilder.php: -------------------------------------------------------------------------------- 1 | modelService = new ModelService; 20 | $this->tableColumnsService = new TableColumnsService; 21 | } 22 | 23 | public function create(array $modelData, bool $overwrite = false): string 24 | { 25 | return $this->fileService->createFromStub($modelData, 'resource', 'Http/Resources', 'Resource', $overwrite, function ($modelData) { 26 | return ['{{ data }}' => HelperService::formatArrayToPhpSyntax($this->getResourcesData($modelData), true)]; 27 | }); 28 | } 29 | 30 | private function getResourcesData(array $modelData): array 31 | { 32 | $columns = $this->getAvailableColumns($modelData); 33 | 34 | $data = []; 35 | 36 | foreach ($columns as $column) { 37 | $columnName = $column['name']; 38 | $data[$columnName] = '$this->'.$columnName; 39 | } 40 | 41 | return $data; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Builders/RouteBuilder.php: -------------------------------------------------------------------------------- 1 | createRoutes($routesPath, $routeCode); 20 | } 21 | 22 | if (in_array('web', $types)) { 23 | $routesPath = base_path('routes/web.php'); 24 | $routeCode = "Route::resource('/{$modelName}', {$controller}::class);"; 25 | $this->createRoutes($routesPath, $routeCode); 26 | } 27 | } 28 | 29 | private function createRoutes(string $routesPath, string $routeCode): void 30 | { 31 | if (! file_exists($routesPath)) { 32 | file_put_contents($routesPath, "fileService->createFromStub($modelData, 'service', 'Services', 'Service', $overwrite, function ($modelData) use ($repository) { 12 | $model = $this->getFullModelNamespace($modelData); 13 | $repositorySplitting = explode('\\', $repository); 14 | $repositoryNamespace = $repository; 15 | $repository = end($repositorySplitting); 16 | $repositoryVariable = lcfirst($repository); 17 | 18 | return [ 19 | '{{ modelNamespace }}' => $model, 20 | '{{ model }}' => $modelData['modelName'], 21 | '{{ modelVariable }}' => lcfirst($modelData['modelName']), 22 | '{{ repository }}' => $repository, 23 | '{{ repositoryNamespace }}' => $repositoryNamespace, 24 | '{{ repositoryVariable }}' => $repositoryVariable, 25 | ]; 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Builders/SpatieDataBuilder.php: -------------------------------------------------------------------------------- 1 | enumBuilder = new EnumBuilder; 22 | $this->modelService = new ModelService; 23 | $this->tableColumnsService = new TableColumnsService; 24 | } 25 | 26 | public function create(array $modelData, bool $overwrite = false): string 27 | { 28 | return $this->fileService->createFromStub($modelData, 'spatie_data', 'Data', 'Data', $overwrite, function ($modelData) use ($overwrite) { 29 | $supportedData = $this->getHelperData($modelData, $overwrite); 30 | 31 | return [ 32 | '{{ namespaces }}' => SpatieDataTransformer::convertNamespacesToString($supportedData['namespaces']), 33 | '{{ data }}' => SpatieDataTransformer::convertDataToString($supportedData['properties'] ?? []), 34 | ]; 35 | }); 36 | } 37 | 38 | private function getHelperData(array $modelData, $overwrite = false): array 39 | { 40 | $columns = $this->getAvailableColumns($modelData); 41 | $properties = []; 42 | $validationNamespaces = []; 43 | $validationNamespace = 'use Spatie\LaravelData\Attributes\Validation\{{ validationNamespace }};'; 44 | 45 | foreach ($columns as $column) { 46 | $rules = []; 47 | $isNullable = $column['is_nullable']; 48 | $columnType = $column['type']; 49 | $maxLength = $column['max_length']; 50 | $isUnique = $column['is_unique']; 51 | $allowedValues = $column['allowed_values']; 52 | $validation = '#[{{ validation }}]'; 53 | $property = 'public '.($isNullable ? '?' : '').'{{ type }} $'.$column['name'].';'; 54 | 55 | // Handle column types 56 | switch ($columnType) { 57 | case 'string': 58 | case 'char': 59 | case 'varchar': 60 | case 'text': 61 | case 'longtext': 62 | case 'mediumtext': 63 | case 'binary': 64 | case 'blob': 65 | $property = str_replace('{{ type }}', 'string', $property); 66 | if ($maxLength) { 67 | $rules[] = 'Max('.$maxLength.')'; 68 | $validationNamespaces[] = str_replace('{{ validationNamespace }}', 'Max', $validationNamespace); 69 | } 70 | break; 71 | 72 | case 'integer': 73 | case 'int': 74 | case 'bigint': 75 | case 'smallint': 76 | case 'tinyint': 77 | $property = str_replace('{{ type }}', 'int', $property); 78 | if (str_contains($columnType, 'unsigned')) { 79 | $rules[] = 'Min(0)'; 80 | $validationNamespaces[] = str_replace('{{ validationNamespace }}', 'Min', $validationNamespace); 81 | } 82 | break; 83 | 84 | case 'boolean': 85 | $property = str_replace('{{ type }}', 'bool', $property); 86 | break; 87 | 88 | case 'date': 89 | case 'datetime': 90 | case 'timestamp': 91 | $property = str_replace('{{ type }}', 'Carbon', $property); 92 | $rules[] = 'Date'; 93 | $validationNamespaces[] = str_replace('{{ validationNamespace }}', 'Date', $validationNamespace); 94 | $validationNamespaces[] = 'use Carbon\Carbon;'; 95 | break; 96 | 97 | case 'decimal': 98 | case 'float': 99 | case 'double': 100 | $property = str_replace('{{ type }}', 'int', $property); 101 | $rules[] = 'Numeric'; 102 | $validationNamespaces[] = str_replace('{{ validationNamespace }}', 'Numeric', $validationNamespace); 103 | break; 104 | 105 | case 'enum': 106 | if (! empty($allowedValues)) { 107 | $enum = $this->enumBuilder->create($modelData, $allowedValues, $overwrite); 108 | $enumClass = explode('\\', $enum); 109 | $enumClass = end($enumClass); 110 | $rules[] = "Enum($enumClass::class)"; 111 | $property = str_replace('{{ type }}', $enumClass, $property); 112 | $validationNamespaces[] = str_replace('{{ validationNamespace }}', 'Enum', $validationNamespace); 113 | $validationNamespaces[] = 'use '.$enum.';'; 114 | } 115 | break; 116 | 117 | case 'json': 118 | $property = str_replace('{{ type }}', 'array', $property); 119 | $rules[] = 'Json'; 120 | $validationNamespaces[] = str_replace('{{ validationNamespace }}', 'Json', $validationNamespace); 121 | break; 122 | 123 | default: 124 | $property = str_replace('{{ type }}', 'string', $property); 125 | break; 126 | } 127 | 128 | // Handle unique columns 129 | if ($isUnique) { 130 | $table = $column['table']; 131 | $columnName = $column['name']; 132 | $rules[] = "Unique('$table', '$columnName')"; 133 | $validationNamespaces[] = str_replace('{{ validationNamespace }}', 'Unique', $validationNamespace); 134 | } 135 | 136 | $properties['properties'][$property] = count($rules) ? str_replace('{{ validation }}', implode(', ', $rules), $validation) : ''; 137 | } 138 | $properties['namespaces'] = array_unique($validationNamespaces); 139 | 140 | return $properties; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Builders/ViewBuilder.php: -------------------------------------------------------------------------------- 1 | modelService = new ModelService; 22 | $this->tableColumnsService = new TableColumnsService; 23 | } 24 | 25 | public function create(array $modelData, $overwrite = false): void 26 | { 27 | $data = $this->getAvailableColumns($modelData); 28 | $modelName = HelperService::toSnakeCase(Str::plural($modelData['modelName'])); 29 | $viewPath = base_path("resources/views/{$modelName}"); 30 | 31 | if (! is_dir($viewPath)) { 32 | mkdir($viewPath, 0755, true); 33 | } 34 | 35 | $modelName = $modelData['modelName']; 36 | $methods = [ 37 | 'index' => $this->generateIndexPage($modelName, $data), 38 | 'show' => $this->generateViewPage($modelName, $data), 39 | 'create' => $this->generateCreatePage($modelName, $data), 40 | 'edit' => $this->generateEditPage($modelName, $data), 41 | ]; 42 | 43 | foreach ($methods as $view => $data) { 44 | $filePath = "$viewPath/{$view}.blade.php"; 45 | if (! file_exists($filePath)) { 46 | file_put_contents($filePath, $data); 47 | info('Created '.$filePath); 48 | } else { 49 | if ($overwrite) { 50 | file_put_contents($filePath, $data); 51 | info('Created '.$filePath); 52 | } 53 | } 54 | } 55 | } 56 | 57 | private function generateViewPage(string $modelName, array $data): string 58 | { 59 | $var = lcfirst($modelName); 60 | $html = ''; 61 | foreach ($data as $item) { 62 | $html .= '

'.$item['name'].': {{ $'.$var.' ->'.$item['name'].' }}

'."\n"; 63 | } 64 | 65 | return << 67 |

$var Details

68 | $html 69 | 70 | BLADE; 71 | } 72 | 73 | private function generateIndexPage(string $modelName, array $data): string 74 | { 75 | $var = HelperService::toSnakeCase(Str::plural($modelName)); 76 | $header = ''; 77 | $body = ''; 78 | foreach ($data as $item) { 79 | $header .= ''.$item['name'].''; 80 | $body .= '{{$item->'.$item['name'].'}}'."\n"; 81 | } 82 | $header .= ''; 83 | 84 | return << 86 |

$var List

87 | Create $var 88 | 89 | 90 | $header 91 | 92 | 93 | @foreach (\$$var as \$item) 94 | 95 | $body 103 | 104 | @endforeach 105 | 106 |
96 | Edit 97 |
98 | @csrf 99 | @method('DELETE') 100 | 101 |
102 |
107 | 108 | BLADE; 109 | } 110 | 111 | private function generateCreatePage(string $modelName, array $data): string 112 | { 113 | $var = HelperService::toSnakeCase(Str::plural($modelName)); 114 | $html = ''; 115 | foreach ($data as $item) { 116 | $html .= '
117 | 118 | 119 | @error("'.$item['name'].'") 120 |

{{$message}}

121 | @enderror 122 |
'."\n"; 123 | } 124 | 125 | return << 127 |

Create $var

128 |
129 | @csrf 130 | $html 131 | 132 |
133 | 134 | BLADE; 135 | } 136 | 137 | private function generateEditPage(string $modelName, array $data): string 138 | { 139 | $var = lcfirst($modelName); 140 | $plural = HelperService::toSnakeCase(Str::plural($modelName)); 141 | $parameter = '$'.$var.'->id'; 142 | $html = ''; 143 | foreach ($data as $item) { 144 | $html .= '
145 | 146 | 147 | @error("'.$item['name'].'") 148 |

{{$message}}

149 | @enderror 150 |
'."\n"; 151 | } 152 | 153 | return << 155 |

Edit $var

156 |
157 | @csrf 158 | @method("PATCH") 159 | $html 160 | 161 |
162 | 163 | BLADE; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Console/Commands/GenerateAutoCrudCommand.php: -------------------------------------------------------------------------------- 1 | option('model-path') ?? config('laravel_auto_crud.fallback_models_path', 'app/Models/'); 45 | 46 | HelperService::displaySignature(); 47 | 48 | if (! $this->everythingIsOk()) { 49 | return; 50 | } 51 | 52 | $models = []; 53 | if (count($this->option('model'))) { 54 | foreach ($this->option('model') as $model) { 55 | $modelExists = ModelService::isModelExists($model, $modelPath); 56 | if (! $modelExists) { 57 | alert('Model '.$model.' does not exist'); 58 | 59 | continue; 60 | } 61 | $models[] = $modelExists; 62 | } 63 | } else { 64 | $models = ModelService::showModels($modelPath); 65 | } 66 | 67 | if (empty($models)) { 68 | alert("Can't find models, if the models folder outside app directory , make sure it's already loaded in psr-4."); 69 | 70 | return; 71 | } 72 | $this->parsingOptions(); 73 | $this->generate($models); 74 | } 75 | 76 | private function generate(array $models): void 77 | { 78 | foreach ($models as $model) { 79 | $modelData = ModelService::resolveModelName($model); 80 | $table = ModelService::getFullModelNamespace($modelData, fn ($modelName) => new $modelName); 81 | if (! $this->databaseValidatorService->checkTableExists($table)) { 82 | $createFiles = confirm( 83 | label: 'Table '.$table.' not found, Do you want to create empty auto CRUD files?.' 84 | ); 85 | if (! $createFiles) { 86 | alert('Auto CRUD files not generated for model '.$model.'.'); 87 | 88 | continue; 89 | } 90 | } 91 | $this->CRUDGenerator->generate($modelData, $this->options()); 92 | $this->documentationGenerator->generate($modelData, $this->options(), count($models) > 1); 93 | } 94 | 95 | } 96 | 97 | private function everythingIsOk(): bool 98 | { 99 | if (! $this->databaseValidatorService->checkDataBaseConnection()) { 100 | alert('DB Connection Error.'); 101 | 102 | return false; 103 | } 104 | 105 | if ($this->option('type') && empty(array_intersect($this->option('type'), ['api', 'web']))) { 106 | alert('Make sure that the type is "api", "web" or "both".'); 107 | 108 | return false; 109 | } 110 | 111 | if ($this->option('pattern') == 'spatie-data' && ! class_exists(\Spatie\LaravelData\Data::class)) { 112 | alert('Make sure that the "spatie-data" package is installed."'); 113 | 114 | return false; 115 | } 116 | 117 | return true; 118 | } 119 | 120 | private function parsingOptions(): void 121 | { 122 | if ($this->option('all')) { 123 | $this->forceAllBooleanOptions(); 124 | $this->input->setOption('overwrite', false); 125 | } 126 | 127 | if ($this->option('force-all')) { 128 | $this->forceAllBooleanOptions(); 129 | $this->input->setOption('overwrite', true); 130 | } 131 | 132 | if (! $this->option('all') && ! $this->option('force-all')) { 133 | HelperService::askForType($this->input, $this->option('type')); 134 | } 135 | } 136 | 137 | private function forceAllBooleanOptions(): void 138 | { 139 | $this->input->setOption('repository', true); 140 | $this->input->setOption('curl', true); 141 | $this->input->setOption('postman', true); 142 | $this->input->setOption('swagger-api', true); 143 | $this->input->setOption('type', ['api', 'web']); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/LaravelAutoCrudServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(TableColumnsService::class, function ($app) { 14 | return new TableColumnsService; 15 | }); 16 | } 17 | 18 | public function boot(): void 19 | { 20 | $this->publishes([ 21 | __DIR__.'/../Config/laravel_auto_crud.php' => config_path('laravel_auto_crud.php'), 22 | ], 'auto-crud-config'); 23 | 24 | // Boot any package services here 25 | if ($this->app->runningInConsole()) { 26 | $this->commands([ 27 | GenerateAutoCrudCommand::class, 28 | ]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/CRUDGenerator.php: -------------------------------------------------------------------------------- 1 | controllerBuilder = new ControllerBuilder; 31 | $this->resourceBuilder = new ResourceBuilder; 32 | $this->requestBuilder = new RequestBuilder; 33 | $this->routeBuilder = new RouteBuilder; 34 | $this->viewBuilder = new ViewBuilder; 35 | $this->repositoryBuilder = new RepositoryBuilder; 36 | $this->serviceBuilder = new ServiceBuilder; 37 | $this->spatieDataBuilder = new SpatieDataBuilder; 38 | } 39 | 40 | public function generate($modelData, array $options): void 41 | { 42 | $checkForType = $options['type']; 43 | 44 | if ($options['pattern'] == 'spatie-data') { 45 | $spatieDataName = $this->spatieDataBuilder->create($modelData, $options['overwrite']); 46 | } else { 47 | $requestName = $this->requestBuilder->create($modelData, $options['overwrite']); 48 | } 49 | 50 | $repository = $service = null; 51 | if ($options['repository']) { 52 | $repository = $this->repositoryBuilder->create($modelData, $options['overwrite']); 53 | $service = $this->serviceBuilder->create($modelData, $repository, $options['overwrite']); 54 | } 55 | 56 | $data = [ 57 | 'requestName' => $requestName ?? '', 58 | 'repository' => $repository ?? '', 59 | 'service' => $service ?? '', 60 | 'spatieData' => $spatieDataName ?? '', 61 | ]; 62 | 63 | $controllerName = $this->generateController($checkForType, $modelData, $data, $options); 64 | $this->routeBuilder->create($modelData['modelName'], $controllerName, $checkForType); 65 | 66 | info('Auto CRUD files generated successfully for '.$modelData['modelName'].' Model'); 67 | } 68 | 69 | private function generateController(array $types, array $modelData, array $data, array $options): string 70 | { 71 | $controllerName = null; 72 | 73 | if (in_array('api', $types)) { 74 | $controllerName = $this->generateAPIController($modelData, $data['requestName'], $data['repository'], $data['service'], $options, $data['spatieData']); 75 | } 76 | 77 | if (in_array('web', $types)) { 78 | $controllerName = $this->generateWebController($modelData, $data['requestName'], $data['repository'], $data['service'], $options, $data['spatieData']); 79 | } 80 | 81 | if (! $controllerName) { 82 | throw new InvalidArgumentException('Unsupported controller type'); 83 | } 84 | 85 | return $controllerName; 86 | } 87 | 88 | private function generateAPIController(array $modelData, string $requestName, string $repository, string $service, array $options, ?string $spatieData = null): string 89 | { 90 | $controllerName = null; 91 | 92 | if ($options['pattern'] == 'spatie-data') { 93 | $controllerName = $repository 94 | ? $this->controllerBuilder->createAPIRepositorySpatieData($modelData, $spatieData, $service, $options['overwrite']) 95 | : $this->controllerBuilder->createAPISpatieData($modelData, $spatieData, $options['overwrite']); 96 | } elseif ($options['pattern'] == 'normal') { 97 | $resourceName = $this->resourceBuilder->create($modelData, $options['overwrite']); 98 | $controllerName = $repository 99 | ? $this->controllerBuilder->createAPIRepository($modelData, $resourceName, $requestName, $service, $options['overwrite']) 100 | : $this->controllerBuilder->createAPI($modelData, $resourceName, $requestName, $options['overwrite']); 101 | } 102 | 103 | if (! $controllerName) { 104 | throw new InvalidArgumentException('Unsupported controller type'); 105 | } 106 | 107 | return $controllerName; 108 | } 109 | 110 | private function generateWebController(array $modelData, string $requestName, string $repository, string $service, array $options, string $spatieData = ''): string 111 | { 112 | $controllerName = null; 113 | 114 | if ($options['pattern'] == 'spatie-data') { 115 | $controllerName = $repository 116 | ? $this->controllerBuilder->createWebRepositorySpatieData($modelData, $spatieData, $service, $options['overwrite']) 117 | : $this->controllerBuilder->createWebSpatieData($modelData, $spatieData, $options['overwrite']); 118 | } elseif ($options['pattern'] == 'normal') { 119 | $controllerName = $repository 120 | ? $this->controllerBuilder->createWebRepository($modelData, $requestName, $service, $options['overwrite']) 121 | : $this->controllerBuilder->createWeb($modelData, $requestName, $options['overwrite']); 122 | } 123 | 124 | $this->viewBuilder->create($modelData, $options['overwrite']); 125 | 126 | if (! $controllerName) { 127 | throw new InvalidArgumentException('Unsupported controller type'); 128 | } 129 | 130 | return $controllerName; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Services/DatabaseValidatorService.php: -------------------------------------------------------------------------------- 1 | getPdo(); 16 | 17 | return true; 18 | } catch (\PDOException $e) { 19 | return false; 20 | } 21 | } 22 | 23 | public function checkTableExists(string $table): bool 24 | { 25 | return Schema::hasTable($table); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/DocumentationGenerator.php: -------------------------------------------------------------------------------- 1 | CURLBuilder = new CURLBuilder; 18 | $this->postmanBuilder = new PostmanBuilder; 19 | $this->swaggerAPIBuilder = new SwaggerAPIBuilder; 20 | } 21 | 22 | public function generate(array $modelData, array $options, bool $multipleModels = false): void 23 | { 24 | if (in_array('api', $options['type'])) { 25 | $overwrite = $options['overwrite']; 26 | 27 | if ($multipleModels && $options['overwrite']) { 28 | $overwrite = false; 29 | } 30 | 31 | $this->generatePostman($modelData, $options['postman'], $overwrite); 32 | $this->generateCURL($modelData, $options['curl'], $overwrite); 33 | $this->generateSwaggerAPI($modelData, $options['swagger-api'], $overwrite); 34 | } 35 | } 36 | 37 | private function generatePostman(array $modelData, bool $generate, bool $overwrite): void 38 | { 39 | if ($generate) { 40 | $this->postmanBuilder->create($modelData, $overwrite); 41 | } 42 | } 43 | 44 | private function generateCURL(array $modelData, bool $generate, bool $overwrite): void 45 | { 46 | if ($generate) { 47 | $this->CURLBuilder->create($modelData, $overwrite); 48 | } 49 | } 50 | 51 | private function generateSwaggerAPI(array $modelData, bool $generate, bool $overwrite): void 52 | { 53 | if ($generate) { 54 | $this->swaggerAPIBuilder->create($modelData, $overwrite); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Services/FileService.php: -------------------------------------------------------------------------------- 1 | generateFilePath($modelData, $basePath, $suffix); 22 | 23 | if (! $overwrite) { 24 | if (file_exists($filePath)) { 25 | $overwrite = confirm( 26 | label: ucfirst($suffix).' file already exists, do you want to overwrite it? '.$filePath 27 | ); 28 | if (! $overwrite) { 29 | return $namespace.'\\'.$modelData['modelName'].$suffix; 30 | } 31 | } 32 | } 33 | 34 | File::ensureDirectoryExists(dirname($filePath), 0777, true); 35 | 36 | $data = $dataCallback ? $dataCallback($modelData) : []; 37 | $content = $this->generateContent($stubPath, $modelData, $namespace, $suffix, $data); 38 | 39 | File::put($filePath, $content); 40 | 41 | info("Created: $filePath"); 42 | 43 | return $namespace.'\\'.$modelData['modelName'].$suffix; 44 | } 45 | 46 | private function generateFilePath(array $modelData, string $basePath, string $suffix): string 47 | { 48 | if ($modelData['folders']) { 49 | return app_path("{$basePath}/{$modelData['folders']}/{$modelData['modelName']}{$suffix}.php"); 50 | } 51 | 52 | return app_path("{$basePath}/{$modelData['modelName']}{$suffix}.php"); 53 | } 54 | 55 | private function generateContent(string $stubPath, array $modelData, string $namespace, string $suffix, array $data = []): string 56 | { 57 | $replacements = [ 58 | '{{ class }}' => $modelData['modelName'].$suffix, 59 | '{{ namespace }}' => $namespace, 60 | ...$data, 61 | ]; 62 | 63 | return str_replace(array_keys($replacements), array_values($replacements), file_get_contents($stubPath)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Services/HelperService.php: -------------------------------------------------------------------------------- 1 | $value) { 17 | if ($removeValuesQuotes) { 18 | $formattedArray .= $indent."'$key' => $value,\n"; 19 | } else { 20 | $formattedArray .= $indent."'$key' => '$value',\n"; 21 | } 22 | } 23 | 24 | $formattedArray .= str_repeat(' ', $indentation - 4).']'; 25 | 26 | return $formattedArray; 27 | } 28 | 29 | public static function toSnakeCase(string $text): string 30 | { 31 | // Convert camelCase or PascalCase (ModelName -> model_name) 32 | $text = preg_replace('/([a-z])([A-Z])/', '$1_$2', $text); 33 | 34 | // Replace any non-alphanumeric characters (spaces, dashes, etc.) with underscores 35 | $text = preg_replace('/[^a-zA-Z0-9]+/', '_', $text); 36 | 37 | // Convert to lowercase 38 | return strtolower(trim($text, '_')); 39 | } 40 | 41 | public static function displaySignature(): void 42 | { 43 | $asciiArt = << 0 ? $types : multiselect( 61 | label: 'Do you want to create an "api", "web" or "both" controller?', 62 | options: ['api', 'web'], 63 | default: ['api'], 64 | required: true, 65 | validate: function ($value) { 66 | if (empty(array_intersect($value, ['api', 'web']))) { 67 | return 'Please enter a valid type api or web'; 68 | } 69 | 70 | return null; 71 | }, 72 | hint: 'Select api, web or both', 73 | ); 74 | 75 | $types = array_map('strtolower', $newTypes); 76 | 77 | $inputType->setOption('type', $types); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Services/ModelService.php: -------------------------------------------------------------------------------- 1 | filter(function ($fullNamespace) use ($modelName) { 19 | if (! $fullNamespace) { 20 | return false; 21 | } 22 | 23 | // Check if the class name matches (without namespace) 24 | $classParts = explode('\\', $fullNamespace); 25 | $actualClassName = end($classParts); 26 | 27 | return $actualClassName === $modelName; 28 | }) 29 | ->first(); 30 | } 31 | 32 | public static function showModels(string $modelsPath): ?array 33 | { 34 | $models = self::getAllModels($modelsPath) 35 | ->filter(function ($fullNamespace) { 36 | if (! $fullNamespace) { 37 | return false; 38 | } 39 | 40 | // Ensure the class exists and is an instance of Model 41 | if (! class_exists($fullNamespace)) { 42 | return false; 43 | } 44 | 45 | return is_subclass_of($fullNamespace, Model::class); 46 | }) 47 | ->values() // Reset array keys 48 | ->toArray(); 49 | 50 | $models = array_values($models); 51 | 52 | return count($models) ? multiselect(label: 'Select your model, use your space-bar to select.', options: $models) : null; 53 | } 54 | 55 | public static function resolveModelName($modelName): array 56 | { 57 | $parts = explode('\\', $modelName); 58 | 59 | return [ 60 | 'modelName' => array_pop($parts), 61 | 'folders' => implode('/', $parts) !== 'App/Models' ? implode('/', $parts) : null, 62 | 'namespace' => str_replace('/', '\\', implode('/', $parts)) ?: null, 63 | ]; 64 | } 65 | 66 | public static function getFullModelNamespace(array $modelData, ?callable $modelFactory = null): string 67 | { 68 | if (isset($modelData['namespace']) && $modelData['namespace']) { 69 | $modelName = $modelData['namespace'].'\\'.$modelData['modelName']; 70 | } else { 71 | $modelName = $modelData['modelName']; 72 | } 73 | 74 | // استخدم الـ Factory إذا تم تمريره، وإلا أنشئ الكائن بالطريقة العادية 75 | $model = $modelFactory ? $modelFactory($modelName) : new $modelName; 76 | 77 | if (is_subclass_of($model, Model::class)) { 78 | return $model->getTable(); 79 | } 80 | 81 | throw new InvalidArgumentException("Model {$modelName} does not exist"); 82 | } 83 | 84 | public static function handleModelsPath(string $modelsPath): string 85 | { 86 | return str_ends_with($modelsPath, '/') ? $modelsPath : $modelsPath.DIRECTORY_SEPARATOR; 87 | } 88 | 89 | private static function getAllModels(string $modelsPath): \Illuminate\Support\Collection 90 | { 91 | $modelsPath = static::handleModelsPath($modelsPath); 92 | 93 | return collect(File::allFiles(static::getModelNameFromPath($modelsPath)))->map(function ($file) { 94 | $content = static::getClassContent($file->getRealPath()); 95 | $namespace = ''; 96 | if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) { 97 | $namespace = trim($matches[1]); 98 | } 99 | $className = pathinfo($file->getFilename(), PATHINFO_FILENAME); 100 | 101 | return $namespace ? $namespace.'\\'.$className : null; 102 | }); 103 | } 104 | 105 | private static function getModelNameFromPath(string $modelsPath): string 106 | { 107 | return base_path($modelsPath); 108 | } 109 | 110 | private static function getClassContent($file): false|string 111 | { 112 | return File::get($file); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Services/TableColumnsService.php: -------------------------------------------------------------------------------- 1 | getDriverName(); 17 | 18 | $output = []; 19 | 20 | foreach ($columns as $column) { 21 | $columnDetails = $this->getColumnDetails($driver, $table, $column); 22 | 23 | if (count($columnDetails)) { 24 | // Exclude primary keys and timestamps 25 | if ($columnDetails['is_primary_key'] || in_array($column, $excludeColumns)) { 26 | continue; 27 | } 28 | 29 | $output[] = $columnDetails; 30 | } 31 | } 32 | 33 | return $output; 34 | } 35 | 36 | private function getColumnDetails(string $driver, string $table, string $column): array 37 | { 38 | return match ($driver) { 39 | 'mysql' => $this->getMySQLColumnDetails($table, $column), 40 | 'pgsql' => $this->getPostgresColumnDetails($table, $column), 41 | 'sqlite' => $this->getSQLiteColumnDetails($table, $column), 42 | 'sqlsrv' => $this->getSQLServerColumnDetails($table, $column), 43 | default => [], 44 | }; 45 | } 46 | 47 | private function getMysqlColumnDetails(string $table, string $column): array 48 | { 49 | $columnDetails = DB::select("SHOW COLUMNS FROM `$table` WHERE Field = ?", [$column]); 50 | 51 | if (empty($columnDetails)) { 52 | return []; 53 | } 54 | 55 | $columnInfo = $columnDetails[0]; 56 | $isPrimaryKey = ($columnInfo->Key === 'PRI'); 57 | $isUnique = ($columnInfo->Key === 'UNI'); 58 | $isNullable = ($columnInfo->Null === 'YES'); 59 | $maxLength = isset($columnInfo->Type) && preg_match('/\((\d+)\)/', $columnInfo->Type, $matches) ? $matches[1] : null; 60 | 61 | if (str_starts_with($columnInfo->Type, 'enum')) { 62 | preg_match("/^enum\((.+)\)$/", $columnInfo->Type, $matches); 63 | $allowedValues = isset($matches[1]) ? str_getcsv(str_replace("'", '', $matches[1])) : []; 64 | } 65 | 66 | return [ 67 | 'is_primary_key' => $isPrimaryKey, 68 | 'is_unique' => $isUnique, 69 | 'is_nullable' => $isNullable, 70 | 'type' => Schema::getColumnType($table, $column), 71 | 'max_length' => $maxLength, 72 | 'allowed_values' => $allowedValues ?? [], 73 | 'name' => $column, 74 | 'table' => $table, 75 | ]; 76 | } 77 | 78 | private function getPostgresColumnDetails(string $table, string $column): array 79 | { 80 | $columnDetails = DB::select(" 81 | SELECT 82 | column_name, 83 | column_default, 84 | is_nullable, 85 | data_type, 86 | character_maximum_length, 87 | udt_name, 88 | (SELECT COUNT(*) > 0 FROM information_schema.table_constraints tc 89 | JOIN information_schema.constraint_column_usage ccu 90 | ON tc.constraint_name = ccu.constraint_name 91 | WHERE tc.table_name = ? AND ccu.column_name = ? AND tc.constraint_type = 'PRIMARY KEY') AS is_primary, 92 | (SELECT COUNT(*) > 0 FROM information_schema.table_constraints tc 93 | JOIN information_schema.constraint_column_usage ccu 94 | ON tc.constraint_name = ccu.constraint_name 95 | WHERE tc.table_name = ? AND ccu.column_name = ? AND tc.constraint_type = 'UNIQUE') AS is_unique 96 | FROM information_schema.columns 97 | WHERE table_name = ? AND column_name = ?", 98 | [$table, $column, $table, $column, $table, $column] 99 | ); 100 | 101 | if (empty($columnDetails)) { 102 | return []; 103 | } 104 | 105 | $columnInfo = $columnDetails[0]; 106 | $isPrimaryKey = $columnInfo->is_primary; 107 | $isUnique = $columnInfo->is_unique; 108 | $isNullable = ($columnInfo->is_nullable === 'YES'); 109 | $maxLength = $columnInfo->character_maximum_length ?? null; 110 | 111 | if (str_starts_with($columnInfo->udt_name, '_')) { 112 | preg_match('/^_(.+)$/', $columnInfo->udt_name, $matches); 113 | $allowedValues = isset($matches[1]) ? str_getcsv(str_replace("'", '', $matches[1])) : []; 114 | } 115 | 116 | return [ 117 | 'is_primary_key' => $isPrimaryKey, 118 | 'is_unique' => $isUnique, 119 | 'is_nullable' => $isNullable, 120 | 'type' => Schema::getColumnType($table, $column), 121 | 'max_length' => $maxLength, 122 | 'allowed_values' => $allowedValues ?? [], 123 | 'name' => $column, 124 | 'table' => $table, 125 | ]; 126 | } 127 | 128 | private function getSqliteColumnDetails(string $table, string $column): array 129 | { 130 | $columnDetails = DB::select("PRAGMA table_info($table)"); 131 | foreach ($columnDetails as $col) { 132 | if ($col->name === $column) { 133 | return [ 134 | 'is_primary_key' => $col->pk == 1, 135 | 'is_unique' => false, 136 | 'is_nullable' => $col->notnull == 0, 137 | 'type' => Schema::getColumnType($table, $column), 138 | 'max_length' => null, 139 | 'allowed_values' => [], 140 | 'name' => $column, 141 | 'table' => $table, 142 | ]; 143 | } 144 | } 145 | 146 | return []; 147 | } 148 | 149 | private function getSQLServerColumnDetails(string $table, string $column): array 150 | { 151 | $columnDetails = DB::select( 152 | "SELECT COLUMN_NAME, COLUMNPROPERTY(object_id(?), COLUMN_NAME, 'IsIdentity') AS is_identity, IS_NULLABLE, DATA_TYPE 153 | FROM INFORMATION_SCHEMA.COLUMNS 154 | WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", 155 | [$table, $table, $column] 156 | ); 157 | 158 | if (empty($columnDetails)) { 159 | return []; 160 | } 161 | 162 | $columnInfo = $columnDetails[0]; 163 | 164 | return [ 165 | 'is_primary_key' => $columnInfo->is_identity, 166 | 'is_unique' => false, 167 | 'is_nullable' => $columnInfo->IS_NULLABLE === 'YES', 168 | 'type' => Schema::getColumnType($table, $column), 169 | 'max_length' => null, 170 | 'allowed_values' => [], 171 | 'name' => $column, 172 | 'table' => $table, 173 | ]; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Stubs/api.controller.stub: -------------------------------------------------------------------------------- 1 | paginate(10)); 17 | } 18 | 19 | public function store({{ request }} $request): {{ resource }}|\Illuminate\Http\JsonResponse 20 | { 21 | try { 22 | ${{ modelVariable }} = {{ model }}::create($request->validated()); 23 | return new {{ resource }}(${{ modelVariable }}); 24 | } catch (\Exception $exception) { 25 | report($exception); 26 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 27 | } 28 | } 29 | 30 | public function show({{ model }} ${{ modelVariable }}): {{ resource }} 31 | { 32 | return {{ resource }}::make(${{ modelVariable }}); 33 | } 34 | 35 | public function update({{ request }} $request, {{ model }} ${{ modelVariable }}): {{ resource }}|\Illuminate\Http\JsonResponse 36 | { 37 | try { 38 | ${{ modelVariable }}->update($request->validated()); 39 | return new {{ resource }}(${{ modelVariable }}); 40 | } catch (\Exception $exception) { 41 | report($exception); 42 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 43 | } 44 | } 45 | 46 | public function destroy({{ model }} ${{ modelVariable }}): \Illuminate\Http\JsonResponse 47 | { 48 | try { 49 | ${{ modelVariable }}->delete(); 50 | return response()->json(['message' => 'Deleted successfully'], Response::HTTP_OK); 51 | } catch (\Exception $exception) { 52 | report($exception); 53 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Stubs/api_repository.controller.stub: -------------------------------------------------------------------------------- 1 | {{ serviceVariable }} = ${{ serviceVariable }}; 28 | } 29 | 30 | public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection 31 | { 32 | return {{ resource }}::collection($this->{{ serviceVariable }}->getAll()); 33 | } 34 | 35 | public function store({{ request }} $request): {{ resource }}|\Illuminate\Http\JsonResponse 36 | { 37 | try { 38 | return new {{ resource }}($this->{{ serviceVariable }}->save($request->validated())); 39 | } catch (\Exception $exception) { 40 | report($exception); 41 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 42 | } 43 | } 44 | 45 | public function show(int $id): {{ resource }} 46 | { 47 | return {{ resource }}::make($this->{{ serviceVariable }}->getById($id)); 48 | } 49 | 50 | public function update({{ request }} $request, int $id): {{ resource }}|\Illuminate\Http\JsonResponse 51 | { 52 | try { 53 | return new {{ resource }}($this->{{ serviceVariable }}->update($request->validated(), $id)); 54 | } catch (\Exception $exception) { 55 | report($exception); 56 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 57 | } 58 | } 59 | 60 | public function destroy(int $id): \Illuminate\Http\JsonResponse 61 | { 62 | try { 63 | $this->{{ serviceVariable }}->deleteById($id); 64 | return response()->json(['message' => 'Deleted successfully'], Response::HTTP_OK); 65 | } catch (\Exception $exception) { 66 | report($exception); 67 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Stubs/api_repository_spatie_data.controller.stub: -------------------------------------------------------------------------------- 1 | {{ serviceVariable }} = ${{ serviceVariable }}; 27 | } 28 | 29 | public function index(): array|\Illuminate\Contracts\Pagination\CursorPaginator|\Illuminate\Contracts\Pagination\Paginator|\Illuminate\Pagination\AbstractCursorPaginator|\Illuminate\Pagination\AbstractPaginator|\Illuminate\Support\Collection|\Illuminate\Support\Enumerable|\Illuminate\Support\LazyCollection|\Spatie\LaravelData\CursorPaginatedDataCollection|\Spatie\LaravelData\DataCollection|\Spatie\LaravelData\PaginatedDataCollection 30 | { 31 | return {{ spatieData }}::collect($this->{{ serviceVariable }}->getAll()); 32 | } 33 | 34 | public function store({{ spatieData }} $data): {{ spatieData }}|\Illuminate\Http\JsonResponse 35 | { 36 | try { 37 | return {{ spatieData }}::from($this->{{ serviceVariable }}->save($data->all())); 38 | } catch (\Exception $exception) { 39 | report($exception); 40 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 41 | } 42 | } 43 | 44 | public function show(int $id): {{ spatieData }} 45 | { 46 | return {{ spatieData }}::from($this->{{ serviceVariable }}->getById($id)); 47 | } 48 | 49 | public function update({{ spatieData }} $data, int $id): {{ spatieData }}|\Illuminate\Http\JsonResponse 50 | { 51 | try { 52 | return {{ spatieData }}::from($this->{{ serviceVariable }}->update($data->all(), $id)); 53 | } catch (\Exception $exception) { 54 | report($exception); 55 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 56 | } 57 | } 58 | 59 | public function destroy(int $id): \Illuminate\Http\JsonResponse 60 | { 61 | try { 62 | $this->{{ serviceVariable }}->deleteById($id); 63 | return response()->json(['message' => 'Deleted successfully'], Response::HTTP_OK); 64 | } catch (\Exception $exception) { 65 | report($exception); 66 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Stubs/api_spatie_data.controller.stub: -------------------------------------------------------------------------------- 1 | paginate(10)); 15 | } 16 | 17 | public function store({{ spatieData }} $data): {{ spatieData }}|\Illuminate\Http\JsonResponse 18 | { 19 | try { 20 | ${{ modelVariable }} = {{ model }}::create($data->all()); 21 | return new {{ spatieData }}(${{ modelVariable }}); 22 | } catch (\Exception $exception) { 23 | report($exception); 24 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 25 | } 26 | } 27 | 28 | public function show({{ model }} ${{ modelVariable }}): {{ spatieData }} 29 | { 30 | return {{ spatieData }}::from(${{ modelVariable }}); 31 | } 32 | 33 | public function update({{ spatieData }} $data, {{ model }} ${{ modelVariable }}): {{ spatieData }}|\Illuminate\Http\JsonResponse 34 | { 35 | try { 36 | ${{ modelVariable }}->update($data->all()); 37 | return {{ spatieData }}::from(${{ modelVariable }}); 38 | } catch (\Exception $exception) { 39 | report($exception); 40 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 41 | } 42 | } 43 | 44 | public function destroy({{ model }} ${{ modelVariable }}): \Illuminate\Http\JsonResponse 45 | { 46 | try { 47 | ${{ modelVariable }}->delete(); 48 | return response()->json(['message' => 'Deleted successfully'], Response::HTTP_OK); 49 | } catch (\Exception $exception) { 50 | report($exception); 51 | return response()->json(['error' => 'There is an error.'], Response::HTTP_INTERNAL_SERVER_ERROR); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Stubs/enum.stub: -------------------------------------------------------------------------------- 1 | {{ modelVariable }} = ${{ modelVariable }}; 21 | } 22 | 23 | /** 24 | * Get all {{ modelVariable }}. 25 | * 26 | * @return {{ model }} ${{ modelVariable }} 27 | */ 28 | public function all() 29 | { 30 | return $this->{{ modelVariable }}->get(); 31 | } 32 | 33 | /** 34 | * Get {{ modelVariable }} by id 35 | * 36 | * @param $id 37 | * @return mixed 38 | */ 39 | public function getById(int $id) 40 | { 41 | return $this->{{ modelVariable }}->find($id); 42 | } 43 | 44 | /** 45 | * Save {{ model }} 46 | * 47 | * @param $data 48 | * @return {{ model }} 49 | */ 50 | public function save(array $data) 51 | { 52 | return {{ model }}::create($data); 53 | } 54 | 55 | /** 56 | * Update {{ model }} 57 | * 58 | * @param $data 59 | * @return {{ model }} 60 | */ 61 | public function update(array $data, int $id) 62 | { 63 | ${{ modelVariable }} = $this->{{ modelVariable }}->find($id); 64 | ${{ modelVariable }}->update($data); 65 | return ${{ modelVariable }}; 66 | } 67 | 68 | /** 69 | * Delete {{ model }} 70 | * 71 | * @param $data 72 | * @return {{ model }} 73 | */ 74 | public function delete(int $id) 75 | { 76 | ${{ modelVariable }} = $this->{{ modelVariable }}->find($id); 77 | ${{ modelVariable }}->delete(); 78 | return ${{ modelVariable }}; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Stubs/request.stub: -------------------------------------------------------------------------------- 1 | {{ repositoryVariable }} = ${{ repositoryVariable }}; 25 | } 26 | 27 | /** 28 | * Get all {{ repositoryVariable }}. 29 | * 30 | * @return String 31 | */ 32 | public function getAll() 33 | { 34 | return $this->{{ repositoryVariable }}->all(); 35 | } 36 | 37 | /** 38 | * Get {{ repositoryVariable }} by id. 39 | * 40 | * @param $id 41 | * @return String 42 | */ 43 | public function getById(int $id) 44 | { 45 | return $this->{{ repositoryVariable }}->getById($id); 46 | } 47 | 48 | /** 49 | * Validate {{ repositoryVariable }} data. 50 | * Store to DB if there are no errors. 51 | * 52 | * @param array $data 53 | * @return String 54 | */ 55 | public function save(array $data) 56 | { 57 | return $this->{{ repositoryVariable }}->save($data); 58 | } 59 | 60 | /** 61 | * Update {{ repositoryVariable }} data 62 | * Store to DB if there are no errors. 63 | * 64 | * @param array $data 65 | * @return String 66 | */ 67 | public function update(array $data, int $id) 68 | { 69 | DB::beginTransaction(); 70 | try { 71 | ${{ repositoryVariable }} = $this->{{ repositoryVariable }}->update($data, $id); 72 | DB::commit(); 73 | return ${{ repositoryVariable }}; 74 | } catch (Exception $e) { 75 | DB::rollBack(); 76 | report($e); 77 | throw new InvalidArgumentException('Unable to update post data'); 78 | } 79 | } 80 | 81 | /** 82 | * Delete {{ repositoryVariable }} by id. 83 | * 84 | * @param $id 85 | * @return String 86 | */ 87 | public function deleteById(int $id) 88 | { 89 | DB::beginTransaction(); 90 | try { 91 | ${{ repositoryVariable }} = $this->{{ repositoryVariable }}->delete($id); 92 | DB::commit(); 93 | return ${{ repositoryVariable }}; 94 | } catch (Exception $e) { 95 | DB::rollBack(); 96 | report($e); 97 | throw new InvalidArgumentException('Unable to delete post data'); 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/Stubs/spatie_data.stub: -------------------------------------------------------------------------------- 1 | paginate(10); 14 | return view('{{ viewPath }}.index', compact('{{ modelPlural }}')); 15 | } 16 | 17 | public function create(): \Illuminate\Contracts\View\View 18 | { 19 | return view('{{ viewPath }}.create'); 20 | } 21 | 22 | public function store({{ request }} $request): \Illuminate\Http\RedirectResponse 23 | { 24 | {{ model }}::create($request->validated()); 25 | return redirect()->route('{{ routeName }}.index')->with('success', 'Created successfully'); 26 | } 27 | 28 | public function show({{ model }} ${{ modelVariable }}): \Illuminate\Contracts\View\View 29 | { 30 | return view('{{ viewPath }}.show', compact('{{ modelVariable }}')); 31 | } 32 | 33 | public function edit({{ model }} ${{ modelVariable }}): \Illuminate\Contracts\View\View 34 | { 35 | return view('{{ viewPath }}.edit', compact('{{ modelVariable }}')); 36 | } 37 | 38 | public function update({{ request }} $request, {{ model }} ${{ modelVariable }}): \Illuminate\Http\RedirectResponse 39 | { 40 | ${{ modelVariable }}->update($request->validated()); 41 | return redirect()->route('{{ routeName }}.index')->with('success', 'Updated successfully'); 42 | } 43 | 44 | public function destroy({{ model }} ${{ modelVariable }}): \Illuminate\Http\RedirectResponse 45 | { 46 | ${{ modelVariable }}->delete(); 47 | return redirect()->route('{{ routeName }}.index')->with('success', 'Deleted successfully'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Stubs/web_repository.controller.stub: -------------------------------------------------------------------------------- 1 | {{ serviceVariable }} = ${{ serviceVariable }}; 25 | } 26 | 27 | public function index(): \Illuminate\Contracts\View\View 28 | { 29 | ${{ modelPlural }} = $this->{{ serviceVariable }}->getAll(); 30 | return view('{{ viewPath }}.index', compact('{{ modelPlural }}')); 31 | } 32 | 33 | public function create(): \Illuminate\Contracts\View\View 34 | { 35 | return view('{{ viewPath }}.create'); 36 | } 37 | 38 | public function store({{ request }} $request): \Illuminate\Http\RedirectResponse 39 | { 40 | $this->{{ serviceVariable }}->save($request->validated()); 41 | return redirect()->route('{{ routeName }}.index')->with('success', 'Created successfully'); 42 | } 43 | 44 | public function show(int $id): \Illuminate\Contracts\View\View 45 | { 46 | ${{ modelVariable }} = $this->{{ serviceVariable }}->getById($id); 47 | return view('{{ viewPath }}.show', compact('{{ modelVariable }}')); 48 | } 49 | 50 | public function edit(int $id): \Illuminate\Contracts\View\View 51 | { 52 | ${{ modelVariable }} = $this->{{ serviceVariable }}->getById($id); 53 | return view('{{ viewPath }}.edit', compact('{{ modelVariable }}')); 54 | } 55 | 56 | public function update({{ request }} $request, int $id): \Illuminate\Http\RedirectResponse 57 | { 58 | $this->{{ serviceVariable }}->update($request->validated(), $id); 59 | return redirect()->route('{{ routeName }}.index')->with('success', 'Updated successfully'); 60 | } 61 | 62 | public function destroy(int $id): \Illuminate\Http\RedirectResponse 63 | { 64 | $this->{{ serviceVariable }}->deleteById($id); 65 | return redirect()->route('{{ routeName }}.index')->with('success', 'Deleted successfully'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Stubs/web_repository_spatie_data.controller.stub: -------------------------------------------------------------------------------- 1 | {{ serviceVariable }} = ${{ serviceVariable }}; 25 | } 26 | 27 | public function index(): \Illuminate\Contracts\View\View 28 | { 29 | ${{ modelPlural }} = {{ spatieData }}::collect($this->{{ serviceVariable }}->getAll()); 30 | return view('{{ viewPath }}.index', compact('{{ modelPlural }}')); 31 | } 32 | 33 | public function create(): \Illuminate\Contracts\View\View 34 | { 35 | return view('{{ viewPath }}.create'); 36 | } 37 | 38 | public function store({{ spatieData }} $data): \Illuminate\Http\RedirectResponse 39 | { 40 | $this->{{ serviceVariable }}->save($data->all()); 41 | return redirect()->route('{{ routeName }}.index')->with('success', 'Created successfully'); 42 | } 43 | 44 | public function show(int $id): \Illuminate\Contracts\View\View 45 | { 46 | ${{ modelVariable }} = $this->{{ serviceVariable }}->getById($id); 47 | return view('{{ viewPath }}.show', compact('{{ modelVariable }}')); 48 | } 49 | 50 | public function edit(int $id): \Illuminate\Contracts\View\View 51 | { 52 | ${{ modelVariable }} = $this->{{ serviceVariable }}->getById($id); 53 | return view('{{ viewPath }}.edit', compact('{{ modelVariable }}')); 54 | } 55 | 56 | public function update({{ spatieData }} $data, int $id): \Illuminate\Http\RedirectResponse 57 | { 58 | $this->{{ serviceVariable }}->update($data->all(), $id); 59 | return redirect()->route('{{ routeName }}.index')->with('success', 'Updated successfully'); 60 | } 61 | 62 | public function destroy(int $id): \Illuminate\Http\RedirectResponse 63 | { 64 | $this->{{ serviceVariable }}->deleteById($id); 65 | return redirect()->route('{{ routeName }}.index')->with('success', 'Deleted successfully'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Stubs/web_spatie_data.controller.stub: -------------------------------------------------------------------------------- 1 | paginate(10)); 14 | return view('{{ viewPath }}.index', compact('{{ modelPlural }}')); 15 | } 16 | 17 | public function create(): \Illuminate\Contracts\View\View 18 | { 19 | return view('{{ viewPath }}.create'); 20 | } 21 | 22 | public function store({{ spatieData }} $data): \Illuminate\Http\RedirectResponse 23 | { 24 | {{ model }}::create($data->all()); 25 | return redirect()->route('{{ routeName }}.index')->with('success', 'Created successfully'); 26 | } 27 | 28 | public function show({{ model }} ${{ modelVariable }}): \Illuminate\Contracts\View\View 29 | { 30 | return view('{{ viewPath }}.show', compact('{{ modelVariable }}')); 31 | } 32 | 33 | public function edit({{ model }} ${{ modelVariable }}): \Illuminate\Contracts\View\View 34 | { 35 | return view('{{ viewPath }}.edit', compact('{{ modelVariable }}')); 36 | } 37 | 38 | public function update({{ spatieData }} $data, {{ model }} ${{ modelVariable }}): \Illuminate\Http\RedirectResponse 39 | { 40 | ${{ modelVariable }}->update($data->all()); 41 | return redirect()->route('{{ routeName }}.index')->with('success', 'Updated successfully'); 42 | } 43 | 44 | public function destroy({{ model }} ${{ modelVariable }}): \Illuminate\Http\RedirectResponse 45 | { 46 | ${{ modelVariable }}->delete(); 47 | return redirect()->route('{{ routeName }}.index')->with('success', 'Deleted successfully'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Traits/TableColumnsTrait.php: -------------------------------------------------------------------------------- 1 | modelService->getFullModelNamespace($modelData); 17 | 18 | return $this->tableColumnsService->getAvailableColumns($table); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Transformers/EnumTransformer.php: -------------------------------------------------------------------------------- 1 | $value) { 12 | $string .= $indent.$value."\n".$indent.$key."\n"; 13 | } 14 | 15 | return $string; 16 | } 17 | 18 | public static function convertNamespacesToString(array $data): string 19 | { 20 | $string = ''; 21 | foreach ($data as $value) { 22 | $string .= $value."\n"; 23 | } 24 | 25 | return $string; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Models/User.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class)->in(__DIR__); 15 | 16 | use Tests\TestCase; 17 | 18 | // uses(TestCase::class)->in(__DIR__); 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | // expect()->extend('toBeOne', function () { 31 | // return $this->toBe(1); 32 | // }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function something() 46 | { 47 | // .. 48 | } 49 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | service = new DatabaseValidatorService; 9 | }); 10 | 11 | test('checkDataBaseConnection returns true when connection is successful', function () { 12 | DB::shouldReceive('connection->getPdo')->once()->andReturn(true); 13 | 14 | expect($this->service->checkDataBaseConnection())->toBeTrue(); 15 | }); 16 | 17 | test('checkDataBaseConnection returns false when connection fails', function () { 18 | DB::shouldReceive('connection->getPdo')->once()->andThrow(new \PDOException); 19 | 20 | expect($this->service->checkDataBaseConnection())->toBeFalse(); 21 | }); 22 | 23 | test('checkTableExists returns true when table exists', function () { 24 | Schema::shouldReceive('hasTable')->with('users')->once()->andReturn(true); 25 | 26 | expect($this->service->checkTableExists('users'))->toBeTrue(); 27 | }); 28 | 29 | test('checkTableExists returns false when table does not exist', function () { 30 | Schema::shouldReceive('hasTable')->with('non_existent_table')->once()->andReturn(false); 31 | 32 | expect($this->service->checkTableExists('non_existent_table'))->toBeFalse(); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/Unit/EnumTransformerTest.php: -------------------------------------------------------------------------------- 1 | toBe("{$indent}case name = 'name';"."\n"); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/Unit/FileServiceTest.php: -------------------------------------------------------------------------------- 1 | app->setBasePath(__DIR__.'/../'); 8 | File::partialMock(); 9 | }); 10 | 11 | it('can create from stub file', function () { 12 | $modelData = [ 13 | 'modelName' => 'User', 14 | 'folders' => null, 15 | 'namespace' => null, 16 | ]; 17 | $service = Mockery::mock(FileService::class)->makePartial(); 18 | 19 | $service->shouldReceive('createFromStub') 20 | ->withArgs([$modelData, 'enum', 'Enums', 'Enum']) 21 | ->andReturn('App\\Enums\\UserEnum'); 22 | 23 | $results = $service->createFromStub($modelData, 'enum', 'Enums', 'Enum'); // Call on mock 24 | 25 | expect($results)->toBe('App\\Enums\\UserEnum'); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /tests/Unit/HelperServiceTest.php: -------------------------------------------------------------------------------- 1 | toBe($expectedOutput); 29 | }); 30 | 31 | it('format array to php syntax', function () { 32 | $expected = HelperService::formatArrayToPhpSyntax(['testing' => 'me']); 33 | $indent = 12; 34 | $indentation = str_repeat(' ', $indent); 35 | $anotherIndent = str_repeat(' ', $indent - 4); 36 | expect($expected)->toBe("[\n{$indentation}'testing' => 'me',\n{$anotherIndent}]"); 37 | }); 38 | 39 | it('format array to php syntax with remove value quotes', function () { 40 | $expected = HelperService::formatArrayToPhpSyntax(['testing' => 'me'], true); 41 | $indent = 12; 42 | $indentation = str_repeat(' ', $indent); 43 | $anotherIndent = str_repeat(' ', $indent - 4); 44 | expect($expected)->toBe("[\n{$indentation}'testing' => me,\n{$anotherIndent}]"); 45 | }); 46 | 47 | it('convert to snake case', function () { 48 | $expected = HelperService::toSnakeCase('hello world'); 49 | expect($expected)->toBe('hello_world'); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/Unit/LaravelAutoCrudServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | app->register(LaravelAutoCrudServiceProvider::class); 10 | }); 11 | 12 | it('registers the GenerateAutoCrudCommand command', function () { 13 | expect(Artisan::all())->toHaveKey('auto-crud:generate'); 14 | expect(Artisan::all()['auto-crud:generate'])->toBeInstanceOf(GenerateAutoCrudCommand::class); 15 | }); 16 | 17 | it('publishes the config file', function () { 18 | $this->artisan('vendor:publish', ['--tag' => 'auto-crud-config']); 19 | expect(file_exists(config_path('laravel_auto_crud.php')))->toBeTrue(); 20 | }); 21 | 22 | it('binds TableColumnsService as a singleton', function () { 23 | // Resolve the service from the container 24 | $instance1 = app(TableColumnsService::class); 25 | $instance2 = app(TableColumnsService::class); 26 | 27 | // Ensure it's an instance of TableColumnsService 28 | expect($instance1)->toBeInstanceOf(TableColumnsService::class); 29 | 30 | // Ensure it returns the same instance (singleton behavior) 31 | expect($instance1)->toBe($instance2); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/Unit/ModelServiceTest.php: -------------------------------------------------------------------------------- 1 | modelsPath = 'app/Models'; 9 | }); 10 | 11 | test('isModelExists returns model namespace if it exists', function () { 12 | $mockFile = Mockery::mock(); 13 | $mockFile->shouldReceive('getRealPath')->andReturn('app/Models/User.php'); 14 | $mockFile->shouldReceive('getFilename')->andReturn('User.php'); 15 | 16 | File::shouldReceive('allFiles')->once()->andReturn([$mockFile]); 17 | 18 | File::shouldReceive('get')->with('app/Models/User.php')->andReturn('modelsPath))->toBe('App\\Models\\User'); 21 | }); 22 | 23 | test('isModelExists returns null if model does not exist', function () { 24 | File::shouldReceive('allFiles')->once()->andReturn([]); 25 | 26 | expect(ModelService::isModelExists('NonExistent', $this->modelsPath))->toBeNull(); 27 | }); 28 | 29 | test('resolveModelName correctly extracts model details', function () { 30 | $result = ModelService::resolveModelName('App\\Models\\User'); 31 | 32 | expect($result)->toMatchArray([ 33 | 'modelName' => 'User', 34 | 'folders' => null, 35 | 'namespace' => 'App\\Models', 36 | ]); 37 | }); 38 | 39 | test('handleModelsPath ensures trailing slash', function () { 40 | expect(ModelService::handleModelsPath('app/Models'))->toBe('app/Models/'); 41 | expect(ModelService::handleModelsPath('app/Models/'))->toBe('app/Models/'); 42 | }); 43 | 44 | it('returns table name when model has namespace and is valid', function () { 45 | // Create an anonymous class extending Model 46 | $mockModel = new class extends Model 47 | { 48 | public function getTable() 49 | { 50 | return 'mock_table'; 51 | } 52 | }; 53 | 54 | // Mock the class creation 55 | $modelData = [ 56 | 'namespace' => 'App\\Models', 57 | 'modelName' => 'TestModel', 58 | ]; 59 | 60 | // Mock the class existence 61 | class_alias(get_class($mockModel), 'App\\Models\\TestModel'); 62 | 63 | $result = ModelService::getFullModelNamespace($modelData); 64 | 65 | expect($result)->toBe('mock_table'); 66 | }); 67 | 68 | it('returns table name when model has no namespace and is valid', function () { 69 | $mockModel = new class extends Model 70 | { 71 | public function getTable() 72 | { 73 | return 'mock_table'; 74 | } 75 | }; 76 | 77 | $modelData = [ 78 | 'namespace' => '', 79 | 'modelName' => 'TestModelNoNamespace', 80 | ]; 81 | 82 | class_alias(get_class($mockModel), 'TestModelNoNamespace'); 83 | 84 | $result = ModelService::getFullModelNamespace($modelData); 85 | 86 | expect($result)->toBe('mock_table'); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/Unit/SpatieDataTransformerTest.php: -------------------------------------------------------------------------------- 1 | '#[Max(254)]']); 8 | expect($service)->toBe("$indent#[Max(254)]\n{$indent}name\n"); 9 | }); 10 | 11 | it('can convert to array', function () { 12 | $service = SpatieDataTransformer::convertNamespacesToString(['userNamespace', 'AnotherNamespace']); 13 | expect($service)->toBe("userNamespace\nAnotherNamespace\n"); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/Unit/TableColumnsServiceTest.php: -------------------------------------------------------------------------------- 1 | once() 11 | ->with('users') 12 | ->andReturn(['id', 'name', 'email', 'status', 'password', 'created_at', 'updated_at']); 13 | }); 14 | 15 | it('returns available columns excluding primary keys and specified columns with mysql', function () { 16 | // Mock DB facade to return MySQL driver 17 | DB::shouldReceive('connection') 18 | ->andReturnSelf() 19 | ->shouldReceive('getDriverName') 20 | ->andReturn('mysql'); 21 | 22 | DB::shouldReceive('select') 23 | ->andReturnUsing(function ($query, $bindings) { 24 | $column = $bindings[0]; 25 | 26 | $mockColumns = [ 27 | 'id' => (object) ['Key' => 'PRI', 'Null' => 'NO', 'Type' => 'int(11)'], 28 | 'name' => (object) ['Key' => '', 'Null' => 'NO', 'Type' => 'varchar(255)'], 29 | 'email' => (object) ['Key' => '', 'Null' => 'NO', 'Type' => 'varchar(255)'], 30 | 'status' => (object) ['Key' => '', 'Null' => 'YES', 'Type' => "enum('active', 'inactive', 'pending')"], 31 | 'password' => (object) ['Key' => '', 'Null' => 'NO', 'Type' => 'varchar(255)'], 32 | 'created_at' => (object) ['Key' => '', 'Null' => 'YES', 'Type' => 'timestamp'], 33 | 'updated_at' => (object) ['Key' => '', 'Null' => 'YES', 'Type' => 'timestamp'], 34 | ]; 35 | 36 | return isset($mockColumns[$column]) ? [$mockColumns[$column]] : []; 37 | }); 38 | 39 | Schema::shouldReceive('getColumnType') 40 | ->andReturn('varchar'); 41 | 42 | // Mock the service and override getColumnDetails 43 | $service = new TableColumnsService; 44 | 45 | // Act: Call getAvailableColumns 46 | $result = $service->getAvailableColumns('users'); 47 | 48 | // Assert: Should return only non-primary, non-excluded columns 49 | expect($result)->toBe([ 50 | [ 51 | 'is_primary_key' => false, 52 | 'is_unique' => false, 53 | 'is_nullable' => false, 54 | 'type' => 'varchar', 55 | 'max_length' => '255', 56 | 'allowed_values' => [], 57 | 'name' => 'name', 58 | 'table' => 'users', 59 | ], 60 | [ 61 | 'is_primary_key' => false, 62 | 'is_unique' => false, 63 | 'is_nullable' => false, 64 | 'type' => 'varchar', 65 | 'max_length' => '255', 66 | 'allowed_values' => [], 67 | 'name' => 'email', 68 | 'table' => 'users', 69 | ], 70 | [ 71 | 'is_primary_key' => false, 72 | 'is_unique' => false, 73 | 'is_nullable' => true, 74 | 'type' => 'varchar', 75 | 'max_length' => null, 76 | 'allowed_values' => [ 77 | 'active', 78 | ' inactive', 79 | ' pending', 80 | ], 81 | 'name' => 'status', 82 | 'table' => 'users', 83 | ], 84 | [ 85 | 'is_primary_key' => false, 86 | 'is_unique' => false, 87 | 'is_nullable' => false, 88 | 'type' => 'varchar', 89 | 'max_length' => '255', 90 | 'allowed_values' => [], 91 | 'name' => 'password', 92 | 'table' => 'users', 93 | ], 94 | ]); 95 | }); 96 | 97 | it('returns empty columns excluding primary keys and specified columns with mysql', function () { 98 | // Mock DB facade to return MySQL driver 99 | DB::shouldReceive('connection') 100 | ->andReturnSelf() 101 | ->shouldReceive('getDriverName') 102 | ->andReturn('mysql'); 103 | 104 | DB::shouldReceive('select') 105 | ->andReturnUsing(function ($query, $bindings) { 106 | $column = $bindings[0]; 107 | $mockColumns = []; 108 | 109 | return isset($mockColumns[$column]) ? [$mockColumns[$column]] : []; 110 | }); 111 | 112 | Schema::shouldReceive('getColumnType') 113 | ->andReturn('varchar'); 114 | 115 | // Mock the service and override getColumnDetails 116 | $service = new TableColumnsService; 117 | 118 | // Act: Call getAvailableColumns 119 | $result = $service->getAvailableColumns('users'); 120 | 121 | // Assert: Should return only non-primary, non-excluded columns 122 | expect($result)->toBe([]); 123 | }); 124 | 125 | it('returns available columns excluding primary keys and specified columns with postgres', function () { 126 | // Mock DB facade to return MySQL driver 127 | DB::shouldReceive('connection') 128 | ->andReturnSelf() 129 | ->shouldReceive('getDriverName') 130 | ->andReturn('pgsql'); 131 | 132 | DB::shouldReceive('select') 133 | ->andReturnUsing(function ($query, $bindings) { 134 | $column = $bindings[1]; // PostgreSQL query uses second binding for column name 135 | 136 | $mockColumns = [ 137 | 'id' => (object) [ 138 | 'column_name' => 'id', 139 | 'is_nullable' => 'NO', 140 | 'data_type' => 'integer', 141 | 'character_maximum_length' => null, 142 | 'udt_name' => 'int4', 143 | 'is_primary' => true, 144 | 'is_unique' => false, 145 | ], 146 | 'name' => (object) [ 147 | 'column_name' => 'name', 148 | 'is_nullable' => 'NO', 149 | 'data_type' => 'character varying', 150 | 'character_maximum_length' => 255, 151 | 'udt_name' => 'varchar', 152 | 'is_primary' => false, 153 | 'is_unique' => false, 154 | ], 155 | 'email' => (object) [ 156 | 'column_name' => 'email', 157 | 'is_nullable' => 'NO', 158 | 'data_type' => 'character varying', 159 | 'character_maximum_length' => 255, 160 | 'udt_name' => 'varchar', 161 | 'is_primary' => false, 162 | 'is_unique' => true, 163 | ], 164 | 'status' => (object) [ 165 | 'column_name' => 'status', 166 | 'is_nullable' => 'NO', 167 | 'data_type' => 'USER-DEFINED', // Enum types in PostgreSQL are user-defined 168 | 'character_maximum_length' => null, // Enum types don't have a character length 169 | 'udt_name' => '_my_enum_type', // Replace with your actual enum type name 170 | 'is_primary' => false, 171 | 'is_unique' => false, 172 | ], 173 | 'password' => (object) [ 174 | 'column_name' => 'password', 175 | 'is_nullable' => 'NO', 176 | 'data_type' => 'character varying', 177 | 'character_maximum_length' => 255, 178 | 'udt_name' => 'varchar', 179 | 'is_primary' => false, 180 | 'is_unique' => false, 181 | ], 182 | 'created_at' => (object) [ 183 | 'column_name' => 'created_at', 184 | 'is_nullable' => 'YES', 185 | 'data_type' => 'timestamp without time zone', 186 | 'character_maximum_length' => null, 187 | 'udt_name' => 'timestamp', 188 | 'is_primary' => false, 189 | 'is_unique' => false, 190 | ], 191 | 'updated_at' => (object) [ 192 | 'column_name' => 'updated_at', 193 | 'is_nullable' => 'YES', 194 | 'data_type' => 'timestamp without time zone', 195 | 'character_maximum_length' => null, 196 | 'udt_name' => 'timestamp', 197 | 'is_primary' => false, 198 | 'is_unique' => false, 199 | ], 200 | ]; 201 | 202 | return isset($mockColumns[$column]) ? [$mockColumns[$column]] : []; 203 | }); 204 | 205 | Schema::shouldReceive('getColumnType') 206 | ->andReturn('varchar'); 207 | 208 | // Mock the service and override getColumnDetails 209 | $service = new TableColumnsService; 210 | 211 | // Act: Call getAvailableColumns 212 | $result = $service->getAvailableColumns('users'); 213 | 214 | // Assert: Should return only non-primary, non-excluded columns 215 | expect($result)->toBe([ 216 | [ 217 | 'is_primary_key' => false, 218 | 'is_unique' => false, 219 | 'is_nullable' => false, 220 | 'type' => 'varchar', 221 | 'max_length' => 255, 222 | 'allowed_values' => [], 223 | 'name' => 'name', 224 | 'table' => 'users', 225 | ], 226 | [ 227 | 'is_primary_key' => false, 228 | 'is_unique' => true, 229 | 'is_nullable' => false, 230 | 'type' => 'varchar', 231 | 'max_length' => 255, 232 | 'allowed_values' => [], 233 | 'name' => 'email', 234 | 'table' => 'users', 235 | ], 236 | [ 237 | 'is_primary_key' => false, 238 | 'is_unique' => false, 239 | 'is_nullable' => false, 240 | 'type' => 'varchar', 241 | 'max_length' => null, 242 | 'allowed_values' => [ 243 | 'my_enum_type', 244 | ], 245 | 'name' => 'status', 246 | 'table' => 'users', 247 | ], 248 | [ 249 | 'is_primary_key' => false, 250 | 'is_unique' => false, 251 | 'is_nullable' => false, 252 | 'type' => 'varchar', 253 | 'max_length' => 255, 254 | 'allowed_values' => [], 255 | 'name' => 'password', 256 | 'table' => 'users', 257 | ], 258 | ]); 259 | }); 260 | 261 | it('returns empty columns excluding primary keys and specified columns with postgres', function () { 262 | // Mock DB facade to return MySQL driver 263 | DB::shouldReceive('connection') 264 | ->andReturnSelf() 265 | ->shouldReceive('getDriverName') 266 | ->andReturn('pgsql'); 267 | 268 | DB::shouldReceive('select') 269 | ->andReturnUsing(function ($query, $bindings) { 270 | $column = $bindings[1]; // PostgreSQL query uses second binding for column name 271 | 272 | $mockColumns = []; 273 | 274 | return isset($mockColumns[$column]) ? [$mockColumns[$column]] : []; 275 | }); 276 | 277 | Schema::shouldReceive('getColumnType') 278 | ->andReturn('varchar'); 279 | 280 | // Mock the service and override getColumnDetails 281 | $service = new TableColumnsService; 282 | 283 | // Act: Call getAvailableColumns 284 | $result = $service->getAvailableColumns('users'); 285 | 286 | // Assert: Should return only non-primary, non-excluded columns 287 | expect($result)->toBe([]); 288 | }); 289 | 290 | it('returns available columns excluding primary keys and specified columns with sqlite', function () { 291 | // Mock DB facade to return MySQL driver 292 | DB::shouldReceive('connection') 293 | ->andReturnSelf() 294 | ->shouldReceive('getDriverName') 295 | ->andReturn('sqlite'); 296 | 297 | DB::shouldReceive('select') 298 | ->andReturnUsing(function ($query) { 299 | $mockColumns = [ 300 | (object) ['name' => 'id', 'type' => 'INTEGER', 'notnull' => 1, 'pk' => 1], 301 | (object) ['name' => 'name', 'type' => 'TEXT', 'notnull' => 1, 'pk' => 0], 302 | (object) ['name' => 'email', 'type' => 'TEXT', 'notnull' => 1, 'pk' => 0], 303 | (object) ['name' => 'password', 'type' => 'TEXT', 'notnull' => 1, 'pk' => 0], 304 | (object) ['name' => 'created_at', 'type' => 'DATETIME', 'notnull' => 0, 'pk' => 0], 305 | (object) ['name' => 'updated_at', 'type' => 'DATETIME', 'notnull' => 0, 'pk' => 0], 306 | ]; 307 | 308 | return $mockColumns; 309 | }); 310 | 311 | Schema::shouldReceive('getColumnType') 312 | ->andReturn('varchar'); 313 | 314 | // Mock the service and override getColumnDetails 315 | $service = new TableColumnsService; 316 | 317 | // Act: Call getAvailableColumns 318 | $result = $service->getAvailableColumns('users'); 319 | 320 | // Assert: Should return only non-primary, non-excluded columns 321 | expect($result)->toBe([ 322 | [ 323 | 'is_primary_key' => false, 324 | 'is_unique' => false, 325 | 'is_nullable' => false, 326 | 'type' => 'varchar', 327 | 'max_length' => null, 328 | 'allowed_values' => [], 329 | 'name' => 'name', 330 | 'table' => 'users', 331 | ], 332 | [ 333 | 'is_primary_key' => false, 334 | 'is_unique' => false, 335 | 'is_nullable' => false, 336 | 'type' => 'varchar', 337 | 'max_length' => null, 338 | 'allowed_values' => [], 339 | 'name' => 'email', 340 | 'table' => 'users', 341 | ], 342 | [ 343 | 'is_primary_key' => false, 344 | 'is_unique' => false, 345 | 'is_nullable' => false, 346 | 'type' => 'varchar', 347 | 'max_length' => null, 348 | 'allowed_values' => [], 349 | 'name' => 'password', 350 | 'table' => 'users', 351 | ], 352 | ]); 353 | }); 354 | 355 | it('returns empty columns excluding primary keys and specified columns with sqlite', function () { 356 | // Mock DB facade to return MySQL driver 357 | DB::shouldReceive('connection') 358 | ->andReturnSelf() 359 | ->shouldReceive('getDriverName') 360 | ->andReturn('sqlite'); 361 | 362 | DB::shouldReceive('select') 363 | ->andReturnUsing(function ($query) { 364 | $mockColumns = []; 365 | 366 | return $mockColumns; 367 | }); 368 | 369 | Schema::shouldReceive('getColumnType') 370 | ->andReturn('varchar'); 371 | 372 | // Mock the service and override getColumnDetails 373 | $service = new TableColumnsService; 374 | 375 | // Act: Call getAvailableColumns 376 | $result = $service->getAvailableColumns('users'); 377 | 378 | // Assert: Should return only non-primary, non-excluded columns 379 | expect($result)->toBe([]); 380 | }); 381 | 382 | it('returns available columns excluding primary keys and specified columns with sql server', function () { 383 | // Mock DB facade to return MySQL driver 384 | DB::shouldReceive('connection') 385 | ->andReturnSelf() 386 | ->shouldReceive('getDriverName') 387 | ->andReturn('sqlsrv'); 388 | 389 | DB::shouldReceive('select') 390 | ->andReturnUsing(function ($query, $bindings) { 391 | $column = $bindings[2]; // The column name is typically the third binding 392 | 393 | $mockColumns = [ 394 | 'id' => (object) [ 395 | 'COLUMN_NAME' => 'id', 396 | 'DATA_TYPE' => 'int', 397 | 'IS_NULLABLE' => 'NO', 398 | 'is_identity' => 1, // Primary key (Identity column) 399 | ], 400 | 'name' => (object) [ 401 | 'COLUMN_NAME' => 'name', 402 | 'DATA_TYPE' => 'varchar', 403 | 'IS_NULLABLE' => 'NO', 404 | 'is_identity' => 0, 405 | ], 406 | 'email' => (object) [ 407 | 'COLUMN_NAME' => 'email', 408 | 'DATA_TYPE' => 'varchar', 409 | 'IS_NULLABLE' => 'NO', 410 | 'is_identity' => 0, 411 | ], 412 | 'password' => (object) [ 413 | 'COLUMN_NAME' => 'password', 414 | 'DATA_TYPE' => 'varchar', 415 | 'IS_NULLABLE' => 'NO', 416 | 'is_identity' => 0, 417 | ], 418 | 'created_at' => (object) [ 419 | 'COLUMN_NAME' => 'created_at', 420 | 'DATA_TYPE' => 'datetime', 421 | 'IS_NULLABLE' => 'YES', 422 | 'is_identity' => 0, 423 | ], 424 | 'updated_at' => (object) [ 425 | 'COLUMN_NAME' => 'updated_at', 426 | 'DATA_TYPE' => 'datetime', 427 | 'IS_NULLABLE' => 'YES', 428 | 'is_identity' => 0, 429 | ], 430 | ]; 431 | 432 | return isset($mockColumns[$column]) ? [$mockColumns[$column]] : []; 433 | }); 434 | 435 | Schema::shouldReceive('getColumnType') 436 | ->andReturn('varchar'); 437 | 438 | // Mock the service and override getColumnDetails 439 | $service = new TableColumnsService; 440 | 441 | // Act: Call getAvailableColumns 442 | $result = $service->getAvailableColumns('users'); 443 | 444 | // Assert: Should return only non-primary, non-excluded columns 445 | expect($result)->toBe([ 446 | [ 447 | 'is_primary_key' => 0, 448 | 'is_unique' => false, 449 | 'is_nullable' => false, 450 | 'type' => 'varchar', 451 | 'max_length' => null, 452 | 'allowed_values' => [], 453 | 'name' => 'name', 454 | 'table' => 'users', 455 | ], 456 | [ 457 | 'is_primary_key' => 0, 458 | 'is_unique' => false, 459 | 'is_nullable' => false, 460 | 'type' => 'varchar', 461 | 'max_length' => null, 462 | 'allowed_values' => [], 463 | 'name' => 'email', 464 | 'table' => 'users', 465 | ], 466 | [ 467 | 'is_primary_key' => 0, 468 | 'is_unique' => false, 469 | 'is_nullable' => false, 470 | 'type' => 'varchar', 471 | 'max_length' => null, 472 | 'allowed_values' => [], 473 | 'name' => 'password', 474 | 'table' => 'users', 475 | ], 476 | ]); 477 | }); 478 | 479 | it('returns empty columns excluding primary keys and specified columns with sql server', function () { 480 | // Mock DB facade to return MySQL driver 481 | DB::shouldReceive('connection') 482 | ->andReturnSelf() 483 | ->shouldReceive('getDriverName') 484 | ->andReturn('sqlsrv'); 485 | 486 | DB::shouldReceive('select') 487 | ->andReturnUsing(function ($query, $bindings) { 488 | $column = $bindings[2]; // The column name is typically the third binding 489 | 490 | $mockColumns = []; 491 | 492 | return isset($mockColumns[$column]) ? [$mockColumns[$column]] : []; 493 | }); 494 | 495 | Schema::shouldReceive('getColumnType') 496 | ->andReturn('varchar'); 497 | 498 | // Mock the service and override getColumnDetails 499 | $service = new TableColumnsService; 500 | 501 | // Act: Call getAvailableColumns 502 | $result = $service->getAvailableColumns('users'); 503 | 504 | // Assert: Should return only non-primary, non-excluded columns 505 | expect($result)->toBe([]); 506 | }); 507 | --------------------------------------------------------------------------------