├── .gitignore ├── config └── marygen.php ├── src ├── MaryGen.php ├── Facades │ └── MaryGen.php ├── MaryGenServiceProvider.php └── Commands │ └── MaryGenCommand.php ├── phpunit.xml ├── tests ├── TestCase.php └── Unit │ └── Commands │ └── MaryGenCommandTest.php ├── license.md ├── .phpunit.result.cache ├── composer.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /node_modules 3 | .idea/ 4 | -------------------------------------------------------------------------------- /config/marygen.php: -------------------------------------------------------------------------------- 1 | 'App\Models', 5 | 6 | 'use_mg_like_eloquent_directive' => true, 7 | ]; 8 | -------------------------------------------------------------------------------- /src/MaryGen.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_generates_livewire_component_for_existing_model":7,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_uses_default_view_name_when_not_provided":7,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_generates_correct_form_fields_based_on_model_schema":7,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_fails_when_model_does_not_exist":8,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_fails_when_view_file_already_exists":8},"times":{"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_generates_livewire_component_for_existing_model":0.045,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_fails_when_model_does_not_exist":0.002,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_fails_when_view_file_already_exists":0.001,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_uses_default_view_name_when_not_provided":0.001,"Soysaltan\\MaryGen\\Tests\\Unit\\Commands\\MaryGenCommandTest::it_generates_correct_form_fields_based_on_model_schema":0.001}} -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soysaltan/marygen", 3 | "description": "Generate MaryUI based pages with one line command", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://marygen.expozy.co", 7 | "authors": [ 8 | { 9 | "name": "Soysal Tan", 10 | "email": "soysaltan@hotmail.it", 11 | "homepage": "https://github.com/paramientos" 12 | } 13 | ], 14 | "keywords": [ 15 | "laravel", 16 | "livewire", 17 | "livewire 3", 18 | "livewire ui", 19 | "livewire ui components", 20 | "tallstack", 21 | "tallstack ui", 22 | "tallstackui", 23 | "tallstack components", 24 | "blade", 25 | "ui", 26 | "blade ui components", 27 | "livewire-components", 28 | "livewire components", 29 | "livewire-packages", 30 | "livewire packages", 31 | "tailwind", 32 | "components", 33 | "daisyUI", 34 | "alpinejs" 35 | ], 36 | "require": { 37 | "illuminate/support": "^10.0|^11.0", 38 | "stichoza/google-translate-php": "^5.2" 39 | }, 40 | "require-dev": { 41 | "orchestra/testbench": "^6.0|^7.0|^8.0", 42 | "phpunit/phpunit": "^9.0|^10.0", 43 | "phpstan/phpstan": "^1.12" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "SoysalTan\\MaryGen\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "SoysalTan\\MaryGen\\Tests\\": "tests/" 53 | } 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "SoysalTan\\MaryGen\\MaryGenServiceProvider" 59 | ], 60 | "aliases": { 61 | "MaryGen": "SoysalTan\\MaryGen\\Facades\\MaryGen" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MaryGenServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 16 | $this->commands([ 17 | MaryGenCommand::class, 18 | ]); 19 | } 20 | 21 | $this->publishes([ 22 | __DIR__ . '/../config/marygen.php' => config_path('marygen.php'), 23 | ], 'config'); 24 | 25 | if (config('marygen.use_mg_like_eloquent_directive')) { 26 | Builder::macro('mgLike', function ($attributes, string $searchTerm) { 27 | $this->where(function (Builder $query) use ($attributes, $searchTerm) { 28 | foreach (Arr::wrap($attributes) as $attribute) { 29 | $query->when( 30 | str_contains($attribute, '.'), 31 | function (Builder $query) use ($attribute, $searchTerm) { 32 | [$relationName, $relationAttribute] = explode('.', $attribute); 33 | 34 | $query->orWhereHas($relationName, function (Builder $query) use ($relationAttribute, $searchTerm) { 35 | $query->where($relationAttribute, 'ILIKE', "%{$searchTerm}%"); 36 | }); 37 | }, 38 | function (Builder $query) use ($attribute, $searchTerm) { 39 | $query->orWhere($attribute, 'ILIKE', "%{$searchTerm}%"); 40 | } 41 | ); 42 | } 43 | }); 44 | 45 | return $this; 46 | }); 47 | } 48 | } 49 | 50 | public function provides() 51 | { 52 | return [ 53 | \SoysalTan\MaryGen\MaryGen::class 54 | ]; 55 | } 56 | 57 | public function register() 58 | { 59 | $this->mergeConfigFrom( 60 | __DIR__ . '/../config/marygen.php', 'marygen' 61 | ); 62 | 63 | // Register the service the package provides. 64 | $this->app->singleton('marygen', function ($app) { 65 | return new MaryGen(); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/Commands/MaryGenCommandTest.php: -------------------------------------------------------------------------------- 1 | mockFileFacade(); 19 | $this->mockSchemaFacade(); 20 | 21 | // Ensure the MaryUI package is "installed" 22 | $this->mockComposerJson(); 23 | } 24 | 25 | protected function tearDown(): void 26 | { 27 | Mockery::close(); 28 | parent::tearDown(); 29 | } 30 | 31 | /** @test */ 32 | public function it_generates_livewire_component_for_existing_model() 33 | { 34 | // Arrange 35 | $modelName = 'Admin'; 36 | $viewName = 'admin'; 37 | 38 | // Act 39 | $exitCode = $this->artisan("marygen:make {$modelName} {$viewName}")->run(); 40 | 41 | // Assert 42 | $this->assertEquals(0, $exitCode); 43 | $this->assertFileExists(config('livewire.view_path') . "/{$viewName}.blade.php"); 44 | } 45 | 46 | /** @test */ 47 | public function it_fails_when_model_does_not_exist() 48 | { 49 | // Arrange 50 | $modelName = 'NonExistentModel'; 51 | 52 | // Act 53 | $exitCode = $this->artisan("marygen:make {$modelName}")->run(); 54 | 55 | 56 | // Assert 57 | $this->assertEquals(1, $exitCode); 58 | } 59 | 60 | /** @test */ 61 | public function it_fails_when_view_file_already_exists() 62 | { 63 | // Arrange 64 | $modelName = 'User'; 65 | $viewName = 'existing-view'; 66 | File::shouldReceive('exists')->andReturn(true); 67 | 68 | // Act 69 | $exitCode = $this->artisan("marygen:make {$modelName} {$viewName}")->run(); 70 | 71 | // Assert 72 | $this->assertEquals(1, $exitCode); 73 | } 74 | 75 | /** @test */ 76 | public function it_uses_default_view_name_when_not_provided() 77 | { 78 | // Arrange 79 | $modelName = 'User'; 80 | 81 | // Act 82 | $exitCode = $this->artisan("marygen:make {$modelName}")->run(); 83 | 84 | // Assert 85 | $this->assertEquals(0, $exitCode); 86 | $this->assertFileExists(config('livewire.view_path') . "/user.blade.php"); 87 | } 88 | 89 | /** @test */ 90 | public function it_generates_correct_form_fields_based_on_model_schema() 91 | { 92 | // Arrange 93 | $modelName = 'User'; 94 | $viewName = 'users'; 95 | 96 | // Mock the Schema facade to return specific columns 97 | Schema::shouldReceive('getColumnListing')->andReturn(['id', 'name', 'email', 'password']); 98 | Schema::shouldReceive('getColumnType')->andReturn('integer', 'string', 'string', 'string'); 99 | 100 | // Act 101 | $exitCode = $this->artisan("marygen:make {$modelName} {$viewName}")->run(); 102 | 103 | // Assert 104 | $this->assertEquals(0, $exitCode); 105 | $generatedView = File::get(config('livewire.view_path') . "/{$viewName}.blade.php"); 106 | $this->assertStringContainsString('assertStringContainsString('assertStringContainsString('andReturn(''); 114 | File::shouldReceive('put')->andReturn(true); 115 | File::shouldReceive('exists')->andReturn(false); 116 | } 117 | 118 | private function mockSchemaFacade() 119 | { 120 | Schema::shouldReceive('getColumnListing')->andReturn(['id', 'name', 'email']); 121 | Schema::shouldReceive('getColumnType')->andReturn('integer', 'string', 'string'); 122 | } 123 | 124 | private function mockComposerJson() 125 | { 126 | $composerJson = json_encode([ 127 | 'require' => [ 128 | 'robsontenorio/mary' => '^1.0' 129 | ] 130 | ]); 131 | File::shouldReceive('get')->with('composer.json')->andReturn($composerJson); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # MaryGen Documentation 2 | 3 | ## Table of Contents 4 | 1. [Introduction](#introduction) 5 | 2. [Features](#features) 6 | 3. [Requirements](#requirements) 7 | 4. [Installation](#installation) 8 | 5. [Configuration](#configuration) 9 | 6. [Usage](#usage) 10 | 7. [Command Structure](#command-structure) 11 | 8. [Generated Components](#generated-components) 12 | 9. [Customization](#customization) 13 | 10. [Translation Feature](#translation-feature) 14 | 11. [Troubleshooting](#troubleshooting) 15 | 12. [Contributing](#contributing) 16 | 13. [License](#license) 17 | 14. [Support](#support) 18 | 19 | ## Introduction 20 | 21 | MaryGen is a powerful Laravel package designed to streamline the process of generating MaryUI components and Livewire pages for your Laravel models. It automates the creation of CRUD (Create, Read, Update, Delete) interfaces, saving developers significant time and effort in setting up admin panels or data management systems. 22 | 23 | ## Features 24 | 25 | - Automatic generation of MaryUI components for Laravel models 26 | - Creation of Livewire pages with full CRUD functionality 27 | - Intelligent form field generation based on database schema 28 | - Automatic table column generation 29 | - Built-in sorting, pagination, and search capabilities 30 | - Easy customization options 31 | - Automatic route generation 32 | - Translation support for generated content (new in version 0.35.0) 33 | 34 | ## Requirements 35 | 36 | - Laravel 10.x or higher 37 | - PHP 8.0 or higher 38 | - [MaryUI](https://github.com/robsontenorio/mary) package 39 | - [Livewire Volt](https://livewire.laravel.com/docs/volt) package 40 | 41 | ## Installation 42 | 43 | 1. Ensure you have a Laravel project set up. 44 | 45 | 2. Install the MaryUI: 46 | 47 | https://mary-ui.com/docs/installation 48 | 49 | 50 | 3. Install the Livewire, Livewire Volt packages (if not already installed with MaryUI): 51 | ```bash 52 | composer require livewire/livewire livewire/volt && php artisan volt:install 53 | ``` 54 | 55 | 4. Install the MaryGen package: 56 | ```bash 57 | composer require soysaltan/marygen 58 | ``` 59 | 60 | 5. (Optional) Publish the configuration file: 61 | ```bash 62 | php artisan vendor:publish --provider="SoysalTan\MaryGen\MaryGenServiceProvider" --tag="config" 63 | ``` 64 | 65 | ## Configuration 66 | 67 | After publishing the configuration file, you can modify `config/marygen.php` to customize the package behavior: 68 | 69 | ```php 70 | return [ 71 | 'model_namespace' => 'App\Models', 72 | 'use_mg_like_eloquent_directive' => true, 73 | ]; 74 | ``` 75 | 76 | - `model_namespace`: Define the namespace for your models. Default is `App\Models`. 77 | - `use_mg_like_eloquent_directive`: Determine whether to use the MgLike Eloquent directive for search functionality. For example: 78 | 79 | ```php 80 | $q->mgLike(['id', 'username', 'email', 'password', 'name', 'lastname', 'title', 'phone', 'avatar', 'time_zone', 'last_login_at', 'status', 'created_at', 'updated_at'], $this->search)) 81 | ``` 82 | 83 | ## Usage 84 | 85 | To generate a MaryUI component and Livewire page for a model, use the following command: 86 | 87 | ```bash 88 | php artisan marygen:make {--m|model=} {--w|view=} {--d|dest_lang=} {--s|source_lang=} {--nr|no-route} 89 | ``` 90 | 91 | - `--m|model`: The name of the model for which you want to generate the components. 92 | - `--w|view`: (Optional) The name of the view file. If not provided, it will use the lowercase model name. 93 | - `--d|dest_lang`: (Required if source_lang presents) The destination language code for translation. 94 | - `--s|source_lang`: (Optional) The source language code for translation.If not present, it detects the source language automatically. 95 | - `--nr|no-route`: (Optional - as of `v0.35.2`) Prevent automatic route addition to routes/web.php. 96 | 97 | 98 | Example: 99 | ```bash 100 | php artisan marygen:make --model=User --view=admin-users --dest_lang=es --no-route 101 | ``` 102 | 103 | This command will generate a Livewire page for the User model with CRUD functionality, name the view file `admin-users.blade.php`, and translate the content from English to Spanish and skip the automatic route generation. 104 | 105 | ## Prevent Automatic Route Generation (as of `v0.35.2`) 106 | Starting from version `0.35.2`, MaryGen prevents automatic route generation feature. By default, when you generate a new component without using --no-route option with the `marygen:make` command, a corresponding route is automatically added to your `routes/web.php` file. 107 | 108 | ## New in Version 0.36.1: Multi-Database Connection Support 109 | 110 | As of version 0.36.1, MaryGen now supports models that use different database connections. This feature allows you to generate components and pages for models that are associated with databases other than your default connection. 111 | 112 | ### How it works 113 | 114 | MaryGen now respects the `$connection` property of your Eloquent models. When generating components and pages, it will use the specified connection to: 115 | 116 | 1. Retrieve the correct table schema 117 | 2. Generate appropriate form fields 118 | 3. Create table columns 119 | 4. Set up sorting and filtering 120 | 121 | ### Usage 122 | 123 | No additional configuration is required. Simply ensure that your model specifies the correct connection: 124 | 125 | ```php 126 | class User extends Model 127 | { 128 | protected $connection = 'secondary_db'; 129 | 130 | // ... rest of your model 131 | } 132 | ``` 133 | 134 | When you run the `marygen:make` command for this model, MaryGen will automatically use the 'secondary_db' connection for all database operations. 135 | 136 | ### Example 137 | 138 | ```bash 139 | php artisan marygen:make --model=User 140 | ``` 141 | 142 | If the User model specifies a different connection, MaryGen will use that connection to generate the component and page. 143 | 144 | ### Notes 145 | 146 | - Ensure that all specified connections are properly configured in your `config/database.php` file. 147 | - If a model doesn't specify a connection, MaryGen will use the default database connection. 148 | - This feature is particularly useful for applications that interact with multiple databases or use database sharding. 149 | 150 | ## Customization 151 | 152 | You can customize the generated components by modifying the following methods in the `MaryGenCommand` class: 153 | 154 | - `getMaryUIComponent()`: Adjust the mapping between database column types and MaryUI components. 155 | - `getIconForColumn()`: Modify the icon selection for form fields. 156 | - `generateLivewirePage()`: Customize the structure of the generated Livewire component and Blade view. 157 | 158 | Additionally, you can edit the generated files directly to further tailor them to your specific needs. 159 | 160 | ## Translation Feature 161 | 162 | MaryGen includes a translation feature that allows you to generate content in different languages. This feature uses the Google Translate API to translate field names, labels, and other text elements in the generated components. 163 | It uses `stichoza/google-translate-php` package. (https://github.com/Stichoza/google-translate-php) 164 | To use the translation feature: 165 | 166 | 1. Specify the destination language using the `--dest_lang` option when running the `marygen:make` command. 167 | 2. Optionally, specify the source language using the `--source_lang` option. If not provided, Google Translate will attempt to auto-detect the source language. 168 | 169 | Example: 170 | ```bash 171 | php artisan marygen:make --model=Product --view=product-management --dest_lang=fr --source_lang=en 172 | ``` 173 | 174 | This command will generate the components for the Product model and translate the content from English to French. 175 | 176 | Note: The translation feature requires an active internet connection to communicate with the Google Translate API. 177 | 178 | ## Troubleshooting 179 | 180 | Common issues and their solutions: 181 | 182 | 1. **MaryUI package not found**: 183 | - Error: `MaryUI package not found! Please install using: 'composer req robsontenorio/mary'` 184 | - Solution: Run `composer require robsontenorio/mary` to install the MaryUI package. 185 | 186 | 2. **Livewire Volt package not found**: 187 | - Error: `Livewire Volt package not found! Please see doc: 'https://livewire.laravel.com/docs/volt#installation'` 188 | - Solution: Install Livewire Volt using `composer require livewire/livewire livewire/volt && php artisan volt:install`. 189 | 190 | 3. **Model not found**: 191 | - Error: `Model {modelName} does not exist!` 192 | - Solution: Ensure the specified model exists in your model namespace (default: `App\Models`). 193 | 194 | 4. **View file already exists**: 195 | - Error: `File {viewName}.blade.php already exists!` 196 | - Solution: Choose a different name for your view or manually delete the existing file if you want to overwrite it. 197 | 198 | 5. **Translation errors**: 199 | - Error: Various Google Translate API errors 200 | - Solution: Ensure you have an active internet connection and that the language codes you're using are valid. Check the Google Translate documentation for supported language codes. 201 | 202 | 6. **Route generation issues**: 203 | - Problem: Unwanted routes being added to routes/web.php 204 | - Solution: Use the --no-route option when running the marygen:make command to prevent automatic route generation. 205 | 206 | ## Contributing 207 | 208 | Contributions to MaryGen are welcome! Here's how you can contribute: 209 | 210 | 1. Fork the repository 211 | 2. Create a new branch for your feature or bug fix 212 | 3. Write your code and tests 213 | 4. Submit a pull request with a clear description of your changes 214 | 215 | Please ensure your code adheres to the existing style conventions and includes appropriate tests. 216 | 217 | ## License 218 | 219 | MaryGen is open-source software licensed under the MIT license. 220 | 221 | ## Support 222 | 223 | For more information or support: 224 | - Open an issue on the [GitHub repository](https://github.com/soysaltan/mary-gen) 225 | - Contact the package maintainer through the repository's contact information 226 | 227 | --- 228 | 229 | For the latest updates and more detailed information about MaryUI, please visit the [official MaryUI documentation](https://mary-ui.com/docs/installation). 230 | -------------------------------------------------------------------------------- /src/Commands/MaryGenCommand.php: -------------------------------------------------------------------------------- 1 | checkPackageInComposerJson('robsontenorio/mary')) { 32 | $this->error('MaryUI package not found! Please install using: `composer req robsontenorio/mary`'); 33 | return Command::FAILURE; 34 | } 35 | 36 | if (!$this->checkPackageInComposerJson('livewire/volt')) { 37 | $this->error('Livewire Volt package not found! Please run: composer require livewire/livewire livewire/volt && php artisan volt:install or see docs at: `https://livewire.laravel.com/docs/volt#installation`'); 38 | return Command::FAILURE; 39 | } 40 | 41 | $modelName = $this->option('model'); 42 | $modelNamespace = config('marygen.model_namespace'); 43 | 44 | $modelFqdn = "{$modelNamespace}\\{$modelName}"; 45 | 46 | if (!class_exists($modelFqdn)) { 47 | $this->error("Model {$modelName} does not exist!"); 48 | return Command::FAILURE; 49 | } 50 | 51 | $viewName = strtolower($this->option('view') ?? $modelName); 52 | $viewFilePath = $this->getViewFilePath($viewName); 53 | 54 | if (file_exists($viewFilePath)) { 55 | $this->error("File {$viewName}.blade.php already exists!"); 56 | return Command::FAILURE; 57 | } 58 | 59 | if ($this->option('source_lang') && !$this->option('dest_lang')) { 60 | $this->error("Destination language is required if source language presents!"); 61 | return Command::FAILURE; 62 | } 63 | 64 | /** @var Model $modelInstance */ 65 | $modelInstance = new $modelFqdn; 66 | 67 | $table = $modelInstance->setConnection($modelInstance->getConnectionName())->getTable(); 68 | $columns = Schema::connection($modelInstance->getConnectionName())->getColumns($table); 69 | 70 | $modelKey = $modelInstance->setConnection($modelInstance->getConnectionName())->getKeyName(); 71 | 72 | $formFields = $this->generateFormFields($table, $columns, $modelKey, $modelInstance->getConnectionName()); 73 | 74 | $tableColumns = $this->generateTableColumns($columns, $modelKey); 75 | $fieldTypes = $this->getTableFieldTypes($columns); 76 | $accessModifiers = $this->createAccessModifiers($fieldTypes, $modelKey); 77 | 78 | $cols = collect($columns)->pluck('name')->toArray(); 79 | 80 | $livewirePage = $this->generateLivewirePage($cols, $modelName, $formFields, $tableColumns, $accessModifiers, $modelNamespace); 81 | 82 | $this->createLivewireFile($livewirePage, $viewFilePath); 83 | 84 | if (!$this->option('no-route')) { 85 | $route = $this->updateRoute($table, $viewName); 86 | $fullUrl = config('app.url') . '/' . $route; 87 | } 88 | 89 | Artisan::call('view:clear'); 90 | 91 | $this->info("✅ Done! Livewire page for `{$modelName}` has been generated successfully at `{$viewFilePath}`!"); 92 | 93 | if (!$this->option('no-route')) { 94 | $this->info("✅ Route has been added to the routes/web.php file"); 95 | $this->info("🌐 You can access your generated page via `{$fullUrl}`"); 96 | } 97 | 98 | return Command::SUCCESS; 99 | } 100 | 101 | protected function getOptions() 102 | { 103 | return [ 104 | ['--model', 'm', InputOption::VALUE_REQUIRED, 'The name of the model to generate components for'], 105 | ['--view', 'w', InputOption::VALUE_OPTIONAL, 'The name of the view file (defaults to lowercase model name)'], 106 | ['--dest_lang', 'd', InputOption::VALUE_OPTIONAL, 'The destination language for translation'], 107 | ['--source_lang', 's', InputOption::VALUE_OPTIONAL, 'The source language for translation'], 108 | ['--no-route', 'nr', InputOption::VALUE_OPTIONAL, 'Prevent automatic route addition'], 109 | ]; 110 | } 111 | 112 | private function getViewFilePath(string $viewName): string 113 | { 114 | $livewireViewDir = config('livewire.view_path'); 115 | return "{$livewireViewDir}{$this->ds}{$viewName}.blade.php"; 116 | } 117 | 118 | private function checkPackageInComposerJson(string $packageName): bool 119 | { 120 | $composerJson = file_get_contents(base_path('composer.json')); 121 | 122 | if ($composerJson === false) { 123 | throw new RuntimeException("Unable to read composer.json file"); 124 | } 125 | 126 | $composerData = json_decode($composerJson, true); 127 | if ($composerData === null) { 128 | throw new RuntimeException("Invalid JSON in composer.json"); 129 | } 130 | 131 | return isset($composerData['require'][$packageName]) || isset($composerData['require-dev'][$packageName]); 132 | } 133 | 134 | /** 135 | * @throws LargeTextException 136 | * @throws RateLimitException 137 | * @throws TranslationRequestException 138 | */ 139 | private function generateFormFields(string $table, array $columns, string $modelKey, ?string $connectionName = null): string 140 | { 141 | if (is_null($connectionName)) { 142 | $connectionName = config('database.default'); 143 | } 144 | 145 | $fields = ''; 146 | $prefix = config('mary.prefix'); 147 | 148 | if ($this->option('dest_lang')) { 149 | $tr = new GoogleTranslate(); 150 | $tr->setSource($this->option('source_lang') ?? null); 151 | $tr->setTarget($this->option('dest_lang')); 152 | } 153 | 154 | foreach ($columns as $column) { 155 | if ($column['name'] === $modelKey) { 156 | continue; 157 | } 158 | 159 | $colName = $column['name']; 160 | $required = !$column['nullable'] ? 'required' : ''; 161 | 162 | $type = Schema::connection($connectionName)->getColumnType($table, $colName); 163 | 164 | $component = $this->getMaryUIComponent($type); 165 | $typeProp = $colName === 'password' ? 'type="password"' : ''; 166 | $icon = $this->getIconForColumn($colName); 167 | 168 | $label = Str::headline($colName); 169 | 170 | if ($this->option('dest_lang')) { 171 | $label = $tr->translate($label); 172 | } 173 | 174 | $fields .= "\n"; 175 | } 176 | 177 | return $fields; 178 | } 179 | 180 | private function getIconForColumn(string $column): string 181 | { 182 | $iconMap = [ 183 | 'mail' => 'o-envelope', 184 | 'password' => 'o-lock-closed', 185 | 'username' => 'o-user', 186 | 'avatar' => 'o-user-circle', 187 | 'phone' => 'o-phone', 188 | 'time' => 'o-clock', 189 | 'status' => 'o-check-circle', 190 | ]; 191 | 192 | foreach ($iconMap as $key => $icon) { 193 | if (str_contains($column, $key)) { 194 | return "icon=\"{$icon}\""; 195 | } 196 | } 197 | 198 | return ''; 199 | } 200 | 201 | private function getMaryUIComponent(string $type): string 202 | { 203 | $componentMap = [ 204 | 'varchar' => 'input', 205 | 'text' => 'textarea', 206 | 'integer' => 'input', 207 | 'bigint' => 'input', 208 | 'bool' => 'checkbox', 209 | 'time' => 'datepicker', 210 | 'timestamp' => 'datepicker', 211 | ]; 212 | 213 | return $componentMap[$type] ?? 'input'; 214 | } 215 | 216 | private function getPropTypeFromTableField(string $type): string 217 | { 218 | $typeMap = [ 219 | 'integer' => 'int', 220 | 'bigint' => 'int', 221 | 'bool' => 'bool', 222 | ]; 223 | 224 | return $typeMap[$type] ?? 'string'; 225 | } 226 | 227 | private function getTableFieldTypes(array $columns): array 228 | { 229 | $fields = []; 230 | 231 | foreach ($columns as $column) { 232 | $fields[$column['name']] = [ 233 | 'type' => $this->getPropTypeFromTableField($column['type_name']), 234 | 'required' => !$column['nullable'] 235 | ]; 236 | } 237 | return $fields; 238 | } 239 | 240 | private function createAccessModifiers(array $fields, string $modelKey): string 241 | { 242 | return collect($fields)->except([$modelKey])->map(function (array $field, string $id) { 243 | $validationRule = $field['required'] ? 'required' : 'nullable'; 244 | 245 | return sprintf( 246 | '#[Validate(\'%s\')]%spublic %s%s $%s%s;%s%s', 247 | $validationRule, 248 | PHP_EOL, 249 | $field['required'] ? '' : '?', 250 | $field['type'], 251 | $id, 252 | $field['required'] ? '' : '= null', 253 | PHP_EOL, 254 | PHP_EOL 255 | ); 256 | })->implode(''); 257 | } 258 | 259 | /** 260 | * @throws LargeTextException 261 | * @throws RateLimitException 262 | * @throws TranslationRequestException 263 | */ 264 | private function generateTableColumns(array $columns, string $modelKey): string 265 | { 266 | $tableColumns = []; 267 | 268 | if ($this->option('dest_lang')) { 269 | $tr = new GoogleTranslate(); 270 | $tr->setSource($this->option('source_lang') ?? null); 271 | $tr->setTarget($this->option('dest_lang')); 272 | } 273 | 274 | foreach ($columns as $column) { 275 | // Ignore id field 276 | if ($column['name'] === $modelKey) { 277 | continue; 278 | } 279 | 280 | $label = Str::headline($column['name']); 281 | 282 | if ($this->option('dest_lang')) { 283 | $label = addslashes($tr->translate($label)); 284 | } 285 | 286 | $tableColumns[] = "['key' => '{$column['name']}', 'label' => '{$label}', 'sortable' => true],\n"; 287 | } 288 | 289 | $tableColumns[] = "['key' => 'actions', 'label' => 'Actions', 'sortable' => false],"; 290 | 291 | return implode('', $tableColumns); 292 | } 293 | 294 | private function generateLivewirePage(array $dbTableCols, string $modelName, string $formFields, string $tableColumns, string $accessModifiers = '', string $modelNamespace = 'App\Models'): string 295 | { 296 | $modelVariable = Str::camel($modelName); 297 | $pluralModelVariable = Str::plural($modelVariable); 298 | $pluralModelTitle = Str::title($pluralModelVariable); 299 | 300 | $modelFqdn = "{$modelNamespace}\\{$modelName}"; 301 | 302 | $prefix = config('mary.prefix'); 303 | 304 | /** @var Model $modelInstance */ 305 | $modelInstance = new $modelFqdn; 306 | 307 | $modelKey = $modelInstance->getKeyName(); 308 | $hasUuid = $modelInstance->usesUniqueIds(); 309 | 310 | $sortingCol = in_array($modelInstance->getCreatedAtColumn(), $dbTableCols) 311 | ? $modelInstance->getCreatedAtColumn() 312 | : $modelKey; 313 | 314 | $singleQuote = $hasUuid ? "'" : ''; 315 | 316 | $whereLikes = "['" . implode("','", $dbTableCols) . "']"; 317 | 318 | $useMgDirective = (bool)config('marygen.use_mg_like_eloquent_directive'); 319 | 320 | $mgSearchQuery = ''; 321 | 322 | if ($useMgDirective) { 323 | $mgSearchQuery = "->when(\$this->search, fn(Builder \$q) => \$q->mgLike($whereLikes, \$this->search))"; 324 | } 325 | 326 | $lowerModelName = mb_strtolower($modelName, 'UTF-8'); 327 | 328 | $_ = [ 329 | 'listTitle' => $pluralModelTitle, 330 | 'listSubTitle' => "{$modelName} List", 331 | 332 | 'addNew' => "Add New {$modelName}", 333 | 'createModalTitle' => "Create New {$modelName}", 334 | 'createModalDescription' => "Enter the details for the new {$lowerModelName}.", 335 | 336 | 'updateModalTitle' => "Update {$lowerModelName}", 337 | 'updateModalDescription' => "Edit the details of the {$lowerModelName}.", 338 | 339 | 'deleteModalTitle' => "Delete {$lowerModelName}", 340 | 'deleteModalDescription' => 'Are you sure you want to delete this record ?', 341 | 342 | 'save' => 'Save', 343 | 'update' => 'Update', 344 | 'delete' => 'Delete', 345 | 'create' => 'Create', 346 | 'edit' => 'Edit', 347 | 'search' => 'Search', 348 | 'cancel' => 'Cancel', 349 | 'yes' => 'Yes', 350 | 'no' => 'No', 351 | ]; 352 | 353 | if ($this->option('dest_lang')) { 354 | $tr = new GoogleTranslate(); 355 | $tr->setSource($this->option('source_lang') ?? null); 356 | $tr->setTarget($this->option('dest_lang')); 357 | 358 | $_['listTitle'] = $tr->translate($_['listTitle']); 359 | $_['listSubTitle'] = $tr->translate($_['listSubTitle']); 360 | 361 | $_['addNew'] = $tr->translate($_['addNew']); 362 | $_['createModalTitle'] = $tr->translate($_['createModalTitle']); 363 | $_['createModalDescription'] = $tr->translate($_['createModalDescription']); 364 | 365 | $_['updateModalTitle'] = $tr->translate($_['updateModalTitle']); 366 | $_['updateModalDescription'] = $tr->translate($_['updateModalDescription']); 367 | 368 | $_['deleteModalTitle'] = $tr->translate($_['deleteModalTitle']); 369 | $_['deleteModalDescription'] = $tr->translate($_['deleteModalDescription']); 370 | 371 | $_['save'] = $tr->translate($_['save']); 372 | $_['update'] = $tr->translate($_['update']); 373 | $_['delete'] = $tr->translate($_['delete']); 374 | $_['create'] = $tr->translate($_['create']); 375 | $_['cancel'] = $tr->translate($_['cancel']); 376 | $_['yes'] = $tr->translate($_['yes']); 377 | $_['no'] = $tr->translate($_['no']); 378 | $_['search'] = $tr->translate($_['search']); 379 | $_['edit'] = $tr->translate($_['edit']); 380 | } 381 | 382 | return << '$sortingCol', 'direction' => 'desc']; 400 | public int \$perPage = 10; 401 | public string \$search = ''; 402 | 403 | public bool \$isEditModalOpen = false; 404 | public bool \$isCreateModalOpen = false; 405 | public bool \$isDeleteModalOpen = false; 406 | 407 | public {$modelName}|Model|null \$editingModel = null; 408 | public {$modelName}|Model|null \$modelToDelete = null; 409 | 410 | {$accessModifiers} 411 | 412 | public function openEditModal(string \$modelId): void 413 | { 414 | \$this->editingModel = {$modelName}::findOrFail(\$modelId); 415 | \$this->fill(\$this->editingModel->toArray()); 416 | \$this->isEditModalOpen = true; 417 | } 418 | 419 | public function openCreateModal(): void 420 | { 421 | \$this->reset(); 422 | \$this->isCreateModalOpen = true; 423 | } 424 | 425 | public function openDeleteModal({$modelName} \$model): void 426 | { 427 | \$this->modelToDelete = \$model; 428 | \$this->isDeleteModalOpen = true; 429 | } 430 | 431 | public function closeModal(): void 432 | { 433 | \$this->isEditModalOpen = false; 434 | \$this->isCreateModalOpen = false; 435 | \$this->editingModel = null; 436 | } 437 | 438 | public function deleteModel(): void 439 | { 440 | \$this->modelToDelete->delete(); 441 | 442 | \$this->isDeleteModalOpen = false; 443 | } 444 | 445 | public function saveModel(): void 446 | { 447 | \$validated = \$this->validate(); 448 | 449 | if (\$this->editingModel) { 450 | \$this->editingModel->update(\$validated); 451 | \$this->success('Record updated successfully.'); 452 | } else { 453 | {$modelName}::create(\$validated); 454 | \$this->success('Record created successfully.'); 455 | } 456 | \$this->closeModal(); 457 | } 458 | 459 | public function headers(): array 460 | { 461 | return [ 462 | {$tableColumns} 463 | ]; 464 | } 465 | 466 | public function {$pluralModelVariable}(): \Illuminate\Pagination\LengthAwarePaginator 467 | { 468 | return {$modelName}::query() 469 | $mgSearchQuery 470 | ->orderBy(\$this->sortBy['column'], \$this->sortBy['direction']) 471 | ->paginate(15); 472 | } 473 | 474 | public function with(): array 475 | { 476 | return [ 477 | 'headers' => \$this->headers(), 478 | '{$pluralModelVariable}' => \$this->{$pluralModelVariable}(), 479 | ]; 480 | } 481 | } 482 | ?> 483 | 484 |
485 | 486 | 487 | 489 | 490 | 491 | 493 | {$_['addNew']} 494 | 495 | 496 | 497 | 498 | 499 | @php 500 | /** @var $modelName \${$modelVariable} */ 501 | @endphp 502 | @scope('cell_actions', \${$modelVariable}) 503 |
504 | 505 | 506 |
507 | @endscope 508 |
509 | 510 | @if (\$isEditModalOpen) 511 | 512 | 513 |

514 | {$_['updateModalDescription']} 515 |

516 |
517 | {$formFields} 518 |
519 | 520 |
521 | 522 | 523 |
524 |
525 |
526 |
527 | @endif 528 | 529 | 530 | @if (\$isCreateModalOpen) 531 | 532 | 533 |

534 | {$_['createModalDescription']} 535 |

536 |
537 | {$formFields} 538 |
539 | 540 |
541 | 542 | 543 |
544 |
545 |
546 |
547 | @endif 548 | 549 | @if (\$isDeleteModalOpen) 550 | 551 |
{$_['deleteModalDescription']}
552 | 553 | 554 | 555 | 556 | 557 |
558 | @endif 559 |
560 | EOT; 561 | } 562 | 563 | /** 564 | * @throws FileNotFoundException 565 | */ 566 | private function updateRoute(string $tableName, string $viewName): string 567 | { 568 | $webRouteContent = File::get(base_path('routes/web.php')); 569 | $uri = str($tableName)->kebab(); 570 | 571 | $viewName = str_replace('/', '.', $viewName); 572 | $route = "Volt::route('/{$uri}', '{$viewName}');"; 573 | 574 | if (!str($webRouteContent)->contains($route)) { 575 | File::put(base_path('routes/web.php'), str($webRouteContent)->append("\r\n")->append($route)); 576 | } 577 | 578 | return $uri; 579 | } 580 | 581 | private function createLivewireFile(string $content, string $filePath): void 582 | { 583 | $this->createFile($filePath, $content); 584 | } 585 | 586 | private function createFile(string $filePath, string $content = ''): bool 587 | { 588 | $pathInfo = pathinfo($filePath); 589 | 590 | $dirPath = $pathInfo['dirname']; 591 | 592 | if (!is_dir($dirPath)) { 593 | mkdir($dirPath, 0755, true); 594 | } 595 | 596 | if (file_put_contents($filePath, $content) !== false) { 597 | return true; 598 | } 599 | 600 | return false; 601 | } 602 | } 603 | --------------------------------------------------------------------------------