├── LICENSE ├── README.md ├── bin ├── fresns └── plugin ├── composer.json ├── config └── plugins.php ├── package-lock.json └── src ├── Commands ├── BackCommand.php ├── CustomCommand.php ├── EnterCommand.php ├── FresnsCommand.php ├── MakeCmdWordProviderCommand.php ├── MakeEventProviderCommand.php ├── MakeExceptionProviderCommand.php ├── MakeScheduleProviderCommand.php ├── MakeSqlProviderCommand.php ├── NewCommand.php ├── PluginActivateCommand.php ├── PluginCommand.php ├── PluginComposerUpdateCommand.php ├── PluginDeactivateCommand.php ├── PluginInstallCommand.php ├── PluginListCommand.php ├── PluginMakeCommand.php ├── PluginMigrateCommand.php ├── PluginMigrateRefreshCommand.php ├── PluginMigrateResetCommand.php ├── PluginMigrateRollbackCommand.php ├── PluginPublishCommand.php ├── PluginSeedCommand.php ├── PluginUninstallCommand.php ├── PluginUnpublishCommand.php ├── PluginUnzipCommand.php ├── Traits │ ├── StubTrait.php │ └── WorkPluginFskeyTrait.php └── stubs │ ├── app │ ├── Http │ │ └── Controllers │ │ │ ├── admin-controller.stub │ │ │ ├── api-controller.stub │ │ │ ├── controller.stub │ │ │ └── web-controller.stub │ ├── Providers │ │ ├── cmd-word-provider.stub │ │ ├── command-provider.stub │ │ ├── event-provider.stub │ │ ├── exception-provider.stub │ │ ├── route-provider.stub │ │ ├── schedule-provider.stub │ │ ├── service-provider.stub │ │ └── sql-provider.stub │ └── Services │ │ └── cmd-word-service.stub │ ├── composer.json.stub │ ├── config │ └── config.stub │ ├── database │ ├── migrations │ │ └── init_plugin_config.stub │ └── seeders │ │ └── seeder.stub │ ├── gitignore.stub │ ├── package.json.stub │ ├── plugin.json.stub │ ├── readme.stub │ ├── resources │ ├── assets │ │ ├── css │ │ │ └── fresns.stub │ │ └── js │ │ │ └── fresns.stub │ └── views │ │ ├── app.stub │ │ ├── index.stub │ │ ├── layouts │ │ ├── footer.stub │ │ ├── header.stub │ │ ├── master.stub │ │ └── tips.stub │ │ └── settings.stub │ └── routes │ ├── api.stub │ └── web.stub ├── Exceptions └── FileAlreadyExistException.php ├── Generators ├── FileGenerator.php └── Generator.php ├── Helpers.php ├── Manager └── FileManager.php ├── Plugin.php ├── Providers └── PluginServiceProvider.php ├── Support ├── Config │ ├── GenerateConfigReader.php │ └── GeneratorPath.php ├── Json.php ├── Migrations │ ├── NameParser.php │ └── SchemaParser.php ├── Process.php ├── Stub.php └── Zip.php └── Workaround ├── FactoryMakeCommand.php ├── SeedCommand.php ├── SeederMakeCommand.php ├── TestCommand.php ├── TestMakeCommand.php └── stubs ├── factory.stub ├── seeder.stub ├── test.stub └── test.unit.stub /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | PHP 5 | Laravel 6 | License 7 |

