├── 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 |
5 |
6 |
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 |
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 |
2 |
3 |
Navbar
4 |
5 |
6 |
7 |
33 |
34 |
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 |
35 | @include('$STUDLY_NAME$::layouts.footer')
36 |
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 |
4 |
10 |
11 | {{ session('success') }}
12 |
13 |
14 |
15 | @elseif (session('failure'))
16 |
17 |
18 |
28 |
29 | {{ session('failure') }}
30 |
31 |
32 |
33 | @endif
34 |
--------------------------------------------------------------------------------
/src/Commands/stubs/resources/views/settings.stub:
--------------------------------------------------------------------------------
1 | @extends('$STUDLY_NAME$::layouts.master')
2 |
3 | @section('content')
4 |
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 |
--------------------------------------------------------------------------------