diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b7dd1d2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: composer + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..f2270ea --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +## WHY + +### BEFORE - What was wrong? What was happening before this PR? + +?? + +### AFTER - What is happening after this PR? + +?? + + +## HOW + +### How did you achieve that, in technical terms? + +?? + + + +### Is it a breaking change or non-breaking change? + +?? + + +### How can we test the before & after? + +?? diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..12d0b41 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,42 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +name-template: 'v$NEXT_PATCH_VERSION 🌈' +tag-template: 'v$NEXT_PATCH_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - 'added' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - 'fixed' + - title: '⚙️ Changes' + labels: + - 'changed' + - 'dependencies' + - title: '🧰 Removed' + label: 'removed' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +template: | + ## Changes + + $CHANGES \ No newline at end of file diff --git a/.github/stale.yml b/.github/stale.yml index 6dd0457..192b9b5 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -4,7 +4,7 @@ # https://probot.github.io/apps/stale/ # Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 +daysUntilStale: 120 # Number of days of inactivity before a stale issue is closed daysUntilClose: 14 # Issues with these labels will never be considered stale diff --git a/.github/support.yml b/.github/support.yml index 4694a98..68ebdf4 100644 --- a/.github/support.yml +++ b/.github/support.yml @@ -16,7 +16,7 @@ supportComment: > Here are all the Backpack communication mediums: - Long questions (_I have done X and Y and it won't do Z wtf_) - [Stackoverflow](https://stackoverflow.com/questions/tagged/backpack-for-laravel), using the ```backpack-for-laravel``` tag; this is recommended for most questions, since other developers can then find the answer on a simple Google search; also, people get points for answering - and who doesn't like StackOverflow points?! - - Quick help (_How do I do X_) - [Gitter Chatroom](gitter.im/BackpackForLaravel/Lobby); + - Quick help (_How do I do X_) - [Gitter Chatroom](https://gitter.im/BackpackForLaravel/Lobby); - Bug Reports, Feature Requests - Github Issues (here); Please keep in mind Backpack offers no official / paid support. Whatever help you receive here, on Gitter, Slack or StackOverflow is thanks to our awesome _awesome_ community members, who give up some of their time to help their peers. If you want to join our community, just start pitching in. We take pride in being a welcoming bunch. diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml new file mode 100644 index 0000000..0998755 --- /dev/null +++ b/.github/workflows/add-to-project.yml @@ -0,0 +1,20 @@ +name: Add new bugs & PRs to This Week project + +on: + issues: + types: + - opened + - transferred + pull_request: + types: + - opened + +jobs: + add-to-project: + name: Add new bugs and PRs to This Week project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/Laravel-Backpack/projects/13 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7de61b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +vendor/ +node_modules/ +.DS_Store +.composer.lock +composer.lock +.phpunit.result.cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index e7859d2..43fbf86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,16 @@ language: php php: - - 7.0 - - 7.1 - 7.2 + - 7.3 + - 7.4 + - 8.0 - nightly matrix: allow_failures: - php: nightly - - php: 7.2 + - php: 8.0 sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 417f9b0..7203cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,36 +2,82 @@ All Notable changes to `Backpack Generators` will be documented in this file -## NEXT - YYYY-MM-DD +------------ +IMPORTANT +------------ + +Starting with version 3, the changelog is kept inside the Releases tab in our this repo's Github page. Please check https://github.com/Laravel-Backpack/Generators/releases + +------------ + +## 2.0.7 - 2020-03-05 + +### Fixed +- Upgraded PHPUnit; + + +## 2.0.6 - 2020-01-06 + +### Fixed +- CrudController typehint; + + +## 2.0.5 - 2019-11-11 + +### Fixed +- when generating CRUDs, route name should only have lowercase letters; + + +## 2.0.4 - 2019-09-28 + +### Fixed +- removed applyConfigurationFromSettings() call from operation stub, since it's now being called automatically by CrudController, before it reaches that method; + + +## 2.0.3 - 2019-09-28 + +### Fixed +- fixed crud command had Class 'Str' not found error in CrudBackpackCommand:53; +- models are now generated with ```$guarded``` instead of ```$fillable``` by default, since the CRUDs now only save what fields have been added by CRUD, not everything that's inside the Request; this should speed up CRUD generation A LOT, by not having to edit the model before you edit the CRUD; it's an opinionated way to do things though - some people prefer $fillable, others $guarded; both work; it's just the default that has changed; + + +## 2.0.2 - 2019-09-17 ### Added -- Nothing +- command to generate a CRUD operation; ex: ```php artisan backpack:crud-operation Moderate``` -### Deprecated -- Nothing + +## 2.0.1 - 2019-09-12 ### Fixed -- Nothing +- it's better for ```setupXxxOperation()``` methods to be ```protected```; + + +## 2.0.0 - 2019-09-12 + +### Added +- Backpack v4 support; +- ```php artisan backpack:crud``` now also generates route and sidebar item; ### Removed -- Nothing +- Backpack v3 support; -### Security -- Nothing + +------------ ## 1.2.7 - 2019-02-27 -## Added +### Added - Backpack\Base 1.1 compatibility; ## 1.2.6 - 2019-01-16 -## Added +### Added - CrudPanel reference to CrudController stb, for IDE code completion; ## 1.2.5 - 2018-11-22 -## Added +### Added - support for Backpack/Base 1.0.0 ## 1.2.4 - 2018-08-27 diff --git a/LICENSE.md b/LICENSE.md index f0007bb..028b2f1 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,21 +1,21 @@ -# The MIT License (MIT) +MIT License -Copyright (c) 2016 Cristian Tone +Copyright (c) 2022 Cristian Tabacitu -> Permission is hereby granted, free of charge, to any person obtaining a copy -> of this software and associated documentation files (the "Software"), to deal -> in the Software without restriction, including without limitation the rights -> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -> copies of the Software, and to permit persons to whom the Software is -> furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in -> all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -> THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 228f972..536e88a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Style CI](https://styleci.io/repos/53490941/shield)](https://styleci.io/repos/53490941) [![Total Downloads](https://img.shields.io/packagist/dt/backpack/generators.svg?style=flat-square)](https://packagist.org/packages/backpack/generators) -Quickly generate Backpack templated Models, Requests, Views and Config files. +Quickly generate Backpack templated Models, Requests, Views and Config files for projects using [Backpack for Laravel](https://backpackforlaravel.com) as their admin panel. > ### Security updates and breaking changes > Please **[subscribe to the Backpack Newsletter](http://backpackforlaravel.com/newsletter)** so you can find out about any security updates, breaking changes or major features. We send an email every 1-2 months. @@ -18,62 +18,110 @@ Quickly generate Backpack templated Models, Requests, Views and Config files. Via Composer ``` bash -composer require backpack/generators --dev +composer require --dev backpack/generators ``` -For Laravel 5.5 - you're done. +## Usage + +Open the console and enter one of the commands: + -For Laravel 5.4 or 5.3 you'll only want to use these generators for ```local``` development, so you don't want to update the ```production``` providers array in ```config/app.php```. Instead, add the provider in ```app/Providers/AppServiceProvider.php```, like so: +- **Generate Backpack\CRUD interfaces for all Eloquent models that don't already have one:** -```php -public function register() -{ - if ($this->app->environment() == 'local') { - // $this->app->register('Laracasts\Generators\GeneratorsServiceProvider'); // you're using Jeffrey way's generators, too, right? - $this->app->register('Backpack\Generators\GeneratorsServiceProvider'); - } -} +```bash +php artisan backpack:build ``` -## Usage +- **Generate all files for one new Backpack\CRUD interface:** + +``` bash +php artisan backpack:crud {Entity_name} + +# Use singular, either PascalCase, snake_case or kebab-case. +# This will create a Model if there isn't one, or add +# our CrudTrait to the model if it already exists. +``` + +- **Generate all files for a custom admin panel page:** -Open the console and enter one of the commands to generate: +``` bash +php artisan backpack:page {PageName} + +# You can use either PascalCase, snake_case or kebab-case. +# This will generate you a Controller, a view and a route. +``` + +- Generate a new Backpack\CRUD file: +``` bash +php artisan backpack:crud-controller {Entity_name} +php artisan backpack:crud-model {Entity_name} +php artisan backpack:crud-request {Entity_name} +``` -- Models (available options: --softdelete) +- Generate a model (available options: --softdelete) ``` bash php artisan backpack:model {Entity_name} ``` -- Requests +- Generate a request ``` bash php artisan backpack:request {Entity_name} ``` -- Views (available options: --plain) +- Generate a view (available options: --plain) ``` bash php artisan backpack:view {Entity_name} ``` -- Config files +- Generate a config file ``` bash php artisan backpack:config {Entity_name} ``` -- All files for a new Backpack\CRUD interface: +- Generate a button ``` bash -php artisan backpack:crud {Entity_name} +php artisan backpack:button {button_name} ``` -- A new Backpack\CRUD file: +- Generate a field + ``` bash -php artisan backpack:crud-controller {Entity_name} -php artisan backpack:crud-model {Entity_name} -php artisan backpack:crud-request {Entity_name} +php artisan backpack:field {field_name} + +// or generate a field starting from another field +php artisan backpack:field {field_name} --from={original_field_name} +``` + +- Generate a column + +``` bash +php artisan backpack:column {column_name} + +// or generate a column starting from another column +php artisan backpack:column {column_name} --from={original_column_name} +``` + +- Generate a filter + +``` bash +php artisan backpack:filter {filter_name} + +// or generate a filter starting from another filter +php artisan backpack:filter {filter_name} --from={original_filter_name} +``` + +- Generate a widget + +``` bash +php artisan backpack:widget {widget_name} + +// or generate a widget starting from another widget +php artisan backpack:widget {widget_name} --from={original_widget_name} ``` ## Change log @@ -92,10 +140,10 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details. ## License -The MIT License (MIT). Please see [License File](LICENSE.md) for more information. +Backpack is free for non-commercial use and 69 EUR/project for commercial use. Please see [License File](LICENSE.md) and [backpackforlaravel.com](https://backpackforlaravel.com/#pricing) for more information. ## Hire us We've spend more than 50.000 hours creating, polishing and maintaining administration panels on Laravel. We've developed e-Commerce, e-Learning, ERPs, social networks, payment gateways and much more. We've worked on admin panels _so much_, that we've created one of the most popular software in its niche - just from making public what was repetitive in our projects. -If you are looking for a developer/team to help you build an admin panel on Laravel, look no further. You'll have a difficult time finding someone with more experience & enthusiasm for this. This is _what we do_. [Contact us - let's see if we can work together](https://backpackforlaravel.com/need-freelancer-or-development-team). +If you are looking for a developer/team to help you build an admin panel on Laravel, look no further. You'll have a difficult time finding someone with more experience & enthusiasm for this. This is _what we do_. [Contact us](https://backpackforlaravel.com/need-freelancer-or-development-team). Let's see if we can work together. diff --git a/composer.json b/composer.json index cc53c2f..f2012fc 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,12 @@ "homepage": "https://github.com/laravel-backpack/generators", "license": "proprietary", "authors": [ + { + "name": "Cristian Tabacitu", + "email": "tabacitu@backpackforlaravel.com", + "homepage": "https://backpackforlaravel.com", + "role": "Lead Developer & Maintainer" + }, { "name": "Cristian Tone", "email": "cristitone@outlook.com", @@ -19,10 +25,11 @@ } ], "require": { - "backpack/base": "0.9.*|1.0.*|1.1.*" + "php": "^7.4|^8.0", + "backpack/crud": "^5.3.11" }, "require-dev": { - "phpunit/phpunit" : "4.*", + "phpunit/phpunit" : "^9.0||^7.0", "scrutinizer/ocular": "~1.1" }, "autoload": { @@ -31,7 +38,7 @@ } }, "scripts": { - "test": "phpunit" + "test": "vendor/bin/phpunit --testdox" }, "extra": { "branch-alias": { diff --git a/phpunit.xml b/phpunit.xml index 3347b75..422eeac 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" > diff --git a/src/Console/Commands/BuildBackpackCommand.php b/src/Console/Commands/BuildBackpackCommand.php new file mode 100644 index 0000000..99a5a2a --- /dev/null +++ b/src/Console/Commands/BuildBackpackCommand.php @@ -0,0 +1,112 @@ +getModels(base_path('app')); + + if (! count($models)) { + $this->errorBlock('No models found.'); + + return false; + } + + foreach ($models as $model) { + $this->call('backpack:crud', ['name' => $model, '--validation' => $this->option('validation')]); + $this->line(' ----------'); + } + + $this->deleteLines(); + } + + private function getModels(string $path): array + { + $out = []; + $results = scandir($path); + + foreach ($results as $result) { + $filepath = "$path/$result"; + + // ignore `.` (dot) prefixed files + if ($result[0] === '.') { + continue; + } + + if (is_dir($filepath)) { + $out = array_merge($out, $this->getModels($filepath)); + continue; + } + + // Try to load it by path as namespace + $class = (string) Str::of($filepath) + ->after(base_path()) + ->trim('\\/') + ->replace('/', '\\') + ->before('.php') + ->ucfirst(); + + $result = $this->validateModelClass($class); + if ($result) { + $out[] = $result; + continue; + } + + // Try to load it from file content + $fileContent = Str::of(file_get_contents($filepath)); + $namespace = $fileContent->match('/namespace (.*);/')->value(); + $classname = $fileContent->match('/class (\w+)/')->value(); + + $result = $this->validateModelClass("$namespace\\$classname"); + if ($result) { + $out[] = $result; + continue; + } + } + + return $out; + } + + private function validateModelClass(string $class): ?string + { + try { + $reflection = new \ReflectionClass($class); + + if ($reflection->isSubclassOf(Model::class) && ! $reflection->isAbstract()) { + return Str::of($class)->afterLast('\\'); + } + } catch (\Throwable$e) { + } + + return null; + } +} diff --git a/src/Console/Commands/ChartBackpackCommand.php b/src/Console/Commands/ChartBackpackCommand.php new file mode 100644 index 0000000..73dc37d --- /dev/null +++ b/src/Console/Commands/ChartBackpackCommand.php @@ -0,0 +1,42 @@ +argument('name')); // aka Pascal Case + $kebabName = Str::kebab($this->argument('name')); + + // Create the ChartController and show output + $this->call('backpack:chart-controller', ['name' => $studlyName]); + + // Create the chart route + $this->call('backpack:add-custom-route', [ + 'code' => "Route::get('charts/{$kebabName}', 'Charts\\{$studlyName}ChartController@response')->name('charts.{$kebabName}.index');", + ]); + } +} diff --git a/src/Console/Commands/ChartControllerBackpackCommand.php b/src/Console/Commands/ChartControllerBackpackCommand.php new file mode 100644 index 0000000..01aa5ca --- /dev/null +++ b/src/Console/Commands/ChartControllerBackpackCommand.php @@ -0,0 +1,100 @@ +laravel->getNamespace(), '', $name); + + return $this->laravel['path'].'/'.str_replace('\\', '/', $name).'ChartController.php'; + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__.'/../stubs/chart-controller.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace.'\Http\Controllers\Admin\Charts'; + } + + /** + * Replace the table name for the given stub. + * + * @param string $stub + * @param string $name + * @return string + */ + protected function replaceRouteStrings(&$stub) + { + $stub = str_replace('dummy-class', Str::kebab($this->argument('name')), $stub); + + return $this; + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name) + { + $stub = $this->files->get($this->getStub()); + + return $this->replaceNamespace($stub, $name) + ->replaceRouteStrings($stub) + ->replaceClass($stub, $name); + } +} diff --git a/src/Console/Commands/ConfigBackpackCommand.php b/src/Console/Commands/ConfigBackpackCommand.php index 6d8d233..84f939a 100644 --- a/src/Console/Commands/ConfigBackpackCommand.php +++ b/src/Console/Commands/ConfigBackpackCommand.php @@ -44,30 +44,19 @@ protected function getStub() return __DIR__.'/../stubs/config.stub'; } - /** - * Alias for the fire method. - * - * In Laravel 5.5 the fire() method has been renamed to handle(). - * This alias provides support for both Laravel 5.4 and 5.5. - */ - public function handle() - { - $this->fire(); - } - /** * Execute the console command. * * @return bool|null */ - public function fire() + public function handle() { $name = $this->getNameInput(); $path = $this->getPath($name); if ($this->alreadyExists($this->getNameInput())) { - $this->error($this->type.' already exists!'); + $this->error($this->type.' already existed!'); return false; } @@ -82,8 +71,7 @@ public function fire() /** * Determine if the class already exists. * - * @param string $name - * + * @param string $name * @return bool */ protected function alreadyExists($name) @@ -94,8 +82,7 @@ protected function alreadyExists($name) /** * Get the destination class path. * - * @param string $name - * + * @param string $name * @return string */ protected function getPath($name) @@ -106,24 +93,11 @@ protected function getPath($name) /** * Build the class with the given name. * - * @param string $name - * + * @param string $name * @return string */ protected function buildClass($name) { return $this->files->get($this->getStub()); } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - - ]; - } } diff --git a/src/Console/Commands/CrudBackpackCommand.php b/src/Console/Commands/CrudBackpackCommand.php index 0bd5694..466d840 100644 --- a/src/Console/Commands/CrudBackpackCommand.php +++ b/src/Console/Commands/CrudBackpackCommand.php @@ -2,17 +2,20 @@ namespace Backpack\Generators\Console\Commands; -use Artisan; -use Illuminate\Console\Command; +use Backpack\Generators\Services\BackpackCommand; +use Illuminate\Support\Str; -class CrudBackpackCommand extends Command +class CrudBackpackCommand extends BackpackCommand { + use \Backpack\CRUD\app\Console\Commands\Traits\PrettyCommandOutput; + /** * The name and signature of the console command. * * @var string */ - protected $signature = 'backpack:crud {name}'; + protected $signature = 'backpack:crud {name} + {--validation= : Validation type, must be request, array or field}'; /** * The console command description. @@ -28,18 +31,100 @@ class CrudBackpackCommand extends Command */ public function handle() { - $name = ucfirst($this->argument('name')); + $name = $this->getNameInput(); + $nameTitle = $this->buildCamelName($name); + $nameKebab = $this->buildKebabName($nameTitle); + $fullNameWithSpaces = $this->buildNameWithSpaces($nameTitle); - // Create the CRUD Controller and show output - Artisan::call('backpack:crud-controller', ['name' => $name]); - echo Artisan::output(); + // Validate if the name is reserved + if ($this->isReservedName($nameTitle)) { + $this->errorBlock("The name '$nameTitle' is reserved by PHP."); + + return false; + } + + $this->infoBlock("Creating CRUD for the $nameTitle model:"); + + // Validate validation option + $validation = $this->handleValidationOption(); + if (! $validation) { + return false; + } // Create the CRUD Model and show output - Artisan::call('backpack:crud-model', ['name' => $name]); - echo Artisan::output(); + $this->call('backpack:crud-model', ['name' => $name]); + + // Create the CRUD Controller and show output + $this->call('backpack:crud-controller', ['name' => $name, '--validation' => $validation]); // Create the CRUD Request and show output - Artisan::call('backpack:crud-request', ['name' => $name]); - echo Artisan::output(); + if ($validation === 'request') { + $this->call('backpack:crud-request', ['name' => $name]); + } + + // Create the CRUD route + $this->call('backpack:add-custom-route', [ + 'code' => "Route::crud('$nameKebab', '{$this->convertSlashesForNamespace($nameTitle)}CrudController');", + ]); + + // Create the sidebar item + $this->call('backpack:add-sidebar-content', [ + 'code' => "
  • $fullNameWithSpaces
  • ", + ]); + + // if the application uses cached routes, we should rebuild the cache so the previous added route will + // be acessible without manually clearing the route cache. + if (app()->routesAreCached()) { + $this->call('route:cache'); + } + + $url = Str::of(config('app.url'))->finish('/')->append('admin/')->append($nameKebab); + + $this->newLine(); + $this->line(" Done! Go to $url to see the CRUD in action."); + $this->newLine(); + } + + /** + * Handle validation Option. + * + * @return string + */ + private function handleValidationOption() + { + $options = ['request', 'array', 'field']; + + // Validate validation option + $validation = $this->option('validation'); + + if (! $validation) { + $validation = $this->askHint( + 'How would you like to define your validation rules, for the Create and Update operations?', [ + 'More info at https://backpackforlaravel.com/docs/5.x/crud-operation-create#validation', + 'Valid options are request, array or field', + ], $options[0]); + + if (! $this->option('no-interaction')) { + $this->deleteLines(5); + } + } + + if (! in_array($validation, $options)) { + $this->errorBlock("The validation must be request, array or field. '$validation' is not valid."); + + return false; + } + + return $validation; + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return false; } } diff --git a/src/Console/Commands/CrudControllerBackpackCommand.php b/src/Console/Commands/CrudControllerBackpackCommand.php index c40c270..60f5a56 100644 --- a/src/Console/Commands/CrudControllerBackpackCommand.php +++ b/src/Console/Commands/CrudControllerBackpackCommand.php @@ -2,10 +2,13 @@ namespace Backpack\Generators\Console\Commands; -use Illuminate\Console\GeneratorCommand; +use Backpack\Generators\Services\BackpackCommand; +use Illuminate\Support\Str; -class CrudControllerBackpackCommand extends GeneratorCommand +class CrudControllerBackpackCommand extends BackpackCommand { + use \Backpack\CRUD\app\Console\Commands\Traits\PrettyCommandOutput; + /** * The console command name. * @@ -18,7 +21,8 @@ class CrudControllerBackpackCommand extends GeneratorCommand * * @var string */ - protected $signature = 'backpack:crud-controller {name}'; + protected $signature = 'backpack:crud-controller {name} + {--validation=request : Validation type, must be request, array or field}'; /** * The console command description. @@ -35,10 +39,45 @@ class CrudControllerBackpackCommand extends GeneratorCommand protected $type = 'Controller'; /** - * Get the destination class path. + * Execute the console command. * - * @param string $name + * @return bool|null * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function handle() + { + $name = $this->getNameInput(); + $nameTitle = $this->buildCamelName($name); + $qualifiedClassName = $this->qualifyClass($nameTitle); + $path = $this->getPath($qualifiedClassName); + $relativePath = Str::of($path)->after(base_path())->trim('\\/'); + + $this->progressBlock("Creating Controller $relativePath"); + + // Next, We will check to see if the class already exists. If it does, we don't want + // to create the class and overwrite the user's code. So, we will bail out so the + // code is untouched. Otherwise, we will continue generating this class' files. + if ((! $this->hasOption('force') || ! $this->option('force')) && $this->alreadyExists($this->getNameInput())) { + $this->closeProgressBlock('Already existed', 'yellow'); + + return false; + } + + // Next, we will generate the path to the location where this class' file should get + // written. Then, we will build the class and make the proper replacements on the + // stub files so that it gets the correctly formatted namespace and class name. + $this->makeDirectory($path); + + $this->files->put($path, $this->sortImports($this->buildClass($nameTitle))); + + $this->closeProgressBlock(); + } + + /** + * Get the destination class path. + * + * @param string $name * @return string */ protected function getPath($name) @@ -61,8 +100,7 @@ protected function getStub() /** * Get the default namespace for the class. * - * @param string $rootNamespace - * + * @param string $rootNamespace * @return string */ protected function getDefaultNamespace($rootNamespace) @@ -73,44 +111,139 @@ protected function getDefaultNamespace($rootNamespace) /** * Replace the table name for the given stub. * - * @param string $stub - * @param string $name - * + * @param string $stub + * @param string $name * @return string */ protected function replaceNameStrings(&$stub, $name) { - $table = str_plural(ltrim(strtolower(preg_replace('/[A-Z]/', '_$0', str_replace($this->getNamespace($name).'\\', '', $name))), '_')); + $nameTitle = Str::afterLast($name, '\\'); + $nameKebab = $this->buildKebabName($name); + $nameSingular = $this->buildSingularName($nameKebab); + $namePlural = $this->buildPluralName($nameSingular); - $stub = str_replace('DummyTable', $table, $stub); - $stub = str_replace('dummy_class', strtolower(str_replace($this->getNamespace($name).'\\', '', $name)), $stub); + $stub = str_replace('DummyModelClass', $this->convertSlashesForNamespace($nameTitle), $stub); + $stub = str_replace('DummyClass', $this->buildClassName($nameTitle), $stub); + $stub = str_replace('dummy-class', $nameKebab, $stub); + $stub = str_replace('dummy singular', $nameSingular, $stub); + $stub = str_replace('dummy plural', $namePlural, $stub); return $this; } + protected function getAttributes($model) + { + $attributes = []; + $model = new $model(); + + // if fillable was defined, use that as the attributes + if (count($model->getFillable())) { + $attributes = $model->getFillable(); + } else { + // otherwise, if guarded is used, just pick up the columns straight from the bd table + $attributes = \Schema::getColumnListing($model->getTable()); + } + + return $attributes; + } + /** - * Build the class with the given name. + * Replace the table name for the given stub. * - * @param string $name + * @param string $stub + * @param string $name + * @return string + */ + protected function replaceSetFromDb(&$stub, $name) + { + $class = Str::afterLast($name, '\\'); + $model = "App\\Models\\$class"; + + if (! class_exists($model)) { + return $this; + } + + $attributes = $this->getAttributes($model); + $fields = array_diff($attributes, ['id', 'created_at', 'updated_at', 'deleted_at']); + $columns = array_diff($attributes, ['id']); + $glue = PHP_EOL.' '; + + // create an array with the needed code for defining fields + $fields = collect($fields) + ->map(function ($field) { + return "CRUD::field('$field');"; + }) + ->join($glue); + + // create an array with the needed code for defining columns + $columns = collect($columns) + ->map(function ($column) { + return "CRUD::column('$column');"; + }) + ->join($glue); + + // replace setFromDb with actual fields and columns + $stub = str_replace('CRUD::setFromDb(); // fields', $fields, $stub); + $stub = str_replace('CRUD::setFromDb(); // columns', $columns, $stub); + + return $this; + } + + /** + * Replace the class name for the given stub. * + * @param string $stub + * @param string $name * @return string */ - protected function buildClass($name) + protected function replaceModel(&$stub, $name) { - $stub = $this->files->get($this->getStub()); + $class = str_replace($this->getNamespace($name).'\\', '', $name); + $stub = str_replace(['DummyClass', '{{ class }}', '{{class}}'], $this->buildClassName($name), $stub); + + return $this; + } + + /** + * Replace the class name for the given stub. + * + * @param string $stub + * @param string $name + * @return string + */ + protected function replaceRequest(&$stub) + { + $validation = $this->option('validation'); + + // replace request class when validation is array + if ($validation === 'array') { + $stub = str_replace('DummyClassRequest::class', "[\n // 'name' => 'required|min:2',\n ]", $stub); + } + + // remove the validation class when validation is field + if ($validation === 'field') { + $stub = str_replace(" CRUD::setValidation(DummyClassRequest::class);\n\n", '', $stub); + } - return $this->replaceNamespace($stub, $name)->replaceNameStrings($stub, $name)->replaceClass($stub, $name); + return $this; } /** - * Get the console command options. + * Build the class with the given name. * - * @return array + * @param string $name + * @return string */ - protected function getOptions() + protected function buildClass($name) { - return [ + $stub = $this->files->get($this->getStub()); + + $this->replaceNamespace($stub, $this->qualifyClass($name)) + ->replaceRequest($stub) + ->replaceNameStrings($stub, $this->buildCamelName($name)) + ->replaceModel($stub, $this->buildCamelName($name)) + ->replaceSetFromDb($stub, $this->buildCamelName($name)); - ]; + return $stub; } } diff --git a/src/Console/Commands/CrudModelBackpackCommand.php b/src/Console/Commands/CrudModelBackpackCommand.php index f5bceeb..cb000bb 100644 --- a/src/Console/Commands/CrudModelBackpackCommand.php +++ b/src/Console/Commands/CrudModelBackpackCommand.php @@ -2,11 +2,14 @@ namespace Backpack\Generators\Console\Commands; +use Backpack\Generators\Services\BackpackCommand; +use Illuminate\Support\Facades\File; use Illuminate\Support\Str; -use Illuminate\Console\GeneratorCommand; -class CrudModelBackpackCommand extends GeneratorCommand +class CrudModelBackpackCommand extends BackpackCommand { + use \Backpack\CRUD\app\Console\Commands\Traits\PrettyCommandOutput; + /** * The console command name. * @@ -36,38 +39,127 @@ class CrudModelBackpackCommand extends GeneratorCommand protected $type = 'Model'; /** - * Get the stub file for the generator. + * The trait that allows a model to have an admin panel. * - * @return string + * @var string */ - protected function getStub() + protected $crudTrait = 'Backpack\CRUD\app\Models\Traits\CrudTrait'; + + /** + * Execute the console command. + * + * @return bool|null + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function handle() { - return __DIR__.'/../stubs/crud-model.stub'; + $name = $this->getNameInput(); + $nameTitle = $this->buildCamelName($name); + $namespaceApp = $this->qualifyClass($nameTitle); + $namespaceModels = $this->qualifyClass('/Models/'.$nameTitle); + $relativePath = $this->buildRelativePath($namespaceModels); + + $this->progressBlock("Creating Model $relativePath"); + + // Check if exists on app or models + $existsOnApp = $this->alreadyExists($namespaceApp); + $existsOnModels = $this->alreadyExists($namespaceModels); + + // If no model was found, we will generate the path to the location where this class file + // should be written. Then, we will build the class and make the proper replacements on + // the stub files so that it gets the correctly formatted namespace and class name. + if (! $existsOnApp && ! $existsOnModels) { + $this->makeDirectory($this->getPath($namespaceModels)); + + $this->files->put($this->getPath($namespaceModels), $this->sortImports($this->buildClass($nameTitle))); + + $this->closeProgressBlock(); + + return false; + } + + // Model exists + $this->closeProgressBlock('Already existed', 'yellow'); + + // If it was found on both namespaces, we'll ask user to pick one of them + if ($existsOnApp && $existsOnModels) { + $result = $this->choice('Multiple models with this name were found, which one do you want to use?', [ + 1 => "Use $namespaceApp", + 2 => "Use $namespaceModels", + ]); + + // Disable the namespace not selected + $existsOnApp = $result === 1; + $existsOnModels = $result === 2; + } + + $name = $existsOnApp ? $namespaceApp : $namespaceModels; + $path = $this->getPath($name); + + // As the class already exists, we don't want to create the class and overwrite the + // user's code. We just make sure it uses CrudTrait. We add that one line. + if (! $this->hasOption('force') || ! $this->option('force')) { + $this->progressBlock('Adding CrudTrait to the Model'); + + $content = Str::of($this->files->get($path)); + + // check if it already uses CrudTrait + // if it does, do nothing + if ($content->contains($this->crudTrait)) { + $this->closeProgressBlock('Already existed', 'yellow'); + + return false; + } else { + $modifiedContent = Str::of($content->before('namespace')) + ->append('namespace'.$content->after('namespace')->before(';')) + ->append(';'.PHP_EOL.PHP_EOL.'use Backpack\CRUD\app\Models\Traits\CrudTrait;'); + + $content = $content->after('namespace')->after(';'); + + while (str_starts_with($content, PHP_EOL) || str_starts_with($content, "\n")) { + $content = substr($content, 1); + } + + $modifiedContent = $modifiedContent->append(PHP_EOL.$content); + + // use the CrudTrait on the class + $modifiedContent = $modifiedContent->replaceFirst('{', '{'.PHP_EOL.' use CrudTrait;'); + + // save the file + $this->files->put($path, $modifiedContent); + // let the user know what we've done + $this->closeProgressBlock(); + + return true; + } + // In case we couldn't add the CrudTrait + $this->errorProgressBlock(); + $this->note("Model already existed on '$name' and we couldn't add CrudTrait. Please add it manually.", 'red'); + } } /** - * Get the default namespace for the class. - * - * @param string $rootNamespace + * Get the stub file for the generator. * * @return string */ - protected function getDefaultNamespace($rootNamespace) + protected function getStub() { - return $rootNamespace.'\Models'; + return __DIR__.'/../stubs/crud-model.stub'; } /** * Replace the table name for the given stub. * - * @param string $stub - * @param string $name - * + * @param string $stub + * @param string $name * @return string */ protected function replaceTable(&$stub, $name) { - $name = ltrim(strtolower(preg_replace('/[A-Z]/', '_$0', str_replace($this->getNamespace($name).'\\', '', $name))), '_'); + $name = str_replace('/', '', $this->buildCamelName($name)); + $name = ltrim(strtolower(preg_replace('/[A-Z]/', '_$0', $name)), '_'); $table = Str::snake(Str::plural($name)); @@ -79,26 +171,13 @@ protected function replaceTable(&$stub, $name) /** * Build the class with the given name. * - * @param string $name - * + * @param string $name * @return string */ protected function buildClass($name) { $stub = $this->files->get($this->getStub()); - return $this->replaceNamespace($stub, $name)->replaceTable($stub, $name)->replaceClass($stub, $name); - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - - ]; + return $this->replaceNamespace($stub, $this->qualifyClass('/Models/'.$name))->replaceTable($stub, $name)->replaceClass($stub, $this->buildClassName($name)); } } diff --git a/src/Console/Commands/CrudOperationBackpackCommand.php b/src/Console/Commands/CrudOperationBackpackCommand.php new file mode 100644 index 0000000..29f9cd5 --- /dev/null +++ b/src/Console/Commands/CrudOperationBackpackCommand.php @@ -0,0 +1,119 @@ +laravel->getNamespace(), '', $name); + + return $this->laravel['path'].'/'.str_replace('\\', '/', $name).'Operation.php'; + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__.'/../stubs/crud-operation.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace.'\Http\Controllers\Admin\Operations'; + } + + /** + * Replace the table name for the given stub. + * + * @param string $stub + * @param string $name + * @return string + */ + protected function replaceNameStrings(&$stub, $name) + { + $name = Str::of($name)->afterLast('\\'); + + $stub = str_replace('DummyClass', $name->studly(), $stub); + $stub = str_replace('dummyClass', $name->lcfirst(), $stub); + $stub = str_replace('Dummy Class', $name->snake()->replace('_', ' ')->title(), $stub); + $stub = str_replace('dummy-class', $name->snake('-'), $stub); + $stub = str_replace('dummy_class', $name->snake(), $stub); + + return $this; + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name) + { + $stub = $this->files->get($this->getStub()); + + return $this + ->replaceNamespace($stub, $name) + ->replaceNameStrings($stub, $name) + ->replaceClass($stub, $name); + } + + /** + * Get the desired class name from the input. + * + * @return string + */ + protected function getNameInput() + { + return Str::of($this->argument('name')) + ->trim() + ->studly(); + } +} diff --git a/src/Console/Commands/CrudRequestBackpackCommand.php b/src/Console/Commands/CrudRequestBackpackCommand.php index 4f345e6..cc5cf09 100644 --- a/src/Console/Commands/CrudRequestBackpackCommand.php +++ b/src/Console/Commands/CrudRequestBackpackCommand.php @@ -2,10 +2,13 @@ namespace Backpack\Generators\Console\Commands; -use Illuminate\Console\GeneratorCommand; +use Backpack\Generators\Services\BackpackCommand; +use Illuminate\Support\Str; -class CrudRequestBackpackCommand extends GeneratorCommand +class CrudRequestBackpackCommand extends BackpackCommand { + use \Backpack\CRUD\app\Console\Commands\Traits\PrettyCommandOutput; + /** * The console command name. * @@ -35,10 +38,45 @@ class CrudRequestBackpackCommand extends GeneratorCommand protected $type = 'Request'; /** - * Get the destination class path. + * Execute the console command. * - * @param string $name + * @return bool|null * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function handle() + { + $name = $this->getNameInput(); + $nameTitle = $this->buildCamelName($name); + $qualifiedClassName = $this->qualifyClass($nameTitle); + $path = $this->getPath($qualifiedClassName); + $relativePath = Str::of($path)->after(base_path())->trim('\\/'); + + $this->progressBlock("Creating Request $relativePath"); + + // Next, We will check to see if the class already exists. If it does, we don't want + // to create the class and overwrite the user's code. So, we will bail out so the + // code is untouched. Otherwise, we will continue generating this class' files. + if ((! $this->hasOption('force') || ! $this->option('force')) && $this->alreadyExists($this->getNameInput())) { + $this->closeProgressBlock('Already existed', 'yellow'); + + return false; + } + + // Next, we will generate the path to the location where this class' file should get + // written. Then, we will build the class and make the proper replacements on the + // stub files so that it gets the correctly formatted namespace and class name. + $this->makeDirectory($path); + + $this->files->put($path, $this->sortImports($this->buildClass($qualifiedClassName))); + + $this->closeProgressBlock(); + } + + /** + * Get the destination class path. + * + * @param string $name * @return string */ protected function getPath($name) @@ -61,24 +99,11 @@ protected function getStub() /** * Get the default namespace for the class. * - * @param string $rootNamespace - * + * @param string $rootNamespace * @return string */ protected function getDefaultNamespace($rootNamespace) { return $rootNamespace.'\Http\Requests'; } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - - ]; - } } diff --git a/src/Console/Commands/ModelBackpackCommand.php b/src/Console/Commands/ModelBackpackCommand.php index d9679c0..942a8b8 100644 --- a/src/Console/Commands/ModelBackpackCommand.php +++ b/src/Console/Commands/ModelBackpackCommand.php @@ -2,8 +2,8 @@ namespace Backpack\Generators\Console\Commands; -use Illuminate\Support\Str; use Illuminate\Console\GeneratorCommand; +use Illuminate\Support\Str; class ModelBackpackCommand extends GeneratorCommand { @@ -52,21 +52,19 @@ protected function getStub() /** * Get the default namespace for the class. * - * @param string $rootNamespace - * + * @param string $rootNamespace * @return string */ protected function getDefaultNamespace($rootNamespace) { - return $rootNamespace; + return $rootNamespace.'\Models'; } /** * Replace the table name for the given stub. * - * @param string $stub - * @param string $name - * + * @param string $stub + * @param string $name * @return string */ protected function replaceTable(&$stub, $name) @@ -83,8 +81,7 @@ protected function replaceTable(&$stub, $name) /** * Build the class with the given name. * - * @param string $name - * + * @param string $name * @return string */ protected function buildClass($name) @@ -93,16 +90,4 @@ protected function buildClass($name) return $this->replaceNamespace($stub, $name)->replaceTable($stub, $name)->replaceClass($stub, $name); } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - - ]; - } } diff --git a/src/Console/Commands/PageBackpackCommand.php b/src/Console/Commands/PageBackpackCommand.php new file mode 100644 index 0000000..1553c65 --- /dev/null +++ b/src/Console/Commands/PageBackpackCommand.php @@ -0,0 +1,147 @@ +getNameInput()) + ->replace('\\', '/') + ->replace('.', '/') + ->start('/') + ->prepend($this->option('view-path')) + ->replace('\\', '/') + ->replace('//', '/') + ->trim('/'); + + $name = $input->afterLast('/'); + $nameTitle = $name->snake()->replace('-', ' ')->replace('_', ' ')->title(); + $nameSnake = $nameTitle->snake(); + + $path = $input->beforeLast($name)->trim('/\\'); + $filePath = Str::of("$path/$nameSnake")->trim('/\\'); + $fullPath = $this->getPath($filePath); + $layout = Str::of($this->option('layout'))->replace('\\', '/')->replace('/', '.'); + $route = Str::of($this->option('route') ?? $nameSnake)->replace('\\', '/')->trim('/'); + + $this->infoBlock("Creating {$nameTitle} page"); + + $this->progressBlock("Creating view resources/views/{$filePath}.blade.php"); + + // check if the file already exists + if ((! $this->hasOption('force') || ! $this->option('force')) && $this->alreadyExists($filePath)) { + $this->closeProgressBlock('Already existed', 'yellow'); + + return false; + } + + $this->makeDirectory($fullPath); + + // create page view + $stub = $this->buildClass($filePath); + $stub = str_replace('layout', $layout, $stub); + $stub = str_replace('Dummy Name', $nameTitle, $stub); + $this->files->put($fullPath, $stub); + + $this->closeProgressBlock(); + + // create controller + $this->call('backpack:page-controller', [ + 'name' => $nameTitle, + '--view-path' => $path, + ]); + + // create route + $this->call('backpack:add-custom-route', [ + 'code' => "Route::get('{$route}', '{$nameTitle->studly()}Controller@index')->name('page.{$nameSnake}.index');", + ]); + + // create the sidebar item + $this->call('backpack:add-sidebar-content', [ + 'code' => "
  • {$nameTitle}
  • ", + ]); + + $url = backpack_url($route); + + $this->newLine(); + $this->note("Page {$nameTitle} created."); + $this->note("Go to {$url} to access your new page."); + $this->newLine(); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__.'/../stubs/page.stub'; + } + + /** + * Determine if the class already exists. + * + * @param string $name + * @return bool + */ + protected function alreadyExists($name) + { + return $this->files->exists($this->getPath($name)); + } + + /** + * Get the destination class path. + * + * @param string $name + * @return string + */ + protected function getPath($name) + { + return resource_path("views/$name.blade.php"); + } +} diff --git a/src/Console/Commands/PageControllerBackpackCommand.php b/src/Console/Commands/PageControllerBackpackCommand.php new file mode 100644 index 0000000..d9fbb0b --- /dev/null +++ b/src/Console/Commands/PageControllerBackpackCommand.php @@ -0,0 +1,185 @@ +getNameInput(); + $class = $this->qualifyClass($name); + $fullPath = $this->getPath($class); + $path = Str::of($fullPath)->after(base_path())->trim('\\/'); + + $this->progressBlock("Creating controller {$path}"); + + if ($this->isReservedName($name)) { + $this->errorProgressBlock(); + $this->note("The name '$name' is reserved by PHP.", 'red'); + + return false; + } + + // Next, We will check to see if the class already exists. If it does, we don't want + // to create the class and overwrite the user's code. So, we will bail out so the + // code is untouched. Otherwise, we will continue generating this class' files. + if ((! $this->hasOption('force') || + ! $this->option('force')) && + $this->alreadyExists($class)) { + $this->closeProgressBlock('Already existed', 'yellow'); + + return false; + } + + // Next, we will generate the path to the location where this class' file should get + // written. Then, we will build the class and make the proper replacements on the + // stub files so that it gets the correctly formatted namespace and class name. + $this->makeDirectory($fullPath); + + $this->files->put($fullPath, $this->sortImports($this->buildClass($class))); + + $this->closeProgressBlock(); + } + + /** + * Get the desired class name from the input. + * + * @return Stringable + */ + protected function getNameInput() + { + return Str::of($this->argument('name'))->trim()->studly(); + } + + /** + * Get the destination class path. + * + * @param string $name + * @return string + */ + protected function getPath($name) + { + return str_replace('.php', 'Controller.php', parent::getPath($name)); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__.'/../stubs/page-controller.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace.'\Http\Controllers\Admin'; + } + + /** + * Replace the path name for the given stub. + * + * @param string $stub + * @param string $name + * @return string + */ + protected function replacePathStrings(&$stub) + { + $viewName = $this->getNameInput()->snake('_'); + $pathDot = Str::of($this->option('view-path')) + ->replace('/', '.') + ->replace('\\', '.') + ->append('.'.$viewName) + ->trim('.'); + $pathSlash = $pathDot->replace('.', '/'); + + $stub = str_replace('dummy.path', $pathDot, $stub); + $stub = str_replace('dummy/path', $pathSlash, $stub); + + return $this; + } + + /** + * Replace the name for the given stub. + * + * @param string $stub + * @param string $name + * @return string + */ + protected function replaceNameStrings(&$stub) + { + $name = $this->getNameInput(); + + $stub = str_replace('DummyName', (string) $name, $stub); + $stub = str_replace('dummyName', lcfirst((string) $name), $stub); + $stub = str_replace('Dummy Name', $name->kebab()->replace('-', ' ')->title(), $stub); + + return $this; + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name) + { + $stub = $this->files->get($this->getStub()); + + return $this + ->replaceNamespace($stub, $name) + ->replacePathStrings($stub) + ->replaceNameStrings($stub) + ->replaceClass($stub, $name); + } +} diff --git a/src/Console/Commands/RequestBackpackCommand.php b/src/Console/Commands/RequestBackpackCommand.php index 0658956..89e1637 100644 --- a/src/Console/Commands/RequestBackpackCommand.php +++ b/src/Console/Commands/RequestBackpackCommand.php @@ -47,24 +47,11 @@ protected function getStub() /** * Get the default namespace for the class. * - * @param string $rootNamespace - * + * @param string $rootNamespace * @return string */ protected function getDefaultNamespace($rootNamespace) { return $rootNamespace.'\Http\Requests'; } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - - ]; - } } diff --git a/src/Console/Commands/ViewBackpackCommand.php b/src/Console/Commands/ViewBackpackCommand.php index d9e8efb..0375cb1 100644 --- a/src/Console/Commands/ViewBackpackCommand.php +++ b/src/Console/Commands/ViewBackpackCommand.php @@ -48,30 +48,19 @@ protected function getStub() return __DIR__.'/../stubs/view.stub'; } - /** - * Alias for the fire method. - * - * In Laravel 5.5 the fire() method has been renamed to handle(). - * This alias provides support for both Laravel 5.4 and 5.5. - */ - public function handle() - { - $this->fire(); - } - /** * Execute the console command. * * @return bool|null */ - public function fire() + public function handle() { $name = $this->getNameInput(); $path = $this->getPath($name); if ($this->alreadyExists($this->getNameInput())) { - $this->error($this->type.' already exists!'); + $this->error($this->type.' already existed!'); return false; } @@ -86,8 +75,7 @@ public function fire() /** * Determine if the class already exists. * - * @param string $name - * + * @param string $name * @return bool */ protected function alreadyExists($name) @@ -98,8 +86,7 @@ protected function alreadyExists($name) /** * Get the destination class path. * - * @param string $name - * + * @param string $name * @return string */ protected function getPath($name) @@ -110,24 +97,11 @@ protected function getPath($name) /** * Build the class with the given name. * - * @param string $name - * + * @param string $name * @return string */ protected function buildClass($name) { return $this->files->get($this->getStub()); } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - - ]; - } } diff --git a/src/Console/Commands/Views/ButtonBackpackCommand.php b/src/Console/Commands/Views/ButtonBackpackCommand.php new file mode 100644 index 0000000..42f1ac1 --- /dev/null +++ b/src/Console/Commands/Views/ButtonBackpackCommand.php @@ -0,0 +1,48 @@ +stub; + } + + /** + * Execute the console command. + * + * @return bool|null + */ + public function handle() + { + $name = Str::of($this->getNameInput()); + $path = Str::of($this->getPath($name)); + $pathRelative = $path->after(base_path())->replace('\\', '/')->trim('/'); + + $this->infoBlock("Creating {$name->replace('_', ' ')->title()} {$this->type}"); + $this->progressBlock("Creating view {$pathRelative}"); + + if ($this->alreadyExists($name)) { + $this->closeProgressBlock('Already existed', 'yellow'); + + return false; + } + + $source = null; + if ($this->option('from')) { + $from = $this->option('from'); + $namespaces = ViewNamespaces::getFor($this->viewNamespace); + foreach ($namespaces as $namespace) { + $viewPath = "$namespace.$from"; + if (view()->exists($viewPath)) { + $source = view($viewPath)->getPath(); + break; + } + } + + // full or relative file path may be provided + if (file_exists($from)) { + $source = realpath($from); + } + // remove the first slash to make absolute paths relative in unix systems + elseif (file_exists(substr($from, 1))) { + $source = realpath(substr($from, 1)); + } + + if (! $source) { + $this->errorProgressBlock(); + $this->note("$this->type '$from' does not exist!", 'red'); + $this->newLine(); + + return false; + } + } + + $this->makeDirectory($path); + + if ($source) { + $this->files->copy($source, $path); + } else { + $this->files->put($path, $this->buildClass($name)); + } + + $this->closeProgressBlock(); + } + + /** + * Determine if the class already exists. + * + * @param string $name + * @return bool + */ + protected function alreadyExists($name) + { + return $this->files->exists($this->getPath($name)); + } + + /** + * Get the destination class path. + * + * @param string $name + * @return string + */ + protected function getPath($name) + { + return resource_path("views/vendor/backpack/crud/{$this->viewNamespace}/$name.blade.php"); + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name) + { + $stub = $this->files->get($this->getStub()); + $stub = str_replace('dummy', $name, $stub); + + return $stub; + } + + /** + * Get the desired class name from the input. + * + * @return string + */ + protected function getNameInput() + { + $name = Str::of($this->argument('name')); + $from = Str::of($this->option('from')); + + if ($name->isEmpty() && $from->isEmpty()) { + throw new \Exception('Not enough arguments (missing: "name" or "--from").'); + } + + // Name may come from the --from option + if ($name->isEmpty()) { + $name = $from->afterLast('/')->afterLast('\\'); + } + + return (string) $name->trim()->snake('_'); + } +} diff --git a/src/Console/Commands/Views/WidgetBackpackCommand.php b/src/Console/Commands/Views/WidgetBackpackCommand.php new file mode 100644 index 0000000..be4f031 --- /dev/null +++ b/src/Console/Commands/Views/WidgetBackpackCommand.php @@ -0,0 +1,59 @@ +viewNamespace}/$name.blade.php"); + } +} diff --git a/src/Console/stubs/button.stub b/src/Console/stubs/button.stub new file mode 100644 index 0000000..18149c2 --- /dev/null +++ b/src/Console/stubs/button.stub @@ -0,0 +1,3 @@ +@if ($crud->hasAccess('dummy')) + dummy +@endif \ No newline at end of file diff --git a/src/Console/stubs/chart-controller.stub b/src/Console/stubs/chart-controller.stub new file mode 100644 index 0000000..7f19f5b --- /dev/null +++ b/src/Console/stubs/chart-controller.stub @@ -0,0 +1,47 @@ +chart = new Chart(); + + // MANDATORY. Set the labels for the dataset points + $this->chart->labels([ + 'Today', + ]); + + // RECOMMENDED. Set URL that the ChartJS library should call, to get its data using AJAX. + $this->chart->load(backpack_url('charts/dummy-class')); + + // OPTIONAL + // $this->chart->minimalist(false); + // $this->chart->displayLegend(true); + } + + /** + * Respond to AJAX calls with all the chart data points. + * + * @return json + */ + // public function data() + // { + // $users_created_today = \App\User::whereDate('created_at', today())->count(); + + // $this->chart->dataset('Users Created', 'bar', [ + // $users_created_today, + // ]) + // ->color('rgba(205, 32, 31, 1)') + // ->backgroundColor('rgba(205, 32, 31, 0.4)'); + // } +} \ No newline at end of file diff --git a/src/Console/stubs/column.stub b/src/Console/stubs/column.stub new file mode 100644 index 0000000..ae6c19e --- /dev/null +++ b/src/Console/stubs/column.stub @@ -0,0 +1,23 @@ +{{-- regular object attribute --}} +@php + $column['value'] = $column['value'] ?? data_get($entry, $column['name']); + $column['text'] = $column['default'] ?? '-'; + + if($column['value'] instanceof \Closure) { + $column['value'] = $column['value']($entry); + } + + if(is_array($column['value'])) { + $column['value'] = json_encode($column['value']); + } + + if(!empty($column['value'])) { + $column['text'] = $column['value']; + } +@endphp + + + @includeWhen(!empty($column['wrapper']), 'crud::columns.inc.wrapper_start') + {{ $column['text'] }} + @includeWhen(!empty($column['wrapper']), 'crud::columns.inc.wrapper_end') + \ No newline at end of file diff --git a/src/Console/stubs/crud-controller.stub b/src/Console/stubs/crud-controller.stub index b28332a..f2148fe 100644 --- a/src/Console/stubs/crud-controller.stub +++ b/src/Console/stubs/crud-controller.stub @@ -2,60 +2,79 @@ namespace DummyNamespace; +use App\Http\Requests\DummyModelClassRequest; use Backpack\CRUD\app\Http\Controllers\CrudController; - -// VALIDATION: change the requests to match your own file names if you need form validation -use App\Http\Requests\DummyClassRequest as StoreRequest; -use App\Http\Requests\DummyClassRequest as UpdateRequest; -use Backpack\CRUD\CrudPanel; +use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; /** * Class DummyClassCrudController * @package App\Http\Controllers\Admin - * @property-read CrudPanel $crud + * @property-read \Backpack\CRUD\app\Library\CrudPanel\CrudPanel $crud */ class DummyClassCrudController extends CrudController { + use \Backpack\CRUD\app\Http\Controllers\Operations\ListOperation; + use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation; + use \Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation; + use \Backpack\CRUD\app\Http\Controllers\Operations\DeleteOperation; + use \Backpack\CRUD\app\Http\Controllers\Operations\ShowOperation; + + /** + * Configure the CrudPanel object. Apply settings to all operations. + * + * @return void + */ public function setup() { - /* - |-------------------------------------------------------------------------- - | CrudPanel Basic Information - |-------------------------------------------------------------------------- - */ - $this->crud->setModel('App\Models\DummyClass'); - $this->crud->setRoute(config('backpack.base.route_prefix') . '/dummy_class'); - $this->crud->setEntityNameStrings('dummy_class', 'DummyTable'); - - /* - |-------------------------------------------------------------------------- - | CrudPanel Configuration - |-------------------------------------------------------------------------- - */ - - // TODO: remove setFromDb() and manually define Fields and Columns - $this->crud->setFromDb(); - - // add asterisk for fields that are required in DummyClassRequest - $this->crud->setRequiredFields(StoreRequest::class, 'create'); - $this->crud->setRequiredFields(UpdateRequest::class, 'edit'); + CRUD::setModel(\App\Models\DummyModelClass::class); + CRUD::setRoute(config('backpack.base.route_prefix') . '/dummy-class'); + CRUD::setEntityNameStrings('dummy singular', 'dummy plural'); + } + + /** + * Define what happens when the List operation is loaded. + * + * @see https://backpackforlaravel.com/docs/crud-operation-list-entries + * @return void + */ + protected function setupListOperation() + { + CRUD::setFromDb(); // columns + + /** + * Columns can be defined using the fluent syntax or array syntax: + * - CRUD::column('price')->type('number'); + * - CRUD::addColumn(['name' => 'price', 'type' => 'number']); + */ } - public function store(StoreRequest $request) + /** + * Define what happens when the Create operation is loaded. + * + * @see https://backpackforlaravel.com/docs/crud-operation-create + * @return void + */ + protected function setupCreateOperation() { - // your additional operations before save here - $redirect_location = parent::storeCrud($request); - // your additional operations after save here - // use $this->data['entry'] or $this->crud->entry - return $redirect_location; + CRUD::setValidation(DummyClassRequest::class); + + CRUD::setFromDb(); // fields + + /** + * Fields can be defined using the fluent syntax or array syntax: + * - CRUD::field('price')->type('number'); + * - CRUD::addField(['name' => 'price', 'type' => 'number'])); + */ } - public function update(UpdateRequest $request) + /** + * Define what happens when the Update operation is loaded. + * + * @see https://backpackforlaravel.com/docs/crud-operation-update + * @return void + */ + protected function setupUpdateOperation() { - // your additional operations before save here - $redirect_location = parent::updateCrud($request); - // your additional operations after save here - // use $this->data['entry'] or $this->crud->entry - return $redirect_location; + $this->setupCreateOperation(); } } diff --git a/src/Console/stubs/crud-model.stub b/src/Console/stubs/crud-model.stub index 2b809cb..3a86499 100644 --- a/src/Console/stubs/crud-model.stub +++ b/src/Console/stubs/crud-model.stub @@ -2,12 +2,14 @@ namespace DummyNamespace; +use Backpack\CRUD\app\Models\Traits\CrudTrait; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Backpack\CRUD\CrudTrait; class DummyClass extends Model { use CrudTrait; + use HasFactory; /* |-------------------------------------------------------------------------- @@ -18,8 +20,8 @@ class DummyClass extends Model protected $table = 'DummyTable'; // protected $primaryKey = 'id'; // public $timestamps = false; - // protected $guarded = ['id']; - protected $fillable = []; + protected $guarded = ['id']; + // protected $fillable = []; // protected $hidden = []; // protected $dates = []; diff --git a/src/Console/stubs/crud-operation.stub b/src/Console/stubs/crud-operation.stub new file mode 100644 index 0000000..05b0f9c --- /dev/null +++ b/src/Console/stubs/crud-operation.stub @@ -0,0 +1,59 @@ + $routeName.'.dummyClass', + 'uses' => $controller.'@dummyClass', + 'operation' => 'dummyClass', + ]); + } + + /** + * Add the default settings, buttons, etc that this operation needs. + */ + protected function setupDummyClassDefaults() + { + CRUD::allowAccess('dummyClass'); + + CRUD::operation('dummyClass', function () { + CRUD::loadDefaultOperationSettingsFromConfig(); + }); + + CRUD::operation('list', function () { + // CRUD::addButton('top', 'dummy_class', 'view', 'crud::buttons.dummy_class'); + // CRUD::addButton('line', 'dummy_class', 'view', 'crud::buttons.dummy_class'); + }); + } + + /** + * Show the view for performing the operation. + * + * @return Response + */ + public function dummyClass() + { + CRUD::hasAccessOrFail('dummyClass'); + + // prepare the fields you need to show + $this->data['crud'] = $this->crud; + $this->data['title'] = CRUD::getTitle() ?? 'Dummy Class '.$this->crud->entity_name; + + // load the view + return view('crud::operations.dummy_class', $this->data); + } +} \ No newline at end of file diff --git a/src/Console/stubs/crud-request.stub b/src/Console/stubs/crud-request.stub index 9e83994..3df40ec 100644 --- a/src/Console/stubs/crud-request.stub +++ b/src/Console/stubs/crud-request.stub @@ -2,7 +2,6 @@ namespace DummyNamespace; -use DummyRootNamespaceHttp\Requests\Request; use Illuminate\Foundation\Http\FormRequest; class DummyClassRequest extends FormRequest diff --git a/src/Console/stubs/field.stub b/src/Console/stubs/field.stub new file mode 100644 index 0000000..4757cef --- /dev/null +++ b/src/Console/stubs/field.stub @@ -0,0 +1,54 @@ +{{-- dummy_field field --}} +@php + $field['value'] = old_empty_or_null($field['name'], '') ?? ($field['value'] ?? ($field['default'] ?? '')); +@endphp + +@include('crud::fields.inc.wrapper_start') + + @include('crud::fields.inc.translatable_icon') + + + + {{-- HINT --}} + @if (isset($field['hint'])) +

    {!! $field['hint'] !!}

    + @endif +@include('crud::fields.inc.wrapper_end') + +{{-- CUSTOM CSS --}} +@push('crud_fields_styles') + {{-- How to load a CSS file? --}} + @loadOnce('dummyFieldStyle.css') + + {{-- How to add some CSS? --}} + @loadOnce('dummy_field_style') + + @endLoadOnce +@endpush + +{{-- CUSTOM JS --}} +@push('crud_fields_scripts') + {{-- How to load a JS file? --}} + @loadOnce('dummyFieldScript.js') + + {{-- How to add some JS to the field? --}} + @loadOnce('bpFieldInitDummyFieldElement') + + @endLoadOnce +@endpush diff --git a/src/Console/stubs/filter.stub b/src/Console/stubs/filter.stub new file mode 100644 index 0000000..82dedea --- /dev/null +++ b/src/Console/stubs/filter.stub @@ -0,0 +1,72 @@ +{{-- Simple Backpack CRUD filter --}} + + + +{{-- ########################################### --}} +{{-- Extra CSS and JS for this particular filter --}} + +{{-- FILTERS EXTRA CSS --}} +{{-- push things in the after_styles section --}} + +{{-- @push('crud_list_styles') + no css +@endpush --}} + + +{{-- FILTERS EXTRA JS --}} +{{-- push things in the after_scripts section --}} + +@push('crud_list_scripts') + +@endpush +{{-- End of Extra CSS and JS --}} +{{-- ########################################## --}} diff --git a/src/Console/stubs/model.stub b/src/Console/stubs/model.stub index 5d07c34..1f1daad 100644 --- a/src/Console/stubs/model.stub +++ b/src/Console/stubs/model.stub @@ -81,7 +81,7 @@ class DummyClass extends Model /* |-------------------------------------------------------------------------- - | ACCESORS + | ACCESSORS |-------------------------------------------------------------------------- */ diff --git a/src/Console/stubs/page-controller.stub b/src/Console/stubs/page-controller.stub new file mode 100644 index 0000000..d995245 --- /dev/null +++ b/src/Console/stubs/page-controller.stub @@ -0,0 +1,26 @@ + 'Dummy Name', + 'breadcrumbs' => [ + trans('backpack::crud.admin') => backpack_url('dashboard'), + 'DummyName' => false, + ], + 'page' => 'resources/views/dummy/path.blade.php', + 'controller' => 'app/Http/Controllers/Admin/DummyNameController.php', + ]); + } +} diff --git a/src/Console/stubs/page.stub b/src/Console/stubs/page.stub new file mode 100644 index 0000000..862fe9c --- /dev/null +++ b/src/Console/stubs/page.stub @@ -0,0 +1,9 @@ +@extends(backpack_view('layout')) + +@section('content') +
    +

    Dummy Name

    + +

    Go to {{ $page }} to edit this view or {{ $controller }} to edit the controller.

    +
    +@endsection diff --git a/src/Console/stubs/widget.stub b/src/Console/stubs/widget.stub new file mode 100644 index 0000000..25b17b6 --- /dev/null +++ b/src/Console/stubs/widget.stub @@ -0,0 +1,13 @@ +{{-- Include widget wrapper --}} +@includeWhen(!empty($widget['wrapper']), 'backpack::widgets.inc.wrapper_start') + +{{-- Define your widget --}} +
    + + @if (isset($widget['content'])) +

    {!! $widget['content'] !!}

    + @endif + +
    + +@includeWhen(!empty($widget['wrapper']), 'backpack::widgets.inc.wrapper_end') \ No newline at end of file diff --git a/src/GeneratorsServiceProvider.php b/src/GeneratorsServiceProvider.php index 7f30031..3170f2c 100644 --- a/src/GeneratorsServiceProvider.php +++ b/src/GeneratorsServiceProvider.php @@ -2,27 +2,49 @@ namespace Backpack\Generators; -use Illuminate\Support\ServiceProvider; -use Backpack\Generators\Console\Commands\CrudBackpackCommand; -use Backpack\Generators\Console\Commands\ViewBackpackCommand; -use Backpack\Generators\Console\Commands\ModelBackpackCommand; +use Backpack\Generators\Console\Commands\BuildBackpackCommand; +use Backpack\Generators\Console\Commands\ChartBackpackCommand; +use Backpack\Generators\Console\Commands\ChartControllerBackpackCommand; use Backpack\Generators\Console\Commands\ConfigBackpackCommand; -use Backpack\Generators\Console\Commands\RequestBackpackCommand; +use Backpack\Generators\Console\Commands\CrudBackpackCommand; +use Backpack\Generators\Console\Commands\CrudControllerBackpackCommand; use Backpack\Generators\Console\Commands\CrudModelBackpackCommand; +use Backpack\Generators\Console\Commands\CrudOperationBackpackCommand; use Backpack\Generators\Console\Commands\CrudRequestBackpackCommand; -use Backpack\Generators\Console\Commands\CrudControllerBackpackCommand; +use Backpack\Generators\Console\Commands\ModelBackpackCommand; +use Backpack\Generators\Console\Commands\PageBackpackCommand; +use Backpack\Generators\Console\Commands\PageControllerBackpackCommand; +use Backpack\Generators\Console\Commands\RequestBackpackCommand; +use Backpack\Generators\Console\Commands\ViewBackpackCommand; +use Backpack\Generators\Console\Commands\Views\ButtonBackpackCommand; +use Backpack\Generators\Console\Commands\Views\ColumnBackpackCommand; +use Backpack\Generators\Console\Commands\Views\FieldBackpackCommand; +use Backpack\Generators\Console\Commands\Views\FilterBackpackCommand; +use Backpack\Generators\Console\Commands\Views\WidgetBackpackCommand; +use Illuminate\Support\ServiceProvider; class GeneratorsServiceProvider extends ServiceProvider { protected $commands = [ + BuildBackpackCommand::class, + ButtonBackpackCommand::class, + ColumnBackpackCommand::class, ConfigBackpackCommand::class, CrudModelBackpackCommand::class, CrudControllerBackpackCommand::class, + ChartControllerBackpackCommand::class, + CrudOperationBackpackCommand::class, CrudRequestBackpackCommand::class, CrudBackpackCommand::class, + ChartBackpackCommand::class, + FieldBackpackCommand::class, + FilterBackpackCommand::class, ModelBackpackCommand::class, + PageBackpackCommand::class, + PageControllerBackpackCommand::class, RequestBackpackCommand::class, ViewBackpackCommand::class, + WidgetBackpackCommand::class, ]; /** diff --git a/src/Services/BackpackCommand.php b/src/Services/BackpackCommand.php new file mode 100644 index 0000000..6c35a7a --- /dev/null +++ b/src/Services/BackpackCommand.php @@ -0,0 +1,85 @@ + $type === self::STR_CAMEL ? ucfirst($title) : strtolower($title); + + foreach ($nameTitles as $key => $title) { + $nameTitle .= $key > 0 ? '/' : ''; + $nameTitle .= $applyCasing(Str::$type($title)); + } + + return $nameTitle; + } + + public function buildPluralName(string $nameKebab) + { + return Str::plural(str_replace('-', ' ', Arr::last(explode('/', $nameKebab)))); + } + + public function buildNameWithSpaces(string $nameTitle): string + { + $words = preg_split('/(?=[A-Z])/', str_replace('/', '', $nameTitle)); + + // Transform last word into plural + $lastWord = Arr::last($words); + array_pop($words); + $words[] = Str::plural($lastWord); + + $name = []; + + foreach ($words as $word) { + if ($word === '') { + continue; + } + $name[] = count($name) === 0 ? ucfirst($word) : strtolower($word); + } + + return implode(' ', $name); + } + + public function buildSingularName(string $nameKebab) + { + return str_replace('-', ' ', Arr::last(explode('/', $nameKebab))); + } + + public function buildRelativePath(string $name): string + { + return lcfirst(Str::of("$name.php")->replace('\\', '/')); + } + + public function buildClassName(string $name): string + { + return ucfirst(Arr::last(explode('/', $name))); + } + + public function convertSlashesForNamespace(string $name): string + { + return Str::replace('/', '\\', $name); + } +} diff --git a/tests/BaseTest.php b/tests/BaseTest.php index 31cfbe0..f34de18 100644 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -1,6 +1,8 @@