8 | 9 | ## About Plugin Manager 10 | 11 | Enhance Laravel Apps: Organized & Scalable 12 | 13 | `fresns/plugin-manager` is a convenient Laravel extension package designed for modular management of your large-scale Laravel applications. Each plugin acts as an independent Laravel application or microservice, allowing you to define your own views, controllers and models. 14 | 15 | Plugin Manager Docs: [https://pm.fresns.org](https://pm.fresns.org/) 16 | 17 | ## Sponsors 18 | 19 | Fresns is an Apache-2.0-licensed open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring Fresns development](https://github.com/sponsors/fresns). 20 | 21 | ## Install 22 | 23 | To install through Composer, by run the following command: 24 | 25 | ```bash 26 | composer require fresns/plugin-manager 27 | ``` 28 | 29 | The package will automatically register a service provider and alias. 30 | 31 | Optionally, publish the package's configuration file by running: 32 | 33 | ```bash 34 | php artisan vendor:publish --provider="Fresns\PluginManager\Providers\PluginServiceProvider" 35 | ``` 36 | 37 | ## Development Docs 38 | 39 | [https://pm.fresns.org](https://pm.fresns.org/) 40 | 41 | ## Contributing 42 | 43 | You can contribute in one of three ways: 44 | 45 | 1. File bug reports using the [issue tracker](https://github.com/fresns/plugin-manager/issues). 46 | 2. Answer questions or fix bugs on the [issue tracker](https://github.com/fresns/plugin-manager/issues). 47 | 3. Contribute new features or update the wiki. 48 | 49 | *The code contribution process is not very formal. You just need to make sure that you follow the PSR-0, PSR-1, and PSR-2 coding guidelines. Any new code contributions must be accompanied by unit tests where applicable.* 50 | 51 | ## License 52 | 53 | Fresns Plugin Manager is open-sourced software licensed under the [Apache-2.0 license](https://github.com/fresns/plugin-manager/blob/main/LICENSE). 54 | -------------------------------------------------------------------------------- /bin/fresns: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Fresns (https://fresns.org) 5 | # Copyright (C) 2021-Present Jevan Tang 6 | # Released under the Apache-2.0 License. 7 | # 8 | 9 | workDir=$PWD 10 | rootDir=$workDir 11 | 12 | while [ ! -f "$rootDir/vendor/autoload.php" ]; do 13 | rootDir=${rootDir%/*} 14 | 15 | if [ -z $rootDir ]; then 16 | echo "Can't find laravel project in current path" 17 | echo "You should run 'plugin' under a laravel project" 18 | exit -1 19 | fi 20 | done 21 | 22 | if [ ! -f "$rootDir/vendor/autoload.php" ]; then 23 | echo "You should run composer install first" 24 | exit -1 25 | fi 26 | 27 | COMMAND=$1 28 | case $COMMAND in 29 | *) 30 | if [[ $PATH == *$rootDir/vendor/bin* ]]; then 31 | plugin "$@" 32 | else 33 | echo 'Please input this content in your terminal:' 34 | echo 35 | echo export PATH=$rootDir:$rootDir/vendor/bin:'$PATH' 36 | fi 37 | ;; 38 | esac 39 | -------------------------------------------------------------------------------- /bin/plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getFirstArgument(); 85 | if (is_null($grabCommand)) { 86 | $grabCommand = ''; 87 | } 88 | 89 | if (!\Illuminate\Support\Str::startsWith($grabCommand, $usableCommands)) { 90 | $asPlugin = false; 91 | } 92 | 93 | // change path for plugin 94 | if ($asPlugin) { 95 | $pluginFskey = substr($workDir, strrpos($workDir, DIRECTORY_SEPARATOR) + 1); 96 | echo "Work Plugin: " . $pluginFskey . PHP_EOL; 97 | 98 | $composer = json_decode(file_get_contents($workDir . DIRECTORY_SEPARATOR . 'composer.json'), true); 99 | 100 | if ($classNamespace = array_search('src', $composer['autoload']['psr-4'], true)) { 101 | $classNamespace = substr($classNamespace, 0, -1); 102 | } else { 103 | $classNamespace = array_keys($composer['autoload']['psr-4'])[0]; 104 | $classNamespace = \Illuminate\Support\Str::before($classNamespace, '\\'); 105 | $classNamespace .= '\\' . \Illuminate\Support\Str::studly($pluginFskey); 106 | } 107 | 108 | unset($composer); 109 | 110 | $app->useAppPath($workDir . '/app'); 111 | $app->useDatabasePath($workDir . '/database'); 112 | $app->pluginClassNamespace = $classNamespace; 113 | 114 | // inject namespace 115 | $property = new ReflectionProperty($app, 'namespace'); 116 | $property->setAccessible(true); 117 | $property->setValue($app, $classNamespace . '\\'); 118 | $property->setAccessible(false); 119 | 120 | require __DIR__ . '/../src/Workaround/TestMakeCommand.php'; 121 | require __DIR__ . '/../src/Workaround/FactoryMakeCommand.php'; 122 | require __DIR__ . '/../src/Workaround/SeedCommand.php'; 123 | require __DIR__ . '/../src/Workaround/SeederMakeCommand.php'; 124 | require __DIR__ . '/../src/Workaround/TestCommand.php'; 125 | } 126 | 127 | /* 128 | |-------------------------------------------------------------------------- 129 | | Run The Artisan Application 130 | |-------------------------------------------------------------------------- 131 | | 132 | | When we run the console application, the current CLI command will be 133 | | executed in this console and the response sent back to a terminal 134 | | or another output device for the developers. Here goes nothing! 135 | | 136 | */ 137 | 138 | $kernel = $app->make(Illuminate\Contracts\Console\Kernel::class); 139 | 140 | $status = $kernel->handle($input, new Symfony\Component\Console\Output\ConsoleOutput); 141 | 142 | // make:controller workaround for plugin 143 | if ($asPlugin && $grabCommand === 'make:controller' && $input->getArgument('name')) { 144 | $fileName = $workDir . '/app/Http/Controllers/' . str_replace('\\', DIRECTORY_SEPARATOR, $input->getArgument('name')) . '.php'; 145 | $content = file_get_contents($fileName); 146 | 147 | if (strpos($content, 'use App\Http\Controllers\Controller;') === false) { 148 | $content = str_replace( 149 | "use Illuminate\Http\Request;", 150 | "use Illuminate\Http\Request;\nuse App\Http\Controllers\Controller;", 151 | $content 152 | ); 153 | } 154 | 155 | file_put_contents($fileName, $content); 156 | } 157 | 158 | // make:request workaround for laravel and plugin 159 | if ($grabCommand === 'make:request' && $input->getArgument('name')) { 160 | $fileName = $workDir . '/app/Http/Requests/' . str_replace('\\', DIRECTORY_SEPARATOR, $input->getArgument('name')) . '.php'; 161 | $content = file_get_contents($fileName); 162 | 163 | if (strpos($content, 'use App\Http\Controllers\Controller;') === false) { 164 | $content = str_replace( 165 | "return false;", 166 | "return true;", 167 | $content 168 | ); 169 | 170 | $content = str_replace( 171 | <<<"TXT" 172 | public function rules(): array 173 | { 174 | return [ 175 | // 176 | ]; 177 | } 178 | TXT, 179 | <<<'TXT' 180 | public function rules(): array 181 | { 182 | return match (\request()->route()->getActionMethod()) { 183 | default => [], 184 | }; 185 | } 186 | 187 | public function attributes(): array 188 | { 189 | return [ 190 | // 191 | ]; 192 | } 193 | TXT, 194 | $content 195 | ); 196 | } 197 | 198 | file_put_contents($fileName, $content); 199 | } 200 | 201 | /* 202 | |-------------------------------------------------------------------------- 203 | | Shutdown The Application 204 | |-------------------------------------------------------------------------- 205 | | 206 | | Once Artisan has finished running, we will fire off the shutdown events 207 | | so that any final work may be done by the application before we shut 208 | | down the process. This is the last thing to happen to the request. 209 | | 210 | */ 211 | 212 | $kernel->terminate($input, $status); 213 | 214 | exit($status); 215 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fresns/plugin-manager", 3 | "type": "library", 4 | "description": "Enhance Laravel Apps: Organized & Scalable", 5 | "keywords": ["laravel", "laravel-package", "laravel-modules", "laravel-application", "laravel-plugin", "laravel-apps", "laravel-extensions"], 6 | "license": "Apache-2.0", 7 | "homepage": "https://pm.fresns.org", 8 | "support": { 9 | "issues": "https://github.com/fresns/plugin-manager/issues", 10 | "source": "https://github.com/fresns/plugin-manager", 11 | "docs": "https://pm.fresns.org" 12 | }, 13 | "authors": [ 14 | { 15 | "name": "Jevan Tang", 16 | "email": "jevan@fresns.org", 17 | "homepage": "https://github.com/jevantang", 18 | "role": "Creator" 19 | }, 20 | { 21 | "name": "mouyong", 22 | "email": "my24251325@gmail.com", 23 | "homepage": "https://github.com/mouyong", 24 | "role": "Developer" 25 | } 26 | ], 27 | "bin": [ 28 | "bin/fresns", 29 | "bin/plugin" 30 | ], 31 | "require": { 32 | "php": "^8.0.2", 33 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0", 34 | "wikimedia/composer-merge-plugin": "dev-master", 35 | "nelexa/zip": "^4.0" 36 | }, 37 | "require-dev": {}, 38 | "autoload": { 39 | "psr-4": { 40 | "Fresns\\PluginManager\\": "src" 41 | } 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Fresns\\PluginManager\\Providers\\PluginServiceProvider" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/plugins.php: -------------------------------------------------------------------------------- 1 | $pluginsNamespace = 'Plugins', 11 | 12 | // YOU COULD CUSTOM HERE 13 | 'namespaces' => [ 14 | $pluginsNamespace => [ 15 | base_path('plugins'), 16 | ], 17 | ], 18 | 19 | 'autoload_files' => [ 20 | base_path('vendor/fresns/plugin-manager/src/Helpers.php'), 21 | ], 22 | 23 | 'merge_plugin_config' => [ 24 | 'include' => [ 25 | ltrim(str_replace(base_path(), '', base_path('plugins/*/composer.json')), '/'), 26 | ], 27 | 'recurse' => true, 28 | 'replace' => false, 29 | 'ignore-duplicates' => false, 30 | 'merge-dev' => true, 31 | 'merge-extra' => true, 32 | 'merge-extra-deep' => true, 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Composer File Template 38 | |-------------------------------------------------------------------------- 39 | | 40 | | YOU COULD CUSTOM HERE 41 | | 42 | */ 43 | 'composer' => [ 44 | 'vendor' => 'fresns', 45 | 'author' => [ 46 | [ 47 | 'name' => 'Jevan Tang', 48 | 'email' => 'jevan@fresns.org', 49 | 'homepage' => 'https://github.com/jevantang', 50 | 'role' => 'Creator', 51 | ], 52 | ], 53 | ], 54 | 55 | 'paths' => [ 56 | 'unzip_target_path' => base_path('storage/extensions/.tmp'), 57 | 'backups' => base_path('storage/extensions/backups'), 58 | 'plugins' => base_path('plugins'), 59 | 'assets' => public_path('assets'), 60 | 'migration' => base_path('database/migrations'), 61 | 62 | 'generator' => [ 63 | 'command' => ['path' => 'app/Console', 'generate' => false], 64 | 'controller' => ['path' => 'app/Http/Controllers', 'generate' => false], 65 | 'filter' => ['path' => 'app/Http/Middleware', 'generate' => false], 66 | 'request' => ['path' => 'app/Http/Requests', 'generate' => false], 67 | 'resource' => ['path' => 'app/Http/Resources', 'generate' => false], 68 | 'model' => ['path' => 'app/Models', 'generate' => true], 69 | 'provider' => ['path' => 'app/Providers', 'generate' => true], 70 | 'policies' => ['path' => 'app/Policies', 'generate' => false], 71 | 'repository' => ['path' => 'app/Repositories', 'generate' => false], 72 | 'event' => ['path' => 'app/Events', 'generate' => false], 73 | 'listener' => ['path' => 'app/Listeners', 'generate' => false], 74 | 'rules' => ['path' => 'app/Rules', 'generate' => false], 75 | 'jobs' => ['path' => 'app/Jobs', 'generate' => false], 76 | 'emails' => ['path' => 'app/Mail', 'generate' => false], 77 | 'notifications' => ['path' => 'app/Notifications', 'generate' => false], 78 | 'config' => ['path' => 'config', 'generate' => true], 79 | 'migration' => ['path' => 'database/migrations', 'generate' => true], 80 | 'seeder' => ['path' => 'database/seeders', 'generate' => true], 81 | 'factory' => ['path' => 'database/factories', 'generate' => true], 82 | 'routes' => ['path' => 'routes', 'generate' => true], 83 | 'assets' => ['path' => 'resources/assets', 'generate' => true], 84 | 'lang' => ['path' => 'resources/lang', 'generate' => true], 85 | 'views' => ['path' => 'resources/views', 'generate' => true], 86 | 'test' => ['path' => 'tests/Unit', 'generate' => true], 87 | 'test-feature' => ['path' => 'tests/Feature', 'generate' => true], 88 | ], 89 | ], 90 | 91 | 'stubs' => [ 92 | 'path' => dirname(__DIR__).'/src/Commands/stubs', 93 | 'files' => [ 94 | 'app/Http/Controllers/controller' => 'app/Http/Controllers/Controller.php', 95 | 'app/Http/Controllers/admin-controller' => 'app/Http/Controllers/AdminController.php', 96 | 'app/Http/Controllers/api-controller' => 'app/Http/Controllers/ApiController.php', 97 | 'app/Http/Controllers/web-controller' => 'app/Http/Controllers/WebController.php', 98 | 'app/Providers/service-provider' => 'app/Providers/PluginServiceProvider.php', 99 | 'app/Providers/command-provider' => 'app/Providers/CommandServiceProvider.php', 100 | 'app/Providers/route-provider' => 'app/Providers/RouteServiceProvider.php', 101 | 'config/config' => 'config/$KEBAB_NAME$.php', 102 | 'database/migrations/init_plugin_config' => 'database/migrations/init_$SNAKE_NAME$_config.php', 103 | 'database/seeders/seeder' => 'database/seeders/DatabaseSeeder.php', 104 | 'resources/assets/css/fresns' => 'resources/assets/css/fresns.css', 105 | 'resources/assets/js/fresns' => 'resources/assets/js/fresns.js', 106 | 'resources/views/layouts/master' => 'resources/views/layouts/master.blade.php', 107 | 'resources/views/layouts/header' => 'resources/views/layouts/header.blade.php', 108 | 'resources/views/layouts/footer' => 'resources/views/layouts/footer.blade.php', 109 | 'resources/views/layouts/tips' => 'resources/views/layouts/tips.blade.php', 110 | 'resources/views/app' => 'resources/views/app.blade.php', 111 | 'resources/views/index' => 'resources/views/index.blade.php', 112 | 'resources/views/settings' => 'resources/views/settings.blade.php', 113 | 'routes/web' => 'routes/web.php', 114 | 'routes/api' => 'routes/api.php', 115 | 'package.json' => 'package.json', 116 | 'composer.json' => 'composer.json', 117 | 'plugin.json' => 'plugin.json', 118 | 'readme' => 'README.md', 119 | 'gitignore' => '.gitignore', 120 | ], 121 | 'gitkeep' => true, 122 | ], 123 | 124 | 'manager' => [ 125 | 'default' => [ 126 | 'file' => base_path('fresns.json'), 127 | ], 128 | ], 129 | ]; 130 | -------------------------------------------------------------------------------- /src/Commands/BackCommand.php: -------------------------------------------------------------------------------- 1 | warn('Back to the root directory'); 29 | $this->line(''); 30 | $this->warn('Please input this command on your terminal:'); 31 | 32 | $command = sprintf('cd %s', $basePath); 33 | $this->line($command); 34 | $this->line(''); 35 | } else { 36 | $this->info('Currently in the root directory'); 37 | $this->line($basePath); 38 | 39 | $this->line(''); 40 | $this->info('Now you can run command:'); 41 | $this->line('fresns or php artisan'); 42 | } 43 | 44 | return Command::SUCCESS; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Commands/CustomCommand.php: -------------------------------------------------------------------------------- 1 | error('config/plugins.php is already existed'); 25 | 26 | return Command::FAILURE; 27 | } 28 | 29 | $from = dirname(__DIR__, 2).'/config/plugins.php'; 30 | 31 | copy($from, $to); 32 | 33 | $this->line('Config file copied to ['.$to.']'); 34 | 35 | return Command::SUCCESS; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Commands/EnterCommand.php: -------------------------------------------------------------------------------- 1 | error('Plugin directory not retrieved'); 24 | 25 | return Command::FAILURE; 26 | } 27 | 28 | $fskey = $this->argument('fskey'); 29 | 30 | $pluginPath = "{$pluginRootPath}/{$fskey}"; 31 | if (! file_exists($pluginPath)) { 32 | $this->error("Plugin directory {$fskey} does not exist"); 33 | 34 | return Command::FAILURE; 35 | } 36 | 37 | if (str_contains(strtolower(PHP_OS_FAMILY), 'win')) { 38 | $pluginPath = str_replace(['\\', '/'], DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR, $pluginPath); 39 | } 40 | 41 | if (getenv('PWD') != $pluginPath) { 42 | $this->warn("Go to the plugin {$fskey} directory"); 43 | $this->line(''); 44 | $this->warn('Please input this command on your terminal:'); 45 | 46 | $command = sprintf('cd %s', $pluginPath); 47 | $this->line($command); 48 | $this->line(''); 49 | } else { 50 | $this->info("Currently in the plugin {$fskey} directory"); 51 | $this->line($pluginPath); 52 | 53 | $this->line(''); 54 | $this->info("Now you can run command in your plugin: {$fskey}"); 55 | $this->line('fresns'); 56 | } 57 | 58 | return Command::SUCCESS; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Commands/FresnsCommand.php: -------------------------------------------------------------------------------- 1 | argument('action') ?? 'activate'; 24 | if (method_exists($this, $action)) { 25 | $this->{$action}($vendorBinPath); 26 | } 27 | 28 | return Command::SUCCESS; 29 | } 30 | 31 | public function activate(string $vendorBinPath) 32 | { 33 | $rootDir = base_path(); 34 | $command = sprintf('export %s', "PATH=$rootDir:$vendorBinPath:".'$PATH'); 35 | if (! str_contains(getenv('PATH'), $vendorBinPath)) { 36 | $this->warn('Add Project vendorBinPath'); 37 | $this->line(''); 38 | $this->warn('Please input this command on your terminal:'); 39 | $this->line($command); 40 | 41 | $this->line(''); 42 | $this->warn('Then rerun command to get usage help:'); 43 | $this->line('fresns plugin'); 44 | } else { 45 | $this->warn('Already Add Project vendorBinPath: '); 46 | $this->line($vendorBinPath); 47 | 48 | $this->line(''); 49 | $this->info('Now you can run command:'); 50 | $this->line('fresns'); 51 | } 52 | } 53 | 54 | public function deactivate(string $vendorBinPath) 55 | { 56 | $pathExcludeProjectBin = str_replace($vendorBinPath, '', getenv('PATH')); 57 | $fixErrorPath = str_replace(['::'], '', $pathExcludeProjectBin); 58 | $command = sprintf('export %s', "PATH=$fixErrorPath"); 59 | 60 | $this->warn('Remove Project vendorBinPath'); 61 | $this->line(''); 62 | $this->warn('Please input this command on your terminal:'); 63 | $this->line(''); 64 | $this->line($command); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Commands/MakeCmdWordProviderCommand.php: -------------------------------------------------------------------------------- 1 | getPath('Providers/'.$this->getNameInput()); 24 | $pluginFskey = basename(dirname($path, 3)); 25 | $pluginJsonPath = dirname($path, 3).'/plugin.json'; 26 | 27 | $this->generateCmdWordService($pluginFskey); 28 | 29 | parent::handle(); 30 | 31 | if (is_file($pluginJsonPath)) { 32 | $this->installPluginProviderAfter( 33 | $this->getPluginJsonSearchContent($pluginFskey), 34 | $this->getPluginJsonReplaceContent($this->getNameInput(), $pluginFskey), 35 | $pluginJsonPath 36 | ); 37 | } 38 | } 39 | 40 | protected function getStubName(): string 41 | { 42 | return 'app/Providers/cmd-word-provider'; 43 | } 44 | 45 | protected function getDefaultNamespace($rootNamespace) 46 | { 47 | return $rootNamespace."\Providers"; 48 | } 49 | 50 | protected function generateCmdWordService($pluginFskey) 51 | { 52 | $path = $this->getPath('Services/CmdWordService'); 53 | $dirpath = dirname($path); 54 | 55 | if (! is_dir($dirpath)) { 56 | @mkdir($dirpath, 0755, true); 57 | } 58 | 59 | if (! is_file($path)) { 60 | $stubPath = __DIR__.'/stubs/app/Services/cmd-word-service.stub'; 61 | 62 | $content = file_get_contents($stubPath); 63 | 64 | $newContent = str_replace('$STUDLY_NAME$', $pluginFskey, $content); 65 | 66 | file_put_contents($path, $newContent); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Commands/MakeEventProviderCommand.php: -------------------------------------------------------------------------------- 1 | getPath('Providers/'.$this->getNameInput()); 24 | $pluginFskey = basename(dirname($path, 3)); 25 | $pluginJsonPath = dirname($path, 3).'/plugin.json'; 26 | 27 | parent::handle(); 28 | 29 | if (is_file($pluginJsonPath)) { 30 | $this->installPluginProviderAfter( 31 | $this->getPluginJsonSearchContent($pluginFskey), 32 | $this->getPluginJsonReplaceContent($this->getNameInput(), $pluginFskey), 33 | $pluginJsonPath 34 | ); 35 | } 36 | } 37 | 38 | protected function getStubName(): string 39 | { 40 | return 'app/Providers/event-provider'; 41 | } 42 | 43 | protected function getDefaultNamespace($rootNamespace) 44 | { 45 | return $rootNamespace."\Providers"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/MakeExceptionProviderCommand.php: -------------------------------------------------------------------------------- 1 | getPath('Providers/'.$this->getNameInput()); 24 | $pluginFskey = basename(dirname($path, 3)); 25 | $pluginJsonPath = dirname($path, 3).'/plugin.json'; 26 | 27 | parent::handle(); 28 | 29 | if (is_file($pluginJsonPath)) { 30 | $this->installPluginProviderAfter( 31 | $this->getPluginJsonSearchContent($pluginFskey), 32 | $this->getPluginJsonReplaceContent($this->getNameInput(), $pluginFskey), 33 | $pluginJsonPath 34 | ); 35 | } 36 | } 37 | 38 | protected function getStubName(): string 39 | { 40 | return 'app/Providers/exception-provider'; 41 | } 42 | 43 | protected function getDefaultNamespace($rootNamespace) 44 | { 45 | return $rootNamespace."\Providers"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/MakeScheduleProviderCommand.php: -------------------------------------------------------------------------------- 1 | getPath('Providers/'.$this->getNameInput()); 24 | $pluginFskey = basename(dirname($path, 3)); 25 | $pluginJsonPath = dirname($path, 3).'/plugin.json'; 26 | 27 | parent::handle(); 28 | 29 | if (is_file($pluginJsonPath)) { 30 | $this->installPluginProviderAfter( 31 | $this->getPluginJsonSearchContent($pluginFskey), 32 | $this->getPluginJsonReplaceContent($this->getNameInput(), $pluginFskey), 33 | $pluginJsonPath 34 | ); 35 | } 36 | } 37 | 38 | protected function getStubName(): string 39 | { 40 | return 'app/Providers/schedule-provider'; 41 | } 42 | 43 | protected function getDefaultNamespace($rootNamespace) 44 | { 45 | return $rootNamespace."\Providers"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/MakeSqlProviderCommand.php: -------------------------------------------------------------------------------- 1 | getPath('Providers/'.$this->getNameInput()); 24 | $pluginFskey = basename(dirname($path, 3)); 25 | $pluginJsonPath = dirname($path, 3).'/plugin.json'; 26 | 27 | parent::handle(); 28 | 29 | if (is_file($pluginJsonPath)) { 30 | $this->installPluginProviderAfter( 31 | $this->getPluginJsonSearchContent($pluginFskey), 32 | $this->getPluginJsonReplaceContent($this->getNameInput(), $pluginFskey), 33 | $pluginJsonPath 34 | ); 35 | } 36 | } 37 | 38 | protected function getStubName(): string 39 | { 40 | return 'app/Providers/sql-provider'; 41 | } 42 | 43 | protected function getDefaultNamespace($rootNamespace) 44 | { 45 | return $rootNamespace."\Providers"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/NewCommand.php: -------------------------------------------------------------------------------- 1 | filesystem = $this->laravel['files']; 52 | $this->pluginFskey = Str::afterLast($this->argument('fskey'), '/'); 53 | 54 | $this->plugin = new Plugin($this->pluginFskey); 55 | 56 | // clear directory or exit when plugin exists. 57 | if (File::exists($this->plugin->getPluginPath())) { 58 | if (! $this->option('force')) { 59 | $this->error("Plugin {$this->plugin->getFskey()} exists"); 60 | 61 | return Command::FAILURE; 62 | } 63 | 64 | File::deleteDirectory($this->plugin->getPluginPath()); 65 | } 66 | 67 | $this->generateFolders(); 68 | $this->generateFiles(); 69 | 70 | // composer dump-autoload 71 | Process::run('composer dump-autoload', $this->output); 72 | 73 | $this->info("Package [{$this->pluginFskey}] created successfully"); 74 | 75 | return Command::SUCCESS; 76 | } 77 | 78 | /** 79 | * Get the list of folders will created. 80 | * 81 | * @return array 82 | */ 83 | public function getFolders() 84 | { 85 | return config('plugins.paths.generator'); 86 | } 87 | 88 | /** 89 | * Generate the folders. 90 | */ 91 | public function generateFolders() 92 | { 93 | foreach ($this->getFolders() as $key => $folder) { 94 | $folder = GenerateConfigReader::read($key); 95 | 96 | if ($folder->generate() === false) { 97 | continue; 98 | } 99 | 100 | $path = config('plugins.paths.plugins').'/'.$this->argument('fskey').'/'.$folder->getPath(); 101 | 102 | $this->filesystem->makeDirectory($path, 0755, true); 103 | if (config('plugins.stubs.gitkeep')) { 104 | $this->generateGitKeep($path); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Generate git keep to the specified path. 111 | * 112 | * @param string $path 113 | */ 114 | public function generateGitKeep($path) 115 | { 116 | $this->filesystem->put($path.'/.gitkeep', ''); 117 | } 118 | 119 | /** 120 | * Remove git keep from the specified path. 121 | * 122 | * @param string $path 123 | */ 124 | public function removeParentDirGitKeep(string $path) 125 | { 126 | if (config('plugins.stubs.gitkeep')) { 127 | $dirName = dirname($path); 128 | if (count($this->filesystem->glob("$dirName/*")) >= 1) { 129 | $this->filesystem->delete("$dirName/.gitkeep"); 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Get the list of files will created. 136 | * 137 | * @return array 138 | */ 139 | public function getFiles() 140 | { 141 | return config('plugins.stubs.files'); 142 | } 143 | 144 | /** 145 | * Generate the files. 146 | */ 147 | public function generateFiles() 148 | { 149 | foreach ($this->getFiles() as $stub => $file) { 150 | $pluginFskey = $this->argument('fskey'); 151 | 152 | $path = config('plugins.paths.plugins').'/'.$pluginFskey.'/'.$file; 153 | 154 | if ($keys = $this->getReplaceKeys($path)) { 155 | $file = $this->getReplacedContent($file, $keys); 156 | $path = $this->getReplacedContent($path, $keys); 157 | } 158 | 159 | $content = $this->getStubContents($stub); 160 | 161 | if ($stub == 'controller.web') { 162 | if (class_exists(App\Http\Controllers\Controller::class)) { 163 | $content = str_replace("use Illuminate\Routing\Controller;", "use App\Http\Controllers\Controller;", $content); 164 | } 165 | } 166 | 167 | if ($stub == 'composer.json') { 168 | $content = str_replace('"require": []', '"require": {}', $content); 169 | } 170 | 171 | if (! $this->filesystem->isDirectory($dir = dirname($path))) { 172 | $this->filesystem->makeDirectory($dir, 0775, true); 173 | $this->removeParentDirGitKeep($dir); 174 | } 175 | 176 | $this->filesystem->put($path, $content); 177 | $this->removeParentDirGitKeep($path); 178 | 179 | $this->info("Created : {$path}"); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Commands/PluginActivateCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 25 | 26 | if ($pluginFskey) { 27 | $status = $this->activate($pluginFskey); 28 | } else { 29 | // Activate all plugins 30 | $status = $this->activateAll(); 31 | } 32 | 33 | if (! $status) { 34 | return Command::FAILURE; 35 | } 36 | 37 | return Command::SUCCESS; 38 | } 39 | 40 | public function activateAll() 41 | { 42 | $plugin = new Plugin(); 43 | 44 | $status = true; 45 | 46 | collect($plugin->all())->each(function ($pluginFskey) use (&$status) { 47 | if (! $this->activate($pluginFskey)) { 48 | $status = false; 49 | } 50 | }); 51 | 52 | return $status; 53 | } 54 | 55 | public function activate(?string $pluginFskey = null) 56 | { 57 | $plugin = new Plugin($pluginFskey); 58 | $fskey = $plugin->getStudlyName(); 59 | 60 | event('plugin:activating', [[ 61 | 'fskey' => $fskey, 62 | ]]); 63 | 64 | if ($plugin->activate()) { 65 | $this->info(sprintf('Plugin %s activated successfully', $pluginFskey)); 66 | 67 | event('plugin:activated', [[ 68 | 'fskey' => $fskey, 69 | ]]); 70 | 71 | return true; 72 | } 73 | 74 | $this->error(sprintf('Plugin %s activation failed', $pluginFskey)); 75 | 76 | return false; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Commands/PluginCommand.php: -------------------------------------------------------------------------------- 1 | info(static::$logo); 41 | 42 | $this->comment(''); 43 | $this->comment('Available commands:'); 44 | 45 | $this->comment(''); 46 | $this->comment('plugin'); 47 | $this->listAdminCommands(); 48 | } 49 | 50 | protected function listAdminCommands(): void 51 | { 52 | $commands = collect(Artisan::all())->mapWithKeys(function ($command, $key) { 53 | if ( 54 | Str::endsWith($key, 'fresns') 55 | || Str::startsWith($key, 'new') 56 | || Str::startsWith($key, 'custom') 57 | || Str::startsWith($key, 'make') 58 | || Str::startsWith($key, 'plugin') 59 | ) { 60 | return [$key => $command]; 61 | } 62 | 63 | return []; 64 | })->toArray(); 65 | 66 | \ksort($commands); 67 | 68 | $width = $this->getColumnWidth($commands); 69 | 70 | /** @var Command $command */ 71 | foreach ($commands as $command) { 72 | $this->info(sprintf(" %-{$width}s %s", $command->getName(), $command->getDescription())); 73 | } 74 | } 75 | 76 | private function getColumnWidth(array $commands): int 77 | { 78 | $widths = []; 79 | 80 | foreach ($commands as $command) { 81 | $widths[] = static::strlen($command->getName()); 82 | foreach ($command->getAliases() as $alias) { 83 | $widths[] = static::strlen($alias); 84 | } 85 | } 86 | 87 | return $widths ? max($widths) + 2 : 0; 88 | } 89 | 90 | /** 91 | * Returns the length of a string, using mb_strwidth if it is available. 92 | * 93 | * @param string $string The string to check its length 94 | * @return int The length of the string 95 | */ 96 | public static function strlen($string): int 97 | { 98 | if (false === $encoding = mb_detect_encoding($string, null, true)) { 99 | return strlen($string); 100 | } 101 | 102 | return mb_strwidth($string, $encoding); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Commands/PluginComposerUpdateCommand.php: -------------------------------------------------------------------------------- 1 | output); 47 | 48 | if (! $process->isSuccessful()) { 49 | $this->error('Failed to install packages, calc composer.json hash value fail'); 50 | 51 | return Command::FAILURE; 52 | } 53 | 54 | return Command::SUCCESS; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Commands/PluginDeactivateCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 25 | 26 | if ($pluginFskey) { 27 | $status = $this->deactivate($pluginFskey); 28 | } else { 29 | // Deactivate all plugins 30 | $status = $this->deactivateAll(); 31 | } 32 | 33 | if (! $status) { 34 | return Command::FAILURE; 35 | } 36 | 37 | return Command::SUCCESS; 38 | } 39 | 40 | public function deactivateAll() 41 | { 42 | $plugin = new Plugin(); 43 | 44 | $status = true; 45 | 46 | collect($plugin->all())->each(function ($pluginFskey) use (&$status) { 47 | if (! $this->deactivate($pluginFskey)) { 48 | $status = false; 49 | } 50 | }); 51 | 52 | return $status; 53 | } 54 | 55 | public function deactivate(?string $pluginFskey = null) 56 | { 57 | $plugin = new Plugin($pluginFskey); 58 | $fskey = $plugin->getStudlyName(); 59 | 60 | event('plugin:deactivating', [[ 61 | 'fskey' => $fskey, 62 | ]]); 63 | 64 | if ($plugin->deactivate()) { 65 | $this->info(sprintf('Plugin %s deactivated successfully', $pluginFskey)); 66 | 67 | event('plugin:deactivated', [[ 68 | 'fskey' => $fskey, 69 | ]]); 70 | 71 | return true; 72 | } 73 | 74 | $this->error(sprintf('Plugin %s deactivated failed', $pluginFskey)); 75 | 76 | return false; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Commands/PluginInstallCommand.php: -------------------------------------------------------------------------------- 1 | argument('path'); 30 | 31 | if ($this->option('is_dir')) { 32 | $pluginDirectory = $path; 33 | 34 | if (strpos($pluginDirectory, '/') == false) { 35 | $pluginDirectory = "plugins/{$pluginDirectory}"; 36 | } 37 | 38 | if (str_starts_with($pluginDirectory, '/')) { 39 | $pluginDirectory = realpath($pluginDirectory); 40 | } else { 41 | $pluginDirectory = realpath(base_path($pluginDirectory)); 42 | } 43 | 44 | $path = $pluginDirectory; 45 | } 46 | 47 | if (! $path || ! file_exists($path)) { 48 | $this->error('Failed to unzip, couldn\'t find the plugin path'); 49 | 50 | return Command::FAILURE; 51 | } 52 | 53 | $pluginsPath = config('plugins.paths.plugins'); 54 | if (! str_contains($path, $pluginsPath)) { 55 | $exitCode = $this->call('plugin:unzip', [ 56 | 'path' => $path, 57 | ]); 58 | 59 | if ($exitCode != 0) { 60 | return $exitCode; 61 | } 62 | 63 | $fskey = Cache::pull('install:plugin_fskey'); 64 | } else { 65 | $fskey = basename($path); 66 | } 67 | 68 | if (! $fskey) { 69 | info('Failed to unzip, couldn\'t get the plugin fskey'); 70 | 71 | return Command::FAILURE; 72 | } 73 | 74 | $plugin = new Plugin($fskey); 75 | if (! $plugin->isValidPlugin()) { 76 | $this->error('plugin is invalid'); 77 | 78 | return Command::FAILURE; 79 | } 80 | 81 | $plugin->manualAddNamespace(); 82 | 83 | event('plugin:installing', [[ 84 | 'fskey' => $fskey, 85 | ]]); 86 | 87 | $composerJson = Json::make($plugin->getComposerJsonPath())->get(); 88 | $require = Arr::get($composerJson, 'require', []); 89 | $requireDev = Arr::get($composerJson, 'require-dev', []); 90 | 91 | // Triggers top-level computation of composer.json hash values and installation of extension packages 92 | // @see https://getcomposer.org/doc/03-cli.md#process-exit-codes 93 | if (count($require) || count($requireDev)) { 94 | $exitCode = $this->call('plugin:composer-update'); 95 | if ($exitCode) { 96 | $this->error('Failed to update plugin dependency'); 97 | 98 | return Command::FAILURE; 99 | } 100 | } 101 | 102 | $this->call('plugin:deactivate', [ 103 | 'fskey' => $fskey, 104 | ]); 105 | 106 | $this->call('plugin:migrate', [ 107 | 'fskey' => $fskey, 108 | ]); 109 | 110 | if ($this->option('seed')) { 111 | $this->call('plugin:seed', [ 112 | 'fskey' => $fskey, 113 | ]); 114 | } 115 | 116 | $plugin->install(); 117 | 118 | $this->call('plugin:publish', [ 119 | 'fskey' => $fskey, 120 | ]); 121 | 122 | event('plugin:installed', [[ 123 | 'fskey' => $fskey, 124 | ]]); 125 | 126 | $this->info("Installed: {$fskey}"); 127 | } catch (\Throwable $e) { 128 | info("Install fail: {$e->getMessage()}"); 129 | $this->error("Install fail: {$e->getMessage()}"); 130 | 131 | return Command::FAILURE; 132 | } 133 | 134 | return Command::SUCCESS; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Commands/PluginListCommand.php: -------------------------------------------------------------------------------- 1 | getPluginInfo(); 38 | } 39 | 40 | $this->table([ 41 | 'Plugin Fskey', 42 | 'Validation', 43 | 'Available', 44 | 'Plugin Status', 45 | 'Assets Status', 46 | 'Plugin Path', 47 | 'Assets Path', 48 | ], $rows); 49 | 50 | return Command::SUCCESS; 51 | } 52 | 53 | public function replaceDir(?string $path) 54 | { 55 | if (! $path) { 56 | return null; 57 | } 58 | 59 | return ltrim(str_replace(base_path(), '', $path), '/'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Commands/PluginMakeCommand.php: -------------------------------------------------------------------------------- 1 | call('new', [ 24 | 'fskey' => $this->argument('fskey'), 25 | '--force' => $this->option('force'), 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/PluginMigrateCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 34 | 35 | if ($pluginFskey) { 36 | return $this->migrate($pluginFskey); 37 | } else { 38 | $plugin = new Plugin(); 39 | 40 | collect($plugin->all())->map(function ($pluginFskey) { 41 | $this->migrate($pluginFskey, true); 42 | }); 43 | } 44 | 45 | return Command::SUCCESS; 46 | } 47 | 48 | public function migrate(string $pluginFskey, $isAll = false) 49 | { 50 | $plugin = new Plugin($pluginFskey); 51 | 52 | if (! $plugin->isValidPlugin()) { 53 | return Command::FAILURE; 54 | } 55 | 56 | if ($plugin->isDeactivate() && $isAll) { 57 | return Command::FAILURE; 58 | } 59 | 60 | try { 61 | $this->call('migrate', [ 62 | '--database' => $this->option('database'), 63 | '--force' => $this->option('force') ?? true, 64 | '--path' => $plugin->getMigratePath(), 65 | '--realpath' => $this->option('realpath') ?? true, 66 | '--schema-path' => $this->option('schema-path'), 67 | '--pretend' => $this->option('pretend') ?? false, 68 | '--step' => $this->option('step') ?? false, 69 | ]); 70 | 71 | if ($this->option('seed')) { 72 | $this->call('plugin:seed', [ 73 | '--class' => $this->option('seeder'), 74 | '--database' => $this->option('database'), 75 | '--force' => $this->option('force') ?? true, 76 | ]); 77 | } 78 | 79 | $this->info("Migrated: {$plugin->getFskey()}"); 80 | } catch (\Throwable $e) { 81 | $this->warn("Migrated {$plugin->getFskey()} fail\n"); 82 | $this->error($e->getMessage()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Commands/PluginMigrateRefreshCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 32 | $plugin = new Plugin($pluginFskey); 33 | 34 | if (! $plugin->isValidPlugin()) { 35 | return Command::FAILURE; 36 | } 37 | 38 | if ($plugin->isDeactivate()) { 39 | return Command::FAILURE; 40 | } 41 | 42 | try { 43 | $this->call('migrate:refresh', [ 44 | '--database' => $this->option('database'), 45 | '--force' => $this->option('force') ?? true, 46 | '--path' => $plugin->getMigratePath(), 47 | '--realpath' => $this->option('realpath') ?? true, 48 | '--step' => $this->option('step'), 49 | ]); 50 | 51 | if ($this->option('seed')) { 52 | $this->call('plugin:seed', [ 53 | '--class' => $this->option('seeder'), 54 | '--database' => $this->option('database'), 55 | '--force' => $this->option('force') ?? true, 56 | ]); 57 | } 58 | 59 | $this->info("Migrate Refresh: {$plugin->getFskey()}"); 60 | } catch (\Throwable $e) { 61 | $this->warn("Migrate Refresh {$plugin->getFskey()} fail\n"); 62 | $this->error($e->getMessage()); 63 | } 64 | 65 | return Command::SUCCESS; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Commands/PluginMigrateResetCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 30 | $plugin = new Plugin($pluginFskey); 31 | 32 | if (! $plugin->isValidPlugin()) { 33 | return Command::FAILURE; 34 | } 35 | 36 | if ($plugin->isDeactivate()) { 37 | return Command::FAILURE; 38 | } 39 | 40 | try { 41 | $this->call('migrate:reset', [ 42 | '--database' => $this->option('database'), 43 | '--force' => $this->option('force') ?? true, 44 | '--path' => $plugin->getMigratePath(), 45 | '--realpath' => $this->option('realpath') ?? true, 46 | '--pretend' => $this->option('pretend') ?? false, 47 | ]); 48 | 49 | $this->info("Migrate Reset: {$plugin->getFskey()}"); 50 | } catch (\Throwable $e) { 51 | $this->warn("Migrate Reset {$plugin->getFskey()} fail\n"); 52 | $this->error($e->getMessage()); 53 | } 54 | 55 | return Command::SUCCESS; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Commands/PluginMigrateRollbackCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 31 | $plugin = new Plugin($pluginFskey); 32 | 33 | if (! $plugin->isValidPlugin()) { 34 | return Command::FAILURE; 35 | } 36 | 37 | if (! $plugin->isDeactivate()) { 38 | return Command::FAILURE; 39 | } 40 | 41 | try { 42 | $path = $plugin->getMigratePath(); 43 | if (glob("$path/*")) { 44 | $exitCode = $this->call('migrate:rollback', [ 45 | '--database' => $this->option('database'), 46 | '--force' => $this->option('force') ?? true, 47 | '--path' => $plugin->getMigratePath(), 48 | '--realpath' => $this->option('realpath') ?? true, 49 | '--pretend' => $this->option('pretend') ?? false, 50 | ]); 51 | 52 | $this->info("Migrate Rollback: {$plugin->getFskey()}"); 53 | $this->info('Migrate Rollback Path: '.str_replace(base_path().'/', '', $path)); 54 | 55 | if ($exitCode != 0) { 56 | return $exitCode; 57 | } 58 | } else { 59 | $this->info('Migrate Rollback: Nothing need to rollback'); 60 | } 61 | } catch (\Throwable $e) { 62 | $this->warn("Migrate Rollback {$plugin->getFskey()} fail\n"); 63 | $this->error($e->getMessage()); 64 | 65 | return Command::FAILURE; 66 | } 67 | 68 | return Command::SUCCESS; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Commands/PluginPublishCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 26 | $plugin = new Plugin($pluginFskey); 27 | 28 | if ($this->validatePluginRootPath($plugin)) { 29 | $this->error('Failed to operate plugins root path'); 30 | 31 | return Command::FAILURE; 32 | } 33 | 34 | if (! $plugin->isValidPlugin()) { 35 | return Command::FAILURE; 36 | } 37 | 38 | File::cleanDirectory($plugin->getAssetsPath()); 39 | File::copyDirectory($plugin->getAssetsSourcePath(), $plugin->getAssetsPath()); 40 | 41 | $this->info("Published: {$plugin->getFskey()}"); 42 | 43 | return Command::SUCCESS; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Commands/PluginSeedCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 29 | $plugin = new Plugin($pluginFskey); 30 | 31 | if (! $plugin->isValidPlugin()) { 32 | return Command::FAILURE; 33 | } 34 | 35 | try { 36 | $class = $plugin->getSeederNamespace().$this->option('class'); 37 | 38 | if (class_exists($class)) { 39 | $this->call('db:seed', [ 40 | 'class' => $class, 41 | '--database' => $this->option('database'), 42 | '--force' => $this->option('force') ?? true, 43 | ]); 44 | } 45 | 46 | $this->info("Seed: {$plugin->getFskey()}"); 47 | } catch (\Throwable $e) { 48 | $this->warn("Seed {$plugin->getFskey()} fail\n"); 49 | $this->error($e->getMessage()); 50 | } 51 | 52 | return Command::SUCCESS; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Commands/PluginUninstallCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 30 | $plugin = new Plugin($pluginFskey); 31 | 32 | if ($this->validatePluginRootPath($plugin)) { 33 | $this->error('Failed to operate plugins root path'); 34 | 35 | return Command::FAILURE; 36 | } 37 | 38 | $composerJson = Json::make($plugin->getComposerJsonPath())->get(); 39 | $require = Arr::get($composerJson, 'require', []); 40 | $requireDev = Arr::get($composerJson, 'require-dev', []); 41 | 42 | event('plugin:uninstalling', [[ 43 | 'fskey' => $pluginFskey, 44 | ]]); 45 | 46 | $this->call('plugin:deactivate', [ 47 | 'fskey' => $pluginFskey, 48 | ]); 49 | 50 | if ($this->option('cleardata')) { 51 | $this->call('plugin:migrate-rollback', [ 52 | 'fskey' => $pluginFskey, 53 | ]); 54 | 55 | $this->info("Clear Data: {$pluginFskey}"); 56 | } 57 | 58 | $this->call('plugin:unpublish', [ 59 | 'fskey' => $pluginFskey, 60 | ]); 61 | 62 | File::delete($plugin->getCachedServicesPath()); 63 | File::deleteDirectory($plugin->getPluginPath()); 64 | 65 | // Triggers top-level computation of composer.json hash values and installation of extension packages 66 | if (count($require) || count($requireDev)) { 67 | $exitCode = $this->call('plugin:composer-update'); 68 | if ($exitCode) { 69 | $this->error('Failed to update plugin dependency'); 70 | 71 | return Command::FAILURE; 72 | } 73 | } 74 | 75 | $plugin->uninstall(); 76 | 77 | event('plugin:uninstalled', [[ 78 | 'fskey' => $pluginFskey, 79 | ]]); 80 | 81 | $this->info("Uninstalled: {$pluginFskey}"); 82 | } catch (\Throwable $e) { 83 | info("Uninstall fail: {$e->getMessage()}"); 84 | $this->error("Uninstall fail: {$e->getMessage()}"); 85 | 86 | return Command::FAILURE; 87 | } 88 | 89 | return Command::SUCCESS; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Commands/PluginUnpublishCommand.php: -------------------------------------------------------------------------------- 1 | getPluginFskey(); 26 | $plugin = new Plugin($pluginFskey); 27 | 28 | if (! $plugin->isValidPlugin()) { 29 | return Command::FAILURE; 30 | } 31 | 32 | File::deleteDirectory($plugin->getAssetsPath()); 33 | 34 | $this->info("Unpublished: {$plugin->getFskey()}"); 35 | 36 | return Command::SUCCESS; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/PluginUnzipCommand.php: -------------------------------------------------------------------------------- 1 | argument('path'); 26 | try { 27 | // unzip packaeg and get install command 28 | $zip = new Zip(); 29 | $tmpDirPath = $zip->unpack($filepath); 30 | } catch (\Throwable $e) { 31 | $this->error("Error: file unzip failed, reason: {$e->getMessage()}, filepath is: $filepath"); 32 | 33 | return Command::FAILURE; 34 | } 35 | 36 | if (! is_dir($tmpDirPath)) { 37 | $this->error("install plugin error, plugin unzip dir doesn't exists: {$tmpDirPath}"); 38 | 39 | return Command::FAILURE; 40 | } 41 | 42 | $pluginJsonPath = "{$tmpDirPath}/plugin.json"; 43 | if (! file_exists($tmpDirPath)) { 44 | \info($message = 'Plugin file does not exist: '.$pluginJsonPath); 45 | $this->error('install plugin error '.$message); 46 | 47 | return Command::FAILURE; 48 | } 49 | 50 | $plugin = Json::make($pluginJsonPath); 51 | 52 | $pluginFskey = $plugin->get('fskey'); 53 | if (! $pluginFskey) { 54 | \info('Failed to get plugin fskey: '.var_export($pluginFskey, true)); 55 | $this->error('install plugin error, plugin.json is invalid plugin json'); 56 | 57 | return Command::FAILURE; 58 | } 59 | 60 | $pluginDir = sprintf('%s/%s', 61 | config('plugins.paths.plugins'), 62 | $pluginFskey 63 | ); 64 | 65 | if (file_exists($pluginDir)) { 66 | $this->backup($pluginDir, $pluginFskey); 67 | } 68 | 69 | File::copyDirectory($tmpDirPath, $pluginDir); 70 | File::deleteDirectory($tmpDirPath); 71 | 72 | Cache::put('install:plugin_fskey', $pluginFskey, now()->addMinutes(5)); 73 | 74 | return Command::SUCCESS; 75 | } 76 | 77 | public function backup(string $pluginDir, string $pluginFskey) 78 | { 79 | $backupDir = config('plugins.paths.backups'); 80 | 81 | File::ensureDirectoryExists($backupDir); 82 | 83 | if (! is_file($backupDir.'/.gitignore')) { 84 | file_put_contents($backupDir.'/.gitignore', '*'.PHP_EOL.'!.gitignore'); 85 | } 86 | 87 | $dirs = File::glob("$backupDir/$pluginFskey*"); 88 | 89 | $currentBackupCount = count($dirs); 90 | 91 | $targetPath = sprintf('%s/%s-%s-%s', $backupDir, $pluginFskey, date('YmdHis'), $currentBackupCount + 1); 92 | 93 | File::copyDirectory($pluginDir, $targetPath); 94 | File::cleanDirectory($pluginDir); 95 | 96 | return true; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Commands/Traits/StubTrait.php: -------------------------------------------------------------------------------- 1 | replaceInFile( 89 | $providers, 90 | $modifiedProviders, 91 | $pluginJsonPath, 92 | ); 93 | } 94 | } 95 | 96 | protected function getNameInput(): string 97 | { 98 | return trim($this->argument('fskey')); 99 | } 100 | 101 | protected function buildClass($fskey): string 102 | { 103 | $this->runningAsRootDir = false; 104 | if (str_starts_with($fskey, 'App')) { 105 | $this->runningAsRootDir = true; 106 | $this->buildClassName = $fskey; 107 | } 108 | 109 | $content = $this->getStubContents($this->getStub()); 110 | 111 | return $content; 112 | } 113 | 114 | protected function getPath($fskey): mixed 115 | { 116 | $path = parent::getPath($fskey); 117 | 118 | $this->type = $path; 119 | 120 | return $path; 121 | } 122 | 123 | protected function getDefaultNamespace($rootNamespace): mixed 124 | { 125 | return $rootNamespace; 126 | } 127 | 128 | protected function getStubName(): ?string 129 | { 130 | return null; 131 | } 132 | 133 | /** 134 | * implement from \Illuminate\Console\GeneratorCommand. 135 | * 136 | * @see \Illuminate\Console\GeneratorCommand 137 | */ 138 | protected function getStub(): string 139 | { 140 | $stubName = $this->getStubName(); 141 | if (! $stubName) { 142 | throw new \RuntimeException('Please provider stub fskey in getStubName method'); 143 | } 144 | 145 | $baseStubPath = base_path("stubs/{$stubName}.stub"); 146 | if (file_exists($baseStubPath)) { 147 | return $baseStubPath; 148 | } 149 | 150 | $stubPath = dirname(__DIR__)."/stubs/{$stubName}.stub"; 151 | if (file_exists($stubPath)) { 152 | return $stubPath; 153 | } 154 | 155 | throw new \RuntimeException("stub path does not exists: {$stubPath}"); 156 | } 157 | 158 | /** 159 | * Get class name. 160 | */ 161 | public function getClass(): string 162 | { 163 | return class_basename($this->argument('fskey')); 164 | } 165 | 166 | /** 167 | * Get the contents of the specified stub file by given stub name. 168 | * 169 | * @param $stub 170 | */ 171 | protected function getStubContents($stubPath): string 172 | { 173 | $method = sprintf('get%sStubPath', Str::studly(strtolower($stubPath))); 174 | 175 | // custom stubPath 176 | if (method_exists($this, $method)) { 177 | $stubFilePath = $this->$method(); 178 | } else { 179 | // run in command: fresns new Xxx 180 | $stubFilePath = dirname(__DIR__)."/stubs/{$stubPath}.stub"; 181 | 182 | if (file_exists($stubFilePath)) { 183 | $stubFilePath = $stubFilePath; 184 | } 185 | // run in command: fresns make:xxx 186 | else { 187 | $stubFilePath = $stubPath; 188 | } 189 | } 190 | 191 | if (! file_exists($stubFilePath)) { 192 | throw new \RuntimeException("stub path does not exists: {$stubPath}"); 193 | } 194 | 195 | $mimeType = File::mimeType($stubFilePath); 196 | if ( 197 | str_contains($mimeType, 'application/') 198 | || str_contains($mimeType, 'text/') 199 | ) { 200 | $stubFile = new Stub($stubFilePath, $this->getReplacement($stubFilePath)); 201 | $content = $stubFile->render(); 202 | } else { 203 | $content = File::get($stubFilePath); 204 | } 205 | 206 | // format json style 207 | if (str_contains($stubPath, 'json')) { 208 | $content = Json::make()->decode($content)->encode(); 209 | 210 | return $content; 211 | } 212 | 213 | return $content; 214 | } 215 | 216 | public function getReplaceKeys($content): ?array 217 | { 218 | preg_match_all('/(\$[^\s.>\[]*?\$)/', $content, $matches); 219 | 220 | $keys = $matches[1] ?? []; 221 | 222 | return $keys; 223 | } 224 | 225 | public function getReplacesByKeys(array $keys): ?array 226 | { 227 | $replaces = []; 228 | foreach ($keys as $key) { 229 | $currentReplacement = str_replace('$', '', $key); 230 | 231 | $currentReplacementLower = Str::of($currentReplacement)->lower()->toString(); 232 | $method = sprintf('get%sReplacement', Str::studly($currentReplacementLower)); 233 | 234 | if (method_exists($this, $method)) { 235 | $replaces[$currentReplacement] = $this->$method(); 236 | } else { 237 | \info($currentReplacement.' does match any replace content'); 238 | // keep origin content 239 | $replaces[$currentReplacement] = $key; 240 | } 241 | } 242 | 243 | return $replaces; 244 | } 245 | 246 | public function getReplacedContent(string $content, array $keys = []): string 247 | { 248 | if (! $keys) { 249 | $keys = $this->getReplaceKeys($content); 250 | } 251 | 252 | $replaces = $this->getReplacesByKeys($keys); 253 | 254 | return str_replace($keys, $replaces, $content); 255 | } 256 | 257 | /** 258 | * Get array replacement for the specified stub. 259 | * 260 | * @param $stub 261 | */ 262 | protected function getReplacement($stubPath): array 263 | { 264 | if (! file_exists($stubPath)) { 265 | throw new \RuntimeException("stubPath $stubPath not exists"); 266 | } 267 | 268 | $stubContent = @file_get_contents($stubPath); 269 | 270 | $keys = $this->getReplaceKeys($stubContent); 271 | 272 | $replaces = $this->getReplacesByKeys($keys); 273 | 274 | return $replaces; 275 | } 276 | 277 | public function getAuthorsReplacement(): mixed 278 | { 279 | return Json::make()->encode(config('plugins.composer.author')); 280 | } 281 | 282 | public function getAuthorNameReplacement(): mixed 283 | { 284 | $authors = config('plugins.composer.author'); 285 | if (count($authors)) { 286 | return $authors[0]['name'] ?? 'Fresns'; 287 | } 288 | 289 | return 'Fresns'; 290 | } 291 | 292 | public function getAuthorUrlReplacement(): mixed 293 | { 294 | $authors = config('plugins.composer.author'); 295 | if (count($authors)) { 296 | return $authors[0]['homepage'] ?? 'https://fresns.org'; 297 | } 298 | 299 | return 'https://fresns.org'; 300 | } 301 | 302 | /** 303 | * Get namespace for plugin service provider. 304 | */ 305 | protected function getNamespaceReplacement(): string 306 | { 307 | if ($this->runningAsRootDir) { 308 | return Str::beforeLast($this->buildClassName, '\\'); 309 | } 310 | 311 | $namespace = $this->plugin->getClassNamespace(); 312 | $namespace = $this->getDefaultNamespace($namespace); 313 | 314 | return str_replace('\\\\', '\\', $namespace); 315 | } 316 | 317 | public function getClassReplacement(): string 318 | { 319 | return $this->getClass(); 320 | } 321 | 322 | /** 323 | * Get the plugin fskey in lower case. 324 | */ 325 | protected function getLowerNameReplacement(): string 326 | { 327 | return $this->plugin->getLowerName(); 328 | } 329 | 330 | /** 331 | * Get the plugin fskey in studly case. 332 | */ 333 | protected function getStudlyNameReplacement(): string 334 | { 335 | return $this->plugin->getStudlyName(); 336 | } 337 | 338 | /** 339 | * Get the plugin fskey in studly case. 340 | */ 341 | protected function getSnakeNameReplacement(): string 342 | { 343 | return $this->plugin->getSnakeName(); 344 | } 345 | 346 | /** 347 | * Get the plugin fskey in kebab case. 348 | */ 349 | protected function getKebabNameReplacement(): string 350 | { 351 | return $this->plugin->getKebabName(); 352 | } 353 | 354 | /** 355 | * Get replacement for $VENDOR$. 356 | */ 357 | protected function getVendorReplacement(): string 358 | { 359 | return $this->plugin->config('composer.vendor'); 360 | } 361 | 362 | /** 363 | * Get replacement for $PLUGIN_NAMESPACE$. 364 | */ 365 | protected function getPluginNamespaceReplacement(): string 366 | { 367 | return str_replace('\\', '\\\\', $this->plugin->config('namespace')); 368 | } 369 | 370 | protected function getProviderNamespaceReplacement(): string 371 | { 372 | return str_replace('\\', '\\\\', GenerateConfigReader::read('provider')->getNamespace()); 373 | } 374 | 375 | public function __get($fskey): mixed 376 | { 377 | if ($fskey === 'plugin') { 378 | // get Plugin Fskey from Namespace: Plugin\DemoTest => DemoTest 379 | $namespace = str_replace('\\', '/', app()->getNamespace()); 380 | $namespace = rtrim($namespace, '/'); 381 | $pluginFskey = basename($namespace); 382 | 383 | // when running in rootDir 384 | if ($pluginFskey == 'App') { 385 | $pluginFskey = null; 386 | } 387 | 388 | if (empty($this->plugin)) { 389 | $this->plugin = new \Fresns\PluginManager\Plugin($pluginFskey); 390 | } 391 | 392 | return $this->plugin; 393 | } 394 | 395 | throw new \RuntimeException("unknown property $fskey"); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/Commands/Traits/WorkPluginFskeyTrait.php: -------------------------------------------------------------------------------- 1 | argument('fskey'); 16 | if (! $pluginFskey) { 17 | $pluginRootPath = config('plugins.paths.plugins'); 18 | if (str_contains(getcwd(), $pluginRootPath)) { 19 | $pluginFskey = basename(getcwd()); 20 | } 21 | } 22 | 23 | return $pluginFskey; 24 | } 25 | 26 | public function validatePluginRootPath($plugin): bool 27 | { 28 | $pluginRootPath = config('plugins.paths.plugins'); 29 | $currentPluginRootPath = rtrim($plugin->getPluginPath(), '/'); 30 | 31 | return $pluginRootPath == $currentPluginRootPath; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Http/Controllers/admin-controller.stub: -------------------------------------------------------------------------------- 1 | 'none']); 12 | config(['session.secure' => uniqid()]); 13 | 14 | // code 15 | $itemKeys = [ 16 | // 'item_key1', 17 | // 'item_key2', 18 | ]; 19 | 20 | // $configs = Config::whereIn('item_key', $itemKeys)->where('item_tag', '$SNAKE_NAME$')->get(); 21 | $configs = []; 22 | 23 | return view('$STUDLY_NAME$::settings', [ 24 | 'configs' => $configs, 25 | ]); 26 | } 27 | 28 | public function update(Request $request) 29 | { 30 | $request->validate([ 31 | // 'item_key1' => 'required|url', 32 | // 'item_key2' => 'nullable|url', 33 | ]); 34 | 35 | $itemKeys = [ 36 | // 'item_key1', 37 | // 'item_key2', 38 | ]; 39 | 40 | // code 41 | // Config updateConfigs with $itemKeys and '$SNAKE_NAME$' 42 | 43 | return redirect(route('$KEBAB_NAME$.admin.index')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Http/Controllers/api-controller.stub: -------------------------------------------------------------------------------- 1 | 0, 16 | 'message' => 'ok', 17 | 'data' => $configs, 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Http/Controllers/controller.stub: -------------------------------------------------------------------------------- 1 | $configs, 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/cmd-word-provider.stub: -------------------------------------------------------------------------------- 1 | AWordService::CMD_TEST, 'provider' => [AWordService::class, 'handleTest']], 29 | // ['word' => BWordService::CMD_STATIC_TEST, 'provider' => [BWordService::class, 'handleStaticTest']], 30 | // ['word' => TestModel::CMD_MODEL_TEST, 'provider' => [TestModel::class, 'handleModelTest']], 31 | // ['word' => 'cmdWord', 'provider' => [CmdWordService::class, 'cmdWord']], 32 | ]; 33 | 34 | /** 35 | * Register the service provider. 36 | */ 37 | public function register(): void 38 | { 39 | $this->registerCmdWordProvider(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/command-provider.stub: -------------------------------------------------------------------------------- 1 | load($commandsDirectory); 27 | } 28 | } 29 | 30 | /** 31 | * Register all of the commands in the given directory. 32 | * 33 | * @param array|string $paths 34 | */ 35 | protected function load($paths): void 36 | { 37 | $paths = array_unique(Arr::wrap($paths)); 38 | 39 | $paths = array_filter($paths, function ($path) { 40 | return is_dir($path); 41 | }); 42 | 43 | if (empty($paths)) { 44 | return; 45 | } 46 | 47 | $commands = []; 48 | foreach ((new Finder)->in($paths)->files() as $command) { 49 | $commandClass = Str::before(self::class, 'Providers\\') . 'Console\\Commands\\' . str_replace('.php', '', $command->getBasename()); 50 | if (class_exists($commandClass)) { 51 | $commands[] = $commandClass; 52 | } 53 | } 54 | 55 | $this->commands($commands); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/event-provider.stub: -------------------------------------------------------------------------------- 1 | > 22 | */ 23 | protected $listen = [ 24 | // Registered::class => [ 25 | // SendEmailVerificationNotification::class, 26 | // ], 27 | ]; 28 | 29 | /** 30 | * The subscribers to register. 31 | * 32 | * @var array 33 | */ 34 | protected $subscribe = [ 35 | // 36 | ]; 37 | 38 | /** 39 | * Register any events for your application. 40 | */ 41 | public function boot(): void 42 | { 43 | // 44 | } 45 | 46 | /** 47 | * Determine if events and listeners should be automatically discovered. 48 | */ 49 | public function shouldDiscoverEvents(): bool 50 | { 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/exception-provider.stub: -------------------------------------------------------------------------------- 1 | > 20 | */ 21 | protected $dontReport = [ 22 | // 23 | ]; 24 | 25 | /** 26 | * Register any services. 27 | */ 28 | public function boot(): void 29 | { 30 | $handler = resolve(ExceptionHandler::class); 31 | 32 | if (method_exists($handler, 'reportable')) { 33 | $handler->reportable($this->reportable()); 34 | } 35 | 36 | if (method_exists($handler, 'renderable')) { 37 | $handler->renderable($this->renderable()); 38 | } 39 | 40 | if (method_exists($handler, 'ignore') && $this->dontReport) { 41 | foreach ($this->dontReport as $exceptionClass) { 42 | $handler->ignore($exceptionClass); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Register a reportable callback. 49 | * 50 | * @param callable $reportUsing 51 | * @return \Illuminate\Foundation\Exceptions\ReportableHandler 52 | */ 53 | public function reportable() 54 | { 55 | return function (\Throwable $e) { 56 | // 57 | }; 58 | } 59 | 60 | /** 61 | * Register a renderable callback. 62 | * 63 | * @param callable $renderUsing 64 | * @return $this 65 | */ 66 | public function renderable() 67 | { 68 | return function (\Throwable $e) { 69 | // 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/route-provider.stub: -------------------------------------------------------------------------------- 1 | where('fskey', $fskey)->first(); 43 | 44 | // Cache::put($cacheKey, $pluginModel, now()->addMinutes(30)); 45 | // } 46 | 47 | // $pluginHost = $pluginModel?->plugin_host ?? ''; 48 | 49 | // $host = str_replace(['http://', 'https://'], '', rtrim($pluginHost, '/')); 50 | // } 51 | // } catch (\Throwable $e) { 52 | // info("get plugin host failed: " . $e->getMessage()); 53 | // } 54 | 55 | Route::group([ 56 | 'domain' => $host, 57 | ], function () { 58 | $this->mapApiRoutes(); 59 | 60 | $this->mapWebRoutes(); 61 | }); 62 | } 63 | 64 | /** 65 | * Define the "web" routes for the application. 66 | * 67 | * These routes all receive session state, CSRF protection, etc. 68 | */ 69 | protected function mapWebRoutes(): void 70 | { 71 | Route::middleware('web')->group(dirname(__DIR__, 2) . '/routes/web.php'); 72 | } 73 | 74 | /** 75 | * Define the "api" routes for the application. 76 | * 77 | * These routes are typically stateless. 78 | */ 79 | protected function mapApiRoutes(): void 80 | { 81 | Route::prefix('api')->name('api.')->middleware('api')->group(dirname(__DIR__, 2) . '/routes/api.php'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/schedule-provider.stub: -------------------------------------------------------------------------------- 1 | app->resolving(Schedule::class, function ($schedule) { 22 | $this->schedule($schedule); 23 | }); 24 | } 25 | 26 | /** 27 | * Prepare schedule from tasks. 28 | * 29 | * @param Schedule $schedule 30 | */ 31 | public function schedule(Schedule $schedule) 32 | { 33 | // $schedule->command('inspire')->hourly(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/service-provider.stub: -------------------------------------------------------------------------------- 1 | registerTranslations(); 21 | $this->registerConfig(); 22 | $this->registerViews(); 23 | 24 | $this->loadMigrationsFrom(dirname(__DIR__, 2) . '/database/migrations'); 25 | 26 | // Event::listen(UserCreated::class, UserCreatedListener::class); 27 | } 28 | 29 | /** 30 | * Register the service provider. 31 | */ 32 | public function register(): void 33 | { 34 | // if ($this->app->runningInConsole()) { 35 | $this->app->register(CommandServiceProvider::class); 36 | // } 37 | } 38 | 39 | /** 40 | * Register config. 41 | */ 42 | protected function registerConfig(): void 43 | { 44 | $this->mergeConfigFrom( 45 | dirname(__DIR__, 2) . '/config/$KEBAB_NAME$.php', '$KEBAB_NAME$' 46 | ); 47 | } 48 | 49 | /** 50 | * Register views. 51 | */ 52 | public function registerViews(): void 53 | { 54 | $this->loadViewsFrom(dirname(__DIR__, 2) . '/resources/views', '$STUDLY_NAME$'); 55 | } 56 | 57 | /** 58 | * Register translations. 59 | */ 60 | public function registerTranslations(): void 61 | { 62 | $this->loadTranslationsFrom(dirname(__DIR__, 2) . '/resources/lang', '$STUDLY_NAME$'); 63 | } 64 | 65 | /** 66 | * Get the services provided by the provider. 67 | */ 68 | public function provides(): array 69 | { 70 | return []; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Providers/sql-provider.stub: -------------------------------------------------------------------------------- 1 | registerQueryLogger(); 32 | } 33 | 34 | /** 35 | * SQL time-consuming query log at development time. 36 | */ 37 | protected function registerQueryLogger() 38 | { 39 | if (! $this->app['config']->get('app.debug') || $this->app['config']->get('app.env') != 'local') { 40 | return; 41 | } 42 | 43 | config(['logging.channels.daily.days' => 2]); 44 | $this->app['config']->set('logging.channels.sql', config('logging.channels.daily')); 45 | $this->app['config']->set('logging.channels.sql.path', storage_path('logs/sql.log')); 46 | 47 | DB::listen(function (QueryExecuted $query) { 48 | $sqlWithPlaceholders = str_replace(['%', '?'], ['%%', '%s'], $query->sql); 49 | $bindings = $query->connection->prepareBindings($query->bindings); 50 | $pdo = $query->connection->getPdo(); 51 | $realSql = vsprintf($sqlWithPlaceholders, array_map([$pdo, 'quote'], $bindings)); 52 | $duration = $this->formatDuration($query->time / 1000); 53 | Log::channel('sql')->debug(sprintf('[%s] %s | %s: %s', $duration, $realSql, request()->method(), request()->getRequestUri())); 54 | }); 55 | } 56 | 57 | /** 58 | * Time unit conversion. 59 | * 60 | * @param $seconds 61 | */ 62 | private function formatDuration($seconds): string 63 | { 64 | if ($seconds < 0.001) { 65 | return round($seconds * 1000000).'μs'; 66 | } elseif ($seconds < 1) { 67 | return round($seconds * 1000, 2).'ms'; 68 | } 69 | 70 | return round($seconds, 2).'s'; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Commands/stubs/app/Services/cmd-word-service.stub: -------------------------------------------------------------------------------- 1 | success([ 22 | 'fskey' => basename(dirname(__DIR__, 2)), 23 | 'cmdWord' => __FUNCTION__, 24 | 'wordBody' => $wordBody, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Commands/stubs/composer.json.stub: -------------------------------------------------------------------------------- 1 | { 2 | "name": "$VENDOR$/$KEBAB_NAME$", 3 | "description": "$STUDLY_NAME$ plugin made by $AUTHOR_NAME$", 4 | "license": "MIT", 5 | "authors": $AUTHORS$, 6 | "autoload": { 7 | "psr-4": { 8 | "$PLUGIN_NAMESPACE$\\$STUDLY_NAME$\\": "app", 9 | "$PLUGIN_NAMESPACE$\\$STUDLY_NAME$\\Database\\Factories\\": "database/factories/", 10 | "$PLUGIN_NAMESPACE$\\$STUDLY_NAME$\\Database\\Seeders\\": "database/seeders/" 11 | } 12 | }, 13 | "autoload-dev": { 14 | "psr-4": { 15 | "$PLUGIN_NAMESPACE$\\$STUDLY_NAME$\\Tests\\": "tests/" 16 | } 17 | }, 18 | "require": {} 19 | } 20 | -------------------------------------------------------------------------------- /src/Commands/stubs/config/config.stub: -------------------------------------------------------------------------------- 1 | '$STUDLY_NAME$', 11 | ]; 12 | -------------------------------------------------------------------------------- /src/Commands/stubs/database/migrations/init_plugin_config.stub: -------------------------------------------------------------------------------- 1 | SubscribeUtility::TYPE_USER_ACTIVITY, 19 | // 'fskey' => '$SNAKE_NAME$', 20 | // 'cmdWord' => 'stats', 21 | ]; 22 | 23 | protected $fresnsConfigItems = [ 24 | // [ 25 | // 'item_tag' => '$SNAKE_NAME$', 26 | // 'item_key' => '$SNAKE_NAME$_config', 27 | // 'item_type' => 'string', 28 | // 'item_value' => null, 29 | // ], 30 | ]; 31 | 32 | /** 33 | * Run the migrations. 34 | */ 35 | public function up(): void 36 | { 37 | // addSubscribeItem 38 | // \FresnsCmdWord::plugin()->addSubscribeItem($this->fresnsWordBody); 39 | 40 | // addKeyValues to Config table 41 | // ConfigUtility::changeFresnsConfigItems($this->fresnsConfigItems); 42 | } 43 | 44 | /** 45 | * Reverse the migrations. 46 | */ 47 | public function down(): void 48 | { 49 | // removeSubscribeItem 50 | // \FresnsCmdWord::plugin()->removeSubscribeItem($this->fresnsWordBody); 51 | 52 | // removeKeyValues from Config table 53 | // ConfigUtility::removeFresnsConfigItems($this->fresnsConfigItems); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/Commands/stubs/database/seeders/seeder.stub: -------------------------------------------------------------------------------- 1 | call("OthersTableSeeder"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Commands/stubs/gitignore.stub: -------------------------------------------------------------------------------- 1 | .phpunit.cache 2 | node_modules 3 | vendor 4 | .env 5 | .env.backup 6 | .env.production 7 | .phpactor.json 8 | .phpunit.result.cache 9 | Homestead.json 10 | Homestead.yaml 11 | npm-debug.log 12 | yarn-error.log 13 | auth.json 14 | .fleet 15 | .idea 16 | .nova 17 | .vscode 18 | .zed 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /src/Commands/stubs/package.json.stub: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "autoprefixer": "^10.4.20", 10 | "axios": "^1.7.4", 11 | "concurrently": "^9.0.1", 12 | "laravel-vite-plugin": "^1.2.0", 13 | "postcss": "^8.4.47", 14 | "tailwindcss": "^3.4.13", 15 | "vite": "^6.0.11" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Commands/stubs/plugin.json.stub: -------------------------------------------------------------------------------- 1 | { 2 | "fskey": "$STUDLY_NAME$", 3 | "name": "$STUDLY_NAME$", 4 | "description": "$STUDLY_NAME$ plugin made by $AUTHOR_NAME$", 5 | "author": "$AUTHOR_NAME$", 6 | "website": "$AUTHOR_URL$", 7 | "version": "1.0.0", 8 | "providers": [ 9 | "$PLUGIN_NAMESPACE$\\$STUDLY_NAME$\\Providers\\PluginServiceProvider", 10 | "$PLUGIN_NAMESPACE$\\$STUDLY_NAME$\\Providers\\RouteServiceProvider" 11 | ], 12 | "autoloadFiles": [], 13 | "aliases": {}, 14 | "panelUsages": [], 15 | "accessPath": "/$KEBAB_NAME$", 16 | "settingsPath": "/$KEBAB_NAME$/admin" 17 | } 18 | -------------------------------------------------------------------------------- /src/Commands/stubs/readme.stub: -------------------------------------------------------------------------------- 1 | # $STUDLY_NAME$ 2 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/assets/css/fresns.stub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fresns/plugin-manager/badbbf855e3f5a1262b087ae524713a8ca295217/src/Commands/stubs/resources/assets/css/fresns.stub -------------------------------------------------------------------------------- /src/Commands/stubs/resources/assets/js/fresns.stub: -------------------------------------------------------------------------------- 1 | import './bootstrap' 2 | import '../css/app.css' 3 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/views/app.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ config('app.name', 'Laravel') }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/assets/js/app.js'], 'assets/plugins/$STUDLY_NAME$/build') 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/views/index.stub: -------------------------------------------------------------------------------- 1 | @extends('$STUDLY_NAME$::layouts.master') 2 | 3 | @section('content') 4 |
5 |

