├── .codeclimate.yml
├── .github
└── workflows
│ ├── coverage.yml
│ ├── php-cs-fixer.yml
│ ├── phpunit.yml
│ └── update-changelog.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── LICENSE
├── README.md
├── art
└── modular.png
├── composer.json
├── config.php
├── phpunit.xml
├── src
├── Console
│ └── Commands
│ │ ├── Database
│ │ └── SeedCommand.php
│ │ ├── Make
│ │ ├── MakeCast.php
│ │ ├── MakeChannel.php
│ │ ├── MakeCommand.php
│ │ ├── MakeComponent.php
│ │ ├── MakeController.php
│ │ ├── MakeEvent.php
│ │ ├── MakeException.php
│ │ ├── MakeFactory.php
│ │ ├── MakeJob.php
│ │ ├── MakeListener.php
│ │ ├── MakeLivewire.php
│ │ ├── MakeMail.php
│ │ ├── MakeMiddleware.php
│ │ ├── MakeMigration.php
│ │ ├── MakeModel.php
│ │ ├── MakeModule.php
│ │ ├── MakeNotification.php
│ │ ├── MakeObserver.php
│ │ ├── MakePolicy.php
│ │ ├── MakeProvider.php
│ │ ├── MakeRequest.php
│ │ ├── MakeResource.php
│ │ ├── MakeRule.php
│ │ ├── MakeSeeder.php
│ │ ├── MakeTest.php
│ │ └── Modularize.php
│ │ ├── Modularize.php
│ │ ├── ModulesCache.php
│ │ ├── ModulesClear.php
│ │ ├── ModulesList.php
│ │ └── ModulesSync.php
├── Exceptions
│ ├── CannotFindModuleForPathException.php
│ └── Exception.php
└── Support
│ ├── AutoDiscoveryHelper.php
│ ├── DatabaseFactoryHelper.php
│ ├── DiscoverEvents.php
│ ├── Facades
│ └── Modules.php
│ ├── FinderCollection.php
│ ├── ModularEventServiceProvider.php
│ ├── ModularServiceProvider.php
│ ├── ModularizedCommandsServiceProvider.php
│ ├── ModuleConfig.php
│ ├── ModuleRegistry.php
│ └── PhpStorm
│ ├── ConfigWriter.php
│ ├── LaravelConfigWriter.php
│ ├── PhpFrameworkWriter.php
│ ├── ProjectImlWriter.php
│ └── WorkspaceWriter.php
├── stubs
├── .gitkeep
├── ServiceProvider.php
├── ServiceProviderTest.php
├── composer-stub-latest.json
├── composer-stub-v7.json
├── migration.php
├── view.blade.php
└── web-routes.php
└── tests
├── AutoDiscoveryHelperTest.php
├── Commands
├── Database
│ └── SeedCommandTest.php
├── Make
│ ├── MakeCastTest.php
│ ├── MakeChannelTest.php
│ ├── MakeCommandTest.php
│ ├── MakeComponentTest.php
│ ├── MakeControllerTest.php
│ ├── MakeEventTest.php
│ ├── MakeExceptionTest.php
│ ├── MakeFactoryTest.php
│ ├── MakeJobTest.php
│ ├── MakeListenerTest.php
│ ├── MakeLivewireTest.php
│ ├── MakeMailTest.php
│ ├── MakeMiddlewareTest.php
│ ├── MakeMigrationTest.php
│ ├── MakeModelTest.php
│ ├── MakeModuleTest.php
│ ├── MakeNotificationTest.php
│ ├── MakeObserverTest.php
│ ├── MakePolicyTest.php
│ ├── MakeProviderTest.php
│ ├── MakeRequestTest.php
│ ├── MakeResourceTest.php
│ ├── MakeRuleTest.php
│ ├── MakeSeederTest.php
│ └── MakeTestTest.php
├── ModulesCacheTest.php
├── ModulesClearTest.php
├── ModulesListTest.php
└── ModulesSyncTest.php
├── Concerns
├── PreloadsAppModules.php
├── TestsMakeCommands.php
└── WritesToAppFilesystem.php
├── EventDiscovery
├── EventDiscoveryExplicitlyDisabledTest.php
├── EventDiscoveryExplicitlyEnabledTest.php
├── EventDiscoveryImplicitlyDisabledTest.php
├── EventDiscoveryImplicitlyEnabledTest.php
└── Laravel11EventDiscoveryImplicitlyEnabledTest.php
├── ModularServiceProviderTest.php
├── ModuleRegistryTest.php
├── TestCase.php
├── stubs
├── laravel-plugin.xml
├── php.xml
├── phpunit.xml
├── project.iml
├── test-stub.php
└── workspace.xml
└── testbench-core
└── app-modules
└── test-module
├── composer.json
├── database
├── factories
│ └── .gitkeep
├── migrations
│ ├── .gitkeep
│ └── 2024_04_03_133130_set_up_test-module_module.php
└── seeders
│ └── .gitkeep
├── resources
└── views
│ └── index.blade.php
├── routes
└── test-module-routes.php
└── src
├── Events
└── TestEvent.php
├── Listeners
└── TestEventListener.php
└── Providers
└── TestModuleServiceProvider.php
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | exclude_patterns:
2 | - ".github/"
3 | - ".idea/"
4 | - "stubs/"
5 | - "tests/"
6 | - "**/vendor/"
7 | - "**/node_modules/"
8 | - "*.md"
9 | - ".*.yml"
10 | - "LICENSE"
11 | - "composer.json"
12 | - "phpunit.xml"
13 | - "src/Console/Commands/Make/MakeChannel.php"
14 | - "src/Console/Commands/Make/MakeController.php"
15 | - "src/Console/Commands/Make/MakeEvent.php"
16 | - "src/Console/Commands/Make/MakeException.php"
17 | - "src/Console/Commands/Make/MakeFactory.php"
18 | - "src/Console/Commands/Make/MakeJob.php"
19 | - "src/Console/Commands/Make/MakeListener.php"
20 | - "src/Console/Commands/Make/MakeMail.php"
21 | - "src/Console/Commands/Make/MakeMiddleware.php"
22 | - "src/Console/Commands/Make/MakeMigration.php"
23 | - "src/Console/Commands/Make/MakeNotification.php"
24 | - "src/Console/Commands/Make/MakeObserver.php"
25 | - "src/Console/Commands/Make/MakePolicy.php"
26 | - "src/Console/Commands/Make/MakeProvider.php"
27 | - "src/Console/Commands/Make/MakeRequest.php"
28 | - "src/Console/Commands/Make/MakeResource.php"
29 | - "src/Console/Commands/Make/MakeRule.php"
30 | - "src/Console/Commands/Make/MakeSeeder.php"
31 |
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: Code Coverage
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | coverage:
10 | runs-on: ubuntu-latest
11 |
12 | name: Publish code coverage
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v3
17 |
18 | - name: Setup PHP
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: 8.3
22 | extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, iconv
23 | coverage: pcov
24 |
25 | - name: Cache dependencies
26 | uses: actions/cache@v2
27 | with:
28 | path: |
29 | vendor
30 | ${{ steps.composer-cache-files-dir.outputs.dir }}
31 | key: ${{ runner.os }}-composer-${{ hashFiles('composer.json') }}
32 | restore-keys: |
33 | ${{ runner.os }}-composer-
34 |
35 | - name: Install dependencies
36 | env:
37 | COMPOSER_DISCARD_CHANGES: true
38 | run: composer install --no-progress --no-interaction --prefer-dist
39 |
40 | - name: Run and publish code coverage
41 | uses: paambaati/codeclimate-action@v5.0.0
42 | env:
43 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
44 | with:
45 | coverageCommand: vendor/bin/phpunit --coverage-clover ${{ github.workspace }}/clover.xml
46 | debug: true
47 | coverageLocations:
48 | "${{github.workspace}}/clover.xml:clover"
49 |
--------------------------------------------------------------------------------
/.github/workflows/php-cs-fixer.yml:
--------------------------------------------------------------------------------
1 | name: Code Style
2 |
3 | on: [ pull_request, push ]
4 |
5 | jobs:
6 | coverage:
7 | runs-on: ubuntu-latest
8 |
9 | name: Run code style checks
10 |
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v3
14 |
15 | - name: Setup PHP
16 | uses: shivammathur/setup-php@v2
17 | with:
18 | php-version: 8.3
19 | extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, iconv
20 |
21 | - name: Cache dependencies
22 | uses: actions/cache@v2
23 | with:
24 | path: |
25 | vendor
26 | ${{ steps.composer-cache-files-dir.outputs.dir }}
27 | key: ${{ runner.os }}-composer-${{ hashFiles('composer.json') }}
28 | restore-keys: |
29 | ${{ runner.os }}-composer-
30 |
31 | - name: Install dependencies
32 | env:
33 | COMPOSER_DISCARD_CHANGES: true
34 | run: composer install --no-progress --no-interaction --prefer-dist
35 |
36 | - name: Run PHP CS Fixer
37 | run: ./vendor/bin/php-cs-fixer fix --diff --dry-run
38 |
--------------------------------------------------------------------------------
/.github/workflows/phpunit.yml:
--------------------------------------------------------------------------------
1 | name: PHPUnit
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: '0 14 * * 3' # Run Wednesdays at 2pm EST
8 |
9 | jobs:
10 | php-tests:
11 | strategy:
12 | matrix:
13 | dependency-version: [ stable, lowest ]
14 | os: [ ubuntu-latest, windows-latest ]
15 | laravel: [ 10.*, 11.*, 12.* ]
16 | php: [ 8.1, 8.2, 8.3, 8.4 ]
17 | exclude:
18 | - php: 8.1
19 | laravel: 11.*
20 | - php: 8.1
21 | laravel: 12.*
22 |
23 | runs-on: ${{ matrix.os }}
24 | timeout-minutes: 10
25 |
26 | name: "${{ matrix.php }} / ${{ matrix.laravel }} (${{ matrix.dependency-version }})"
27 |
28 | steps:
29 | - name: Checkout code
30 | uses: actions/checkout@v3
31 |
32 | - name: Setup PHP
33 | uses: shivammathur/setup-php@v2
34 | with:
35 | php-version: ${{ matrix.php }}
36 | extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, iconv, fileinfo
37 | tools: composer:v2
38 |
39 | - name: Register composer cache directory
40 | id: composer-cache-files-dir
41 | run: |
42 | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
43 |
44 | - name: Cache dependencies
45 | uses: actions/cache@v3
46 | with:
47 | path: |
48 | vendor
49 | ${{ steps.composer-cache-files-dir.outputs.dir }}
50 | key: ${{ runner.os }}-composer-${{ hashFiles('composer.json') }}
51 | restore-keys: |
52 | ${{ runner.os }}-composer-
53 |
54 | - name: Install dependencies
55 | run: |
56 | composer require --no-progress --no-interaction --prefer-dist --update-with-all-dependencies --prefer-${{ matrix.dependency-version }} "illuminate/support:${{ matrix.laravel }}"
57 |
58 | - name: Execute tests
59 | run: vendor/bin/phpunit
60 |
--------------------------------------------------------------------------------
/.github/workflows/update-changelog.yml:
--------------------------------------------------------------------------------
1 | name: Update Changelog
2 |
3 | on:
4 | release:
5 | types: [ published ]
6 |
7 | jobs:
8 | update-publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | with:
13 | repository: ${{ github.event.repository.full_name }}
14 | ref: 'main'
15 |
16 | - name: Update changelog
17 | uses: thomaseizinger/keep-a-changelog-new-release@v1
18 | with:
19 | version: ${{ github.event.release.tag_name }}
20 |
21 | - name: Commit changelog back to repo
22 | uses: EndBug/add-and-commit@v9
23 | with:
24 | add: 'CHANGELOG.md'
25 | message: ${{ github.event.release.tag_name }}
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .rnd
3 | .cache
4 | .php_cs.cache
5 | .php-cs-fixer.cache
6 | .composer-history
7 |
8 | /vendor*
9 | /node_modules
10 |
11 | .env
12 | .aws.json
13 | .idea/*
14 |
15 | _ide_helper.php
16 | _ide_macros.php
17 | _ide_helper_models.php
18 | .phpstorm.meta.php
19 | /.generics/
20 | yarn-error.log
21 |
22 | npm-debug.log*
23 | .phpunit.result.cache
24 | composer.lock
25 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
7 | ->setIndent("\t")
8 | ->setLineEnding("\n")
9 | ->setRules([
10 | '@PSR2' => true,
11 | 'function_declaration' => [
12 | 'closure_function_spacing' => 'none',
13 | 'closure_fn_spacing' => 'none',
14 | ],
15 | 'ordered_imports' => [
16 | 'sort_algorithm' => 'alpha',
17 | ],
18 | 'array_indentation' => true,
19 | 'braces' => [
20 | 'allow_single_line_closure' => true,
21 | ],
22 | 'no_break_comment' => false,
23 | 'return_type_declaration' => [
24 | 'space_before' => 'none',
25 | ],
26 | 'blank_line_after_opening_tag' => true,
27 | 'compact_nullable_typehint' => true,
28 | 'cast_spaces' => true,
29 | 'concat_space' => [
30 | 'spacing' => 'none',
31 | ],
32 | 'declare_equal_normalize' => [
33 | 'space' => 'none',
34 | ],
35 | 'function_typehint_space' => true,
36 | 'new_with_braces' => true,
37 | 'method_argument_space' => true,
38 | 'no_empty_statement' => true,
39 | 'no_empty_comment' => true,
40 | 'no_empty_phpdoc' => true,
41 | 'no_extra_blank_lines' => [
42 | 'tokens' => [
43 | 'extra',
44 | 'use',
45 | 'use_trait',
46 | 'return',
47 | ],
48 | ],
49 | 'no_leading_import_slash' => true,
50 | 'no_leading_namespace_whitespace' => true,
51 | 'no_blank_lines_after_class_opening' => true,
52 | 'no_blank_lines_after_phpdoc' => true,
53 | 'no_whitespace_in_blank_line' => false,
54 | 'no_whitespace_before_comma_in_array' => true,
55 | 'no_useless_else' => true,
56 | 'no_useless_return' => true,
57 | 'single_trait_insert_per_statement' => true,
58 | 'psr_autoloading' => true,
59 | 'dir_constant' => true,
60 | 'single_line_comment_style' => [
61 | 'comment_types' => ['hash'],
62 | ],
63 | 'include' => true,
64 | 'is_null' => true,
65 | 'linebreak_after_opening_tag' => true,
66 | 'lowercase_cast' => true,
67 | 'lowercase_static_reference' => true,
68 | 'magic_constant_casing' => true,
69 | 'magic_method_casing' => true,
70 | 'class_attributes_separation' => [
71 | // TODO: This can be reverted when https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/5869 is merged
72 | 'elements' => ['const' => 'one', 'method' => 'one', 'property' => 'one'],
73 | ],
74 | 'modernize_types_casting' => true,
75 | 'native_function_casing' => true,
76 | 'native_function_type_declaration_casing' => true,
77 | 'no_alias_functions' => true,
78 | 'no_multiline_whitespace_around_double_arrow' => true,
79 | 'multiline_whitespace_before_semicolons' => true,
80 | 'no_short_bool_cast' => true,
81 | 'no_unused_imports' => true,
82 | 'no_php4_constructor' => true,
83 | 'no_singleline_whitespace_before_semicolons' => true,
84 | 'no_spaces_around_offset' => true,
85 | 'no_trailing_comma_in_list_call' => true,
86 | 'no_trailing_comma_in_singleline_array' => true,
87 | 'normalize_index_brace' => true,
88 | 'object_operator_without_whitespace' => true,
89 | 'phpdoc_annotation_without_dot' => true,
90 | 'phpdoc_indent' => true,
91 | 'phpdoc_no_package' => true,
92 | 'phpdoc_no_access' => true,
93 | 'phpdoc_no_useless_inheritdoc' => true,
94 | 'phpdoc_single_line_var_spacing' => true,
95 | 'phpdoc_trim' => true,
96 | 'phpdoc_types' => true,
97 | 'semicolon_after_instruction' => true,
98 | 'array_syntax' => [
99 | 'syntax' => 'short',
100 | ],
101 | 'list_syntax' => [
102 | 'syntax' => 'short',
103 | ],
104 | 'short_scalar_cast' => true,
105 | 'single_blank_line_before_namespace' => true,
106 | 'single_quote' => true,
107 | 'standardize_not_equals' => true,
108 | 'ternary_operator_spaces' => true,
109 | 'whitespace_after_comma_in_array' => true,
110 | 'not_operator_with_successor_space' => true,
111 | 'trailing_comma_in_multiline' => true,
112 | 'trim_array_spaces' => true,
113 | 'binary_operator_spaces' => true,
114 | 'unary_operator_spaces' => true,
115 | 'php_unit_method_casing' => [
116 | 'case' => 'snake_case',
117 | ],
118 | 'php_unit_test_annotation' => [
119 | 'style' => 'prefix',
120 | ],
121 | ])
122 | ->setFinder(
123 | PhpCsFixer\Finder::create()
124 | ->exclude('.circleci')
125 | ->exclude('bin')
126 | ->exclude('node_modules')
127 | ->exclude('vendor')
128 | ->exclude('vendor-10') // used for local offline testing
129 | ->exclude('vendor-11') // used for local offline testing
130 | ->notPath('.phpstorm.meta.php')
131 | ->notPath('_ide_helper.php')
132 | ->notPath('artisan')
133 | ->in(__DIR__)
134 | );
135 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This changelog follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format,
4 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5 |
6 | ## [Unreleased]
7 |
8 | ### Added
9 |
10 | - Added support for auto-aliasing module classes in tinker sessions
11 |
12 | ## [2.2.0] - 2024-04-05
13 |
14 | ### Added
15 |
16 | - The modules sync command now adds modules to PhpStorm exclude path, preventing double-registration of modules
17 |
18 | ## [2.1.0] - 2024-03-18
19 |
20 | ### Added
21 |
22 | - Added support for Laravel 11
23 | - Added support for event discovery
24 |
25 | ### Fixed
26 |
27 | - Fixed an error with how module command signatures were set
28 |
29 | ## [2.0.0] - 2023-05-19
30 |
31 | ### Changed
32 |
33 | - Dropped support for older versions of PHP and Laravel. If you are using Laravel 7 or 8, or PHP 7, please use the 1.x releases
34 |
35 | ## [1.12.0] - 2023-05-19
36 |
37 | ### Added
38 |
39 | - Added support for factory model name resolution inside modules
40 |
41 | ### Fixed
42 |
43 | - Added support for new `make:command` changes in Laravel 10
44 |
45 | ## [1.11.0] - 2023-02-14
46 |
47 | ### Changed
48 |
49 | - Updated version constraints to add Laravel 10 support and drop automated testing for old versions of PHP and Laravel
50 |
51 | ## [1.10.0] - 2022-08-12
52 |
53 | ### Fixed
54 |
55 | - Improved path normalization on Windows (thanks to @Sophist-UK)
56 |
57 | ## [1.9.0] - 2022-07-06
58 |
59 | ### Fixed
60 |
61 | - Addressed issue where `make:migration` and `make:livewire` were not loading the custom `--module` option
62 | - Added additional tests for `make:` commands to catch necessary changes quicker in the future
63 | - Passing a `--module` flag for an unknown module now triggers a console error
64 |
65 | ## [1.8.0] - 2022-06-04
66 |
67 | ### Added
68 |
69 | - Added support for Blade component namespaces (i.e. ``)
70 |
71 | ### Fixed
72 |
73 | - Fixed issue with `make:seeder` command introduced in Laravel 9.6.0
74 |
75 | ## [1.7.0] - 2022-02-11
76 |
77 | ### Added
78 |
79 | - Added support for Laravel 9.x
80 |
81 | ## [1.6.0]
82 |
83 | ### Added
84 |
85 | - Added support for custom module stubs
86 |
87 | ### Fixed
88 |
89 | - Only register the `make:livewire` integration if Livewire is installed
90 |
91 | ## [1.5.2]
92 |
93 | ### Added
94 |
95 | - Added support for syncing modules to PhpStorm library roots
96 |
97 | ## [1.5.1]
98 |
99 | ### Added
100 |
101 | - Added support for `make:cast`
102 |
103 | ## [1.5.0]
104 |
105 | ### Added
106 |
107 | - Added support for Livewire's `make:livewire` command
108 |
109 | ## [1.4.0]
110 |
111 | ### Added
112 |
113 | - Added support for `--module` in `php artisan db:seed`
114 |
115 | ### Fixed
116 |
117 | - Create seeders in the correct namespace when `--module` flag is used in Laravel 8+
118 | - Create factories in the correct namespace when `--module` flag is used in Laravel 8+
119 | - Apply module namespace to models when creating a factory in a module
120 |
121 | ## [1.3.1]
122 |
123 | ### Fixed
124 |
125 | - Added better handling of missing directories
126 |
127 | ## [1.3.0]
128 |
129 | ### Added
130 |
131 | - Added support for translations in modules
132 |
133 | ### Changed
134 |
135 | - Switched to `diglactic/laravel-breadcrumbs` for breadcrumbs check
136 |
137 | ## [1.2.2]
138 |
139 | ### Added
140 |
141 | - Added better patching for PHPStorm config files to minimize diffs
142 |
143 | ## [1.2.0]
144 |
145 | ### Added
146 |
147 | - Support for auto-registering Laravel 8 factory classes
148 |
149 | ### Fixed
150 |
151 | - Better Windows support
152 | - Support for composer 2.0
153 | - Improves the file scanning efficiency of the `AutoDiscoveryHelper`
154 |
155 | ## [1.1.0]
156 |
157 | ### Added
158 |
159 | - Adds support for `php artisan make:component`
160 | - `php artisan modules:sync` will now update additional PhpStorm config files
161 | - Partial support for `--all` on `make:model`
162 | - Initial support for component auto-discovery
163 | - Switched to single `app-modules/*` composer repository rather than new repositories for each module
164 | - Added description field to generated `composer.json` file
165 | - Moved tests from `autoload-dev` to `autoload` because composer doesn't support
166 | `autoload-dev` for non-root configs
167 | - Added improved support for Laravel 8 factory classes
168 |
169 | ## [1.0.1]
170 |
171 | ### Changed
172 |
173 | - Introduces a few improvements to the default composer.json format.
174 |
175 | ## [1.0.0]
176 |
177 | ### Added
178 |
179 | - Initial release
180 |
181 | * * *
182 |
183 | #### "Keep a Changelog" - Types of Changes
184 |
185 | - `Added` for new features.
186 | - `Changed` for changes in existing functionality.
187 | - `Deprecated` for soon-to-be removed features.
188 | - `Removed` for now removed features.
189 | - `Fixed` for any bug fixes.
190 | - `Security` in case of vulnerabilities.
191 |
192 | [Unreleased]: https://github.com/InterNACHI/modular/compare/1.12.0...HEAD
193 |
194 | [1.12.0]: https://github.com/InterNACHI/modular/compare/1.11.0...1.12.0
195 |
196 | [1.11.0]: https://github.com/InterNACHI/modular/compare/1.10.0...1.11.0
197 |
198 | [1.10.0]: https://github.com/InterNACHI/modular/compare/1.9.0...1.10.0
199 |
200 | [1.9.0]: https://github.com/InterNACHI/modular/compare/1.8.0...1.9.0
201 |
202 | [1.8.0]: https://github.com/InterNACHI/modular/compare/1.7.0...1.8.0
203 |
204 | [1.7.0]: https://github.com/InterNACHI/modular/compare/1.6.0...1.7.0
205 |
206 | [1.6.0]: https://github.com/InterNACHI/modular/compare/1.5.2...1.6.0
207 |
208 | [1.5.2]: https://github.com/InterNACHI/modular/compare/1.5.1...1.5.2
209 |
210 | [1.5.1]: https://github.com/InterNACHI/modular/compare/1.5.0...1.5.1
211 |
212 | [1.5.0]: https://github.com/InterNACHI/modular/compare/1.4.0...1.5.0
213 |
214 | [1.4.0]: https://github.com/InterNACHI/modular/compare/1.3.1...1.4.0
215 |
216 | [1.3.1]: https://github.com/InterNACHI/modular/compare/1.3.0...1.3.1
217 |
218 | [1.3.0]: https://github.com/InterNACHI/modular/compare/1.2.2...1.3.0
219 |
220 | [1.2.2]: https://github.com/InterNACHI/modular/compare/1.2.1...1.2.2
221 |
222 | [1.2.1]: https://github.com/InterNACHI/modular/compare/1.2.0...1.2.1
223 |
224 | [1.2.0]: https://github.com/InterNACHI/modular/compare/1.1.0...1.2.0
225 |
226 | [1.0.1]: https://github.com/InterNACHI/modular/compare/1.0.1...1.1.0
227 |
228 | [1.0.1]: https://github.com/InterNACHI/modular/compare/1.0.0...1.0.1
229 |
230 | [1.0.0]: https://github.com/InterNACHI/modular/releases/tag/1.0.0
231 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2022 InterNACHI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `internachi/modular`
4 |
5 |
43 |
44 | `internachi/modular` is a module system for Laravel applications. It uses
45 | [Composer path repositories](https://getcomposer.org/doc/05-repositories.md#path) for autoloading,
46 | and [Laravel package discovery](https://laravel.com/docs/11.x/packages#package-discovery) for module
47 | initialization, and then provides minimal tooling to fill in any gaps.
48 |
49 | This project is as much a set of conventions as it is a package. The fundamental idea
50 | is that you can create “modules” in a separate `app-modules/` directory, which allows you to
51 | better organize large projects. These modules use the existing
52 | [Laravel package system](https://laravel.com/docs/11.x/packages), and follow existing Laravel
53 | conventions.
54 |
55 | - [Walkthrough Video](#walkthrough-video)
56 | - [Installation](#installation)
57 | - [Usage](#usage)
58 | - [Comparison to `nwidart/laravel-modules`](#comparison-to-nwidartlaravel-modules)
59 |
60 | ## Walkthrough Video
61 |
62 | [](https://internachi.wistia.com/medias/pivaxithl7?wvideo=pivaxithl7)
63 |
64 | ## Installation
65 |
66 | To get started, run:
67 |
68 | ```shell script
69 | composer require internachi/modular
70 | ```
71 |
72 | Laravel will auto-discover the package and everything will be automatically set up for you.
73 |
74 | ### Publish the config
75 |
76 | While not required, it's highly recommended that you customize your default namespace
77 | for modules. By default, this is set to `Modules\`, which works just fine but makes it
78 | harder to extract your module to a separate package should you ever choose to.
79 |
80 | We recommend configuring a organization namespace (we use `"InterNACHI"`, for example).
81 | To do this, you'll need to publish the package config:
82 |
83 | ```shell script
84 | php artisan vendor:publish --tag=modular-config
85 | ```
86 |
87 | ### Create a module
88 |
89 | Next, let's create a module:
90 |
91 | ```shell script
92 | php artisan make:module my-module
93 | ```
94 |
95 | Modular will scaffold up a new module for you:
96 |
97 | ```
98 | app-modules/
99 | my-module/
100 | composer.json
101 | src/
102 | tests/
103 | routes/
104 | resources/
105 | database/
106 | ```
107 |
108 | It will also add two new entries to your app's `composer.json` file. The first entry registers
109 | `./app-modules/my-module/` as a [path repository](https://getcomposer.org/doc/05-repositories.md#path),
110 | and the second requires `modules/my-module:*` (like any other Composer dependency).
111 |
112 | Modular will then remind you to perform a Composer update, so let's do that now:
113 |
114 | ```shell script
115 | composer update modules/my-module
116 | ```
117 |
118 | ### Optional: Config synchronization
119 |
120 | You can run the sync command to make sure that your project is set up
121 | for module support:
122 |
123 | ```shell script
124 | php artisan modules:sync
125 | ```
126 |
127 | This will add a `Modules` test suite to your `phpunit.xml` file (if one exists)
128 | and update your [PhpStorm Laravel plugin](https://plugins.jetbrains.com/plugin/7532-laravel)
129 | configuration (if it exists) to properly find your module's views.
130 |
131 | It is safe to run this command at any time, as it will only add missing configurations.
132 | You may even want to add it to your `post-autoload-dump` scripts in your application's
133 | `composer.json` file.
134 |
135 | ## Usage
136 |
137 | All modules follow existing Laravel conventions, and auto-discovery
138 | should work as expected in most cases:
139 |
140 | - Commands are auto-registered with Artisan
141 | - Migrations will be run by the Migrator
142 | - Factories are auto-loaded for `factory()`
143 | - Policies are auto-discovered for your Models
144 | - Blade components will be auto-discovered
145 | - Event listeners will be auto-discovered
146 |
147 | ### Commands
148 |
149 | #### Package Commands
150 |
151 | We provide a few helper commands:
152 |
153 | - `php artisan make:module` — scaffold a new module
154 | - `php artisan modules:cache` — cache the loaded modules for slightly faster auto-discovery
155 | - `php artisan modules:clear` — clear the module cache
156 | - `php artisan modules:sync` — update project configs (like `phpunit.xml`) with module settings
157 | - `php artisan modules:list` — list all modules
158 |
159 | #### Laravel “`make:`” Commands
160 |
161 | We also add a `--module=` option to most Laravel `make:` commands so that you can
162 | use all the existing tooling that you know. The commands themselves are exactly the
163 | same, which means you can use your [custom stubs](https://laravel.com/docs/11.x/artisan#stub-customization)
164 | and everything else Laravel provides:
165 |
166 | - `php artisan make:cast MyModuleCast --module=[module name]`
167 | - `php artisan make:controller MyModuleController --module=[module name]`
168 | - `php artisan make:command MyModuleCommand --module=[module name]`
169 | - `php artisan make:component MyModuleComponent --module=[module name]`
170 | - `php artisan make:channel MyModuleChannel --module=[module name]`
171 | - `php artisan make:event MyModuleEvent --module=[module name]`
172 | - `php artisan make:exception MyModuleException --module=[module name]`
173 | - `php artisan make:factory MyModuleFactory --module=[module name]`
174 | - `php artisan make:job MyModuleJob --module=[module name]`
175 | - `php artisan make:listener MyModuleListener --module=[module name]`
176 | - `php artisan make:mail MyModuleMail --module=[module name]`
177 | - `php artisan make:middleware MyModuleMiddleware --module=[module name]`
178 | - `php artisan make:model MyModule --module=[module name]`
179 | - `php artisan make:notification MyModuleNotification --module=[module name]`
180 | - `php artisan make:observer MyModuleObserver --module=[module name]`
181 | - `php artisan make:policy MyModulePolicy --module=[module name]`
182 | - `php artisan make:provider MyModuleProvider --module=[module name]`
183 | - `php artisan make:request MyModuleRequest --module=[module name]`
184 | - `php artisan make:resource MyModule --module=[module name]`
185 | - `php artisan make:rule MyModuleRule --module=[module name]`
186 | - `php artisan make:seeder MyModuleSeeder --module=[module name]`
187 | - `php artisan make:test MyModuleTest --module=[module name]`
188 |
189 | #### Other Laravel Commands
190 |
191 | In addition to adding a `--module` option to most `make:` commands, we’ve also added the same
192 | option to the `db:seed` command. If you pass the `--module` option to `db:seed`, it will look
193 | for your seeder within your module namespace:
194 |
195 | - `php artisan db:seed --module=[module name]` will try to call `Modules\MyModule\Database\Seeders\DatabaseSeeder`
196 | - `php artisan db:seed --class=MySeeder --module=[module name]` will try to call `Modules\MyModule\Database\Seeders\MySeeder`
197 |
198 | #### Vendor Commands
199 |
200 | We can also add the `--module` option to commands in 3rd-party packages. The first package
201 | that we support is Livewire. If you have Livewire installed, you can run:
202 |
203 | - `php artisan make:livewire counter --module=[module name]`
204 |
205 | ### Blade Components
206 |
207 | Your [Laravel Blade components](https://laravel.com/docs/blade#components) will be
208 | automatically registered for you under a [component namespace](https://laravel.com/docs/9.x/blade#manually-registering-package-components).
209 | A few examples:
210 |
211 | | File | Component |
212 | |--------------------------------------------------------------------|--------------------------------|
213 | | `app-modules/demo/src/View/Components/Basic.php` | `` |
214 | | `app-modules/demo/src/View/Components/Nested/One.php` | `` |
215 | | `app-modules/demo/src/View/Components/Nested/Two.php` | `` |
216 | | `app-modules/demo/resources/components/anonymous.blade.php` | `` |
217 | | `app-modules/demo/resources/components/anonymous/index.blade.php` | `` |
218 | | `app-modules/demo/resources/components/anonymous/nested.blade.php` | `` |
219 |
220 | ### Translations
221 |
222 | Your [Laravel Translations](https://laravel.com/docs/11.x/localization#defining-translation-strings) will also
223 | be automatically registered under a component namespace for you. For example, if you have a translation file
224 | at:
225 |
226 | `app-modules/demo/resources/lang/en/messages.php`
227 |
228 | You could access those translations with: `__('demo::messages.welcome');`
229 |
230 | ### Customizing the Default Module Structure
231 |
232 | When you call `make:module`, Modular will scaffold some basic boilerplate for you. If you
233 | would like to customize this behavior, you can do so by publishing the `app-modules.php`
234 | config file and adding your own stubs.
235 |
236 | Both filenames and file contents support a number of placeholders. These include:
237 |
238 | - `StubBasePath`
239 | - `StubModuleNamespace`
240 | - `StubComposerNamespace`
241 | - `StubModuleNameSingular`
242 | - `StubModuleNamePlural`
243 | - `StubModuleName`
244 | - `StubClassNamePrefix`
245 | - `StubComposerName`
246 | - `StubMigrationPrefix`
247 | - `StubFullyQualifiedTestCaseBase`
248 | - `StubTestCaseBase`
249 |
250 | ## Comparison to `nwidart/laravel-modules`
251 |
252 | [Laravel Modules](https://nwidart.com/laravel-modules) is a great package that’s been
253 | around since 2016 and is used by 1000's of projects. The main reason we decided to build
254 | our own module system rather than using `laravel-modules` comes down to two decisions:
255 |
256 | 1. We wanted something that followed Laravel conventions rather than using its own
257 | directory structure/etc.
258 | 2. We wanted something that felt “lighter weight”
259 |
260 | If you are building a CMS that needs to support 3rd-party modules that can be dynamically
261 | enabled and disabled, Laravel Modules will be a better fit.
262 |
263 | On the other hand, if you're mostly interested in modules for organization, and want to
264 | stick closely to Laravel conventions, we’d highly recommend giving InterNACHI/Modular a try!
265 |
--------------------------------------------------------------------------------
/art/modular.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InterNACHI/modular/e7ff4074001d3df50d9ce877385c55d88e254484/art/modular.png
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "internachi/modular",
3 | "description": "Modularize your Laravel apps",
4 | "keywords": [
5 | "laravel",
6 | "modules",
7 | "modular",
8 | "module"
9 | ],
10 | "authors": [
11 | {
12 | "name": "Chris Morrell",
13 | "homepage": "http://www.cmorrell.com"
14 | }
15 | ],
16 | "type": "library",
17 | "license": "MIT",
18 | "require": {
19 | "php": ">=8.0",
20 | "ext-simplexml": "*",
21 | "ext-dom": "*",
22 | "composer/composer": "^2.1",
23 | "illuminate/support": "^9|^10|^11|^12|13.x-dev|dev-master|dev-main"
24 | },
25 | "require-dev": {
26 | "orchestra/testbench": "^7.52|^8.33|^9.11|^10.0|dev-master|dev-main",
27 | "friendsofphp/php-cs-fixer": "^3.14",
28 | "mockery/mockery": "^1.5",
29 | "phpunit/phpunit": "^9.5|^10.5|^11.5",
30 | "ext-json": "*",
31 | "livewire/livewire": "^2.5|^3.0"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "InterNACHI\\Modular\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "InterNACHI\\Modular\\Tests\\": "tests/"
41 | }
42 | },
43 | "scripts": {
44 | "fix-style": "vendor/bin/php-cs-fixer fix",
45 | "check-style": "vendor/bin/php-cs-fixer fix --diff --dry-run"
46 | },
47 | "extra": {
48 | "laravel": {
49 | "providers": [
50 | "InterNACHI\\Modular\\Support\\ModularServiceProvider",
51 | "InterNACHI\\Modular\\Support\\ModularizedCommandsServiceProvider",
52 | "InterNACHI\\Modular\\Support\\ModularEventServiceProvider"
53 | ],
54 | "aliases": {
55 | "Modules": "InterNACHI\\Modular\\Support\\Facades\\Modules"
56 | }
57 | }
58 | },
59 | "minimum-stability": "dev",
60 | "prefer-stable": true
61 | }
62 |
--------------------------------------------------------------------------------
/config.php:
--------------------------------------------------------------------------------
1 | 'Modules',
22 |
23 | /*
24 | |--------------------------------------------------------------------------
25 | | Composer "Vendor" Name
26 | |--------------------------------------------------------------------------
27 | |
28 | | This is the prefix used for your composer.json file. This should be the
29 | | kebab-case version of your module namespace (if left null, we will
30 | | generate the kebab-case version for you).
31 | |
32 | */
33 |
34 | 'modules_vendor' => null,
35 |
36 | /*
37 | |--------------------------------------------------------------------------
38 | | Modules Directory
39 | |--------------------------------------------------------------------------
40 | |
41 | | If you want to install modules in a custom directory, you can do so here.
42 | | Keeping the default `app-modules/` directory is highly recommended,
43 | | though, as it keeps your modules near the rest of your application code
44 | | in an alpha-sorted directory listing.
45 | |
46 | */
47 |
48 | 'modules_directory' => 'app-modules',
49 |
50 | /*
51 | |--------------------------------------------------------------------------
52 | | Base Test Case
53 | |--------------------------------------------------------------------------
54 | |
55 | | This is the base TestCase class name that auto-generated Tests should
56 | | extend. By default it assumes the default \Tests\TestCase exists.
57 | |
58 | */
59 |
60 | 'tests_base' => 'Tests\TestCase',
61 |
62 | /*
63 | |--------------------------------------------------------------------------
64 | | Custom Stubs
65 | |--------------------------------------------------------------------------
66 | |
67 | | If you would like to use your own custom stubs for new modules, you can
68 | | configure those here. This should be an array where the key is the path
69 | | relative to the module and the value is the absolute path to the stub
70 | | stub file. Destination paths and contents support placeholders. See the
71 | | README.md file for more information.
72 | |
73 | | For example:
74 | |
75 | | 'stubs' => [
76 | | 'src/Providers/StubClassNamePrefixServiceProvider.php' => base_path('stubs/app-modules/ServiceProvider.php'),
77 | | ],
78 | */
79 |
80 | 'stubs' => null,
81 |
82 | /*
83 | |--------------------------------------------------------------------------
84 | | Custom override of event discovery
85 | |--------------------------------------------------------------------------
86 | |
87 | | This is a custom override of the event discovery feature. If you want to
88 | | disable event discovery, set this to false. If you want to enable event
89 | | discovery, set this to true. We will still check the app namespace for
90 | | the presence of event discovery.
91 | */
92 |
93 | 'should_discover_events' => null,
94 | ];
95 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | ./src
11 |
12 |
13 |
14 |
15 | ./tests
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Console/Commands/Database/SeedCommand.php:
--------------------------------------------------------------------------------
1 | module()) {
15 | $default = $this->getDefinition()->getOption('class')->getDefault();
16 | $class = $this->input->getOption('class');
17 |
18 | if ($class === $default) {
19 | $class = $module->qualify($default);
20 | } elseif (! Str::contains($class, 'Database\\Seeders')) {
21 | $class = $module->qualify("Database\\Seeders\\{$class}");
22 | }
23 |
24 | return $this->laravel->make($class)
25 | ->setContainer($this->laravel)
26 | ->setCommand($this);
27 | }
28 |
29 | return parent::getSeeder();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeCast.php:
--------------------------------------------------------------------------------
1 | module();
15 |
16 | $stub = parent::replaceClass($stub, $name);
17 |
18 | if ($module) {
19 | $cli_name = Str::of($name)->classBasename()->kebab();
20 |
21 | $find = [
22 | '{{command}}',
23 | '{{ command }}',
24 | 'dummy:command',
25 | 'command:name',
26 | "app:{$cli_name}",
27 | ];
28 |
29 | $stub = str_replace($find, "{$module->name}:{$cli_name}", $stub);
30 | }
31 |
32 | return $stub;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeComponent.php:
--------------------------------------------------------------------------------
1 | module()) {
14 | return $module->path("resources/views/{$path}");
15 | }
16 |
17 | return parent::viewPath($path);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeController.php:
--------------------------------------------------------------------------------
1 | module()) {
16 | return parent::parseModel($model);
17 | }
18 |
19 | if (preg_match('([^A-Za-z0-9_/\\\\])', $model)) {
20 | throw new InvalidArgumentException('Model name contains invalid characters.');
21 | }
22 |
23 | $model = trim(str_replace('/', '\\', $model), '\\');
24 |
25 | if (! Str::startsWith($model, $namespace = $module->namespaces->first())) {
26 | $model = $namespace.$model;
27 | }
28 |
29 | return $model;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeEvent.php:
--------------------------------------------------------------------------------
1 | module()) {
15 | $model = $this->option('model')
16 | ? $this->qualifyModel($this->option('model'))
17 | : $this->qualifyModel($this->guessModelName($name));
18 |
19 | $models_namespace = $module->qualify('Models');
20 |
21 | if (Str::startsWith($model, "{$models_namespace}\\")) {
22 | $extra_namespace = trim(Str::after(Str::beforeLast($model, '\\'), $models_namespace), '\\');
23 | $namespace = rtrim($module->qualify("Database\\Factories\\{$extra_namespace}"), '\\');
24 | } else {
25 | $namespace = $module->qualify('Database\\Factories');
26 | }
27 |
28 | $replacements = [
29 | '{{ factoryNamespace }}' => $namespace,
30 | '{{factoryNamespace}}' => $namespace,
31 | 'namespace Database\Factories;' => "namespace {$namespace};", // Early Laravel 8 didn't use a placeholder
32 | ];
33 |
34 | $stub = str_replace(array_keys($replacements), array_values($replacements), $stub);
35 | }
36 |
37 | return parent::replaceNamespace($stub, $name);
38 | }
39 |
40 | protected function guessModelName($name)
41 | {
42 | if ($module = $this->module()) {
43 | if (Str::endsWith($name, 'Factory')) {
44 | $name = substr($name, 0, -7);
45 | }
46 |
47 | $modelName = $this->qualifyModel($name);
48 | if (class_exists($modelName)) {
49 | return $modelName;
50 | }
51 |
52 | return $module->qualify('Models\\Model');
53 | }
54 |
55 | return parent::guessModelName($name);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeJob.php:
--------------------------------------------------------------------------------
1 | option('event');
16 |
17 | if (Modules::moduleForClass($name)) {
18 | $stub = str_replace(
19 | ['DummyEvent', '{{ event }}'],
20 | class_basename($event),
21 | GeneratorCommand::buildClass($name)
22 | );
23 |
24 | return str_replace(
25 | ['DummyFullEvent', '{{ eventNamespace }}'],
26 | trim($event, '\\'),
27 | $stub
28 | );
29 | }
30 |
31 | return parent::buildClass($name);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeLivewire.php:
--------------------------------------------------------------------------------
1 | module()) {
26 | Config::set('livewire.class_namespace', $module->qualify('Http\\Livewire'));
27 | Config::set('livewire.view_path', $module->path('resources/views/livewire'));
28 |
29 | $app = $this->getLaravel();
30 |
31 | $defaultManifestPath = $app['livewire']->isRunningServerless()
32 | ? '/tmp/storage/bootstrap/cache/livewire-components.php'
33 | : $app->bootstrapPath('cache/livewire-components.php');
34 |
35 | $componentsFinder = new LivewireComponentsFinder(
36 | new Filesystem(),
37 | Config::get('livewire.manifest_path') ?? $defaultManifestPath,
38 | $module->path('src/Http/Livewire')
39 | );
40 |
41 | $app->instance(LivewireComponentsFinder::class, $componentsFinder);
42 | }
43 |
44 | parent::handle();
45 | }
46 |
47 | protected function createClass($force = false, $inline = false)
48 | {
49 | if ($module = $this->module()) {
50 | $name = Str::of($this->argument('name'))
51 | ->split('/[.\/(\\\\)]+/')
52 | ->map([Str::class, 'studly'])
53 | ->join(DIRECTORY_SEPARATOR);
54 |
55 | $classPath = $module->path('src/Http/Livewire/'.$name.'.php');
56 |
57 | if (File::exists($classPath) && ! $force) {
58 | $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
59 | $this->line("Class already exists:> {$this->parser->relativeClassPath()}");
60 |
61 | return false;
62 | }
63 |
64 | $this->ensureDirectoryExists($classPath);
65 |
66 | File::put($classPath, $this->parser->classContents($inline));
67 |
68 | $component_name = Str::of($name)
69 | ->explode('/')
70 | ->filter()
71 | ->map([Str::class, 'kebab'])
72 | ->implode('.');
73 |
74 | $fully_qualified_component = Str::of($this->argument('name'))
75 | ->prepend('Http/Livewire/')
76 | ->split('/[.\/(\\\\)]+/')
77 | ->map([Str::class, 'studly'])
78 | ->join('\\');
79 |
80 | Livewire::component("{$module->name}::{$component_name}", $module->qualify($fully_qualified_component));
81 |
82 | return $classPath;
83 | }
84 |
85 | return parent::createClass($force, $inline);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeMail.php:
--------------------------------------------------------------------------------
1 | module()) {
17 | $app_directory = $this->laravel->databasePath('migrations');
18 | $module_directory = $module->path('database/migrations');
19 |
20 | $path = str_replace($app_directory, $module_directory, $path);
21 |
22 | $filesystem = $this->getLaravel()->make(Filesystem::class);
23 | if (! $filesystem->isDirectory($module_directory)) {
24 | $filesystem->makeDirectory($module_directory, 0755, true);
25 | }
26 | }
27 |
28 | return $path;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeModel.php:
--------------------------------------------------------------------------------
1 | module()) {
14 | $rootNamespace = rtrim($module->namespaces->first(), '\\');
15 | }
16 |
17 | return $rootNamespace.'\Models';
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeModule.php:
--------------------------------------------------------------------------------
1 | filesystem = $filesystem;
82 | $this->module_registry = $module_registry;
83 | }
84 |
85 | public function handle()
86 | {
87 | $this->module_name = Str::kebab($this->argument('name'));
88 | $this->class_name_prefix = Str::studly($this->argument('name'));
89 | $this->module_namespace = config('app-modules.modules_namespace', 'Modules');
90 | $this->composer_namespace = config('app-modules.modules_vendor') ?? Str::kebab($this->module_namespace);
91 | $this->composer_name = "{$this->composer_namespace}/{$this->module_name}";
92 | $this->base_path = $this->module_registry->getModulesPath().'/'.$this->module_name;
93 |
94 | $this->setUpStyles();
95 |
96 | $this->newLine();
97 |
98 | if ($this->shouldAbortToPublishConfig()) {
99 | return 0;
100 | }
101 |
102 | $this->ensureModulesDirectoryExists();
103 |
104 | $this->writeStubs();
105 | $this->updateCoreComposerConfig();
106 |
107 | $this->call(ModulesClear::class);
108 |
109 | $this->newLine();
110 | $this->line("Please run composer update {$this->composer_name}");
111 | $this->newLine();
112 |
113 | $this->module_registry->reload();
114 |
115 | return 0;
116 | }
117 |
118 | protected function shouldAbortToPublishConfig(): bool
119 | {
120 | if (
121 | 'Modules' !== $this->module_namespace
122 | || true === $this->option('accept-default-namespace')
123 | || $this->module_registry->modules()->isNotEmpty()
124 | ) {
125 | return false;
126 | }
127 |
128 | $this->title('Welcome');
129 |
130 | $message = "You're about to create your first module in the {$this->module_namespace} "
131 | .'namespace. This is the default namespace, and will work for many use-cases. However, '
132 | .'if you ever choose to extract a module into its own package, you will '
133 | ."likely want to use a custom namespace (like your organization name).\n\n"
134 | .'If you would like to use a custom namespace, please publish the config '
135 | ."and customize it first. You can do this by calling:\n\n"
136 | .'php artisan vendor:publish --tag=modular-config';
137 |
138 | $width = min((new Terminal())->getWidth(), 100) - 1;
139 | $messages = explode(PHP_EOL, wordwrap($message, $width, PHP_EOL));
140 | foreach ($messages as $message) {
141 | $this->line(" {$message}");
142 | }
143 |
144 | return $this->confirm('Would you like to cancel and configure your module namespace first?', true);
145 | }
146 |
147 | protected function ensureModulesDirectoryExists()
148 | {
149 | if (! $this->filesystem->isDirectory($this->base_path)) {
150 | $this->filesystem->makeDirectory($this->base_path, 0777, true);
151 | $this->line(" - Created {$this->base_path}");
152 | }
153 | }
154 |
155 | protected function writeStubs()
156 | {
157 | $this->title('Creating initial module files');
158 |
159 | $tests_base = config('app-modules.tests_base', 'Tests\TestCase');
160 |
161 | $placeholders = [
162 | 'StubBasePath' => $this->base_path,
163 | 'StubModuleNamespace' => $this->module_namespace,
164 | 'StubComposerNamespace' => $this->composer_namespace,
165 | 'StubModuleNameSingular' => Str::singular($this->module_name),
166 | 'StubModuleNamePlural' => Str::plural($this->module_name),
167 | 'StubModuleName' => $this->module_name,
168 | 'StubClassNamePrefix' => $this->class_name_prefix,
169 | 'StubComposerName' => $this->composer_name,
170 | 'StubMigrationPrefix' => date('Y_m_d_His'),
171 | 'StubFullyQualifiedTestCaseBase' => $tests_base,
172 | 'StubTestCaseBase' => class_basename($tests_base),
173 | ];
174 |
175 | $search = array_keys($placeholders);
176 | $replace = array_values($placeholders);
177 |
178 | foreach ($this->getStubs() as $destination => $stub_file) {
179 | $contents = file_get_contents($stub_file);
180 | $destination = str_replace($search, $replace, $destination);
181 | $filename = "{$this->base_path}/{$destination}";
182 |
183 | $output = str_replace($search, $replace, $contents);
184 |
185 | if ($this->filesystem->exists($filename)) {
186 | $this->line(" - Skipping {$destination} (already exists)");
187 | continue;
188 | }
189 |
190 | $this->filesystem->ensureDirectoryExists($this->filesystem->dirname($filename));
191 | $this->filesystem->put($filename, $output);
192 |
193 | $this->line(" - Wrote to {$destination}");
194 | }
195 |
196 | $this->newLine();
197 | }
198 |
199 | protected function seedersDirectory(): string
200 | {
201 | return version_compare($this->getLaravel()->version(), '8.0.0', '>=')
202 | ? 'seeders'
203 | : 'seeds';
204 | }
205 |
206 | protected function updateCoreComposerConfig()
207 | {
208 | $this->title('Updating application composer.json file');
209 |
210 | // We're going to move into the Laravel base directory while
211 | // we're updating the composer file so that we're sure we update
212 | // the correct composer.json file (we'll restore CWD at the end)
213 | $original_working_dir = getcwd();
214 | chdir($this->laravel->basePath());
215 |
216 | $json_file = new JsonFile(Factory::getComposerFile());
217 | $definition = $json_file->read();
218 |
219 | if (! isset($definition['repositories'])) {
220 | $definition['repositories'] = [];
221 | }
222 |
223 | if (! isset($definition['require'])) {
224 | $definition['require'] = [];
225 | }
226 |
227 | $module_config = [
228 | 'type' => 'path',
229 | 'url' => str_replace('\\', '/', config('app-modules.modules_directory', 'app-modules')).'/*',
230 | 'options' => [
231 | 'symlink' => true,
232 | ],
233 | ];
234 |
235 | $has_changes = false;
236 |
237 | $repository_already_exists = collect($definition['repositories'])
238 | ->contains(function($repository) use ($module_config) {
239 | return $repository['url'] === $module_config['url'];
240 | });
241 |
242 | if (false === $repository_already_exists) {
243 | $this->line(" - Adding path repository for {$module_config['url']}");
244 | $has_changes = true;
245 |
246 | if (Arr::isAssoc($definition['repositories'])) {
247 | $definition['repositories'][$this->module_name] = $module_config;
248 | } else {
249 | $definition['repositories'][] = $module_config;
250 | }
251 | }
252 |
253 | if (! isset($definition['require'][$this->composer_name])) {
254 | $this->line(" - Adding require statement for {$this->composer_name}:*");
255 | $has_changes = true;
256 |
257 | $definition['require']["{$this->composer_namespace}/{$this->module_name}"] = '*';
258 | $definition['require'] = $this->sortComposerPackages($definition['require']);
259 | }
260 |
261 | if ($has_changes) {
262 | $json_file->write($definition);
263 | $this->line(" - Wrote to {$json_file->getPath()}");
264 | } else {
265 | $this->line(' - Nothing to update (repository & require entry already exist)');
266 | }
267 |
268 | chdir($original_working_dir);
269 |
270 | $this->newLine();
271 | }
272 |
273 | protected function sortComposerPackages(array $packages): array
274 | {
275 | $prefix = function($requirement) {
276 | return preg_replace(
277 | [
278 | '/^php$/',
279 | '/^hhvm-/',
280 | '/^ext-/',
281 | '/^lib-/',
282 | '/^\D/',
283 | '/^(?!php$|hhvm-|ext-|lib-)/',
284 | ],
285 | [
286 | '0-$0',
287 | '1-$0',
288 | '2-$0',
289 | '3-$0',
290 | '4-$0',
291 | '5-$0',
292 | ],
293 | $requirement
294 | );
295 | };
296 |
297 | uksort($packages, function($a, $b) use ($prefix) {
298 | return strnatcmp($prefix($a), $prefix($b));
299 | });
300 |
301 | return $packages;
302 | }
303 |
304 | protected function setUpStyles()
305 | {
306 | $formatter = $this->getOutput()->getFormatter();
307 |
308 | if (! $formatter->hasStyle('kbd')) {
309 | $formatter->setStyle('kbd', new OutputFormatterStyle('cyan'));
310 | }
311 | }
312 |
313 | protected function title($title)
314 | {
315 | $this->getOutput()->title($title);
316 | }
317 |
318 | public function newLine($count = 1)
319 | {
320 | $this->getOutput()->newLine($count);
321 | }
322 |
323 | protected function getStubs(): array
324 | {
325 | if (is_array($custom_stubs = config('app-modules.stubs'))) {
326 | return $custom_stubs;
327 | }
328 |
329 | $composer_stub = version_compare($this->getLaravel()->version(), '8.0.0', '<')
330 | ? 'composer-stub-v7.json'
331 | : 'composer-stub-latest.json';
332 |
333 | return [
334 | 'composer.json' => $this->pathToStub($composer_stub),
335 | 'src/Providers/StubClassNamePrefixServiceProvider.php' => $this->pathToStub('ServiceProvider.php'),
336 | 'tests/Feature/Providers/StubClassNamePrefixServiceProviderTest.php' => $this->pathToStub('ServiceProviderTest.php'),
337 | 'database/migrations/StubMigrationPrefix_set_up_StubModuleName_module.php' => $this->pathToStub('migration.php'),
338 | 'routes/StubModuleName-routes.php' => $this->pathToStub('web-routes.php'),
339 | 'resources/views/index.blade.php' => $this->pathToStub('view.blade.php'),
340 | 'resources/views/create.blade.php' => $this->pathToStub('view.blade.php'),
341 | 'resources/views/show.blade.php' => $this->pathToStub('view.blade.php'),
342 | 'resources/views/edit.blade.php' => $this->pathToStub('view.blade.php'),
343 | 'database/factories/.gitkeep' => $this->pathToStub('.gitkeep'),
344 | 'database/migrations/.gitkeep' => $this->pathToStub('.gitkeep'),
345 | 'database/'.$this->seedersDirectory().'/.gitkeep' => $this->pathToStub('.gitkeep'),
346 | ];
347 | }
348 |
349 | protected function pathToStub($filename): string
350 | {
351 | return str_replace('\\', '/', dirname(__DIR__, 4))."/stubs/{$filename}";
352 | }
353 | }
354 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeNotification.php:
--------------------------------------------------------------------------------
1 | module()) {
17 | $name = Str::replaceFirst($module->qualify('Database\\Seeders\\'), '', $name);
18 | return $this->getModularPath($name);
19 | }
20 |
21 | return parent::getPath($name);
22 | }
23 |
24 | protected function replaceNamespace(&$stub, $name)
25 | {
26 | if ($module = $this->module()) {
27 | if (version_compare($this->getLaravel()->version(), '9.6.0', '<')) {
28 | $namespace = $module->qualify('Database\Seeders');
29 | $stub = str_replace('namespace Database\Seeders;', "namespace {$namespace};", $stub);
30 | }
31 | }
32 |
33 | return parent::replaceNamespace($stub, $name);
34 | }
35 |
36 | protected function rootNamespace()
37 | {
38 | if ($module = $this->module()) {
39 | if (version_compare($this->getLaravel()->version(), '9.6.0', '>=')) {
40 | return $module->qualify('Database\Seeders');
41 | }
42 | }
43 |
44 | return parent::rootNamespace();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/MakeTest.php:
--------------------------------------------------------------------------------
1 | module()) {
17 | $name = '\\'.Str::replaceFirst($module->namespaces->first(), '', $name);
18 | return $this->getModularPath($name);
19 | }
20 |
21 | return parent::getPath($name);
22 | }
23 |
24 | protected function rootNamespace()
25 | {
26 | if ($module = $this->module()) {
27 | return $module->namespaces->first().'Tests';
28 | }
29 |
30 | return 'Tests';
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Console/Commands/Make/Modularize.php:
--------------------------------------------------------------------------------
1 | module();
15 |
16 | if ($module && false === strpos($rootNamespace, $module->namespaces->first())) {
17 | $find = rtrim($rootNamespace, '\\');
18 | $replace = rtrim($module->namespaces->first(), '\\');
19 | $namespace = str_replace($find, $replace, $namespace);
20 | }
21 |
22 | return $namespace;
23 | }
24 |
25 | protected function qualifyClass($name)
26 | {
27 | $name = ltrim($name, '\\/');
28 |
29 | if ($module = $this->module()) {
30 | if (Str::startsWith($name, $module->namespaces->first())) {
31 | return $name;
32 | }
33 | }
34 |
35 | return parent::qualifyClass($name);
36 | }
37 |
38 | protected function qualifyModel(string $model)
39 | {
40 | if ($module = $this->module()) {
41 | $model = str_replace('/', '\\', ltrim($model, '\\/'));
42 |
43 | if (Str::startsWith($model, $module->namespace())) {
44 | return $model;
45 | }
46 |
47 | return $module->qualify('Models\\'.$model);
48 | }
49 |
50 | return parent::qualifyModel($model);
51 | }
52 |
53 | protected function getPath($name)
54 | {
55 | if ($module = $this->module()) {
56 | $name = Str::replaceFirst($module->namespaces->first(), '', $name);
57 | }
58 |
59 | $path = parent::getPath($name);
60 |
61 | if ($module) {
62 | // Set up our replacements as a [find -> replace] array
63 | $replacements = [
64 | $this->laravel->path() => $module->namespaces->keys()->first(),
65 | $this->laravel->basePath('tests/Tests') => $module->path('tests'),
66 | $this->laravel->databasePath() => $module->path('database'),
67 | ];
68 |
69 | // Normalize all our paths for compatibility's sake
70 | $normalize = function($path) {
71 | return rtrim($path, '/').'/';
72 | };
73 |
74 | $find = array_map($normalize, array_keys($replacements));
75 | $replace = array_map($normalize, array_values($replacements));
76 |
77 | // And finally apply the replacements
78 | $path = str_replace($find, $replace, $path);
79 | }
80 |
81 | return $path;
82 | }
83 |
84 | public function call($command, array $arguments = [])
85 | {
86 | // Pass the --module flag on to subsequent commands
87 | if ($module = $this->option('module')) {
88 | $arguments['--module'] = $module;
89 | }
90 |
91 | return $this->runCommand($command, $arguments, $this->output);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Console/Commands/Modularize.php:
--------------------------------------------------------------------------------
1 | option('module')) {
15 | $registry = $this->getLaravel()->make(ModuleRegistry::class);
16 |
17 | if ($module = $registry->module($name)) {
18 | return $module;
19 | }
20 |
21 | throw new InvalidOptionException(sprintf('The "%s" module does not exist.', $name));
22 | }
23 |
24 | return null;
25 | }
26 |
27 | protected function configure()
28 | {
29 | parent::configure();
30 |
31 | $this->getDefinition()->addOption(
32 | new InputOption(
33 | '--module',
34 | null,
35 | InputOption::VALUE_REQUIRED,
36 | 'Run inside an application module'
37 | )
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Console/Commands/ModulesCache.php:
--------------------------------------------------------------------------------
1 | call(ModulesClear::class);
21 |
22 | $export = $registry->modules()
23 | ->map(function(ModuleConfig $module_config) {
24 | return $module_config->toArray();
25 | })
26 | ->toArray();
27 |
28 | $cache_path = $registry->getCachePath();
29 | $cache_contents = 'put($cache_path, $cache_contents);
32 |
33 | try {
34 | require $cache_path;
35 | } catch (Throwable $e) {
36 | $filesystem->delete($cache_path);
37 | throw new LogicException('Unable to cache module configuration.', 0, $e);
38 | }
39 |
40 | $this->info('Modules cached successfully!');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Console/Commands/ModulesClear.php:
--------------------------------------------------------------------------------
1 | delete($registry->getCachePath());
18 | $this->info('Module cache cleared!');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Console/Commands/ModulesList.php:
--------------------------------------------------------------------------------
1 | modules()
21 | ->map(function(ModuleConfig $config) use (&$namespace_title) {
22 | $namespaces = $config->namespaces->map(function($namespace) {
23 | return rtrim($namespace, '\\');
24 | });
25 |
26 | if ($config->namespaces->count() > 1) {
27 | $namespace_title = 'Namespaces';
28 | }
29 |
30 | return [
31 | $config->name,
32 | Str::after(str_replace('\\', '/', $config->base_path), str_replace('\\', '/', $this->laravel->basePath()).'/'),
33 | $namespaces->implode(', '),
34 | ];
35 | })
36 | ->toArray();
37 |
38 | $count = $registry->modules()->count();
39 | $this->line('You have '.$count.' '.Str::plural('module', $count).' installed.');
40 | $this->line('');
41 |
42 | $this->table(['Module', 'Path', $namespace_title], $table);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Console/Commands/ModulesSync.php:
--------------------------------------------------------------------------------
1 | filesystem = $filesystem;
34 | $this->registry = $registry;
35 |
36 | $this->updatePhpUnit();
37 |
38 | if (true !== $this->option('no-phpstorm')) {
39 | $this->updatePhpStormConfig();
40 | }
41 | }
42 |
43 | protected function updatePhpUnit(): void
44 | {
45 | $config_path = $this->getLaravel()->basePath('phpunit.xml');
46 |
47 | if (! $this->filesystem->exists($config_path)) {
48 | $this->warn('No phpunit.xml file found. Skipping PHPUnit configuration.');
49 | return;
50 | }
51 |
52 | $modules_directory = config('app-modules.modules_directory', 'app-modules');
53 |
54 | $config = simplexml_load_string($this->filesystem->get($config_path));
55 |
56 | $existing_nodes = $config->xpath("//phpunit//testsuites//testsuite//directory[text()='./{$modules_directory}/*/tests']");
57 |
58 | if (count($existing_nodes)) {
59 | $this->info('Modules test suite already exists in phpunit.xml');
60 | return;
61 | }
62 |
63 | $testsuites = $config->xpath('//phpunit//testsuites');
64 | if (! count($testsuites)) {
65 | $this->error('Cannot find node in phpunit.xml file. Skipping PHPUnit configuration.');
66 | return;
67 | }
68 |
69 | $testsuite = $testsuites[0]->addChild('testsuite');
70 | $testsuite->addAttribute('name', 'Modules');
71 |
72 | $directory = $testsuite->addChild('directory');
73 | $directory->addAttribute('suffix', 'Test.php');
74 | $directory[0] = "./{$modules_directory}/*/tests";
75 |
76 | $this->filesystem->put($config_path, $config->asXML());
77 | $this->info('Added "Modules" PHPUnit test suite.');
78 | }
79 |
80 | protected function updatePhpStormConfig(): void
81 | {
82 | $this->updatePhpStormLaravelPlugin();
83 | $this->updatePhpStormPhpConfig();
84 | $this->updatePhpStormWorkspaceConfig();
85 | $this->updatePhpStormProjectIml();
86 | }
87 |
88 | protected function updatePhpStormLaravelPlugin(): void
89 | {
90 | $config_path = $this->getLaravel()->basePath('.idea/laravel-plugin.xml');
91 | $writer = new LaravelConfigWriter($config_path, $this->registry);
92 |
93 | if ($writer->handle()) {
94 | $this->info('Updated PhpStorm/Laravel Plugin config file...');
95 | } else {
96 | $this->info('Did not find/update PhpStorm/Laravel Plugin config.');
97 | if ($this->getOutput()->isVerbose()) {
98 | $this->warn($writer->last_error);
99 | }
100 | }
101 | }
102 |
103 | protected function updatePhpStormPhpConfig(): void
104 | {
105 | $config_path = $this->getLaravel()->basePath('.idea/php.xml');
106 | $writer = new PhpFrameworkWriter($config_path, $this->registry);
107 |
108 | if ($writer->handle()) {
109 | $this->info('Updated PhpStorm PHP config file...');
110 | } else {
111 | $this->info('Did not find/update PhpStorm PHP config.');
112 | if ($this->getOutput()->isVerbose()) {
113 | $this->warn($writer->last_error);
114 | }
115 | }
116 | }
117 |
118 | protected function updatePhpStormWorkspaceConfig(): void
119 | {
120 | $config_path = $this->getLaravel()->basePath('.idea/workspace.xml');
121 | $writer = new WorkspaceWriter($config_path, $this->registry);
122 |
123 | if ($writer->handle()) {
124 | $this->info('Updated PhpStorm workspace library roots...');
125 | } else {
126 | $this->info('Did not find/update PhpStorm workspace config.');
127 | if ($this->getOutput()->isVerbose()) {
128 | $this->warn($writer->last_error);
129 | }
130 | }
131 | }
132 |
133 | protected function updatePhpStormProjectIml(): void
134 | {
135 | $idea_directory = $this->getLaravel()->basePath('.idea/');
136 | if (! $this->filesystem->isDirectory($idea_directory)) {
137 | return;
138 | }
139 |
140 | FinderCollection::forFiles()
141 | ->in($idea_directory)
142 | ->name('*.iml')
143 | ->first(function(SplFileInfo $file) {
144 | $config_path = $file->getPathname();
145 | $writer = new ProjectImlWriter($config_path, $this->registry);
146 |
147 | if ($writer->handle()) {
148 | $this->info("Updated PhpStorm project source folders in '{$file->getBasename()}'");
149 | return true;
150 | }
151 |
152 | $this->info("Could not update PhpStorm project source folders in '{$file->getBasename()}'");
153 |
154 | if ($this->getOutput()->isVerbose()) {
155 | $this->warn($writer->last_error);
156 | }
157 |
158 | return false;
159 | });
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Exceptions/CannotFindModuleForPathException.php:
--------------------------------------------------------------------------------
1 | base_path = $module_registry->getModulesPath();
16 | }
17 |
18 | public function commandFileFinder(): FinderCollection
19 | {
20 | return FinderCollection::forFiles()
21 | ->name('*.php')
22 | ->inOrEmpty($this->base_path.'/*/src/Console/Commands');
23 | }
24 |
25 | public function factoryDirectoryFinder(): FinderCollection
26 | {
27 | return FinderCollection::forDirectories()
28 | ->depth(0)
29 | ->name('factories')
30 | ->inOrEmpty($this->base_path.'/*/database/');
31 | }
32 |
33 | public function migrationDirectoryFinder(): FinderCollection
34 | {
35 | return FinderCollection::forDirectories()
36 | ->depth(0)
37 | ->name('migrations')
38 | ->inOrEmpty($this->base_path.'/*/database/');
39 | }
40 |
41 | public function modelFileFinder(): FinderCollection
42 | {
43 | return FinderCollection::forFiles()
44 | ->name('*.php')
45 | ->inOrEmpty($this->base_path.'/*/src/Models');
46 | }
47 |
48 | public function bladeComponentFileFinder(): FinderCollection
49 | {
50 | return FinderCollection::forFiles()
51 | ->name('*.php')
52 | ->inOrEmpty($this->base_path.'/*/src/View/Components');
53 | }
54 |
55 | public function bladeComponentDirectoryFinder(): FinderCollection
56 | {
57 | return FinderCollection::forDirectories()
58 | ->name('Components')
59 | ->inOrEmpty($this->base_path.'/*/src/View');
60 | }
61 |
62 | public function routeFileFinder(): FinderCollection
63 | {
64 | return FinderCollection::forFiles()
65 | ->depth(0)
66 | ->name('*.php')
67 | ->sortByName()
68 | ->inOrEmpty($this->base_path.'/*/routes');
69 | }
70 |
71 | public function viewDirectoryFinder(): FinderCollection
72 | {
73 | return FinderCollection::forDirectories()
74 | ->depth(0)
75 | ->name('views')
76 | ->inOrEmpty($this->base_path.'/*/resources/');
77 | }
78 |
79 | public function langDirectoryFinder(): FinderCollection
80 | {
81 | return FinderCollection::forDirectories()
82 | ->depth(0)
83 | ->name('lang')
84 | ->inOrEmpty($this->base_path.'/*/resources/');
85 | }
86 |
87 | public function listenerDirectoryFinder(): FinderCollection
88 | {
89 | return FinderCollection::forDirectories()
90 | ->name('Listeners')
91 | ->inOrEmpty($this->base_path.'/*/src');
92 | }
93 |
94 | public function livewireComponentFileFinder(): FinderCollection
95 | {
96 | $directory = $this->base_path.'/*/src';
97 |
98 | if (str_contains(config('livewire.class_namespace'), '\\Http\\')) {
99 | $directory .= '/Http';
100 | }
101 |
102 | $directory .= '/Livewire';
103 |
104 | return FinderCollection::forFiles()
105 | ->name('*.php')
106 | ->inOrEmpty($directory);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Support/DatabaseFactoryHelper.php:
--------------------------------------------------------------------------------
1 | =')) {
23 | Factory::flushState();
24 | } else {
25 | $this->unsetProperty(Factory::class, 'modelNameResolver');
26 | $this->unsetProperty(Factory::class, 'modelNameResolvers');
27 | $this->unsetProperty(Factory::class, 'factoryNameResolver');
28 | }
29 | }
30 |
31 | public function modelNameResolver(): Closure
32 | {
33 | return function(Factory $factory) {
34 | if ($module = $this->registry->moduleForClass(get_class($factory))) {
35 | return (string) Str::of(get_class($factory))
36 | ->replaceFirst($module->qualify($this->namespace()), '')
37 | ->replaceLast('Factory', '')
38 | ->prepend($module->qualify('Models'), '\\');
39 | }
40 |
41 | // Temporarily disable the modular resolver if we're not in a module
42 | try {
43 | $this->unsetProperty(Factory::class, 'modelNameResolver');
44 | $this->unsetProperty(Factory::class, 'modelNameResolvers');
45 | return $factory->modelName();
46 | } finally {
47 | Factory::guessModelNamesUsing($this->modelNameResolver());
48 | }
49 | };
50 | }
51 |
52 | public function factoryNameResolver(): Closure
53 | {
54 | return function($model_name) {
55 | if ($module = $this->registry->moduleForClass($model_name)) {
56 | $model_name = Str::startsWith($model_name, $module->qualify('Models\\'))
57 | ? Str::after($model_name, $module->qualify('Models\\'))
58 | : Str::after($model_name, $module->namespace());
59 |
60 | return $module->qualify($this->namespace().$model_name.'Factory');
61 | }
62 |
63 | // Temporarily disable the modular resolver if we're not in a module
64 | try {
65 | $this->unsetProperty(Factory::class, 'factoryNameResolver');
66 | return Factory::resolveFactoryName($model_name);
67 | } finally {
68 | Factory::guessFactoryNamesUsing($this->factoryNameResolver());
69 | }
70 | };
71 | }
72 |
73 | /**
74 | * Because Factory::$namespace is protected, we need to access it via reflection.
75 | */
76 | public function namespace(): string
77 | {
78 | return $this->namespace ??= $this->getProperty(Factory::class, 'namespace');
79 | }
80 |
81 | protected function getProperty($target, $property)
82 | {
83 | $reflection = new ReflectionClass($target);
84 | return $reflection->getStaticPropertyValue($property);
85 | }
86 |
87 | protected function unsetProperty($target, $property): void
88 | {
89 | $reflection = new ReflectionClass($target);
90 | if ($reflection->hasProperty($property)) {
91 | $reflection->setStaticPropertyValue($property, $reflection->getProperty($property)->getDefaultValue());
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Support/DiscoverEvents.php:
--------------------------------------------------------------------------------
1 | getRealPath())) {
13 | return $module->pathToFullyQualifiedClassName($file->getPathname());
14 | }
15 |
16 | return parent::classFromFile($file, $basePath);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Support/Facades/Modules.php:
--------------------------------------------------------------------------------
1 | files());
23 | }
24 |
25 | public static function forDirectories(): self
26 | {
27 | return new static(Finder::create()->directories());
28 | }
29 |
30 | public function __construct(
31 | protected ?Finder $finder = null,
32 | protected ?LazyCollection $collection = null,
33 | ) {
34 | if (! $this->finder && ! $this->collection) {
35 | $this->collection = new LazyCollection();
36 | }
37 | }
38 |
39 | public function inOrEmpty(string|array $dirs): static
40 | {
41 | try {
42 | return $this->in($dirs);
43 | } catch (DirectoryNotFoundException) {
44 | return new static();
45 | }
46 | }
47 |
48 | public function __call($name, $arguments)
49 | {
50 | $result = $this->forwardCallTo($this->forwardCallTargetForMethod($name), $name, $arguments);
51 |
52 | if ($result instanceof Finder) {
53 | return new static($result);
54 | }
55 |
56 | if ($result instanceof LazyCollection) {
57 | return new static($this->finder, $result);
58 | }
59 |
60 | return $result;
61 | }
62 |
63 | protected function forwardCallTargetForMethod(string $name): Finder|LazyCollection
64 | {
65 | if (is_callable([$this->finder, $name]) && ! in_array($name, static::PREFER_COLLECTION_METHODS)) {
66 | return $this->finder;
67 | }
68 |
69 | return $this->forwardCollection();
70 | }
71 |
72 | protected function forwardCollection(): LazyCollection
73 | {
74 | return $this->collection ??= new LazyCollection(function() {
75 | foreach ($this->finder as $key => $value) {
76 | yield $key => $value;
77 | }
78 | });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Support/ModularEventServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->booting(function() {
21 | $events = $this->getEvents();
22 | $provider = Arr::first($this->app->getProviders(EventServiceProvider::class));
23 |
24 | if (! $provider || empty($events)) {
25 | return;
26 | }
27 |
28 | $listen = new ReflectionProperty($provider, 'listen');
29 | $listen->setAccessible(true);
30 | $listen->setValue($provider, array_merge_recursive($listen->getValue($provider), $events));
31 | });
32 | }
33 |
34 | public function getEvents(): array
35 | {
36 | // If events are cached, or Modular event discovery is disabled, then we'll
37 | // just let the normal event service provider handle all the event loading.
38 | if ($this->app->eventsAreCached() || ! $this->shouldDiscoverEvents()) {
39 | return [];
40 | }
41 |
42 | return $this->discoverEvents();
43 | }
44 |
45 | public function shouldDiscoverEvents(): bool
46 | {
47 | return config('app-modules.should_discover_events')
48 | ?? $this->appIsConfiguredToDiscoverEvents();
49 | }
50 |
51 | public function discoverEvents()
52 | {
53 | $modules = $this->app->make(ModuleRegistry::class);
54 |
55 | return $this->app->make(AutoDiscoveryHelper::class)
56 | ->listenerDirectoryFinder()
57 | ->map(fn(SplFileInfo $directory) => $directory->getPathname())
58 | ->reduce(function($discovered, string $directory) use ($modules) {
59 | $module = $modules->moduleForPath($directory);
60 | return array_merge_recursive(
61 | $discovered,
62 | DiscoverEvents::within($directory, $module->path('src'))
63 | );
64 | }, []);
65 | }
66 |
67 | public function appIsConfiguredToDiscoverEvents(): bool
68 | {
69 | return collect($this->app->getProviders(EventServiceProvider::class))
70 | ->filter(fn(EventServiceProvider $provider) => $provider::class === EventServiceProvider::class
71 | || str_starts_with(get_class($provider), $this->app->getNamespace()))
72 | ->contains(fn(EventServiceProvider $provider) => $provider->shouldDiscoverEvents());
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Support/ModularizedCommandsServiceProvider.php:
--------------------------------------------------------------------------------
1 | MakeCast::class,
40 | 'command.controller.make' => MakeController::class,
41 | 'command.console.make' => MakeCommand::class,
42 | 'command.channel.make' => MakeChannel::class,
43 | 'command.event.make' => MakeEvent::class,
44 | 'command.exception.make' => MakeException::class,
45 | 'command.factory.make' => MakeFactory::class,
46 | 'command.job.make' => MakeJob::class,
47 | 'command.listener.make' => MakeListener::class,
48 | 'command.mail.make' => MakeMail::class,
49 | 'command.middleware.make' => MakeMiddleware::class,
50 | 'command.model.make' => MakeModel::class,
51 | 'command.notification.make' => MakeNotification::class,
52 | 'command.observer.make' => MakeObserver::class,
53 | 'command.policy.make' => MakePolicy::class,
54 | 'command.provider.make' => MakeProvider::class,
55 | 'command.request.make' => MakeRequest::class,
56 | 'command.resource.make' => MakeResource::class,
57 | 'command.rule.make' => MakeRule::class,
58 | 'command.seeder.make' => MakeSeeder::class,
59 | 'command.test.make' => MakeTest::class,
60 | 'command.component.make' => MakeComponent::class,
61 | 'command.seed' => SeedCommand::class,
62 | ];
63 |
64 | public function register(): void
65 | {
66 | // Register our overrides via the "booted" event to ensure that we override
67 | // the default behavior regardless of which service provider happens to be
68 | // bootstrapped first (this mostly matters for Livewire).
69 | $this->app->booted(function() {
70 | Artisan::starting(function(Application $artisan) {
71 | $this->registerMakeCommandOverrides();
72 | $this->registerMigrationCommandOverrides();
73 | $this->registerLivewireOverrides($artisan);
74 | });
75 | });
76 | }
77 |
78 | protected function registerMakeCommandOverrides()
79 | {
80 | foreach ($this->overrides as $alias => $class_name) {
81 | $this->app->singleton($alias, $class_name);
82 | $this->app->singleton(get_parent_class($class_name), $class_name);
83 | }
84 | }
85 |
86 | protected function registerMigrationCommandOverrides()
87 | {
88 | // Laravel 8
89 | $this->app->singleton('command.migrate.make', function($app) {
90 | return new MakeMigration($app['migration.creator'], $app['composer']);
91 | });
92 |
93 | // Laravel 9
94 | $this->app->singleton(OriginalMakeMigrationCommand::class, function($app) {
95 | return new MakeMigration($app['migration.creator'], $app['composer']);
96 | });
97 | }
98 |
99 | protected function registerLivewireOverrides(Artisan $artisan)
100 | {
101 | // Don't register commands if Livewire isn't installed
102 | if (! class_exists(Livewire\MakeCommand::class)) {
103 | return;
104 | }
105 |
106 | // Replace the resolved command with our subclass
107 | $artisan->resolveCommands([MakeLivewire::class]);
108 |
109 | // Ensure that if 'make:livewire' or 'livewire:make' is resolved from the container
110 | // in the future, our subclass is used instead
111 | $this->app->extend(Livewire\MakeCommand::class, function() {
112 | return new MakeLivewire();
113 | });
114 | $this->app->extend(Livewire\MakeLivewireCommand::class, function() {
115 | return new MakeLivewire();
116 | });
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Support/ModuleConfig.php:
--------------------------------------------------------------------------------
1 | getContents(), true, 16, JSON_THROW_ON_ERROR);
18 |
19 | $base_path = rtrim(str_replace('\\', '/', $composer_file->getPath()), '/');
20 |
21 | $name = basename($base_path);
22 |
23 | $namespaces = Collection::make($composer_config['autoload']['psr-4'] ?? [])
24 | ->mapWithKeys(function($src, $namespace) use ($base_path) {
25 | $path = $base_path.'/'.$src;
26 | return [$path => $namespace];
27 | });
28 |
29 | return new static($name, $base_path, $namespaces);
30 | }
31 |
32 | public function __construct(
33 | public string $name,
34 | public string $base_path,
35 | ?Collection $namespaces = null
36 | ) {
37 | $this->namespaces = $namespaces ?? new Collection();
38 | }
39 |
40 | public function path(string $to = ''): string
41 | {
42 | return rtrim($this->base_path.'/'.$to, '/');
43 | }
44 |
45 | public function namespace(): string
46 | {
47 | return $this->namespaces->first();
48 | }
49 |
50 | public function qualify(string $class_name): string
51 | {
52 | return $this->namespace().ltrim($class_name, '\\');
53 | }
54 |
55 | public function pathToFullyQualifiedClassName(string $path): string
56 | {
57 | // Handle Windows-style paths
58 | $path = str_replace('\\', '/', $path);
59 |
60 | foreach ($this->namespaces as $namespace_path => $namespace) {
61 | if (str_starts_with($path, $namespace_path)) {
62 | $relative_path = Str::after($path, $namespace_path);
63 | return $namespace.$this->formatPathAsNamespace($relative_path);
64 | }
65 | }
66 |
67 | throw new RuntimeException("Unable to infer qualified class name for '{$path}'");
68 | }
69 |
70 | public function toArray(): array
71 | {
72 | return [
73 | 'name' => $this->name,
74 | 'base_path' => $this->base_path,
75 | 'namespaces' => $this->namespaces->toArray(),
76 | ];
77 | }
78 |
79 | protected function formatPathAsNamespace(string $path): string
80 | {
81 | $path = trim($path, '/');
82 |
83 | $replacements = [
84 | '/' => '\\',
85 | '.php' => '',
86 | ];
87 |
88 | return str_replace(
89 | array_keys($replacements),
90 | array_values($replacements),
91 | $path
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Support/ModuleRegistry.php:
--------------------------------------------------------------------------------
1 | modules_path;
23 | }
24 |
25 | public function getCachePath(): string
26 | {
27 | return $this->cache_path;
28 | }
29 |
30 | public function module(?string $name = null): ?ModuleConfig
31 | {
32 | // We want to allow for gracefully handling empty/null names
33 | return $name
34 | ? $this->modules()->get($name)
35 | : null;
36 | }
37 |
38 | public function moduleForPath(string $path): ?ModuleConfig
39 | {
40 | return $this->module($this->extractModuleNameFromPath($path));
41 | }
42 |
43 | public function moduleForPathOrFail(string $path): ModuleConfig
44 | {
45 | if ($module = $this->moduleForPath($path)) {
46 | return $module;
47 | }
48 |
49 | throw new CannotFindModuleForPathException($path);
50 | }
51 |
52 | public function moduleForClass(string $fqcn): ?ModuleConfig
53 | {
54 | return $this->modules()->first(function(ModuleConfig $module) use ($fqcn) {
55 | foreach ($module->namespaces as $namespace) {
56 | if (Str::startsWith($fqcn, $namespace)) {
57 | return true;
58 | }
59 | }
60 |
61 | return false;
62 | });
63 | }
64 |
65 | public function modules(): Collection
66 | {
67 | return $this->modules ??= $this->loadModules();
68 | }
69 |
70 | public function reload(): Collection
71 | {
72 | $this->modules = null;
73 |
74 | return $this->loadModules();
75 | }
76 |
77 | protected function loadModules(): Collection
78 | {
79 | if (file_exists($this->cache_path)) {
80 | return Collection::make(require $this->cache_path)
81 | ->mapWithKeys(function(array $cached) {
82 | $config = new ModuleConfig($cached['name'], $cached['base_path'], new Collection($cached['namespaces']));
83 | return [$config->name => $config];
84 | });
85 | }
86 |
87 | if (! is_dir($this->modules_path)) {
88 | return new Collection();
89 | }
90 |
91 | return FinderCollection::forFiles()
92 | ->depth('== 1')
93 | ->name('composer.json')
94 | ->in($this->modules_path)
95 | ->collect()
96 | ->mapWithKeys(function(SplFileInfo $path) {
97 | $config = ModuleConfig::fromComposerFile($path);
98 | return [$config->name => $config];
99 | });
100 | }
101 |
102 | protected function extractModuleNameFromPath(string $path): string
103 | {
104 | // Handle Windows-style paths
105 | $path = str_replace('\\', '/', $path);
106 |
107 | // If the modules directory is symlinked, we may get two paths that are actually
108 | // in the same directory, but have different prefixes. This helps resolve that.
109 | if (Str::startsWith($path, $this->modules_path)) {
110 | $path = trim(Str::after($path, $this->modules_path), '/');
111 | } elseif (Str::startsWith($path, $modules_real_path = str_replace('\\', '/', realpath($this->modules_path)))) {
112 | $path = trim(Str::after($path, $modules_real_path), '/');
113 | }
114 |
115 | return explode('/', $path)[0];
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Support/PhpStorm/ConfigWriter.php:
--------------------------------------------------------------------------------
1 | config_path = $config_path;
31 | $this->module_registry = $module_registry;
32 | }
33 |
34 | public function handle(): bool
35 | {
36 | if (! $this->checkConfigFilePermissions()) {
37 | return false;
38 | }
39 |
40 | return $this->write();
41 | }
42 |
43 | protected function checkConfigFilePermissions(): bool
44 | {
45 | if (! is_readable($this->config_path) || ! is_writable($this->config_path)) {
46 | return $this->error("Unable to find or read: '{$this->config_path}'");
47 | }
48 |
49 | if (! is_writable($this->config_path)) {
50 | return $this->error("Config file is not writable: '{$this->config_path}'");
51 | }
52 |
53 | return true;
54 | }
55 |
56 | protected function error(string $message): bool
57 | {
58 | $this->last_error = $message;
59 | return false;
60 | }
61 |
62 | protected function formatXml(SimpleXMLElement $xml): string
63 | {
64 | $dom = new DOMDocument('1.0', 'UTF-8');
65 | $dom->formatOutput = true;
66 | $dom->preserveWhiteSpace = false;
67 | $dom->loadXML($xml->asXML());
68 |
69 | $xml = $dom->saveXML();
70 | $xml = preg_replace('~(\S)/>\s*$~m', '$1 />', $xml);
71 |
72 | return $xml;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Support/PhpStorm/LaravelConfigWriter.php:
--------------------------------------------------------------------------------
1 | getNormalizedPluginConfig();
13 | $template_paths = $plugin_config->xpath('//templatePath');
14 |
15 | // Clean up template paths to prevent duplicates
16 | foreach ($template_paths as $template_path_key => $existing) {
17 | if (null !== $this->module_registry->module((string) $existing['namespace'])) {
18 | unset($template_paths[$template_path_key][0]);
19 | }
20 | }
21 |
22 | // Now add all modules to the config
23 | $modules_directory = config('app-modules.modules_directory', 'app-modules');
24 | $list = $plugin_config->xpath('//option[@name="templatePaths"]//list')[0];
25 | $this->module_registry->modules()
26 | ->sortBy('name')
27 | ->each(function(ModuleConfig $module_config) use ($list, $modules_directory) {
28 | $node = $list->addChild('templatePath');
29 | $node->addAttribute('namespace', $module_config->name);
30 | $node->addAttribute('path', "{$modules_directory}/{$module_config->name}/resources/views");
31 | });
32 |
33 | return false !== file_put_contents($this->config_path, $this->formatXml($plugin_config));
34 | }
35 |
36 | protected function getNormalizedPluginConfig(): SimpleXMLElement
37 | {
38 | $config = simplexml_load_string(file_get_contents($this->config_path));
39 |
40 | // Ensure that exists
41 | $component = $config->xpath('//component[@name="LaravelPluginSettings"]');
42 | if (empty($component)) {
43 | $component = $config->addChild('component');
44 | $component->addAttribute('name', 'LaravelPluginSettings');
45 | } else {
46 | $component = $component[0];
47 | }
48 |
49 | // Ensure that