Plugin: {{ config('$KEBAB_NAME$.name') }}

6 | 7 |

8 | This view is loaded from plugin: {{ config('$KEBAB_NAME$.name') }} 9 |

10 | 11 | Go to the {{ config('$KEBAB_NAME$.name') }} plugin settings page. 12 |
13 | @endsection 14 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/views/layouts/footer.stub: -------------------------------------------------------------------------------- 1 | 4 | 5 | @push('script') 6 | 15 | @endpush 16 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/views/layouts/header.stub: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/views/layouts/master.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | $STUDLY_NAME$ 10 | 11 | 12 | 13 | 14 | 22 | @stack('css') 23 | 24 | 25 | 26 |
27 | @include('$STUDLY_NAME$::layouts.header') 28 |
29 | 30 |
31 | @yield('content') 32 |
33 | 34 | 37 | 38 | 39 |
40 | @include('$STUDLY_NAME$::layouts.tips') 41 |
42 | 43 | 44 | 45 | 46 | 47 | 81 | 82 | @stack('script') 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/views/layouts/tips.stub: -------------------------------------------------------------------------------- 1 | @if (session('success')) 2 |
3 | 14 |
15 | @elseif (session('failure')) 16 |
17 | 32 |
33 | @endif 34 | -------------------------------------------------------------------------------- /src/Commands/stubs/resources/views/settings.stub: -------------------------------------------------------------------------------- 1 | @extends('$STUDLY_NAME$::layouts.master') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |

{{ config('$KEBAB_NAME$.name') }} Settings

8 | Back to {{ config('$KEBAB_NAME$.name') }} plugin homepage. 9 | 10 |
11 | @csrf 12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 | 31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 | @endsection 40 | -------------------------------------------------------------------------------- /src/Commands/stubs/routes/api.stub: -------------------------------------------------------------------------------- 1 | name('$KEBAB_NAME$.')->group(function() { 25 | // Route::get('configs', [ApiController::class, 'configs'])->name('configs'); 26 | 27 | // Route::middleware('auth:api')->get('auth', function (Request $request) { 28 | // return $request->user(); 29 | // })->name('auth'); 30 | // }); 31 | -------------------------------------------------------------------------------- /src/Commands/stubs/routes/web.stub: -------------------------------------------------------------------------------- 1 | name('$KEBAB_NAME$.')->group(function() { 25 | Route::get('/', [WebController::class, 'index'])->name('index'); 26 | 27 | // without VerifyCsrfToken 28 | // Route::withoutMiddleware([ 29 | // \App\Http\Middleware\EncryptCookies::class, 30 | // \App\Http\Middleware\VerifyCsrfToken::class, 31 | // ])->group(function() { 32 | // Route::get('example', [WebController::class, 'index'])->name('example'); 33 | // }); 34 | 35 | Route::prefix('admin')->name('admin.')->group(function() { 36 | Route::get('/', [AdminController::class, 'index'])->name('index'); 37 | Route::put('update', [AdminController::class, 'update'])->name('update'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/Exceptions/FileAlreadyExistException.php: -------------------------------------------------------------------------------- 1 | path = $path; 51 | $this->contents = $contents; 52 | $this->filesystem = $filesystem ?: new Filesystem(); 53 | } 54 | 55 | /** 56 | * Get contents. 57 | * 58 | * @return mixed 59 | */ 60 | public function getContents() 61 | { 62 | return $this->contents; 63 | } 64 | 65 | /** 66 | * Set contents. 67 | * 68 | * @param mixed $contents 69 | * @return $this 70 | */ 71 | public function setContents($contents) 72 | { 73 | $this->contents = $contents; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Get filesystem. 80 | * 81 | * @return mixed 82 | */ 83 | public function getFilesystem() 84 | { 85 | return $this->filesystem; 86 | } 87 | 88 | /** 89 | * Set filesystem. 90 | * 91 | * @param Filesystem $filesystem 92 | * @return $this 93 | */ 94 | public function setFilesystem(Filesystem $filesystem) 95 | { 96 | $this->filesystem = $filesystem; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Get path. 103 | * 104 | * @return mixed 105 | */ 106 | public function getPath() 107 | { 108 | return $this->path; 109 | } 110 | 111 | /** 112 | * Set path. 113 | * 114 | * @param mixed $path 115 | * @return $this 116 | */ 117 | public function setPath($path) 118 | { 119 | $this->path = $path; 120 | 121 | return $this; 122 | } 123 | 124 | public function withFileOverwrite(bool $overwrite): FileGenerator 125 | { 126 | $this->overwriteFile = $overwrite; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Generate the file. 133 | */ 134 | public function generate() 135 | { 136 | $path = $this->getPath(); 137 | if (! $this->filesystem->exists($path)) { 138 | return $this->filesystem->put($path, $this->getContents()); 139 | } 140 | if ($this->overwriteFile === true) { 141 | return $this->filesystem->put($path, $this->getContents()); 142 | } 143 | 144 | throw new FileAlreadyExistException('File already exists!'); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Generators/Generator.php: -------------------------------------------------------------------------------- 1 | file = config('plugins.manager.default.file'); 24 | 25 | $this->pluginsJson = Json::make($this->file); 26 | 27 | $this->status = $this->pluginsJson->get('plugins'); 28 | } 29 | 30 | public function all() 31 | { 32 | return $this->status; 33 | } 34 | 35 | public function install(string $plugin) 36 | { 37 | $this->status[$plugin] = false; 38 | 39 | return $this->write(); 40 | } 41 | 42 | public function uninstall(string $plugin) 43 | { 44 | unset($this->status[$plugin]); 45 | 46 | return $this->write(); 47 | } 48 | 49 | public function activate(string $plugin) 50 | { 51 | $this->status[$plugin] = true; 52 | 53 | return $this->write(); 54 | } 55 | 56 | public function deactivate(string $plugin) 57 | { 58 | $this->status[$plugin] = false; 59 | 60 | return $this->write(); 61 | } 62 | 63 | public function isActivate(string $plugin) 64 | { 65 | if (array_key_exists($plugin, $this->status)) { 66 | return $this->status[$plugin] == true; 67 | } 68 | 69 | return false; 70 | } 71 | 72 | public function isDeactivate(string $plugin) 73 | { 74 | return ! $this->isActivate($plugin); 75 | } 76 | 77 | public function write(): bool 78 | { 79 | $data = $this->pluginsJson->get(); 80 | $data['plugins'] = $this->status; 81 | 82 | try { 83 | $content = json_encode( 84 | $data, 85 | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRETTY_PRINT | \JSON_FORCE_OBJECT 86 | ); 87 | 88 | return (bool) file_put_contents($this->file, $content); 89 | } catch (\Throwable $e) { 90 | info('Failed to update plugin status: %s'.$e->getMessage()); 91 | 92 | return false; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | manager = new FileManager(); 29 | 30 | $this->setPluginFskey($pluginFskey); 31 | } 32 | 33 | public function config(string $key, $default = null) 34 | { 35 | return config('plugins.'.$key, $default); 36 | } 37 | 38 | public function setPluginFskey(?string $pluginFskey = null) 39 | { 40 | $this->pluginFskey = $pluginFskey; 41 | } 42 | 43 | public function getFskey() 44 | { 45 | return $this->getStudlyName(); 46 | } 47 | 48 | public function getLowerName(): string 49 | { 50 | return Str::lower($this->pluginFskey); 51 | } 52 | 53 | public function getStudlyName() 54 | { 55 | return Str::studly($this->pluginFskey); 56 | } 57 | 58 | public function getKebabName() 59 | { 60 | return Str::kebab($this->pluginFskey); 61 | } 62 | 63 | public function getSnakeName() 64 | { 65 | return Str::snake($this->pluginFskey); 66 | } 67 | 68 | public function getClassNamespace() 69 | { 70 | $namespace = $this->config('namespace'); 71 | $namespace .= '\\'.$this->getStudlyName(); 72 | $namespace = str_replace('/', '\\', $namespace); 73 | 74 | return trim($namespace, '\\'); 75 | } 76 | 77 | public function getSeederNamespace(): ?string 78 | { 79 | return "{$this->getClassNamespace()}\\Database\Seeders\\"; 80 | } 81 | 82 | public function getPluginPath(): ?string 83 | { 84 | $path = $this->config('paths.plugins'); 85 | $pluginFskey = $this->getStudlyName(); 86 | 87 | return "{$path}/{$pluginFskey}"; 88 | } 89 | 90 | public function getFactoryPath() 91 | { 92 | $path = $this->getPluginPath(); 93 | 94 | return "{$path}/database/factories"; 95 | } 96 | 97 | public function getMigratePath() 98 | { 99 | $path = $this->getPluginPath(); 100 | 101 | return "{$path}/database/migrations"; 102 | } 103 | 104 | public function getSeederPath(): ?string 105 | { 106 | $path = $this->getPluginPath(); 107 | 108 | return "{$path}/database/seeders"; 109 | } 110 | 111 | public function getAssetsPath(): ?string 112 | { 113 | if (! $this->exists()) { 114 | return null; 115 | } 116 | 117 | $path = $this->config('paths.assets'); 118 | $pluginFskey = $this->getStudlyName(); 119 | 120 | return "{$path}/{$pluginFskey}"; 121 | } 122 | 123 | public function getAssetsSourcePath(): ?string 124 | { 125 | if (! $this->exists()) { 126 | return null; 127 | } 128 | 129 | $path = $this->getPluginPath(); 130 | 131 | return "{$path}/resources/assets"; 132 | } 133 | 134 | public function getComposerJsonPath(): ?string 135 | { 136 | $path = $this->getPluginPath(); 137 | 138 | return "{$path}/composer.json"; 139 | } 140 | 141 | public function getPluginJsonPath(): ?string 142 | { 143 | $path = $this->getPluginPath(); 144 | 145 | return "{$path}/plugin.json"; 146 | } 147 | 148 | public function install() 149 | { 150 | return $this->manager->install($this->getStudlyName()); 151 | } 152 | 153 | public function activate(): bool 154 | { 155 | if (! $this->exists()) { 156 | return false; 157 | } 158 | 159 | return $this->manager->activate($this->getStudlyName()); 160 | } 161 | 162 | public function deactivate(): bool 163 | { 164 | if (! $this->exists()) { 165 | return false; 166 | } 167 | 168 | return $this->manager->deactivate($this->getStudlyName()); 169 | } 170 | 171 | public function uninstall() 172 | { 173 | return $this->manager->uninstall($this->getStudlyName()); 174 | } 175 | 176 | public function isActivate(): bool 177 | { 178 | if (! $this->exists()) { 179 | return false; 180 | } 181 | 182 | return $this->manager->isActivate($this->getStudlyName()); 183 | } 184 | 185 | public function isDeactivate(): bool 186 | { 187 | return ! $this->isActivate(); 188 | } 189 | 190 | public function exists(): bool 191 | { 192 | if (! $pluginFskey = $this->getStudlyName()) { 193 | return false; 194 | } 195 | 196 | if (in_array($pluginFskey, $this->all())) { 197 | return true; 198 | } 199 | 200 | return false; 201 | } 202 | 203 | public function all(): array 204 | { 205 | $path = $this->config('paths.plugins'); 206 | $pluginJsons = File::glob("$path/**/plugin.json"); 207 | 208 | $plugins = []; 209 | foreach ($pluginJsons as $pluginJson) { 210 | $pluginFskey = basename(dirname($pluginJson)); 211 | 212 | if (! $this->isValidPlugin($pluginFskey)) { 213 | continue; 214 | } 215 | 216 | if (! $this->isAvailablePlugin($pluginFskey)) { 217 | continue; 218 | } 219 | 220 | $plugins[] = $pluginFskey; 221 | } 222 | 223 | return $plugins; 224 | } 225 | 226 | public function isValidPlugin(?string $pluginFskey = null) 227 | { 228 | if (! $pluginFskey) { 229 | $pluginFskey = $this->getStudlyName(); 230 | } 231 | 232 | if (! $pluginFskey) { 233 | return false; 234 | } 235 | 236 | $path = $this->config('paths.plugins'); 237 | 238 | $pluginJsonPath = sprintf('%s/%s/plugin.json', $path, $pluginFskey); 239 | 240 | $pluginJson = Json::make($pluginJsonPath); 241 | 242 | return $pluginFskey == $pluginJson->get('fskey'); 243 | } 244 | 245 | public function isAvailablePlugin(?string $pluginFskey = null) 246 | { 247 | if (! $pluginFskey) { 248 | $pluginFskey = $this->getStudlyName(); 249 | } 250 | 251 | if (! $pluginFskey) { 252 | return false; 253 | } 254 | 255 | try { 256 | // Verify that the program is loaded correctly by loading the program 257 | $plugin = new Plugin($pluginFskey); 258 | $plugin->manualAddNamespace(); 259 | 260 | $serviceProvider = sprintf('%s\\Providers\\%sServiceProvider', $plugin->getClassNamespace(), $pluginFskey); 261 | $pluginServiceProvider = sprintf('%s\\Providers\\PluginServiceProvider', $plugin->getClassNamespace()); 262 | 263 | return class_exists($serviceProvider) || class_exists($pluginServiceProvider); 264 | } catch (\Throwable $e) { 265 | \info("{$pluginFskey} registration failed, not a valid plugin: ".$e->getMessage()); 266 | 267 | return false; 268 | } 269 | 270 | return true; 271 | } 272 | 273 | public function allActivate(): array 274 | { 275 | return array_keys(array_filter($this->manager->all())); 276 | } 277 | 278 | public function allDeactivate(): array 279 | { 280 | return array_diff($this->all(), $this->allActivate()); 281 | } 282 | 283 | public function registerFiles() 284 | { 285 | $path = $this->getPluginPath(); 286 | 287 | $files = Json::make($this->getPluginJsonPath())->get('autoloadFiles', []); 288 | foreach ($files as $file) { 289 | if (! is_string($file)) { 290 | continue; 291 | } 292 | 293 | $filepath = "$path/$file"; 294 | if (is_file($filepath)) { 295 | include_once $filepath; 296 | } 297 | } 298 | } 299 | 300 | public function registerProviders() 301 | { 302 | $providers = Json::make($this->getPluginJsonPath())->get('providers', []); 303 | 304 | (new \Illuminate\Foundation\ProviderRepository(app(), app('files'), $this->getCachedServicesPath())) 305 | ->load($providers); 306 | } 307 | 308 | public function registerAliases(): void 309 | { 310 | $aliases = Json::make($this->getPluginJsonPath())->get('aliases', []); 311 | 312 | $loader = AliasLoader::getInstance(); 313 | foreach ($aliases as $aliasName => $aliasClass) { 314 | $loader->alias($aliasName, $aliasClass); 315 | } 316 | } 317 | 318 | public function getCachedServicesPath(): string 319 | { 320 | // This checks if we are running on a Laravel Vapor managed instance 321 | // and sets the path to a writable one (services path is not on a writable storage in Vapor). 322 | if (! is_null(env('VAPOR_MAINTENANCE_MODE', null))) { 323 | return Str::replaceLast('config.php', 'plugin_'.$this->getSnakeName().'.php', app()->getCachedConfigPath()); 324 | } 325 | 326 | return Str::replaceLast('services.php', 'plugin_'.$this->getSnakeName().'.php', app()->getCachedServicesPath()); 327 | } 328 | 329 | public function manualAddNamespace() 330 | { 331 | $fskey = $this->getStudlyName(); 332 | if (! $fskey) { 333 | return; 334 | } 335 | 336 | if (file_exists(base_path('/vendor/autoload.php'))) { 337 | /** @var \Composer\Autoload\ClassLoader $loader */ 338 | $loader = require base_path('/vendor/autoload.php'); 339 | 340 | $namespaces = config('plugins.namespaces', []); 341 | 342 | foreach ($namespaces as $namespace => $paths) { 343 | $appPaths = array_map(function ($path) use ($fskey) { 344 | return "{$path}/{$fskey}/app"; 345 | }, $paths); 346 | $loader->addPsr4("{$namespace}\\{$fskey}\\", $appPaths, true); 347 | 348 | $factoryPaths = array_map(function ($path) use ($fskey) { 349 | return "{$path}/{$fskey}/database/factories"; 350 | }, $paths); 351 | $loader->addPsr4("{$namespace}\\{$fskey}\\Database\\Factories\\", $factoryPaths, true); 352 | 353 | $seederPaths = array_map(function ($path) use ($fskey) { 354 | return "{$path}/{$fskey}/database/seeders"; 355 | }, $paths); 356 | $loader->addPsr4("{$namespace}\\{$fskey}\\Database\\Seeders\\", $seederPaths, true); 357 | 358 | $testPaths = array_map(function ($path) use ($fskey) { 359 | return "{$path}/{$fskey}/tests"; 360 | }, $paths); 361 | $loader->addPsr4("{$namespace}\\{$fskey}\\Tests\\", $testPaths, true); 362 | } 363 | } 364 | } 365 | 366 | public function getPluginInfo() 367 | { 368 | // Validation: Does the directory name and fskey match correctly 369 | // Available: Whether the service provider is registered successfully 370 | $item['Plugin Fskey'] = "{$this->getStudlyName()}"; 371 | $item['Validation'] = $this->isValidPlugin() ? 'true' : 'false'; 372 | $item['Available'] = $this->isAvailablePlugin() ? 'Available' : 'Unavailable'; 373 | $item['Plugin Status'] = $this->isActivate() ? 'Activate' : 'Deactivate'; 374 | $item['Assets Status'] = file_exists($this->getAssetsPath()) ? 'Published' : 'Unpublished'; 375 | $item['Plugin Path'] = $this->replaceDir($this->getPluginPath()); 376 | $item['Assets Path'] = $this->replaceDir($this->getAssetsPath()); 377 | 378 | return $item; 379 | } 380 | 381 | public function replaceDir(?string $path) 382 | { 383 | if (! $path) { 384 | return null; 385 | } 386 | 387 | return ltrim(str_replace(base_path(), '', $path), '/'); 388 | } 389 | 390 | public function __toString() 391 | { 392 | return $this->getStudlyName(); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/Providers/PluginServiceProvider.php: -------------------------------------------------------------------------------- 1 | autoload(); 21 | } 22 | 23 | public function register() 24 | { 25 | $this->mergeConfigFrom(__DIR__.'/../../config/plugins.php', 'plugins'); 26 | $this->publishes([ 27 | __DIR__.'/../../config/plugins.php' => config_path('plugins.php'), 28 | ], 'laravel-plugin-config'); 29 | 30 | $this->addMergePluginConfig(); 31 | 32 | $this->registerCommands([ 33 | __DIR__.'/../Commands/*', 34 | ]); 35 | } 36 | 37 | public function registerCommands($paths) 38 | { 39 | $allCommand = []; 40 | 41 | foreach ($paths as $path) { 42 | $commandPaths = glob($path); 43 | 44 | foreach ($commandPaths as $command) { 45 | $commandPath = realpath($command); 46 | if (! is_file($commandPath)) { 47 | continue; 48 | } 49 | 50 | $commandClass = 'Fresns\\PluginManager\\Commands\\'.pathinfo($commandPath, PATHINFO_FILENAME); 51 | 52 | if (class_exists($commandClass)) { 53 | $allCommand[] = $commandClass; 54 | } 55 | } 56 | } 57 | 58 | $this->commands($allCommand); 59 | } 60 | 61 | protected function autoload() 62 | { 63 | $this->addFiles(); 64 | 65 | $plugin = new Plugin(); 66 | 67 | collect($plugin->all())->map(function ($pluginFskey) { 68 | try { 69 | $plugin = new Plugin($pluginFskey); 70 | 71 | if ($plugin->isAvailablePlugin() && $plugin->isActivate()) { 72 | $plugin->registerFiles(); 73 | $plugin->registerProviders(); 74 | $plugin->registerAliases(); 75 | } 76 | } catch (\Throwable $e) { 77 | info($message = sprintf('Plugin namespace failed to load Fskey: %s, reason: %s, file: %s, line: %s', 78 | $pluginFskey, 79 | $e->getMessage(), 80 | str_replace(base_path().'/', '', $e->getFile()), 81 | $e->getLine(), 82 | )); 83 | } 84 | }); 85 | } 86 | 87 | protected function addFiles() 88 | { 89 | $files = $this->app['config']->get('plugins.autoload_files'); 90 | 91 | foreach ($files as $file) { 92 | if (file_exists($file)) { 93 | require_once $file; 94 | } 95 | } 96 | } 97 | 98 | protected function addMergePluginConfig() 99 | { 100 | $composerPath = base_path('composer.json'); 101 | $composer = Json::make($composerPath)->get(); 102 | if (! $composer) { 103 | info('Failed to get base_path("composer.json") content'); 104 | 105 | return; 106 | } 107 | 108 | $userMergePluginConfig = Arr::get($composer, 'extra.merge-plugin', []); 109 | 110 | $defaultMergePlugin = config('plugins.merge_plugin_config', []); 111 | if (empty($defaultMergePlugin)) { 112 | $config = require config_path('plugins.php'); 113 | $defaultMergePlugin = $config['merge_plugin_config']; 114 | } 115 | 116 | if (empty($defaultMergePlugin)) { 117 | info('Failed to get plugins.merge_plugin_config, please publish the plugins configuration file'); 118 | 119 | return; 120 | } 121 | 122 | $mergePluginConfig = array_merge($defaultMergePlugin, $userMergePluginConfig); 123 | 124 | // merge include 125 | $diffInclude = array_diff($defaultMergePlugin['include'] ?? [], $userMergePluginConfig['include'] ?? []); 126 | $mergePluginConfigInclude = array_merge($diffInclude, $userMergePluginConfig['include'] ?? []); 127 | 128 | $mergePluginConfig['include'] = $mergePluginConfigInclude; 129 | 130 | Arr::set($composer, 'extra.merge-plugin', $mergePluginConfig); 131 | 132 | try { 133 | $content = Json::make()->encode($composer); 134 | $content .= "\n"; 135 | 136 | $fp = fopen($composerPath, 'r+'); 137 | if (flock($fp, LOCK_EX | LOCK_NB)) { 138 | fwrite($fp, $content); 139 | flock($fp, LOCK_UN); 140 | } 141 | fclose($fp); 142 | } catch (\Throwable $e) { 143 | $message = str_replace(['file_put_contents('.base_path().'/', ')'], '', $e->getMessage()); 144 | throw new \RuntimeException('cannot set merge-plugin to '.$message); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Support/Config/GenerateConfigReader.php: -------------------------------------------------------------------------------- 1 | path = $config['path']; 21 | $this->generate = $config['generate']; 22 | $this->namespace = $config['namespace'] ?? $this->convertPathToNamespace($config['path']); 23 | 24 | return; 25 | } 26 | $this->path = $config; 27 | $this->generate = (bool) $config; 28 | $this->namespace = $config; 29 | } 30 | 31 | public function getPath() 32 | { 33 | return $this->path; 34 | } 35 | 36 | public function generate(): bool 37 | { 38 | return $this->generate; 39 | } 40 | 41 | public function getNamespace() 42 | { 43 | return $this->namespace; 44 | } 45 | 46 | private function convertPathToNamespace($path) 47 | { 48 | return str_replace('/', '\\', $path); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Support/Json.php: -------------------------------------------------------------------------------- 1 | filepath = $filepath; 22 | 23 | $this->decode(); 24 | } 25 | 26 | public static function make(?string $filepath = null) 27 | { 28 | return new static($filepath); 29 | } 30 | 31 | public function decode(?string $content = null) 32 | { 33 | if ($this->filepath && file_exists($this->filepath)) { 34 | $content = @file_get_contents($this->filepath); 35 | } 36 | 37 | if (! $content) { 38 | $content = ''; 39 | } 40 | 41 | $this->data = json_decode($content, true) ?? []; 42 | 43 | return $this; 44 | } 45 | 46 | public function encode(?array $data = null, $options = null) 47 | { 48 | $defaultOptions = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT; 49 | 50 | if ($options) { 51 | $options = $defaultOptions | $options; 52 | } else { 53 | $options = $defaultOptions; 54 | } 55 | 56 | if ($data) { 57 | $this->data = $data; 58 | } 59 | 60 | return json_encode($this->data, $options); 61 | } 62 | 63 | public function get(mixed $key = null, $default = null) 64 | { 65 | if (! Arr::has($this->data, $key)) { 66 | return $this->data; 67 | } 68 | 69 | return Arr::get($this->data, $key, $default); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Support/Migrations/NameParser.php: -------------------------------------------------------------------------------- 1 | [ 34 | 'create', 35 | 'make', 36 | ], 37 | 'delete' => [ 38 | 'delete', 39 | 'remove', 40 | ], 41 | 'add' => [ 42 | 'add', 43 | 'update', 44 | 'append', 45 | 'insert', 46 | ], 47 | 'drop' => [ 48 | 'destroy', 49 | 'drop', 50 | ], 51 | ]; 52 | 53 | /** 54 | * The constructor. 55 | * 56 | * @param string $name 57 | */ 58 | public function __construct($name) 59 | { 60 | $this->name = $name; 61 | $this->data = $this->fetchData(); 62 | } 63 | 64 | /** 65 | * Get original migration name. 66 | * 67 | * @return string 68 | */ 69 | public function getOriginalName() 70 | { 71 | return $this->name; 72 | } 73 | 74 | /** 75 | * Get schema type or action. 76 | * 77 | * @return string 78 | */ 79 | public function getAction() 80 | { 81 | return head($this->data); 82 | } 83 | 84 | /** 85 | * Get the table will be used. 86 | * 87 | * @return string 88 | */ 89 | public function getTableName() 90 | { 91 | $matches = array_reverse($this->getMatches()); 92 | 93 | return array_shift($matches); 94 | } 95 | 96 | /** 97 | * Get matches data from regex. 98 | * 99 | * @return array 100 | */ 101 | public function getMatches() 102 | { 103 | preg_match($this->getPattern(), $this->name, $matches); 104 | 105 | return $matches; 106 | } 107 | 108 | /** 109 | * Get name pattern. 110 | * 111 | * @return string 112 | */ 113 | public function getPattern() 114 | { 115 | switch ($action = $this->getAction()) { 116 | case 'add': 117 | case 'append': 118 | case 'update': 119 | case 'insert': 120 | return "/{$action}_(.*)_to_(.*)_table/"; 121 | break; 122 | 123 | case 'delete': 124 | case 'remove': 125 | case 'alter': 126 | return "/{$action}_(.*)_from_(.*)_table/"; 127 | break; 128 | 129 | default: 130 | return "/{$action}_(.*)_table/"; 131 | break; 132 | } 133 | } 134 | 135 | /** 136 | * Fetch the migration name to an array data. 137 | * 138 | * @return array 139 | */ 140 | protected function fetchData() 141 | { 142 | return explode('_', $this->name); 143 | } 144 | 145 | /** 146 | * Get the array data. 147 | * 148 | * @return array 149 | */ 150 | public function getData() 151 | { 152 | return $this->data; 153 | } 154 | 155 | /** 156 | * Determine whether the given type is same with the current schema action or type. 157 | * 158 | * @param $type 159 | * @return bool 160 | */ 161 | public function is($type) 162 | { 163 | return $type === $this->getAction(); 164 | } 165 | 166 | /** 167 | * Determine whether the current schema action is a adding action. 168 | * 169 | * @return bool 170 | */ 171 | public function isAdd() 172 | { 173 | return in_array($this->getAction(), $this->actions['add']); 174 | } 175 | 176 | /** 177 | * Determine whether the current schema action is a deleting action. 178 | * 179 | * @return bool 180 | */ 181 | public function isDelete() 182 | { 183 | return in_array($this->getAction(), $this->actions['delete']); 184 | } 185 | 186 | /** 187 | * Determine whether the current schema action is a creating action. 188 | * 189 | * @return bool 190 | */ 191 | public function isCreate() 192 | { 193 | return in_array($this->getAction(), $this->actions['create']); 194 | } 195 | 196 | /** 197 | * Determine whether the current schema action is a dropping action. 198 | * 199 | * @return bool 200 | */ 201 | public function isDrop() 202 | { 203 | return in_array($this->getAction(), $this->actions['drop']); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Support/Migrations/SchemaParser.php: -------------------------------------------------------------------------------- 1 | 'rememberToken()', 24 | 'soft_delete' => 'softDeletes()', 25 | ]; 26 | 27 | /** 28 | * The migration schema. 29 | * 30 | * @var string 31 | */ 32 | protected $schema; 33 | 34 | /** 35 | * The relationship keys. 36 | * 37 | * @var array 38 | */ 39 | protected $relationshipKeys = [ 40 | 'belongsTo', 41 | ]; 42 | 43 | /** 44 | * Create new instance. 45 | * 46 | * @param string|null $schema 47 | */ 48 | public function __construct($schema = null) 49 | { 50 | $this->schema = $schema; 51 | } 52 | 53 | /** 54 | * Parse a string to array of formatted schema. 55 | * 56 | * @param string $schema 57 | * @return array 58 | */ 59 | public function parse($schema) 60 | { 61 | $this->schema = $schema; 62 | 63 | $parsed = []; 64 | 65 | foreach ($this->getSchemas() as $schemaArray) { 66 | $column = $this->getColumn($schemaArray); 67 | 68 | $attributes = $this->getAttributes($column, $schemaArray); 69 | 70 | $parsed[$column] = $attributes; 71 | } 72 | 73 | return $parsed; 74 | } 75 | 76 | /** 77 | * Get array of schema. 78 | * 79 | * @return array 80 | */ 81 | public function getSchemas() 82 | { 83 | if (is_null($this->schema)) { 84 | return []; 85 | } 86 | 87 | return explode(',', str_replace(' ', '', $this->schema)); 88 | } 89 | 90 | /** 91 | * Convert string migration to array. 92 | * 93 | * @return array 94 | */ 95 | public function toArray() 96 | { 97 | return $this->parse($this->schema); 98 | } 99 | 100 | /** 101 | * Render the migration to formatted script. 102 | * 103 | * @return string 104 | */ 105 | public function render() 106 | { 107 | $results = ''; 108 | 109 | foreach ($this->toArray() as $column => $attributes) { 110 | $results .= $this->createField($column, $attributes); 111 | } 112 | 113 | return $results; 114 | } 115 | 116 | /** 117 | * Render up migration fields. 118 | * 119 | * @return string 120 | */ 121 | public function up() 122 | { 123 | return $this->render(); 124 | } 125 | 126 | /** 127 | * Render down migration fields. 128 | * 129 | * @return string 130 | */ 131 | public function down() 132 | { 133 | $results = ''; 134 | 135 | foreach ($this->toArray() as $column => $attributes) { 136 | $attributes = [head($attributes)]; 137 | $results .= $this->createField($column, $attributes, 'remove'); 138 | } 139 | 140 | return $results; 141 | } 142 | 143 | /** 144 | * Create field. 145 | * 146 | * @param string $column 147 | * @param array $attributes 148 | * @param string $type 149 | * @return string 150 | */ 151 | public function createField($column, $attributes, $type = 'add') 152 | { 153 | $results = "\t\t\t".'$table'; 154 | 155 | foreach ($attributes as $key => $field) { 156 | if (in_array($column, $this->relationshipKeys)) { 157 | $results .= $this->addRelationColumn($key, $field, $column); 158 | } else { 159 | $results .= $this->{"{$type}Column"}($key, $field, $column); 160 | } 161 | } 162 | 163 | return $results.';'.PHP_EOL; 164 | } 165 | 166 | /** 167 | * Add relation column. 168 | * 169 | * @param int $key 170 | * @param string $field 171 | * @param string $column 172 | * @return string 173 | */ 174 | protected function addRelationColumn($key, $field, $column) 175 | { 176 | if ($key === 0) { 177 | $relatedColumn = Str::snake(class_basename($field)).'_id'; 178 | 179 | return "->integer('{$relatedColumn}')->unsigned();".PHP_EOL."\t\t\t"."\$table->foreign('{$relatedColumn}')"; 180 | } 181 | if ($key === 1) { 182 | return "->references('{$field}')"; 183 | } 184 | if ($key === 2) { 185 | return "->on('{$field}')"; 186 | } 187 | if (Str::contains($field, '(')) { 188 | return '->'.$field; 189 | } 190 | 191 | return '->'.$field.'()'; 192 | } 193 | 194 | /** 195 | * Format field to script. 196 | * 197 | * @param int $key 198 | * @param string $field 199 | * @param string $column 200 | * @return string 201 | */ 202 | protected function addColumn($key, $field, $column) 203 | { 204 | if ($this->hasCustomAttribute($column)) { 205 | return '->'.$field; 206 | } 207 | 208 | if ($key == 0) { 209 | return '->'.$field."('".$column."')"; 210 | } 211 | 212 | if (Str::contains($field, '(')) { 213 | return '->'.$field; 214 | } 215 | 216 | return '->'.$field.'()'; 217 | } 218 | 219 | /** 220 | * Format field to script. 221 | * 222 | * @param int $key 223 | * @param string $field 224 | * @param string $column 225 | * @return string 226 | */ 227 | protected function removeColumn($key, $field, $column) 228 | { 229 | if ($this->hasCustomAttribute($column)) { 230 | return '->'.$field; 231 | } 232 | 233 | return '->dropColumn('."'".$column."')"; 234 | } 235 | 236 | /** 237 | * Get column name from schema. 238 | * 239 | * @param string $schema 240 | * @return string 241 | */ 242 | public function getColumn($schema) 243 | { 244 | return Arr::get(explode(':', $schema), 0); 245 | } 246 | 247 | /** 248 | * Get column attributes. 249 | * 250 | * @param string $column 251 | * @param string $schema 252 | * @return array 253 | */ 254 | public function getAttributes($column, $schema) 255 | { 256 | $fields = str_replace($column.':', '', $schema); 257 | 258 | return $this->hasCustomAttribute($column) ? $this->getCustomAttribute($column) : explode(':', $fields); 259 | } 260 | 261 | /** 262 | * Determine whether the given column is exist in customAttributes array. 263 | * 264 | * @param string $column 265 | * @return bool 266 | */ 267 | public function hasCustomAttribute($column) 268 | { 269 | return array_key_exists($column, $this->customAttributes); 270 | } 271 | 272 | /** 273 | * Get custom attributes value. 274 | * 275 | * @param string $column 276 | * @return array 277 | */ 278 | public function getCustomAttribute($column) 279 | { 280 | return (array) $this->customAttributes[$column]; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/Support/Process.php: -------------------------------------------------------------------------------- 1 | setTimeout(900); 23 | 24 | try { 25 | if ($output !== false) { 26 | $output = app(OutputInterface::class); 27 | } 28 | } catch (\Throwable $e) { 29 | $output = $output ?? null; 30 | } 31 | 32 | if ($process->isTty()) { 33 | $process->setTty(true); 34 | } 35 | 36 | $envs = [ 37 | 'PATH' => rtrim(`echo \$PATH`), 38 | 'COMPOSER_HOME' => '~/.config/composer', 39 | 'COMPOSER_MEMORY_LIMIT' => '-1', 40 | 'COMPOSER_ALLOW_SUPERUSER' => 1, 41 | ] + $env; 42 | 43 | if ($output) { 44 | $process->run(function ($type, $line) use ($output) { 45 | $output->write($line); 46 | }, $envs); 47 | } else { 48 | $process->run(null, $envs); 49 | } 50 | 51 | return $process; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Support/Stub.php: -------------------------------------------------------------------------------- 1 | path = $path; 43 | $this->replaces = $replaces; 44 | } 45 | 46 | /** 47 | * Create new self instance. 48 | * 49 | * @param string $path 50 | * @param array $replaces 51 | * @return self 52 | */ 53 | public static function create($path, array $replaces = []) 54 | { 55 | return new static($path, $replaces); 56 | } 57 | 58 | /** 59 | * Set stub path. 60 | * 61 | * @param string $path 62 | * @return self 63 | */ 64 | public function setPath($path) 65 | { 66 | $this->path = $path; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get stub path. 73 | * 74 | * @return string 75 | */ 76 | public function getPath() 77 | { 78 | $path = static::getBasePath().$this->path; 79 | 80 | return file_exists($path) ? $path : __DIR__.'/../Commands/stubs'.$this->path; 81 | } 82 | 83 | /** 84 | * Set base path. 85 | * 86 | * @param string $path 87 | */ 88 | public static function setBasePath($path) 89 | { 90 | static::$basePath = $path; 91 | } 92 | 93 | /** 94 | * Get base path. 95 | * 96 | * @return string|null 97 | */ 98 | public static function getBasePath() 99 | { 100 | return static::$basePath; 101 | } 102 | 103 | /** 104 | * Get stub contents. 105 | * 106 | * @return mixed|string 107 | */ 108 | public function getContents() 109 | { 110 | $contents = file_get_contents($this->getPath()); 111 | 112 | foreach ($this->replaces as $search => $replace) { 113 | $contents = str_replace('$'.strtoupper($search).'$', $replace, $contents); 114 | } 115 | 116 | return $contents; 117 | } 118 | 119 | /** 120 | * Get stub contents. 121 | * 122 | * @return string 123 | */ 124 | public function render() 125 | { 126 | return $this->getContents(); 127 | } 128 | 129 | /** 130 | * Save stub to specific path. 131 | * 132 | * @param string $path 133 | * @param string $filename 134 | * @return bool 135 | */ 136 | public function saveTo($path, $filename) 137 | { 138 | return file_put_contents($path.'/'.$filename, $this->getContents()); 139 | } 140 | 141 | /** 142 | * Set replacements array. 143 | * 144 | * @param array $replaces 145 | * @return $this 146 | */ 147 | public function replace(array $replaces = []) 148 | { 149 | $this->replaces = $replaces; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Get replacements. 156 | * 157 | * @return array 158 | */ 159 | public function getReplaces() 160 | { 161 | return $this->replaces; 162 | } 163 | 164 | /** 165 | * Handle magic method __toString. 166 | * 167 | * @return string 168 | */ 169 | public function __toString() 170 | { 171 | return $this->render(); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Support/Zip.php: -------------------------------------------------------------------------------- 1 | zipFile = new ZipFile(); 21 | } 22 | 23 | public function fixFilesChineseName($sourcePath) 24 | { 25 | $encoding_list = [ 26 | 'ASCII', 'UTF-8', 'GB2312', 'GBK', 'BIG5', 27 | ]; 28 | 29 | try { 30 | $zip = new \ZipArchive(); 31 | $openResult = $zip->open($sourcePath); 32 | if ($openResult !== true) { 33 | throw new \Exception('Cannot Open zip file: '.$sourcePath); 34 | } 35 | $fileNum = $zip->numFiles; 36 | 37 | $files = []; 38 | for ($i = 0; $i < $fileNum; $i++) { 39 | $statInfo = $zip->statIndex($i, \ZipArchive::FL_ENC_RAW); 40 | 41 | $encode = mb_detect_encoding($statInfo['name'], $encoding_list); 42 | $string = mb_convert_encoding($statInfo['name'], 'UTF-8', $encode); 43 | 44 | $zip->renameIndex($i, $string); 45 | $newStatInfo = $zip->statIndex($i, \ZipArchive::FL_ENC_RAW); 46 | 47 | $files[] = $newStatInfo; 48 | } 49 | } catch (\Throwable $e) { 50 | throw $e; 51 | } finally { 52 | $zip->close(); 53 | } 54 | 55 | return $files; 56 | } 57 | 58 | public function pack(string $sourcePath, ?string $filename = null, ?string $targetPath = null): ?string 59 | { 60 | if (! File::exists($sourcePath)) { 61 | throw new \RuntimeException("Directory to be decompressed does not exist {$sourcePath}"); 62 | } 63 | 64 | $filename = $filename ?? File::name($sourcePath); 65 | $targetPath = $targetPath ?? File::dirname($sourcePath); 66 | $targetPath = $targetPath ?: File::dirname($sourcePath); 67 | 68 | File::ensureDirectoryExists($targetPath); 69 | 70 | $zipFilename = str_contains($filename, '.zip') ? $filename : $filename.'.zip'; 71 | $zipFilepath = "{$targetPath}/{$zipFilename}"; 72 | 73 | while (File::exists($zipFilepath)) { 74 | $basename = File::name($zipFilepath); 75 | $zipCount = count(File::glob("{$targetPath}/{$basename}*.zip")); 76 | 77 | $zipFilename = $basename.$zipCount.'.zip'; 78 | $zipFilepath = "{$targetPath}/{$zipFilename}"; 79 | } 80 | 81 | // Compression 82 | $this->zipFile->addDirRecursive($sourcePath, $filename); 83 | $this->zipFile->saveAsFile($zipFilepath); 84 | 85 | return $targetPath; 86 | } 87 | 88 | public function unpack(string $sourcePath, ?string $targetPath = null): ?string 89 | { 90 | try { 91 | // Detects the file type and unpacks only zip files 92 | $mimeType = File::mimeType($sourcePath); 93 | } catch (\Throwable $e) { 94 | \info("Unzip failed {$e->getMessage()}"); 95 | throw new \RuntimeException("Unzip failed {$e->getMessage()}"); 96 | } 97 | 98 | // Get file types (only directories and zip files are processed) 99 | $type = match (true) { 100 | default => null, 101 | str_contains($mimeType, 'directory') => 1, 102 | str_contains($mimeType, 'zip') => 2, 103 | }; 104 | 105 | if (is_null($type)) { 106 | \info("unsupport mime type $mimeType"); 107 | throw new \RuntimeException("unsupport mime type $mimeType"); 108 | } 109 | 110 | // Make sure the unzip destination directory exists 111 | $targetPath = $targetPath ?? config('plugins.paths.unzip_target_path'); 112 | if (empty($targetPath)) { 113 | \info('targetPath cannot be empty'); 114 | throw new \RuntimeException('targetPath cannot be empty'); 115 | } 116 | 117 | if (! is_dir($targetPath)) { 118 | File::ensureDirectoryExists($targetPath); 119 | } 120 | 121 | if ($targetPath == $sourcePath) { 122 | return $targetPath; 123 | } 124 | 125 | // Empty the directory to avoid leaving files of other plugins 126 | File::cleanDirectory($targetPath); 127 | 128 | // Directory without unzip operation, copy the original directory to the temporary directory 129 | if ($type == 1) { 130 | File::copyDirectory($sourcePath, $targetPath); 131 | 132 | // Make sure the directory decompression level is the top level of the plugin directory 133 | $targetPath = $this->ensureDoesntHaveSubdir($targetPath); 134 | 135 | return $targetPath; 136 | } 137 | 138 | if ($type == 2) { 139 | $this->fixFilesChineseName($sourcePath); 140 | 141 | // unzip 142 | $zipFile = $this->zipFile->openFile($sourcePath); 143 | $zipFile->extractTo($targetPath); 144 | 145 | // Make sure the directory decompression level is the top level of the plugin directory 146 | $targetPath = $this->ensureDoesntHaveSubdir($targetPath); 147 | 148 | // Decompress to the specified directory 149 | return $targetPath; 150 | } 151 | 152 | return null; 153 | } 154 | 155 | public function ensureDoesntHaveSubdir(string $targetPath): string 156 | { 157 | $targetPath = $targetPath ?? config('plugins.paths.unzip_target_path'); 158 | 159 | $pattern = sprintf('%s/*', rtrim($targetPath, DIRECTORY_SEPARATOR)); 160 | 161 | $files = []; 162 | foreach (File::glob($pattern) as $file) { 163 | if (str_contains($file, '__MACOSX')) { 164 | continue; 165 | } 166 | 167 | $files[] = $file; 168 | } 169 | 170 | foreach ($files as $file) { 171 | if (str_contains($file, 'plugin.json') || str_contains($file, 'theme.json')) { 172 | return $targetPath; 173 | } 174 | } 175 | 176 | $fileCount = count($files); 177 | if (1 < $fileCount && $fileCount <= 3) { 178 | throw new \RuntimeException("Cannot handle the zip file, zip file count is: {$fileCount}, extract path is: {$targetPath}"); 179 | } 180 | 181 | $tmpDir = $targetPath.'-subdir'; 182 | File::ensureDirectoryExists($tmpDir); 183 | 184 | $firstEntryname = File::basename(current($files)); 185 | 186 | $path = $targetPath."/{$firstEntryname}"; 187 | $tmpTargetPath = $tmpDir."/{$firstEntryname}"; 188 | $parentDir = dirname($tmpTargetPath); 189 | File::ensureDirectoryExists($parentDir); 190 | 191 | if (is_dir($path)) { 192 | File::copyDirectory($path, $tmpDir); 193 | } else { 194 | File::copyDirectory(dirname($path), $parentDir); 195 | } 196 | 197 | File::cleanDirectory($targetPath); 198 | File::copyDirectory($tmpDir, $targetPath); 199 | File::deleteDirectory($tmpDir); 200 | 201 | return $targetPath; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Workaround/FactoryMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveStubPath('/stubs/factory.stub'); 46 | } 47 | 48 | /** 49 | * Resolve the fully-qualified path to the stub. 50 | * 51 | * @param string $stub 52 | * @return string 53 | */ 54 | protected function resolveStubPath($stub) 55 | { 56 | return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) 57 | ? $customPath 58 | : __DIR__.$stub; 59 | } 60 | 61 | /** 62 | * Build the class with the given name. 63 | * 64 | * @param string $name 65 | * @return string 66 | */ 67 | protected function buildClass($name) 68 | { 69 | $factory = class_basename(Str::ucfirst(str_replace('Factory', '', $name))); 70 | 71 | $namespaceModel = $this->option('model') 72 | ? $this->qualifyModel($this->option('model')) 73 | : $this->qualifyModel($this->guessModelName($name)); 74 | 75 | $model = class_basename($namespaceModel); 76 | 77 | if (Str::startsWith($namespaceModel, $this->rootNamespace().'Models')) { 78 | $namespace = Str::beforeLast('Database\\Factories\\'.Str::after($namespaceModel, $this->rootNamespace().'Models\\'), '\\'); 79 | } else { 80 | $namespace = 'Database\\Factories'; 81 | } 82 | 83 | $replace = [ 84 | '{{ factoryNamespace }}' => rtrim($this->laravel->getNamespace(), '\\').'\\'.$namespace, 85 | 'NamespacedDummyModel' => $namespaceModel, 86 | '{{ namespacedModel }}' => $namespaceModel, 87 | '{{namespacedModel}}' => $namespaceModel, 88 | 'DummyModel' => $model, 89 | '{{ model }}' => $model, 90 | '{{model}}' => $model, 91 | '{{ factory }}' => $factory, 92 | '{{factory}}' => $factory, 93 | ]; 94 | 95 | return str_replace( 96 | array_keys($replace), array_values($replace), parent::buildClass($name) 97 | ); 98 | } 99 | 100 | /** 101 | * Get the destination class path. 102 | * 103 | * @param string $name 104 | * @return string 105 | */ 106 | protected function getPath($name) 107 | { 108 | $name = (string) Str::of($name)->replaceFirst($this->rootNamespace(), '')->finish('Factory'); 109 | 110 | return $this->laravel->databasePath().'/factories/'.str_replace('\\', '/', $name).'.php'; 111 | } 112 | 113 | /** 114 | * Guess the model name from the Factory name or return a default model name. 115 | * 116 | * @param string $name 117 | * @return string 118 | */ 119 | protected function guessModelName($name) 120 | { 121 | if (Str::endsWith($name, 'Factory')) { 122 | $name = substr($name, 0, -7); 123 | } 124 | 125 | $modelName = $this->qualifyModel(Str::after($name, $this->rootNamespace())); 126 | 127 | if (class_exists($modelName)) { 128 | return $modelName; 129 | } 130 | 131 | if (is_dir(app_path('Models/'))) { 132 | return $this->rootNamespace().'Models\Model'; 133 | } 134 | 135 | return $this->rootNamespace().'Model'; 136 | } 137 | 138 | /** 139 | * Get the console command options. 140 | * 141 | * @return array 142 | */ 143 | protected function getOptions() 144 | { 145 | return [ 146 | ['model', 'm', InputOption::VALUE_OPTIONAL, 'The name of the model'], 147 | ]; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Workaround/SeedCommand.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 54 | } 55 | 56 | /** 57 | * Execute the console command. 58 | * 59 | * @return int 60 | */ 61 | public function handle() 62 | { 63 | if (! $this->confirmToProceed()) { 64 | return Command::FAILURE; 65 | } 66 | 67 | $previousConnection = $this->resolver->getDefaultConnection(); 68 | 69 | $this->resolver->setDefaultConnection($this->getDatabase()); 70 | 71 | Model::unguarded(function () { 72 | $this->getSeeder()->__invoke(); 73 | }); 74 | 75 | if ($previousConnection) { 76 | $this->resolver->setDefaultConnection($previousConnection); 77 | } 78 | 79 | $this->info('Database seeding completed successfully.'); 80 | 81 | return Command::SUCCESS; 82 | } 83 | 84 | /** 85 | * Get a seeder instance from the container. 86 | * 87 | * @return \Illuminate\Database\Seeder 88 | */ 89 | protected function getSeeder() 90 | { 91 | $class = $this->input->getArgument('class') ?? $this->input->getOption('class'); 92 | 93 | if (strpos($class, '\\') === false) { 94 | $class = rtrim(app()->getNamespace(), '\\').'\\Database\\Seeders\\'.$class; 95 | } 96 | 97 | if ($class === 'Database\\Seeders\\DatabaseSeeder' && 98 | ! class_exists($class)) { 99 | $class = 'DatabaseSeeder'; 100 | } 101 | 102 | return $this->laravel->make($class) 103 | ->setContainer($this->laravel) 104 | ->setCommand($this); 105 | } 106 | 107 | /** 108 | * Get the name of the database connection to use. 109 | * 110 | * @return string 111 | */ 112 | protected function getDatabase() 113 | { 114 | $database = $this->input->getOption('database'); 115 | 116 | return $database ?: $this->laravel['config']['database.default']; 117 | } 118 | 119 | /** 120 | * Get the console command arguments. 121 | * 122 | * @return array 123 | */ 124 | protected function getArguments() 125 | { 126 | return [ 127 | ['class', InputArgument::OPTIONAL, 'The class name of the root seeder', null], 128 | ]; 129 | } 130 | 131 | /** 132 | * Get the console command options. 133 | * 134 | * @return array 135 | */ 136 | protected function getOptions() 137 | { 138 | return [ 139 | ['class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', rtrim(app()->getNamespace(), '\\').'\\Database\\Seeders\\DatabaseSeeder'], 140 | ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to seed'], 141 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 142 | ]; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Workaround/SeederMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveStubPath('/stubs/seeder.stub'); 54 | } 55 | 56 | /** 57 | * Resolve the fully-qualified path to the stub. 58 | * 59 | * @param string $stub 60 | * @return string 61 | */ 62 | protected function resolveStubPath($stub) 63 | { 64 | return is_file($customPath = $this->laravel->basePath(trim($stub, '/'))) 65 | ? $customPath 66 | : __DIR__.$stub; 67 | } 68 | 69 | /** 70 | * Get the destination class path. 71 | * 72 | * @param string $name 73 | * @return string 74 | */ 75 | protected function getPath($name) 76 | { 77 | if (is_dir($this->laravel->databasePath().'/seeds')) { 78 | return $this->laravel->databasePath().'/seeds/'.$name.'.php'; 79 | } else { 80 | return $this->laravel->databasePath().'/seeders/'.$name.'.php'; 81 | } 82 | } 83 | 84 | /** 85 | * Parse the class name and format according to the root namespace. 86 | * 87 | * @param string $name 88 | * @return string 89 | */ 90 | protected function qualifyClass($name) 91 | { 92 | return $name; 93 | } 94 | 95 | /** 96 | * Get the full namespace for a given class, without the class name. 97 | * 98 | * @param string $name 99 | * @return string 100 | */ 101 | protected function getNamespace($name) 102 | { 103 | return rtrim($this->laravel->getNamespace(), '\\').'\\Database\\Seeders'; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Workaround/TestCommand.php: -------------------------------------------------------------------------------- 1 | ignoreValidationErrors(); 59 | } 60 | 61 | /** 62 | * Execute the console command. 63 | * 64 | * @return mixed 65 | */ 66 | public function handle() 67 | { 68 | if ((int) \PHPUnit\Runner\Version::id()[0] < 9) { 69 | throw new RequirementsException('Running Collision ^5.0 artisan test command requires at least PHPUnit ^9.0.'); 70 | } 71 | 72 | // @phpstan-ignore-next-line 73 | if ((int) \Illuminate\Foundation\Application::VERSION[0] < 8) { 74 | throw new RequirementsException('Running Collision ^5.0 artisan test command requires at least Laravel ^8.0.'); 75 | } 76 | 77 | if ($this->option('parallel') && ! $this->isParallelDependenciesInstalled()) { 78 | if (! $this->confirm('Running tests in parallel requires "brianium/paratest". Do you wish to install it as a dev dependency?')) { 79 | return Command::FAILURE; 80 | } 81 | 82 | $this->installParallelDependencies(); 83 | } 84 | 85 | $options = array_slice($_SERVER['argv'], $this->option('without-tty') ? 3 : 2); 86 | 87 | $this->clearEnv(); 88 | 89 | $parallel = $this->option('parallel'); 90 | 91 | $process = (new Process(array_merge( 92 | // Binary ... 93 | $this->binary(), 94 | // Arguments ... 95 | $parallel ? $this->paratestArguments($options) : $this->phpunitArguments($options) 96 | ), 97 | null, 98 | // Envs ... 99 | $parallel ? $this->paratestEnvironmentVariables() : $this->phpunitEnvironmentVariables(), 100 | ))->setTimeout(null); 101 | 102 | try { 103 | $process->setTty(! $this->option('without-tty')); 104 | } catch (RuntimeException $e) { 105 | $this->output->writeln('Warning: '.$e->getMessage()); 106 | } 107 | 108 | try { 109 | return $process->run(function ($type, $line) { 110 | $this->output->write($line); 111 | }); 112 | } catch (ProcessSignaledException $e) { 113 | if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) { 114 | throw $e; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Get the PHP binary to execute. 121 | * 122 | * @return array 123 | */ 124 | protected function binary() 125 | { 126 | if (class_exists(\Pest\Laravel\PestServiceProvider::class)) { 127 | $command = $this->option('parallel') ? ['vendor/pestphp/pest/bin/pest', '--parallel'] : ['vendor/pestphp/pest/bin/pest']; 128 | } else { 129 | $command = $this->option('parallel') ? ['vendor/brianium/paratest/bin/paratest'] : ['vendor/phpunit/phpunit/phpunit']; 130 | } 131 | 132 | $command = [base_path($command[0])]; 133 | 134 | if ('phpdbg' === PHP_SAPI) { 135 | return array_merge([PHP_BINARY, '-qrr'], $command); 136 | } 137 | 138 | return array_merge([PHP_BINARY], $command); 139 | } 140 | 141 | /** 142 | * Get the array of arguments for running PHPUnit. 143 | * 144 | * @param array $options 145 | * @return array 146 | */ 147 | protected function phpunitArguments($options) 148 | { 149 | $options = array_merge(['--printer=NunoMaduro\\Collision\\Adapters\\Phpunit\\Printer'], $options); 150 | 151 | $options = array_values(array_filter($options, function ($option) { 152 | return ! Str::startsWith($option, '--env='); 153 | })); 154 | 155 | if (! file_exists($file = base_path('phpunit.xml'))) { 156 | $file = base_path('phpunit.xml.dist'); 157 | } 158 | 159 | return array_merge(["--configuration=$file"], $options); 160 | } 161 | 162 | /** 163 | * Get the array of arguments for running Paratest. 164 | * 165 | * @param array $options 166 | * @return array 167 | */ 168 | protected function paratestArguments($options) 169 | { 170 | $options = array_values(array_filter($options, function ($option) { 171 | return ! Str::startsWith($option, '--env=') 172 | && ! Str::startsWith($option, '-p') 173 | && ! Str::startsWith($option, '--parallel') 174 | && ! Str::startsWith($option, '--recreate-databases'); 175 | })); 176 | 177 | if (! file_exists($file = base_path('phpunit.xml'))) { 178 | $file = base_path('phpunit.xml.dist'); 179 | } 180 | 181 | return array_merge([ 182 | "--configuration=$file", 183 | "--runner=\Illuminate\Testing\ParallelRunner", 184 | ], $options); 185 | } 186 | 187 | /** 188 | * Get the array of environment variables for running PHPUnit. 189 | * 190 | * @return array 191 | */ 192 | protected function phpunitEnvironmentVariables() 193 | { 194 | return []; 195 | } 196 | 197 | /** 198 | * Get the array of environment variables for running Paratest. 199 | * 200 | * @return array 201 | */ 202 | protected function paratestEnvironmentVariables() 203 | { 204 | return [ 205 | 'LARAVEL_PARALLEL_TESTING' => 1, 206 | 'LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES' => $this->option('recreate-databases'), 207 | ]; 208 | } 209 | 210 | /** 211 | * Clears any set Environment variables set by Laravel if the --env option is empty. 212 | * 213 | * @return void 214 | */ 215 | protected function clearEnv() 216 | { 217 | if (! $this->option('env')) { 218 | $vars = self::getEnvironmentVariables( 219 | // @phpstan-ignore-next-line 220 | $this->laravel->environmentPath(), 221 | // @phpstan-ignore-next-line 222 | $this->laravel->environmentFile() 223 | ); 224 | 225 | $repository = Env::getRepository(); 226 | 227 | foreach ($vars as $name) { 228 | $repository->clear($name); 229 | } 230 | } 231 | } 232 | 233 | /** 234 | * @param string $path 235 | * @param string $file 236 | * @return array 237 | */ 238 | protected static function getEnvironmentVariables($path, $file) 239 | { 240 | try { 241 | $content = StoreBuilder::createWithNoNames() 242 | ->addPath($path) 243 | ->addName($file) 244 | ->make() 245 | ->read(); 246 | } catch (InvalidPathException $e) { 247 | return []; 248 | } 249 | 250 | $vars = []; 251 | 252 | foreach ((new Parser())->parse($content) as $entry) { 253 | $vars[] = $entry->getName(); 254 | } 255 | 256 | return $vars; 257 | } 258 | 259 | /** 260 | * Check if the parallel dependencies are installed. 261 | * 262 | * @return bool 263 | */ 264 | protected function isParallelDependenciesInstalled() 265 | { 266 | return class_exists(\ParaTest\Console\Commands\ParaTestCommand::class); 267 | } 268 | 269 | /** 270 | * Install parallel testing needed dependencies. 271 | * 272 | * @return void 273 | */ 274 | protected function installParallelDependencies() 275 | { 276 | $command = $this->findComposer().' require brianium/paratest --dev'; 277 | 278 | $process = Process::fromShellCommandline($command, null, null, null, null); 279 | 280 | if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { 281 | try { 282 | $process->setTty(true); 283 | } catch (RuntimeException $e) { 284 | $this->output->writeln('Warning: '.$e->getMessage()); 285 | } 286 | } 287 | 288 | try { 289 | $process->run(function ($type, $line) { 290 | $this->output->write($line); 291 | }); 292 | } catch (ProcessSignaledException $e) { 293 | if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) { 294 | throw $e; 295 | } 296 | } 297 | } 298 | 299 | /** 300 | * Get the composer command for the environment. 301 | * 302 | * @return string 303 | */ 304 | protected function findComposer() 305 | { 306 | $composerPath = getcwd().'/composer.phar'; 307 | 308 | if (file_exists($composerPath)) { 309 | return '"'.PHP_BINARY.'" '.$composerPath; 310 | } 311 | 312 | return 'composer'; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/Workaround/TestMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('unit') ? '.unit.stub' : '.stub'; 46 | 47 | return $this->option('pest') 48 | ? $this->resolveStubPath('/stubs/pest'.$suffix) 49 | : $this->resolveStubPath('/stubs/test'.$suffix); 50 | } 51 | 52 | /** 53 | * Resolve the fully-qualified path to the stub. 54 | * 55 | * @param string $stub 56 | * @return string 57 | */ 58 | protected function resolveStubPath($stub) 59 | { 60 | return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) 61 | ? $customPath 62 | : __DIR__.$stub; 63 | } 64 | 65 | /** 66 | * Get the destination class path. 67 | * 68 | * @param string $name 69 | * @return string 70 | */ 71 | protected function getPath($name) 72 | { 73 | $name = Str::replaceFirst($this->rootNamespace(), '', $name); 74 | 75 | return str_replace('/src', '', app_path()).'/tests'.str_replace('\\', '/', $name).'.php'; 76 | // return base_path('tests').str_replace('\\', '/', $name).'.php'; 77 | } 78 | 79 | /** 80 | * Get the default namespace for the class. 81 | * 82 | * @param string $rootNamespace 83 | * @return string 84 | */ 85 | protected function getDefaultNamespace($rootNamespace) 86 | { 87 | if ($this->option('unit')) { 88 | return $rootNamespace.'\Unit'; 89 | } else { 90 | return $rootNamespace.'\Feature'; 91 | } 92 | } 93 | 94 | /** 95 | * Get the root namespace for the class. 96 | * 97 | * @return string 98 | */ 99 | protected function rootNamespace() 100 | { 101 | return rtrim($this->laravel->getNamespace(), '\\').'\\Tests'; 102 | } 103 | 104 | /** 105 | * Get the console command options. 106 | * 107 | * @return array 108 | */ 109 | protected function getOptions() 110 | { 111 | return [ 112 | ['unit', 'u', InputOption::VALUE_NONE, 'Create a unit test.'], 113 | ['pest', 'p', InputOption::VALUE_NONE, 'Create a Pest test.'], 114 | ]; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Workaround/stubs/factory.stub: -------------------------------------------------------------------------------- 1 | get('/'); 17 | 18 | $response->assertStatus(200); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Workaround/stubs/test.unit.stub: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | --------------------------------------------------------------------------------