├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── composer.json ├── config └── structure-discoverer.php ├── phpstan-baseline.neon ├── phpstan.neon └── src ├── Cache ├── DiscoverCacheDriver.php ├── FileDiscoverCacheDriver.php ├── LaravelDiscoverCacheDriver.php ├── NullDiscoverCacheDriver.php └── StaticDiscoverCacheDriver.php ├── Collections ├── TokenCollection.php └── UsageCollection.php ├── Commands ├── CacheStructureScoutsCommand.php └── ClearStructureScoutsCommand.php ├── Data ├── DiscoverProfileConfig.php ├── DiscoveredAttribute.php ├── DiscoveredClass.php ├── DiscoveredEnum.php ├── DiscoveredInterface.php ├── DiscoveredStructure.php ├── DiscoveredTrait.php ├── StructureHeadData.php └── Usage.php ├── Discover.php ├── DiscoverConditions ├── AnyDiscoverCondition.php ├── AttributeDiscoverCondition.php ├── CustomDiscoverCondition.php ├── DiscoverCondition.php ├── ExactDiscoverCondition.php ├── ExtendsDiscoverCondition.php ├── ExtendsWithoutChainDiscoverCondition.php ├── ImplementsDiscoverCondition.php ├── ImplementsWithoutChainDiscoverCondition.php ├── NameDiscoverCondition.php └── TypeDiscoverCondition.php ├── DiscoverWorkers ├── DiscoverWorker.php ├── ParallelDiscoverWorker.php └── SynchronousDiscoverWorker.php ├── Enums ├── DiscoveredEnumType.php ├── DiscoveredStructureType.php └── Sort.php ├── Exceptions ├── CouldNotParseFile.php ├── InvalidReflection.php ├── NoCacheConfigured.php └── StructureScoutsCacheDriverMissing.php ├── StructureDiscovererServiceProvider.php ├── StructureParsers ├── PhpTokenStructureParser.php ├── ReflectionStructureParser.php └── StructureParser.php ├── StructureScout.php ├── Support ├── Conditions │ ├── ConditionBuilder.php │ ├── HasConditions.php │ └── HasConditionsTrait.php ├── DiscoverCacheDriverFactory.php ├── LaravelDetector.php ├── StructureChainResolver.php ├── StructureScoutManager.php ├── StructuresResolver.php └── UseDefinitionsResolver.php └── TokenParsers ├── AttributeTokenParser.php ├── DiscoveredClassTokenParser.php ├── DiscoveredDataTokenParser.php ├── DiscoveredEnumTokenParser.php ├── FileTokenParser.php ├── MultiFileTokenParser.php ├── NamespaceTokenParser.php ├── ReferenceListTokenParser.php ├── ReferenceTokenParser.php ├── StructureHeadTokenParser.php └── UseTokenParser.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `php-structure-discoverer` will be documented in this file. 4 | 5 | ### 2.0.0 - 2023-08-?? 6 | 7 | - Added support for using Reflection instead of PHP token parsing to discover structures 8 | 9 | ## 2.3.1 - 2025-02-14 10 | 11 | ### What's Changed 12 | 13 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/php-structure-discoverer/pull/28 14 | 15 | ### New Contributors 16 | 17 | * @laravel-shift made their first contribution in https://github.com/spatie/php-structure-discoverer/pull/28 18 | 19 | **Full Changelog**: https://github.com/spatie/php-structure-discoverer/compare/2.3.0...2.3.1 20 | 21 | ## 2.3.0 - 2025-01-13 22 | 23 | ### What's Changed 24 | 25 | * Fix parsing classes containing anonymous classes by @stevebauman in https://github.com/spatie/php-structure-discoverer/pull/27 26 | 27 | **Full Changelog**: https://github.com/spatie/php-structure-discoverer/compare/2.2.1...2.3.0 28 | 29 | ## 2.2.1 - 2024-12-16 30 | 31 | ### What's Changed 32 | 33 | * Fix typo in `README.md` by @PerryvanderMeer in https://github.com/spatie/php-structure-discoverer/pull/24 34 | * Fix PHP 8.4 deprecation by @LordSimal in https://github.com/spatie/php-structure-discoverer/pull/26 35 | 36 | ### New Contributors 37 | 38 | * @PerryvanderMeer made their first contribution in https://github.com/spatie/php-structure-discoverer/pull/24 39 | * @LordSimal made their first contribution in https://github.com/spatie/php-structure-discoverer/pull/26 40 | 41 | **Full Changelog**: https://github.com/spatie/php-structure-discoverer/compare/2.2.0...2.2.1 42 | 43 | ## 2.2.0 - 2024-08-29 44 | 45 | - Add a new uses resolver which can be used by external packages 46 | 47 | ## 2.1.2 - 2024-08-13 48 | 49 | - Fix issue where string or int backed enums with interfaces were not discovered 50 | - Added extra types 51 | 52 | ## 2.1.1 - 2024-03-13 53 | 54 | ### What's Changed 55 | 56 | * Create cache when requested (fixes #17) by @francoism90 in https://github.com/spatie/php-structure-discoverer/pull/18 57 | 58 | **Full Changelog**: https://github.com/spatie/php-structure-discoverer/compare/2.1.0...2.1.1 59 | 60 | ## 2.1.0 - 2024-02-16 61 | 62 | ### What's Changed 63 | 64 | * Fix doc example spacing by @stevebauman in https://github.com/spatie/php-structure-discoverer/pull/15 65 | * Laravel 11 by @rubenvanassche in https://github.com/spatie/php-structure-discoverer/pull/16 66 | * Dropped support for Laravel 9 67 | 68 | **Full Changelog**: https://github.com/spatie/php-structure-discoverer/compare/2.0.1...2.1.0 69 | 70 | ## 2.0.1 - 2024-01-08 71 | 72 | ### What's Changed 73 | 74 | * Docs - Fix config publish command by @stevebauman in https://github.com/spatie/php-structure-discoverer/pull/14 75 | * chore: update dependencies by @jameswagoner in https://github.com/spatie/php-structure-discoverer/pull/13 76 | 77 | ### New Contributors 78 | 79 | * @jameswagoner made their first contribution in https://github.com/spatie/php-structure-discoverer/pull/13 80 | 81 | **Full Changelog**: https://github.com/spatie/php-structure-discoverer/compare/2.0.0...2.0.1 82 | 83 | ## 2.0.0 - 2023-12-21 84 | 85 | - Add support for discovering structures using Reflection 86 | 87 | ## 1.2.1 - 2023-08-04 88 | 89 | - Add better support for detecting Laravel 90 | 91 | ## 1.2.0 - 2023-07-27 92 | 93 | ### What's Changed 94 | 95 | - Add ability to sort discovered files by @stevebauman in https://github.com/spatie/php-structure-discoverer/pull/6 96 | 97 | ### New Contributors 98 | 99 | - @stevebauman made their first contribution in https://github.com/spatie/php-structure-discoverer/pull/6 100 | 101 | **Full Changelog**: https://github.com/spatie/php-structure-discoverer/compare/1.1.1...1.2.0 102 | 103 | ## 1.1.1 - 2023-03-24 104 | 105 | - Add symphony finder as a requirement 106 | 107 | ## 1.1.0 - 2023-03-17 108 | 109 | - Allow discovered structures to be built using reflection 110 | 111 | ## 1.0.1 - 2023-02-10 112 | 113 | - Fix require with file cache driver 114 | 115 | ## 0.0.5 - 2023-02-10 116 | 117 | Test release 118 | 119 | ## 0.0.3 - 2022-12-16 120 | 121 | - test release 122 | 123 | ## 0.0.2 - 2022-08-18 124 | 125 | - experimental release 126 | 127 | ## 1.0.0 - 202X-XX-XX 128 | 129 | - initial release 130 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) PHP Structure Discoverer 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automatically discover classes, interfaces, enums, and traits within your PHP application 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/php-structure-discoverer.svg?style=flat-square)](https://packagist.org/packages/spatie/php-structure-discoverer) 4 | [![run-tests](https://github.com/spatie/php-structure-discoverer/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/php-structure-discoverer/actions/workflows/run-tests.yml) 5 | [![PHPStan](https://github.com/spatie/php-structure-discoverer/actions/workflows/phpstan.yml/badge.svg)](https://github.com/spatie/php-structure-discoverer/actions/workflows/phpstan.yml) 6 | [![Check & fix styling](https://github.com/spatie/php-structure-discoverer/actions/workflows/php-cs-fixer.yml/badge.svg)](https://github.com/spatie/php-structure-discoverer/actions/workflows/php-cs-fixer.yml) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/php-structure-discoverer.svg?style=flat-square)](https://packagist.org/packages/spatie/php-structure-discoverer) 8 | 9 | With this package, you'll be able to discover structures in your PHP application that fulfill certain conditions quickly. For example, you could search for classes implementing an interface: 10 | 11 | ```php 12 | use Spatie\StructureDiscoverer\Discover; 13 | 14 | // PostModel::class, Collection::class, ... 15 | Discover::in(__DIR__)->classes()->implementing(Arrayable::class)->get(); 16 | ``` 17 | 18 | As an added benefit, it has a built-in cache functionality that makes the whole process fast in production. 19 | 20 | The package is not only limited to classes but can also find enums, interfaces, and traits and has extra metadata for each structure. 21 | 22 | ## Support us 23 | 24 | [](https://spatie.be/github-ad-click/php-structure-discoverer) 25 | 26 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can 27 | support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 28 | 29 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 30 | You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards 31 | on [our virtual postcard wall](https://spatie.be/open-source/postcards). 32 | 33 | ## Installation 34 | 35 | You can install the package via composer: 36 | 37 | ```bash 38 | composer require spatie/php-structure-discoverer 39 | ``` 40 | 41 | If you're using Laravel, then you can also publish the config file with the following command: 42 | 43 | ```bash 44 | php artisan vendor:publish --tag="structure-discoverer-config" 45 | ``` 46 | 47 | This is the contents of the published config file: 48 | 49 | ```php 50 | return [ 51 | /* 52 | * A list of files that should be ignored during the discovering process. 53 | */ 54 | 'ignored_files' => [ 55 | 56 | ], 57 | 58 | /** 59 | * The directories where the package should search for structure scouts 60 | */ 61 | 'structure_scout_directories' => [ 62 | app_path(), 63 | ], 64 | 65 | /* 66 | * Configure the cache driver for discoverers 67 | */ 68 | 'cache' => [ 69 | 'driver' => \Spatie\StructureDiscoverer\Cache\LaravelDiscoverCacheDriver::class, 70 | 'store' => null, 71 | ] 72 | ]; 73 | ``` 74 | 75 | ## Usage 76 | 77 | You always need to define in which directories you want to look for structures: 78 | 79 | ```php 80 | Discover::in(__DIR__)->... 81 | ``` 82 | 83 | It is possible to look in multiple directories like this: 84 | 85 | ```php 86 | Discover::in(app_path('models'), app_path('enums'))->... 87 | ``` 88 | 89 | You can get the structures as such: 90 | 91 | ```php 92 | Discover::in(__DIR__)->get(); 93 | ``` 94 | 95 | Which will return an array of class FCQN, and because no conditions were added, the package will return all classes, enums, interfaces, and traits. 96 | 97 | You only discover classes like this: 98 | 99 | ```php 100 | Discover::in(__DIR__)->classes()->get(); 101 | ``` 102 | 103 | Interfaces like this: 104 | 105 | ```php 106 | Discover::in(__DIR__)->interfaces()->get(); 107 | ``` 108 | 109 | Enums like this: 110 | 111 | ```php 112 | Discover::in(__DIR__)->enums()->get(); 113 | ``` 114 | 115 | And traits like this: 116 | 117 | ```php 118 | Discover::in(__DIR__)->traits()->get(); 119 | ``` 120 | 121 | When you want to include a specific named structure, you can do the following: 122 | 123 | ```php 124 | Discover::in(__DIR__)->named('MyAwesomeClass')->get(); 125 | ``` 126 | 127 | You can discover classes extending another class as such: 128 | 129 | ```php 130 | Discover::in(__DIR__)->extending(Model::class)->get(); 131 | ``` 132 | 133 | Discovering classes, interfaces, or enums implementing an interface can be done like this: 134 | 135 | ```php 136 | Discover::in(__DIR__)->implementing(Arrayable::class)->get(); 137 | ``` 138 | 139 | Be aware that although interfaces extend another interface, in this context, the implements keyword seemed a more logical choice to find interfaces extended by another interface. Using the `extends` method for such a filter won't work! 140 | 141 | Classes, interfaces, or traits using an attribute can be discovered as such: 142 | 143 | ```php 144 | Discover::in(__DIR__)->withAttribute(Cast::class)->get(); 145 | ``` 146 | 147 | For more fine-grained control, you can use a closure that receives a `DiscoveredStructure` object (more on that later) and should return `true` if the structure should be included: 148 | 149 | ```php 150 | Discover::in(__DIR__) 151 | ->custom(fn(DiscoveredStructure $structure) => $structure->namespace === 'App') 152 | ->get() 153 | ``` 154 | 155 | More complex custom conditions can be embedded in a class: 156 | 157 | ```php 158 | class AppDiscoverCondition extends DiscoverCondition 159 | { 160 | public function satisfies(DiscoveredStructure $discoveredData): bool 161 | { 162 | return $structure->namespace === 'App'; 163 | } 164 | }; 165 | ``` 166 | 167 | This condition can now be used like this: 168 | 169 | ```php 170 | Discover::in(__DIR__) 171 | ->custom(new AppDiscoverCondition()) 172 | ->get() 173 | ``` 174 | 175 | ### Combining conditions 176 | 177 | By default, all conditions will work like an AND operation, so in this case: 178 | 179 | ```php 180 | Discover::in(__DIR__)->classes()->implementing(Arrayable::class)->get(); 181 | ``` 182 | 183 | The package will only look for structures that are a class **and** implement `Arrayble`. 184 | 185 | You can create an OR combination of conditions like this: 186 | 187 | ```php 188 | Discover::in(__DIR__) 189 | ->any( 190 | ConditionBuilder::create()->classes(), 191 | ConditionBuilder::create()->enums() 192 | ) 193 | ->get(); 194 | ``` 195 | 196 | Now, the package will only discover classes **or** enum structures. 197 | 198 | You can also create more complex operations like an or of and's: 199 | 200 | ```php 201 | Discover::in(__DIR__) 202 | ->any( 203 | ConditionBuilder::create()->exact( 204 | ConditionBuilder::create()->classes(), 205 | ConditionBuilder::create()->implementing(Arrayble::class), 206 | ), 207 | ConditionBuilder::create()->exact( 208 | ConditionBuilder::create()->enums(), 209 | ConditionBuilder::create()->implementing(Stringable::class), 210 | ) 211 | ) 212 | ->get(); 213 | ``` 214 | 215 | This example can be written shorter like this: 216 | 217 | ```php 218 | Discover::in(__DIR__) 219 | ->any( 220 | ConditionBuilder::create()->exact( 221 | ConditionBuilder::create()->classes()->implementing(Arrayble::class), 222 | ), 223 | ConditionBuilder::create()->exact( 224 | ConditionBuilder::create()->enums()->implementing(Stringable::class), 225 | ) 226 | ) 227 | ->get(); 228 | ``` 229 | 230 | ### Sorting 231 | 232 | By default, the discovered structures will be sorted according to the OS' default. 233 | 234 | You can change the sorting like this: 235 | 236 | ```php 237 | use Spatie\StructureDiscoverer\Enums\Sort; 238 | 239 | Discover::in(__DIR__)->sortBy(Sort::Name)->get(); 240 | ``` 241 | 242 | Here are all the available sorting options: 243 | 244 | ```php 245 | use Spatie\StructureDiscoverer\Enums\Sort; 246 | 247 | Discover::in(__DIR__)->sortBy(Sort::Name); 248 | Discover::in(__DIR__)->sortBy(Sort::Size); 249 | Discover::in(__DIR__)->sortBy(Sort::Type); 250 | Discover::in(__DIR__)->sortBy(Sort::Extension); 251 | Discover::in(__DIR__)->sortBy(Sort::ChangedTime); 252 | Discover::in(__DIR__)->sortBy(Sort::ModifiedTime); 253 | Discover::in(__DIR__)->sortBy(Sort::AccessedTime); 254 | Discover::in(__DIR__)->sortBy(Sort::CaseInsensitiveName); 255 | ``` 256 | 257 | ### Caching 258 | 259 | This package can cache all discovered structures, so no performance-heavy operations are required in production. 260 | 261 | The fastest way to start caching is by creating a structure scout, which is a class that describes what you want to discover: 262 | 263 | ```php 264 | class EnumsStructureScout extends StructureScout 265 | { 266 | protected function definition(): Discover|DiscoverConditionFactory 267 | { 268 | return Discover::in(__DIR__)->enums(); 269 | } 270 | 271 | public function cacheDriver(): DiscoverCacheDriver 272 | { 273 | return new FileDiscoverCacheDriver('/path/to/temp/directory'); 274 | } 275 | } 276 | ``` 277 | 278 | Each structure scout extends from `StructureScout` and should have 279 | 280 | - a definition where you describe what to discover and where. Just like we did inline earlier 281 | - a driver to be used for the cache. When you're using Laravel, this method is not required since it is already defined in the config file 282 | 283 | Within your application, you can use the discoverer as such: 284 | 285 | ```php 286 | EnumsStructureScout::create()->get(); 287 | ``` 288 | 289 | The first time this method is called, the whole discovery process will run taking a bit more time. The second call will skip the discovery process and use the cached version, making a call to this method amazingly fast! 290 | 291 | #### In production 292 | 293 | When you're deploying to production, you can warm all your structure scout caches as such: 294 | 295 | ```php 296 | StructureScoutManager::cache([__DIR__]); 297 | ``` 298 | 299 | You should provide a directory where the structure scouts are stored. 300 | 301 | If you're using Laravel, you can run the following command:`` 302 | 303 | ````bash 304 | php artisan structure-scouts:cache 305 | ```` 306 | 307 | It is also possible to clear all caches for structure scouts as such: 308 | 309 | ```php 310 | StructureScoutManager::clear([__DIR__]); 311 | ``` 312 | 313 | Or, if you're using Laravel: 314 | 315 | ````bash 316 | php artisan structure-scouts:clear 317 | ```` 318 | 319 | ##### For packages 320 | 321 | Since an individual user defines the directories where structure scouts can be found, packages can't ensure their structure scouts will be discovered with the cache commands. 322 | 323 | It is possible to add structure scouts like this manually: 324 | 325 | ```php 326 | StructureScoutManager::add(SettingsStructureScout::class); 327 | ``` 328 | 329 | In a Laravel application, you typically do this within the package ServiceProvider. 330 | 331 | #### Cache drivers 332 | 333 | ##### File 334 | 335 | The `FileDiscoverCacheDriver` allows you to cache discovered structures in a file. You should provide a `directory` parameter where all the cache files should be stored. 336 | 337 | ##### Laravel 338 | 339 | The `LaravelDiscoverCacheDriver` will use the default Laravel cache. You can provide an optional `store` parameter to define the store to be used and an optional `prefix` parameter for the cache key. 340 | 341 | ##### Null 342 | 343 | The `NullDiscoverCacheDriver` will not cache anything and can be used for testing purposes. 344 | 345 | ##### Your own 346 | 347 | A cache driver can be built by extending the `DiscoverCacheDriver` interface: 348 | 349 | ```php 350 | interface DiscoverCacheDriver 351 | { 352 | public function has(string $id): bool; 353 | 354 | public function get(string $id): array; 355 | 356 | public function put(string $id, array $discovered): void; 357 | 358 | public function forget(string $id): void; 359 | } 360 | ``` 361 | 362 | #### Without structure scouts 363 | 364 | You can also use caching inline without the use of scouts, be aware warming up these caches in production is not possible: 365 | 366 | ```php 367 | Discover::in(__DIR__) 368 | ->withCache( 369 | 'Some identifier', 370 | new FileDiscoverCacheDriver('/path/to/temp/directory); 371 | ) 372 | ->get(); 373 | ``` 374 | 375 | ### Parallel 376 | 377 | Getting all structures in a bigger application can be slow due to many files being scanned. This process can be sped up by parallelized scanning. You can enable this as such: 378 | 379 | ```php 380 | Discover::in(__DIR__)->parallel()->get(); 381 | ``` 382 | 383 | It is possible to set the number of files each process will scan: 384 | 385 | ```php 386 | Discover::in(__DIR__)->parallel(100)->get(); 387 | ``` 388 | 389 | By default, each process will scan 50 files. 390 | 391 | ### Chains 392 | 393 | Often structures inherit other structures with extends and implementations. The package automatically includes these structures when discovering them. So for example 394 | 395 | ```php 396 | class Request 397 | { 398 | } 399 | 400 | class FormRequest extends Request 401 | { 402 | } 403 | 404 | class UserFormRequest extends FormRequest 405 | { 406 | } 407 | ``` 408 | 409 | When using: 410 | 411 | ```php 412 | Discover::in(__DIR__)->extending(Request::class)->get(); 413 | ``` 414 | 415 | Both `FormRequest` and `UserFormRequest` will be found, and although `UserFormRequest` is not a direct descendant of `Request`, it is one through `FormRequest`. 416 | 417 | You can disable this behavior for extending as such: 418 | 419 | ```php 420 | Discover::in(__DIR__)->extendingWithoutChain(Request::class) 421 | ``` 422 | 423 | Or for implementing as such: 424 | 425 | ```php 426 | Discover::in(__DIR__)->implementingWithoutChain(Request::class) 427 | ``` 428 | 429 | Resolving chains is a complicated and resource-heavy process. It can be completely disabled as such: 430 | 431 | ```php 432 | Discover::in(__DIR__)->withoutChains()->extending(Request::class)->get(); 433 | ``` 434 | 435 | ### Full information 436 | 437 | The output will be a reference string to the structure when discovering structures. Internally the package keeps track of a lot more information which can be helpful for all purposes. You can also retrieve this information as such: 438 | 439 | ```php 440 | Discover::in(__DIR__)->full()->get(); 441 | ``` 442 | 443 | Instead of returning an array of strings, now an array of `DiscoveredStructure` objects is returned. Let's go through the different types: 444 | 445 | #### DiscoveredClass 446 | 447 | Represents a class, the `$extends` and `$implements` properties address the direct extend and implements of the class. The `$extendsChain` and `$implementsChain` properties contain all extends and implements for the complete inheritance chain. 448 | 449 | ```php 450 | class DiscoveredClass extends DiscoveredStructure 451 | { 452 | public function __construct( 453 | string $name, 454 | string $file, 455 | string $namespace, 456 | public bool $isFinal, 457 | public bool $isAbstract, 458 | public bool $isReadonly, 459 | public ?string $extends, 460 | public array $implements, 461 | public array $attributes, 462 | public ?array $extendsChain = null, 463 | public ?array $implementsChain = null, 464 | ) { 465 | } 466 | } 467 | ``` 468 | 469 | #### DiscoveredInterface 470 | 471 | Represents a class, the `$extends` property addresses the direct extends of the interface. The `$extendsChain` property contains all extends for the whole inheritance chain. 472 | 473 | ```php 474 | class DiscoveredInterface extends DiscoveredStructure 475 | { 476 | public function __construct( 477 | string $name, 478 | string $file, 479 | string $namespace, 480 | public array $extends, 481 | public array $attributes, 482 | public ?array $extendsChain = null, 483 | ) { 484 | } 485 | ``` 486 | 487 | #### DiscoveredEnum 488 | 489 | Represents an enum, the `$implements` property addresses the direct extends of the enum. The `$implementsChain` property contains all implements for the full inheritance chain. The `$type` property is an enum describing the type: `Unit`, `String`, and `Int`. 490 | 491 | ```php 492 | class DiscoveredEnum extends DiscoveredStructure 493 | { 494 | public function __construct( 495 | public string $name, 496 | public string $namespace, 497 | public string $file, 498 | public DiscoveredEnumType $type, 499 | public array $implements, 500 | public array $attributes, 501 | public ?array $implementsChain = null, 502 | ) { 503 | } 504 | } 505 | ``` 506 | 507 | ### DiscoveredTrait 508 | 509 | Represents a discovered trait within the application. 510 | 511 | ```php 512 | class DiscoveredTrait extends DiscoveredStructure 513 | { 514 | public function __construct( 515 | public string $name, 516 | public string $namespace, 517 | public string $file, 518 | ) { 519 | } 520 | } 521 | ``` 522 | 523 | ### Parsers 524 | 525 | The parser is responsible for parsing a file and returning a list of structures. The package comes with two parsers out of the box: 526 | 527 | - `PhpTokenStructureParser`: Reads a PHP file, tokenizes it, and parses the tokens into structures. 528 | - `ReflectionStructureParser`: Uses the PHP reflection API to read a file and parse it into structures. 529 | 530 | By default, the `PhpTokenStructureParser` is used due to it being more robust, the `ReflectionStructureParser` is quite a bit faster but can completely fail the PHP process. 531 | 532 | You can enable the `ReflectionStructureParser` as such: 533 | 534 | ```php 535 | Discover::in(__DIR__) 536 | ->useReflection( 537 | basePath: '/path/to/project/root', 538 | rootNamespace: null 539 | ) 540 | ->get(); 541 | ``` 542 | 543 | You'll likely need to set the basePath to the root of your project, and optionally the root namespace of your project which will be prepended. 544 | 545 | For default Laravel projects this would be: 546 | 547 | ```php 548 | Discover::in(__DIR__) 549 | ->useReflection(basePath: base_path()) 550 | ->get(); 551 | ``` 552 | 553 | ### Help? My structure cannot be found! 554 | 555 | The internals of this package will scan all files within a directory and try to make a virtual map linking all structures with their extends, uses, and implementations. 556 | 557 | Due to this file scanning, this map is incomplete if referenced structures are not being scanned. 558 | 559 | For example, we scan for all classes extending Laravel's `Model` in our app directory, a lot of models have been found, but the `User` model is missing. 560 | 561 | The reason why this is happening is that: 562 | 563 | - The package searches in the app directory for classes extending `Model` 564 | - `User` extends `Authenticatable`, which itself extends `Model` 565 | - `Authenticatable` is stored within the `vendor/laravel/...` directory, which isn't being scanned 566 | - The package does not know that `Authenticatable` extends `Model` 567 | - `User` will not be found 568 | 569 | A solution to this problem is to include the `laravel` directory in the scanning process. 570 | 571 | ## Testing 572 | 573 | ```bash 574 | composer test 575 | ``` 576 | 577 | ## Changelog 578 | 579 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 580 | 581 | ## Contributing 582 | 583 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 584 | 585 | ## Security Vulnerabilities 586 | 587 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 588 | 589 | ## Credits 590 | 591 | - [Ruben Van Assche](https://github.com/rubenvanassche) 592 | - [Construct Finder](https://github.com/thephpleague/construct-finder) a big influence for this package 593 | - [All Contributors](../../contributors) 594 | 595 | ## License 596 | 597 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 598 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide. 4 | 5 | ## From v1 to v2 6 | 7 | - `DiscoverWorker` now also takes `DiscoverProfileConfig $config` as an argument when running 8 | - If you've written a custom `DiscoverWorker` then take a look at what's changed in the default ones 9 | - `SynchronousDiscoverWorker` has no `$multiFileResolver` constructor argument anymore 10 | - The `discover` method in `StructuresResolver` is now protected and only takes a config 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/php-structure-discoverer", 3 | "description": "Automatically discover structures within your PHP application", 4 | "keywords": [ 5 | "laravel", 6 | "php", 7 | "discover", 8 | "php-structure-discoverer" 9 | ], 10 | "homepage": "https://github.com/spatie/php-structure-discoverer", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Ruben Van Assche", 15 | "email": "ruben@spatie.be", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "amphp/amp": "^v3.0", 22 | "amphp/parallel": "^2.2", 23 | "illuminate/collections": "^10.0|^11.0|^12.0", 24 | "spatie/laravel-package-tools": "^1.4.3", 25 | "symfony/finder": "^6.0|^7.0" 26 | }, 27 | "require-dev": { 28 | "illuminate/console": "^10.0|^11.0|^12.0", 29 | "laravel/pint": "^1.0", 30 | "nunomaduro/collision": "^7.0|^8.0", 31 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 32 | "pestphp/pest": "^2.0|^3.0", 33 | "pestphp/pest-plugin-laravel": "^2.0|^3.0", 34 | "phpstan/extension-installer": "^1.1", 35 | "phpstan/phpstan-deprecation-rules": "^1.0", 36 | "phpstan/phpstan-phpunit": "^1.0", 37 | "phpunit/phpunit": "^9.5|^10.0|^11.5.3", 38 | "spatie/laravel-ray": "^1.26" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Spatie\\StructureDiscoverer\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Spatie\\StructureDiscoverer\\Tests\\": "tests" 48 | } 49 | }, 50 | "scripts": { 51 | "analyse": "vendor/bin/phpstan analyse", 52 | "test": "vendor/bin/pest", 53 | "test-coverage": "vendor/bin/pest --coverage", 54 | "format": "vendor/bin/pint" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "pestphp/pest-plugin": true, 60 | "phpstan/extension-installer": true 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider" 67 | ] 68 | } 69 | }, 70 | "minimum-stability": "dev", 71 | "prefer-stable": true 72 | } 73 | -------------------------------------------------------------------------------- /config/structure-discoverer.php: -------------------------------------------------------------------------------- 1 | [ 8 | 9 | ], 10 | 11 | /** 12 | * The directories where the package should search for structure scouts 13 | */ 14 | 'structure_scout_directories' => [ 15 | app_path(), 16 | ], 17 | 18 | /* 19 | * Configure the cache driver for discoverers 20 | */ 21 | 'cache' => [ 22 | 'driver' => \Spatie\StructureDiscoverer\Cache\LaravelDiscoverCacheDriver::class, 23 | 'store' => null, 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Parameter \\$file of class Spatie\\\\StructureDiscoverer\\\\Data\\\\DiscoveredClass constructor expects string, string\\|false given\\.$#" 5 | count: 1 6 | path: src/Data/DiscoveredClass.php 7 | 8 | - 9 | message: "#^Parameter \\$file of class Spatie\\\\StructureDiscoverer\\\\Data\\\\DiscoveredEnum constructor expects string, string\\|false given\\.$#" 10 | count: 1 11 | path: src/Data/DiscoveredEnum.php 12 | 13 | - 14 | message: "#^Parameter \\$file of class Spatie\\\\StructureDiscoverer\\\\Data\\\\DiscoveredInterface constructor expects string, string\\|false given\\.$#" 15 | count: 1 16 | path: src/Data/DiscoveredInterface.php 17 | 18 | - 19 | message: "#^Parameter \\$file of class Spatie\\\\StructureDiscoverer\\\\Data\\\\DiscoveredTrait constructor expects string, string\\|false given\\.$#" 20 | count: 1 21 | path: src/Data/DiscoveredTrait.php 22 | 23 | - 24 | message: "#^Match arm is unreachable because previous comparison is always true\\.$#" 25 | count: 1 26 | path: src/Enums/DiscoveredStructureType.php 27 | 28 | - 29 | message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionEnum constructor expects class\\-string\\\\|T of UnitEnum, class\\-string\\ given\\.$#" 30 | count: 1 31 | path: src/StructureParsers/ReflectionStructureParser.php 32 | 33 | - 34 | message: "#^Unsafe usage of new static\\(\\)\\.$#" 35 | count: 1 36 | path: src/StructureScout.php 37 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | 6 | paths: 7 | - src 8 | 9 | # The level 9 is the highest level 10 | level: 7 11 | 12 | excludePaths: 13 | - ./*/*/FileToBeExcluded.php 14 | -------------------------------------------------------------------------------- /src/Cache/DiscoverCacheDriver.php: -------------------------------------------------------------------------------- 1 | */ 10 | public function get(string $id): array; 11 | 12 | /** @param array $discovered */ 13 | public function put(string $id, array $discovered): void; 14 | 15 | public function forget(string $id): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/Cache/FileDiscoverCacheDriver.php: -------------------------------------------------------------------------------- 1 | directory = rtrim($this->directory, '/'); 15 | 16 | if (! file_exists($this->directory)) { 17 | mkdir($this->directory); 18 | } 19 | } 20 | 21 | public function has(string $id): bool 22 | { 23 | return file_exists($this->resolvePath($id)); 24 | } 25 | 26 | /** @return array */ 27 | public function get(string $id): array 28 | { 29 | $path = $this->resolvePath($id); 30 | 31 | if ($this->serialize === false) { 32 | return require $path; 33 | } 34 | 35 | $file = file_get_contents($path); 36 | 37 | if ($file === false) { 38 | throw new Exception("Could not load file {$path}"); 39 | } 40 | 41 | return unserialize($file); 42 | } 43 | 44 | /** @param array $discovered */ 45 | public function put(string $id, array $discovered): void 46 | { 47 | $export = $this->serialize 48 | ? serialize($discovered) 49 | : 'resolvePath($id), 53 | $export, 54 | ); 55 | } 56 | 57 | public function forget(string $id): void 58 | { 59 | $path = $this->resolvePath($id); 60 | 61 | if (file_exists($path)) { 62 | unlink($path); 63 | } 64 | } 65 | 66 | private function resolvePath(string $id): string 67 | { 68 | return $this->filename 69 | ? "{$this->directory}/{$this->filename}" 70 | : "{$this->directory}/discoverer-cache-{$id}"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Cache/LaravelDiscoverCacheDriver.php: -------------------------------------------------------------------------------- 1 | resolveCacheRepository()->has($this->resolveCacheKey($id)); 18 | } 19 | 20 | /** @return array */ 21 | public function get(string $id): array 22 | { 23 | return $this->resolveCacheRepository()->get($this->resolveCacheKey($id)); 24 | } 25 | 26 | /** @param array $discovered */ 27 | public function put(string $id, array $discovered): void 28 | { 29 | $this->resolveCacheRepository()->put($this->resolveCacheKey($id), $discovered); 30 | } 31 | 32 | public function forget(string $id): void 33 | { 34 | $this->resolveCacheRepository()->forget($this->resolveCacheKey($id)); 35 | } 36 | 37 | private function resolveCacheRepository(): Repository 38 | { 39 | return cache()->store($this->store); 40 | } 41 | 42 | private function resolveCacheKey(string $id): string 43 | { 44 | return $this->prefix 45 | ? "{$this->prefix}-discoverer-cache-{$id}" 46 | : "discoverer-cache-{$id}"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Cache/NullDiscoverCacheDriver.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function get(string $id): array 18 | { 19 | throw new Exception('Null driver cannot get a cached item'); 20 | } 21 | 22 | /** @param array $discovered */ 23 | public function put(string $id, array $discovered): void 24 | { 25 | } 26 | 27 | public function forget(string $id): void 28 | { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Cache/StaticDiscoverCacheDriver.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | public static array $entries = []; 11 | 12 | public function has(string $id): bool 13 | { 14 | return array_key_exists($id, static::$entries); 15 | } 16 | 17 | /** 18 | * @return array 19 | */ 20 | public function get(string $id): array 21 | { 22 | return static::$entries[$id]; 23 | } 24 | 25 | /** 26 | * @param array $discovered 27 | */ 28 | public function put(string $id, array $discovered): void 29 | { 30 | static::$entries[$id] = $discovered; 31 | } 32 | 33 | public function forget(string $id): void 34 | { 35 | unset(static::$entries[$id]); 36 | } 37 | 38 | public static function clear(): void 39 | { 40 | static::$entries = []; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Collections/TokenCollection.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class TokenCollection implements IteratorAggregate, Countable 16 | { 17 | /** 18 | * @param array $tokens 19 | */ 20 | public function __construct( 21 | protected array $tokens, 22 | ) { 23 | } 24 | 25 | public static function fromCode(string $code): self 26 | { 27 | try { 28 | $tokens = PhpToken::tokenize($code, TOKEN_PARSE); 29 | } catch (ParseError) { 30 | $tokens = []; 31 | } 32 | 33 | return new self( 34 | array_values(array_filter($tokens, fn (PhpToken $token) => ! $token->is([T_COMMENT, T_DOC_COMMENT, T_WHITESPACE]))) 35 | ); 36 | } 37 | 38 | public function has(int $index): bool 39 | { 40 | return array_key_exists($index, $this->tokens); 41 | } 42 | 43 | public function get(int $index): ?PhpToken 44 | { 45 | return $this->has($index) 46 | ? $this->tokens[$index] 47 | : null; 48 | } 49 | 50 | /** 51 | * @return Traversable 52 | */ 53 | public function getIterator(): Traversable 54 | { 55 | return new ArrayIterator($this->tokens); 56 | } 57 | 58 | public function count(): int 59 | { 60 | return count($this->tokens); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Collections/UsageCollection.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UsageCollection implements IteratorAggregate 14 | { 15 | /** 16 | * @param array $usages 17 | */ 18 | public function __construct( 19 | public array $usages = [], 20 | ) { 21 | } 22 | 23 | public function add( 24 | Usage ...$usages 25 | ): self { 26 | array_push($this->usages, ...$usages); 27 | 28 | return $this; 29 | } 30 | 31 | public function findForAlias(string $alias): ?Usage 32 | { 33 | foreach ($this->usages as $usage) { 34 | if ($usage->name === $alias) { 35 | return $usage; 36 | } 37 | } 38 | 39 | return null; 40 | } 41 | 42 | public function findFcqnForIdentifier( 43 | string $identifier, 44 | string $namespace, 45 | ): string { 46 | if ($usage = $this->findForAlias($identifier)) { 47 | return $usage->fcqn; 48 | } 49 | 50 | if (empty($namespace)) { 51 | return $identifier; 52 | } 53 | 54 | return "{$namespace}\\{$identifier}"; 55 | } 56 | 57 | public function getIterator(): Traversable 58 | { 59 | return new ArrayIterator($this->usages); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Commands/CacheStructureScoutsCommand.php: -------------------------------------------------------------------------------- 1 | components->info('Caching structure scouts...'); 17 | 18 | $cached = StructureScoutManager::cache(config('structure-discoverer.structure_scout_directories')); 19 | 20 | collect($cached) 21 | ->each(fn (string $identifier) => $this->components->task($identifier)) 22 | ->whenNotEmpty(fn () => $this->newLine()); 23 | 24 | $this->components->info('All done!'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Commands/ClearStructureScoutsCommand.php: -------------------------------------------------------------------------------- 1 | components->info('Clearing structure scouts...'); 17 | 18 | $cached = StructureScoutManager::clear(config('structure-discoverer.structure_scout_directories')); 19 | 20 | collect($cached) 21 | ->each(fn (string $identifier) => $this->components->task($identifier)) 22 | ->whenNotEmpty(fn () => $this->newLine()); 23 | 24 | $this->components->info('All done!'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Data/DiscoverProfileConfig.php: -------------------------------------------------------------------------------- 1 | $directories 15 | * @param array $ignoredFiles 16 | */ 17 | public function __construct( 18 | public array $directories, 19 | public array $ignoredFiles, 20 | public bool $full, 21 | public DiscoverWorker $worker, 22 | public ?DiscoverCacheDriver $cacheDriver, 23 | public ?string $cacheId, 24 | public bool $withChains, 25 | public ExactDiscoverCondition $conditions, 26 | public ?Sort $sort, 27 | public bool $reverseSorting, 28 | public StructureParser $structureParser, 29 | public ?string $reflectionBasePath, 30 | public ?string $reflectionRootNamespace, 31 | ) { 32 | } 33 | 34 | public function shouldUseCache(): bool 35 | { 36 | return $this->cacheId !== null && $this->cacheDriver !== null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Data/DiscoveredAttribute.php: -------------------------------------------------------------------------------- 1 | $reflectionAttribute 16 | */ 17 | public static function fromReflection( 18 | ReflectionAttribute $reflectionAttribute, 19 | ): self { 20 | return new self($reflectionAttribute->getName()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Data/DiscoveredClass.php: -------------------------------------------------------------------------------- 1 | $implements 14 | * @param array $attributes 15 | * @param ?array $extendsChain 16 | * @param ?array $implementsChain 17 | */ 18 | public function __construct( 19 | string $name, 20 | string $file, 21 | string $namespace, 22 | public bool $isFinal, 23 | public bool $isAbstract, 24 | public bool $isReadonly, 25 | public ?string $extends, 26 | public array $implements, 27 | public array $attributes, 28 | public ?array $extendsChain = null, 29 | public ?array $implementsChain = null, 30 | ) { 31 | parent::__construct($name, $file, $namespace); 32 | } 33 | 34 | public function getType(): DiscoveredStructureType 35 | { 36 | return DiscoveredStructureType::ClassDefinition; 37 | } 38 | 39 | /** 40 | * @param ReflectionClass $reflection 41 | */ 42 | public static function fromReflection(ReflectionClass $reflection): DiscoveredStructure 43 | { 44 | if ($reflection->isTrait() || $reflection->isInterface() || $reflection->isEnum()) { 45 | throw InvalidReflection::expectedClass(); 46 | } 47 | 48 | $implements = array_values($reflection->getInterfaceNames()); 49 | 50 | $extends = $reflection->getParentClass() !== false 51 | ? $reflection->getParentClass()->getName() 52 | : null; 53 | 54 | return new self( 55 | name: $reflection->getShortName(), 56 | file: $reflection->getFileName(), 57 | namespace: $reflection->getNamespaceName(), 58 | isFinal: $reflection->isFinal(), 59 | isAbstract: $reflection->isAbstract(), 60 | isReadonly: version_compare(phpversion(), '8.2', '>=') ? $reflection->isReadonly() : false, 61 | extends: $extends, 62 | implements: $implements, 63 | attributes: array_map( 64 | fn (ReflectionAttribute $reflectionAttribute) => DiscoveredAttribute::fromReflection($reflectionAttribute), 65 | $reflection->getAttributes() 66 | ), 67 | extendsChain: array_values(class_parents($reflection->getName())), 68 | implementsChain: $implements 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Data/DiscoveredEnum.php: -------------------------------------------------------------------------------- 1 | $implements 17 | * @param array $attributes 18 | * @param ?array $implementsChain 19 | */ 20 | public function __construct( 21 | public string $name, 22 | public string $file, 23 | public string $namespace, 24 | public DiscoveredEnumType $type, 25 | public array $implements, 26 | public array $attributes, 27 | public ?array $implementsChain = null, 28 | ) { 29 | parent::__construct($name, $file, $namespace); 30 | } 31 | 32 | public function getType(): DiscoveredStructureType 33 | { 34 | return DiscoveredStructureType::Enum; 35 | } 36 | 37 | /** 38 | * @param ReflectionClass $reflection 39 | */ 40 | public static function fromReflection(ReflectionClass $reflection): DiscoveredStructure 41 | { 42 | if (! $reflection instanceof ReflectionEnum) { 43 | throw InvalidReflection::create(ReflectionEnum::class, $reflection); 44 | } 45 | 46 | if (! $reflection->isEnum()) { 47 | throw InvalidReflection::expectedEnum(); 48 | } 49 | 50 | $type = match (true) { 51 | $reflection->isBacked() === false => DiscoveredEnumType::Unit, 52 | $reflection->isBacked() === true && (string) $reflection->getBackingType() === 'string' => DiscoveredEnumType::String, 53 | $reflection->isBacked() === true && (string) $reflection->getBackingType() === 'int' => DiscoveredEnumType::Int, 54 | default => throw new Exception('Unknown enum type') 55 | }; 56 | 57 | $implements = array_values($reflection->getInterfaceNames()); 58 | 59 | return new self( 60 | name: $reflection->getShortName(), 61 | file: $reflection->getFileName(), 62 | namespace: $reflection->getNamespaceName(), 63 | type: $type, 64 | implements: $implements, 65 | attributes: array_map( 66 | fn (ReflectionAttribute $reflectionAttribute) => DiscoveredAttribute::fromReflection($reflectionAttribute), 67 | $reflection->getAttributes() 68 | ), 69 | implementsChain: $implements 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Data/DiscoveredInterface.php: -------------------------------------------------------------------------------- 1 | $extends 14 | * @param array $attributes 15 | * @param ?array $extendsChain 16 | */ 17 | public function __construct( 18 | string $name, 19 | string $file, 20 | string $namespace, 21 | public array $extends, 22 | public array $attributes, 23 | public ?array $extendsChain = null, 24 | ) { 25 | parent::__construct($name, $file, $namespace); 26 | } 27 | 28 | public function getType(): DiscoveredStructureType 29 | { 30 | return DiscoveredStructureType::Interface; 31 | } 32 | 33 | /** 34 | * @param ReflectionClass $reflection 35 | */ 36 | public static function fromReflection(ReflectionClass $reflection): DiscoveredStructure 37 | { 38 | if (! $reflection->isInterface()) { 39 | throw InvalidReflection::expectedInterface(); 40 | } 41 | 42 | $extends = array_values($reflection->getInterfaceNames()); 43 | 44 | return new self( 45 | name: $reflection->getShortName(), 46 | file: $reflection->getFileName(), 47 | namespace: $reflection->getNamespaceName(), 48 | extends: $extends, 49 | attributes: array_map( 50 | fn (ReflectionAttribute $reflectionAttribute) => DiscoveredAttribute::fromReflection($reflectionAttribute), 51 | $reflection->getAttributes() 52 | ), 53 | extendsChain: $extends 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Data/DiscoveredStructure.php: -------------------------------------------------------------------------------- 1 | $reflection 21 | */ 22 | abstract public static function fromReflection(ReflectionClass $reflection): DiscoveredStructure; 23 | 24 | public function getFcqn(): string 25 | { 26 | return empty($this->namespace) ? $this->name : "{$this->namespace}\\{$this->name}"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Data/DiscoveredTrait.php: -------------------------------------------------------------------------------- 1 | $reflection 18 | */ 19 | public static function fromReflection(ReflectionClass $reflection): DiscoveredStructure 20 | { 21 | if (! $reflection->isTrait()) { 22 | throw InvalidReflection::expectedTrait(); 23 | } 24 | 25 | return new self( 26 | name: $reflection->getShortName(), 27 | file: $reflection->getFileName(), 28 | namespace: $reflection->getNamespaceName() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Data/StructureHeadData.php: -------------------------------------------------------------------------------- 1 | $extends 9 | * @param array $implements 10 | */ 11 | public function __construct( 12 | public array $extends, 13 | public array $implements, 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Data/Usage.php: -------------------------------------------------------------------------------- 1 | name ??= $this->resolveNonFcqnName($this->fcqn); 12 | } 13 | 14 | public function resolveNonFcqnName(string $fcqn): string 15 | { 16 | $position = strrpos($fcqn, '\\'); 17 | 18 | if ($position === false) { 19 | return $fcqn; 20 | } 21 | 22 | return substr($fcqn, $position + strlen('\\')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Discover.php: -------------------------------------------------------------------------------- 1 | $directories, 32 | ]); 33 | } 34 | 35 | return new self( 36 | directories: $directories, 37 | ); 38 | } 39 | 40 | /** 41 | * @param array $directories 42 | * @param array $ignoredFiles 43 | */ 44 | public function __construct( 45 | array $directories = [], 46 | array $ignoredFiles = [], 47 | ExactDiscoverCondition $conditions = new ExactDiscoverCondition(), 48 | bool $full = false, 49 | DiscoverWorker $worker = new SynchronousDiscoverWorker(), 50 | ?DiscoverCacheDriver $cacheDriver = null, 51 | ?string $cacheId = null, 52 | bool $withChains = true, 53 | ?Sort $sort = null, 54 | bool $reverseSorting = false, 55 | StructureParser $structureParser = new PhpTokenStructureParser(), 56 | ?string $reflectionBasePath = null, 57 | ?string $reflectionRootNamespace = null, 58 | ) { 59 | $this->config = new DiscoverProfileConfig( 60 | directories: $directories, 61 | ignoredFiles: $ignoredFiles, 62 | full: $full, 63 | worker: $worker, 64 | cacheDriver: $cacheDriver, 65 | cacheId: $cacheId, 66 | withChains: $withChains, 67 | conditions: $conditions, 68 | sort: $sort, 69 | reverseSorting: $reverseSorting, 70 | structureParser: $structureParser, 71 | reflectionBasePath: $reflectionBasePath, 72 | reflectionRootNamespace: $reflectionRootNamespace, 73 | ); 74 | } 75 | 76 | public function inDirectories(string ...$directories): self 77 | { 78 | array_push($this->config->directories, ...$directories); 79 | 80 | return $this; 81 | } 82 | 83 | public function ignoreFiles(string ...$ignoredFiles): self 84 | { 85 | array_push($this->config->ignoredFiles, ...$ignoredFiles); 86 | 87 | return $this; 88 | } 89 | 90 | public function sortBy(Sort $sort, bool $reverse = false): self 91 | { 92 | $this->config->sort = $sort; 93 | $this->config->reverseSorting = $reverse; 94 | 95 | return $this; 96 | } 97 | 98 | public function full(): self 99 | { 100 | $this->config->full = true; 101 | 102 | return $this; 103 | } 104 | 105 | public function usingWorker(DiscoverWorker $worker): self 106 | { 107 | $this->config->worker = $worker; 108 | 109 | return $this; 110 | } 111 | 112 | public function parallel(int $filesPerJob = 50): self 113 | { 114 | return $this->usingWorker(new ParallelDiscoverWorker($filesPerJob)); 115 | } 116 | 117 | public function withCache(string $id, ?DiscoverCacheDriver $cache = null): self 118 | { 119 | $this->config->cacheId = $id; 120 | 121 | if ($this->config->cacheDriver === null && $cache === null) { 122 | throw new NoCacheConfigured(); 123 | } 124 | 125 | $this->config->cacheDriver = $cache; 126 | 127 | return $this; 128 | } 129 | 130 | public function withoutChains(bool $withoutChains = true): self 131 | { 132 | $this->config->withChains = ! $withoutChains; 133 | 134 | return $this; 135 | } 136 | 137 | public function useReflection(?string $basePath = null, ?string $rootNamespace = null): self 138 | { 139 | $this->config->structureParser = new ReflectionStructureParser($this->config); 140 | $this->config->reflectionBasePath = $basePath; 141 | $this->config->reflectionRootNamespace = $rootNamespace; 142 | 143 | return $this; 144 | } 145 | 146 | /** @return array|array */ 147 | public function get(): array 148 | { 149 | if (! $this->config->shouldUseCache()) { 150 | return $this->getWithoutCache(); 151 | } 152 | 153 | return $this->config->cacheDriver->has($this->config->cacheId) 154 | ? $this->config->cacheDriver->get($this->config->cacheId) 155 | : $this->cache(); 156 | } 157 | 158 | /** @return array|array */ 159 | public function getWithoutCache(): array 160 | { 161 | $discoverer = new StructuresResolver($this->config->worker); 162 | 163 | return $discoverer->execute($this); 164 | } 165 | 166 | /** @return array|array */ 167 | public function cache(): array 168 | { 169 | $structures = $this->getWithoutCache(); 170 | 171 | $this->config->cacheDriver->put( 172 | $this->config->cacheId, 173 | $structures 174 | ); 175 | 176 | return $structures; 177 | } 178 | 179 | public function conditionsStore(): ExactDiscoverCondition 180 | { 181 | return $this->config->conditions; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/DiscoverConditions/AnyDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | add($condition); 17 | } 18 | } 19 | 20 | public function add(DiscoverCondition|HasConditions $condition): static 21 | { 22 | $this->conditions[] = $condition instanceof HasConditions 23 | ? $condition->conditionsStore() 24 | : $condition; 25 | 26 | return $this; 27 | } 28 | 29 | public function satisfies(DiscoveredStructure $discoveredData): bool 30 | { 31 | if (empty($this->conditions)) { 32 | return true; 33 | } 34 | 35 | foreach ($this->conditions as $condition) { 36 | if ($condition->satisfies($discoveredData)) { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DiscoverConditions/AttributeDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | classes = $classes; 20 | } 21 | 22 | public function satisfies(DiscoveredStructure $discoveredData): bool 23 | { 24 | $hasAttributes = $discoveredData instanceof DiscoveredInterface 25 | || $discoveredData instanceof DiscoveredEnum 26 | || $discoveredData instanceof DiscoveredClass; 27 | 28 | if (! $hasAttributes) { 29 | return false; 30 | } 31 | 32 | $foundAttributes = array_filter( 33 | $discoveredData->attributes, 34 | fn (DiscoveredAttribute $attribute) => in_array($attribute->class, $this->classes) 35 | ); 36 | 37 | return count($foundAttributes) > 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DiscoverConditions/CustomDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | closure)($discoveredData); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/DiscoverConditions/DiscoverCondition.php: -------------------------------------------------------------------------------- 1 | add($condition); 17 | } 18 | } 19 | 20 | public function add(DiscoverCondition|HasConditions $condition): static 21 | { 22 | $this->conditions[] = $condition instanceof HasConditions 23 | ? $condition->conditionsStore() 24 | : $condition; 25 | 26 | return $this; 27 | } 28 | 29 | public function satisfies(DiscoveredStructure $discoveredData): bool 30 | { 31 | foreach ($this->conditions as $condition) { 32 | if (! $condition->satisfies($discoveredData)) { 33 | return false; 34 | } 35 | } 36 | 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DiscoverConditions/ExtendsDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | classes = $classes; 16 | } 17 | 18 | public function satisfies(DiscoveredStructure $discoveredData): bool 19 | { 20 | if ($discoveredData instanceof DiscoveredClass) { 21 | $extends = $discoveredData->extends === null 22 | ? [] 23 | : [$discoveredData->extends]; 24 | 25 | $foundExtends = array_filter( 26 | $discoveredData->extendsChain ?? $extends, 27 | fn (string $class) => in_array($class, $this->classes) 28 | ); 29 | 30 | return count($foundExtends) > 0; 31 | } 32 | 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/DiscoverConditions/ExtendsWithoutChainDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | classes = $classes; 16 | } 17 | 18 | public function satisfies(DiscoveredStructure $discoveredData): bool 19 | { 20 | if ($discoveredData instanceof DiscoveredClass) { 21 | return in_array($discoveredData->extends, $this->classes); 22 | } 23 | 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DiscoverConditions/ImplementsDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | interfaces = $interfaces; 19 | } 20 | 21 | public function satisfies(DiscoveredStructure $discoveredData): bool 22 | { 23 | if ($discoveredData instanceof DiscoveredClass || $discoveredData instanceof DiscoveredEnum) { 24 | $foundImplements = array_filter( 25 | $discoveredData->implementsChain ?? $discoveredData->implements, 26 | fn (string $interface) => in_array($interface, $this->interfaces) 27 | ); 28 | 29 | return count($foundImplements) > 0; 30 | } 31 | 32 | if ($discoveredData instanceof DiscoveredInterface) { 33 | $foundExtends = array_filter( 34 | $discoveredData->extendsChain ?? $discoveredData->extends, 35 | fn (string $class) => in_array($class, $this->interfaces) 36 | ); 37 | 38 | return count($foundExtends) > 0; 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DiscoverConditions/ImplementsWithoutChainDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | interfaces = $interfaces; 19 | } 20 | 21 | public function satisfies(DiscoveredStructure $discoveredData): bool 22 | { 23 | if ($discoveredData instanceof DiscoveredClass || $discoveredData instanceof DiscoveredEnum) { 24 | $foundImplements = array_filter( 25 | $discoveredData->implements, 26 | fn (string $interface) => in_array($interface, $this->interfaces) 27 | ); 28 | 29 | return count($foundImplements) > 0; 30 | } 31 | 32 | if ($discoveredData instanceof DiscoveredInterface) { 33 | $foundExtends = array_filter( 34 | $discoveredData->extends, 35 | fn (string $class) => in_array($class, $this->interfaces) 36 | ); 37 | 38 | return count($foundExtends) > 0; 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DiscoverConditions/NameDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | names = $names; 15 | } 16 | 17 | public function satisfies(DiscoveredStructure $discoveredData): bool 18 | { 19 | return in_array($discoveredData->name, $this->names); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/DiscoverConditions/TypeDiscoverCondition.php: -------------------------------------------------------------------------------- 1 | types = $types; 16 | } 17 | 18 | public function satisfies(DiscoveredStructure $discoveredData): bool 19 | { 20 | return in_array($discoveredData->getType(), $this->types); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DiscoverWorkers/DiscoverWorker.php: -------------------------------------------------------------------------------- 1 | $filenames 13 | * @param DiscoverProfileConfig $config 14 | * 15 | * @return array 16 | */ 17 | public function run(Collection $filenames, DiscoverProfileConfig $config): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/DiscoverWorkers/ParallelDiscoverWorker.php: -------------------------------------------------------------------------------- 1 | $filenames 21 | * @param DiscoverProfileConfig $config 22 | * 23 | * @return array 24 | */ 25 | public function run(Collection $filenames, DiscoverProfileConfig $config): array 26 | { 27 | $sets = $filenames->chunk($this->filesPerJob)->toArray(); 28 | 29 | $found = await(array_map(function ($set) use ($config) { 30 | return async(fn () => $config->structureParser->execute($set)); 31 | }, $sets)); 32 | 33 | return array_merge(...$found); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/DiscoverWorkers/SynchronousDiscoverWorker.php: -------------------------------------------------------------------------------- 1 | $filenames 17 | * @param DiscoverProfileConfig $config 18 | * 19 | * @return array 20 | */ 21 | public function run(Collection $filenames, DiscoverProfileConfig $config): array 22 | { 23 | return $config->structureParser->execute($filenames->toArray()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Enums/DiscoveredEnumType.php: -------------------------------------------------------------------------------- 1 | id) { 24 | T_CLASS => self::ClassDefinition, 25 | T_ENUM => self::Enum, 26 | T_INTERFACE => self::Interface, 27 | T_TRAIT => self::Trait, 28 | default => null, 29 | }; 30 | } 31 | 32 | /** @return class-string */ 33 | public function getDataClass(): string 34 | { 35 | return match ($this) { 36 | self::ClassDefinition => DiscoveredClass::class, 37 | self::Enum => DiscoveredEnum::class, 38 | self::Interface => DiscoveredInterface::class, 39 | self::Trait => DiscoveredTrait::class, 40 | default => throw new Exception('Unknown type'), 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Enums/Sort.php: -------------------------------------------------------------------------------- 1 | $finder->sortByName(), 22 | self::Size => $finder->sortBySize(), 23 | self::Type => $finder->sortByType(), 24 | self::Extension => $finder->sortByExtension(), 25 | self::ChangedTime => $finder->sortByChangedTime(), 26 | self::ModifiedTime => $finder->sortByModifiedTime(), 27 | self::AccessedTime => $finder->sortByAccessedTime(), 28 | self::CaseInsensitiveName => $finder->sortByCaseInsensitiveName(), 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exceptions/CouldNotParseFile.php: -------------------------------------------------------------------------------- 1 | getMessage()}", 16 | previous: $previous, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidReflection.php: -------------------------------------------------------------------------------- 1 | name('structure-discoverer') 17 | ->hasConfigFile() 18 | ->hasCommand(CacheStructureScoutsCommand::class) 19 | ->hasCommand(ClearStructureScoutsCommand::class); 20 | } 21 | 22 | public function packageRegistered(): void 23 | { 24 | $this->app->bind(Discover::class, fn ($app, $provided) => new Discover( 25 | directories: $provided['directories'] ?? [], 26 | ignoredFiles: config('structure-discoverer.ignored_files'), 27 | cacheDriver: DiscoverCacheDriverFactory::create(config('structure-discoverer.cache')), 28 | )); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/StructureParsers/PhpTokenStructureParser.php: -------------------------------------------------------------------------------- 1 | execute($filenames); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/StructureParsers/ReflectionStructureParser.php: -------------------------------------------------------------------------------- 1 | fullQualifiedClassNameFromFile($filename); 27 | 28 | try { 29 | $reflectionClass = new ReflectionClass($fqcn); 30 | 31 | if ($reflectionClass->isEnum()) { 32 | $discovered[] = DiscoveredEnum::fromReflection( 33 | $reflectionClass instanceof ReflectionEnum ? $reflectionClass : new ReflectionEnum($reflectionClass->name) 34 | ); 35 | } 36 | 37 | if ($reflectionClass->isInterface()) { 38 | $discovered[] = DiscoveredInterface::fromReflection($reflectionClass); 39 | } 40 | 41 | if ($reflectionClass->isTrait()) { 42 | $discovered[] = DiscoveredTrait::fromReflection($reflectionClass); 43 | } 44 | 45 | $discovered[] = DiscoveredClass::fromReflection($reflectionClass); 46 | } catch (Throwable $e) { 47 | continue; 48 | } 49 | } 50 | 51 | return $discovered; 52 | } 53 | 54 | /** @return class-string */ 55 | protected function fullQualifiedClassNameFromFile(string $filename): string 56 | { 57 | $class = preg_replace( 58 | pattern: "#".preg_quote($this->config->reflectionBasePath)."#", 59 | replacement: '', 60 | subject: $filename, 61 | limit: 1 62 | ); 63 | 64 | $class = trim($class, DIRECTORY_SEPARATOR); 65 | 66 | $class = str_replace( 67 | [DIRECTORY_SEPARATOR, 'App\\'], 68 | ['\\', app()->getNamespace()], 69 | ucfirst(str_replace('.php', '', $class)) 70 | ); 71 | 72 | $rootNamespace = $this->config->reflectionRootNamespace === null || str_ends_with($this->config->reflectionRootNamespace, '\\') 73 | ? $this->config->reflectionRootNamespace 74 | : $this->config->reflectionRootNamespace.'\\'; 75 | 76 | /** @var class-string $fqcn */ 77 | $fqcn = $rootNamespace.$class; 78 | 79 | return $fqcn; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/StructureParsers/StructureParser.php: -------------------------------------------------------------------------------- 1 | $filenames 11 | * 12 | * @return array 13 | */ 14 | public function execute(array $filenames): array; 15 | } 16 | -------------------------------------------------------------------------------- /src/StructureScout.php: -------------------------------------------------------------------------------- 1 | |array 36 | */ 37 | public function get(): array 38 | { 39 | return $this->definition() 40 | ->withCache($this->identifier(), $this->cacheDriver()) 41 | ->get(); 42 | } 43 | 44 | /** 45 | * @return array|array 46 | */ 47 | public function cache(): array 48 | { 49 | return $this->definition() 50 | ->withCache($this->identifier(), $this->cacheDriver()) 51 | ->cache(); 52 | } 53 | 54 | public function clear(): static 55 | { 56 | $this->cacheDriver()->forget($this->identifier()); 57 | 58 | return $this; 59 | } 60 | 61 | public function isCached(): bool 62 | { 63 | return $this->cacheDriver()->has($this->identifier()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Support/Conditions/ConditionBuilder.php: -------------------------------------------------------------------------------- 1 | conditions; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Support/Conditions/HasConditions.php: -------------------------------------------------------------------------------- 1 | conditionsStore()->add(new NameDiscoverCondition(...$names)); 24 | 25 | return $this; 26 | } 27 | 28 | public function types(DiscoveredStructureType ...$types): self 29 | { 30 | $this->conditionsStore()->add(new TypeDiscoverCondition(...$types)); 31 | 32 | return $this; 33 | } 34 | 35 | public function classes(): self 36 | { 37 | $this->conditionsStore()->add(new TypeDiscoverCondition(DiscoveredStructureType::ClassDefinition)); 38 | 39 | return $this; 40 | } 41 | 42 | public function enums(): self 43 | { 44 | $this->conditionsStore()->add(new TypeDiscoverCondition(DiscoveredStructureType::Enum)); 45 | 46 | return $this; 47 | } 48 | 49 | public function traits(): self 50 | { 51 | $this->conditionsStore()->add(new TypeDiscoverCondition(DiscoveredStructureType::Trait)); 52 | 53 | return $this; 54 | } 55 | 56 | public function interfaces(): self 57 | { 58 | $this->conditionsStore()->add(new TypeDiscoverCondition(DiscoveredStructureType::Interface)); 59 | 60 | return $this; 61 | } 62 | 63 | public function extending(string ...$classOrInterfaces): self 64 | { 65 | $this->conditionsStore()->add(new ExtendsDiscoverCondition(...$classOrInterfaces)); 66 | 67 | return $this; 68 | } 69 | 70 | public function extendingWithoutChain(string ...$classOrInterfaces): self 71 | { 72 | $this->conditionsStore()->add(new ExtendsWithoutChainDiscoverCondition(...$classOrInterfaces)); 73 | 74 | return $this; 75 | } 76 | 77 | public function implementing(string ...$interfaces): self 78 | { 79 | $this->conditionsStore()->add(new ImplementsDiscoverCondition(...$interfaces)); 80 | 81 | return $this; 82 | } 83 | 84 | public function implementingWithoutChain(string ...$interfaces): self 85 | { 86 | $this->conditionsStore()->add(new ImplementsWithoutChainDiscoverCondition(...$interfaces)); 87 | 88 | return $this; 89 | } 90 | 91 | public function withAttribute(string ...$attributes): self 92 | { 93 | $this->conditionsStore()->add(new AttributeDiscoverCondition(...$attributes)); 94 | 95 | return $this; 96 | } 97 | 98 | public function custom(DiscoverCondition|HasConditions|Closure ...$conditions): self 99 | { 100 | foreach ($conditions as $condition) { 101 | $this->conditionsStore()->add( 102 | $condition instanceof Closure 103 | ? new CustomDiscoverCondition($condition) 104 | : $condition 105 | ); 106 | } 107 | 108 | return $this; 109 | } 110 | 111 | public function any(DiscoverCondition|HasConditions ...$conditions): self 112 | { 113 | $this->conditionsStore()->add(new AnyDiscoverCondition(...$conditions)); 114 | 115 | return $this; 116 | } 117 | 118 | public function exact(DiscoverCondition|HasConditions ...$conditions): self 119 | { 120 | $this->conditionsStore()->add(new ExactDiscoverCondition(...$conditions)); 121 | 122 | return $this; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Support/DiscoverCacheDriverFactory.php: -------------------------------------------------------------------------------- 1 | $config 11 | */ 12 | public static function create(array $config): DiscoverCacheDriver 13 | { 14 | /** @var class-string $driverClass */ 15 | $driverClass = $config['driver']; 16 | 17 | $params = $config; 18 | unset($params['driver']); 19 | 20 | return new $driverClass(...$params); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Support/LaravelDetector.php: -------------------------------------------------------------------------------- 1 | $discovered 13 | */ 14 | public function execute(array &$discovered): void 15 | { 16 | foreach ($discovered as $structure) { 17 | if ($structure instanceof DiscoveredClass && $structure->extendsChain === null) { 18 | $this->resolveExtendsChain($discovered, $structure); 19 | } 20 | 21 | if ($structure instanceof DiscoveredClass && $structure->implementsChain === null) { 22 | $this->resolveImplementsChain($discovered, $structure); 23 | } 24 | 25 | if ($structure instanceof DiscoveredEnum && $structure->implementsChain === null) { 26 | $this->resolveImplementsChain($discovered, $structure); 27 | } 28 | 29 | if ($structure instanceof DiscoveredInterface && $structure->extendsChain === null) { 30 | $this->resolveImplementsChain($discovered, $structure); 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * @param array $discovered 37 | */ 38 | private function resolveExtendsChain( 39 | array &$discovered, 40 | DiscoveredClass $structure 41 | ): void { 42 | if ($structure->extends === null) { 43 | $structure->extendsChain = []; 44 | 45 | return; 46 | } 47 | 48 | if (! array_key_exists($structure->extends, $discovered)) { 49 | $structure->extendsChain = [$structure->extends]; 50 | 51 | return; 52 | } 53 | 54 | /** @var DiscoveredClass $extendedStructure */ 55 | $extendedStructure = $discovered[$structure->extends]; 56 | 57 | if ($extendedStructure->extendsChain === null) { 58 | $this->resolveExtendsChain($discovered, $extendedStructure); 59 | } 60 | 61 | $structure->extendsChain = [$structure->extends, ...$extendedStructure->extendsChain]; 62 | } 63 | 64 | /** 65 | * @param array $discovered 66 | */ 67 | private function resolveImplementsChain( 68 | array &$discovered, 69 | DiscoveredClass|DiscoveredEnum|DiscoveredInterface $structure 70 | ): void { 71 | $implements = $structure instanceof DiscoveredInterface 72 | ? $structure->extends 73 | : $structure->implements; 74 | 75 | $chain = $implements; 76 | 77 | foreach ($implements as $implement) { 78 | if (! array_key_exists($implement, $discovered)) { 79 | $chain[] = $implement; 80 | 81 | continue; 82 | } 83 | /** @var DiscoveredInterface $implementedStructure */ 84 | $implementedStructure = $discovered[$implement]; 85 | 86 | if ($implementedStructure->extendsChain === null) { 87 | $this->resolveImplementsChain($discovered, $implementedStructure); 88 | } 89 | 90 | array_push($chain, ...$implementedStructure->extendsChain); 91 | } 92 | 93 | if ($structure instanceof DiscoveredClass 94 | && $structure->extends 95 | && array_key_exists($structure->extends, $discovered) 96 | ) { 97 | /** @var DiscoveredClass $extendedStructure */ 98 | $extendedStructure = $discovered[$structure->extends] ?? null; 99 | 100 | if ($extendedStructure->implementsChain === null) { 101 | $this->resolveImplementsChain($discovered, $extendedStructure); 102 | } 103 | 104 | array_push($chain, ...$extendedStructure->implementsChain); 105 | } 106 | 107 | $chain = array_unique($chain); 108 | 109 | if ($structure instanceof DiscoveredInterface) { 110 | $structure->extendsChain = $chain; 111 | } else { 112 | $structure->implementsChain = $chain; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Support/StructureScoutManager.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function cache(array $directories): array 20 | { 21 | return self::forEachScout($directories, function (StructureScout $discoverer) { 22 | $discoverer->cacheDriver()->forget($discoverer->identifier()); 23 | 24 | $discoverer->cache(); 25 | }); 26 | } 27 | 28 | /** 29 | * @param string[] $directories 30 | * 31 | * @return array 32 | */ 33 | public static function clear(array $directories): array 34 | { 35 | return self::forEachScout($directories, function (StructureScout $discoverer) { 36 | $discoverer->cacheDriver()->forget($discoverer->identifier()); 37 | }); 38 | } 39 | 40 | public static function add(string $scout): void 41 | { 42 | if (in_array($scout, static::$extra)) { 43 | return; 44 | } 45 | 46 | static::$extra[] = $scout; 47 | } 48 | 49 | /** 50 | * @param array $directories 51 | * @param Closure(StructureScout): void $closure 52 | * 53 | * @return array 54 | */ 55 | private static function forEachScout( 56 | array $directories, 57 | Closure $closure 58 | ): array { 59 | /** @var string[] $discoveredScouts */ 60 | $discoveredScouts = Discover::in(...$directories) 61 | ->classes() 62 | ->extending(StructureScout::class) 63 | ->get(); 64 | 65 | $scouts = array_unique([ 66 | ...$discoveredScouts, 67 | ...static::$extra, 68 | ]); 69 | 70 | $touched = []; 71 | 72 | foreach ($scouts as $scout) { 73 | /** @var StructureScout $scout */ 74 | $scout = LaravelDetector::isRunningLaravel() 75 | ? app($scout) 76 | : new $scout(); 77 | 78 | $closure($scout); 79 | 80 | $touched[] = $scout->identifier(); 81 | } 82 | 83 | return $touched; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Support/StructuresResolver.php: -------------------------------------------------------------------------------- 1 | |array */ 22 | public function execute(Discover $profile): array 23 | { 24 | $structures = $this->discover( 25 | $profile->config 26 | ); 27 | 28 | if ($profile->config->withChains) { 29 | $this->structureChainResolver->execute($structures); 30 | } 31 | 32 | $structures = array_filter( 33 | $structures, 34 | fn (DiscoveredStructure $discovered) => $profile->config->conditions->satisfies($discovered) 35 | ); 36 | 37 | if ($profile->config->full === false) { 38 | $structures = array_map( 39 | fn (DiscoveredStructure $discovered) => $discovered->getFcqn(), 40 | $structures 41 | ); 42 | } 43 | 44 | return array_values($structures); 45 | } 46 | 47 | /** @return array */ 48 | protected function discover(DiscoverProfileConfig $config): array 49 | { 50 | if (empty($config->directories)) { 51 | return []; 52 | } 53 | 54 | $finder = (new Finder())->files(); 55 | 56 | if ($config->sort) { 57 | $config->sort->apply($finder); 58 | } 59 | 60 | if ($config->reverseSorting) { 61 | $finder->reverseSorting(); 62 | } 63 | 64 | $files = $finder->in($config->directories); 65 | 66 | $filenames = collect($files) 67 | ->reject(fn (SplFileInfo $file) => in_array($file->getPathname(), $config->ignoredFiles) || $file->getExtension() !== 'php') 68 | ->map(fn (SplFileInfo $file) => $file->getPathname()); 69 | 70 | return $this->discoverWorker->run($filenames, $config); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Support/UseDefinitionsResolver.php: -------------------------------------------------------------------------------- 1 | $token) { 31 | if ($token->is([T_COMMENT, T_DOC_COMMENT, T_WHITESPACE])) { 32 | continue; 33 | } 34 | 35 | if ($token->is(T_USE)) { 36 | $usages->add(...$this->useResolver->execute($i + 1, $tokens)); 37 | } 38 | 39 | if ($token->is([T_CLASS, T_INTERFACE, T_TRAIT, T_FUNCTION])) { 40 | break; 41 | } 42 | } 43 | 44 | return $usages; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/TokenParsers/AttributeTokenParser.php: -------------------------------------------------------------------------------- 1 | */ 17 | public function execute( 18 | int $index, 19 | TokenCollection $tokens, 20 | string $namespace, 21 | UsageCollection $usages, 22 | ): array { 23 | $attributes = []; 24 | 25 | $parenthesisCount = 0; 26 | 27 | do { 28 | $token = $tokens->get($index); 29 | 30 | if ($token->is(T_STRING) && $parenthesisCount === 0) { 31 | $attributes[] = new DiscoveredAttribute( 32 | $this->referenceTokenResolver->execute($token, $namespace, $usages) 33 | ); 34 | } 35 | 36 | if ($token->is(ord('('))) { 37 | $parenthesisCount++; 38 | } 39 | 40 | if ($token->is(ord(')'))) { 41 | $parenthesisCount--; 42 | } 43 | 44 | if ($token->is(ord(']'))) { 45 | break; 46 | } 47 | 48 | $index++; 49 | } while ($index < count($tokens)); 50 | 51 | return $attributes; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/TokenParsers/DiscoveredClassTokenParser.php: -------------------------------------------------------------------------------- 1 | structureHeadResolver->execute($index, $tokens, $namespace, $usages); 29 | 30 | return new DiscoveredClass( 31 | name: $tokens->get($index)->text, 32 | file: $file, 33 | namespace: $namespace, 34 | isFinal: $this->isClassFinal($index, $tokens), 35 | isAbstract: $this->isClassAbstract($index, $tokens), 36 | isReadonly: $this->isClassReadonly($index, $tokens), 37 | extends: $head->extends[0] ?? null, 38 | implements: $head->implements, 39 | attributes: $attributes, 40 | ); 41 | } 42 | 43 | protected function isClassFinal( 44 | int $index, 45 | TokenCollection $tokens, 46 | ): bool { 47 | $token = $tokens->get($index - 2); 48 | 49 | return $token && $token->is(T_FINAL); 50 | } 51 | 52 | protected function isClassReadonly( 53 | int $index, 54 | TokenCollection $tokens, 55 | ): bool { 56 | $token = $tokens->get($index - 2); 57 | 58 | return defined('T_READONLY') && $token && $token->is(T_READONLY); 59 | } 60 | 61 | protected function isClassAbstract( 62 | int $index, 63 | TokenCollection $tokens, 64 | ): bool { 65 | $token = $tokens->get($index - 2); 66 | 67 | return $token && $token->is(T_ABSTRACT); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/TokenParsers/DiscoveredDataTokenParser.php: -------------------------------------------------------------------------------- 1 | $this->discoveredClassResolver->execute( 39 | $index, 40 | $tokens, 41 | $namespace, 42 | $usages, 43 | $attributes, 44 | $file 45 | ), 46 | DiscoveredStructureType::Interface => $this->resolveInterface( 47 | $index, 48 | $tokens, 49 | $namespace, 50 | $usages, 51 | $attributes, 52 | $file 53 | ), 54 | DiscoveredStructureType::Trait => $this->resolveTrait( 55 | $index, 56 | $tokens, 57 | $namespace, 58 | $usages, 59 | $attributes, 60 | $file 61 | ), 62 | DiscoveredStructureType::Enum => $this->discoveredEnumResolver->execute( 63 | $index, 64 | $tokens, 65 | $namespace, 66 | $usages, 67 | $attributes, 68 | $file 69 | ) 70 | }; 71 | } 72 | 73 | /** 74 | * @param DiscoveredAttribute[] $attributes 75 | */ 76 | protected function resolveInterface( 77 | int $index, 78 | TokenCollection $tokens, 79 | string $namespace, 80 | UsageCollection $usages, 81 | array $attributes, 82 | string $file 83 | ): DiscoveredInterface { 84 | $head = $this->structureHeadResolver->execute($index, $tokens, $namespace, $usages); 85 | 86 | return new DiscoveredInterface( 87 | $tokens->get($index)->text, 88 | $file, 89 | $namespace, 90 | $head->extends, 91 | $attributes, 92 | ); 93 | } 94 | 95 | /** 96 | * @param DiscoveredAttribute[] $attributes 97 | */ 98 | protected function resolveTrait( 99 | int $index, 100 | TokenCollection $tokens, 101 | string $namespace, 102 | UsageCollection $usages, 103 | array $attributes, 104 | string $file 105 | ): DiscoveredTrait { 106 | return new DiscoveredTrait( 107 | $tokens->get($index)->text, 108 | $file, 109 | $namespace, 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/TokenParsers/DiscoveredEnumTokenParser.php: -------------------------------------------------------------------------------- 1 | structureHeadResolver->execute($index, $tokens, $namespace, $usages); 30 | 31 | return new DiscoveredEnum( 32 | $tokens->get($index)->text, 33 | $file, 34 | $namespace, 35 | $this->resolveType($index, $tokens), 36 | $head->implements, 37 | $attributes, 38 | ); 39 | } 40 | 41 | protected function resolveType( 42 | int $index, 43 | TokenCollection $tokens, 44 | ): DiscoveredEnumType { 45 | $typeToken = $tokens->get($index + 2); 46 | 47 | if ($typeToken === null 48 | || ! $typeToken->is(T_STRING) 49 | || ! in_array($typeToken->text, ['int', 'string']) 50 | ) { 51 | return DiscoveredEnumType::Unit; 52 | } 53 | 54 | if ($typeToken->text === 'int') { 55 | return DiscoveredEnumType::Int; 56 | } 57 | 58 | if ($typeToken->text === 'string') { 59 | return DiscoveredEnumType::String; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/TokenParsers/FileTokenParser.php: -------------------------------------------------------------------------------- 1 | */ 23 | public function execute( 24 | string $filename, 25 | string $contents, 26 | ): array { 27 | $found = []; 28 | 29 | $tokens = TokenCollection::fromCode($contents); 30 | 31 | if ($tokens->count() === 0) { 32 | return []; 33 | } 34 | 35 | $currentNamespace = ''; 36 | $usages = new UsageCollection(); 37 | $attributes = []; 38 | $structureDefined = false; 39 | 40 | $index = 0; 41 | 42 | try { 43 | do { 44 | if ($tokens->get($index)->is(T_NAMESPACE)) { 45 | $index++; // move to token after 'namespace' 46 | 47 | $currentNamespace = $this->namespaceResolver->execute($index, $tokens); 48 | 49 | continue; 50 | } 51 | 52 | if ($tokens->get($index)->is(T_USE) && $structureDefined === false) { 53 | $usages->add(...$this->useResolver->execute($index + 1, $tokens)); 54 | } 55 | 56 | if ($tokens->get($index)->is(T_ATTRIBUTE)) { 57 | $attributes = [ 58 | ...$attributes, ...$this->attributeResolver->execute( 59 | $index + 1, 60 | $tokens, 61 | $currentNamespace, 62 | $usages, 63 | ), 64 | ]; 65 | } 66 | 67 | $type = DiscoveredStructureType::fromToken($tokens->get($index)); 68 | 69 | if ($type === null) { 70 | $index++; 71 | 72 | continue; 73 | } 74 | 75 | if ( 76 | $type === DiscoveredStructureType::ClassDefinition 77 | && $this->isAnonymousClass($tokens, $index) 78 | ) { 79 | $index++; 80 | 81 | continue; 82 | } 83 | 84 | $discoveredStructure = $this->discoveredDataResolver->execute( 85 | $index + 1, 86 | $tokens, 87 | $currentNamespace, 88 | $usages, 89 | $attributes, 90 | $type, 91 | $filename, 92 | ); 93 | 94 | $found[$discoveredStructure->getFcqn()] = $discoveredStructure; 95 | $attributes = []; 96 | 97 | $structureDefined = true; 98 | 99 | $index++; 100 | } while ($index < count($tokens)); 101 | } catch (Throwable $throwable) { 102 | throw new CouldNotParseFile($filename, $throwable); 103 | } 104 | 105 | return $found; 106 | } 107 | 108 | private function isAnonymousClass(TokenCollection $tokens, int $index): bool 109 | { 110 | $prevIndex = $index - 1; 111 | 112 | // find the previous non-whitespace token 113 | while ($prevIndex >= 0 && $tokens->get($prevIndex)->is(T_WHITESPACE)) { 114 | $prevIndex--; 115 | } 116 | 117 | // if the token before T_CLASS is T_NEW, it's an anonymous class 118 | if ($prevIndex >= 0 && $tokens->get($prevIndex)->is(T_NEW)) { 119 | return true; 120 | } 121 | 122 | return false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/TokenParsers/MultiFileTokenParser.php: -------------------------------------------------------------------------------- 1 | $filenames 16 | * 17 | * @return array 18 | */ 19 | public function execute(array $filenames): array 20 | { 21 | $found = []; 22 | 23 | foreach ($filenames as $filename) { 24 | $contents = file_get_contents($filename) ?: ''; 25 | 26 | foreach ($this->fileTokenParser->execute($filename, $contents) as $fqcn => $structure) { 27 | $found[$fqcn] = $structure; 28 | } 29 | } 30 | 31 | return $found; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/TokenParsers/NamespaceTokenParser.php: -------------------------------------------------------------------------------- 1 | get($index); 12 | 13 | if (defined('T_NAME_QUALIFIED') && $token && $token->is(T_NAME_QUALIFIED)) { 14 | return $token->text; 15 | } 16 | 17 | $parts = []; 18 | 19 | do { 20 | $token = $tokens->get($index); 21 | 22 | if ($token === null || ! $token->is(T_STRING)) { 23 | break; 24 | } 25 | 26 | $parts[] = $token->text; 27 | $index++; 28 | } while ($index < count($tokens)); 29 | 30 | return implode('', $parts); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TokenParsers/ReferenceListTokenParser.php: -------------------------------------------------------------------------------- 1 | */ 16 | public function execute( 17 | int $index, 18 | TokenCollection $tokens, 19 | string $namespace, 20 | UsageCollection $usages, 21 | ): array { 22 | $classes = []; 23 | 24 | do { 25 | $token = $tokens->get($index); 26 | 27 | if ($token === null) { 28 | break; 29 | } 30 | 31 | $classes[] = $this->referenceTokenResolver->execute( 32 | $token, 33 | $namespace, 34 | $usages, 35 | ); 36 | 37 | if (! $tokens->get($index + 1)->is(ord(','))) { 38 | break; 39 | } 40 | 41 | $index += 2; 42 | } while ($tokens->get($index)?->is([ 43 | T_NAME_FULLY_QUALIFIED, 44 | T_NAME_QUALIFIED, 45 | T_STRING, 46 | T_NAME_RELATIVE, 47 | ])); 48 | 49 | return $classes; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/TokenParsers/ReferenceTokenParser.php: -------------------------------------------------------------------------------- 1 | is(T_NAME_FULLY_QUALIFIED)) { 17 | return ltrim($token->text, '\\'); 18 | } 19 | 20 | if ($token->is(T_NAME_QUALIFIED)) { 21 | return "{$namespace}\\{$token->text}"; 22 | } 23 | 24 | if ($token->is(T_STRING)) { 25 | return $usages->findFcqnForIdentifier( 26 | $token->text, 27 | $namespace 28 | ); 29 | } 30 | 31 | if ($token->is(T_NAME_RELATIVE)) { 32 | return str_replace('namespace', $namespace, $token->text); 33 | } 34 | 35 | throw new Exception('Unknown token type'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/TokenParsers/StructureHeadTokenParser.php: -------------------------------------------------------------------------------- 1 | has($index) === false) { 27 | break; 28 | } 29 | 30 | if ($tokens->get($index)->is(T_EXTENDS)) { 31 | $extends = $this->classListResolver->execute( 32 | $index + 1, 33 | $tokens, 34 | $namespace, 35 | $usages, 36 | ); 37 | } 38 | 39 | if ($tokens->get($index)->is(T_IMPLEMENTS)) { 40 | $implements = $this->classListResolver->execute( 41 | $index + 1, 42 | $tokens, 43 | $namespace, 44 | $usages, 45 | ); 46 | } 47 | 48 | if (! $tokens->get($index)->is([ 49 | T_EXTENDS, 50 | T_IMPLEMENTS, 51 | T_STRING, 52 | T_NAME_FULLY_QUALIFIED, 53 | T_NAME_FULLY_QUALIFIED, 54 | T_NAME_RELATIVE, 55 | T_NAME_QUALIFIED, 56 | ]) && $tokens->get($index)->text !== ':') { 57 | break; 58 | } 59 | 60 | $index++; 61 | } 62 | 63 | return new StructureHeadData( 64 | extends: $extends, 65 | implements: $implements, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/TokenParsers/UseTokenParser.php: -------------------------------------------------------------------------------- 1 | get($index + 1)->is(T_AS)) { 19 | $usages[] = new Usage( 20 | $tokens->get($index)->text, 21 | $tokens->get($index + 2)->text 22 | ); 23 | 24 | $index += 3; 25 | } else { 26 | $usages[] = new Usage($tokens->get($index)->text); 27 | 28 | $index += 1; 29 | } 30 | 31 | if ($tokens->get($index)?->is(ord(','))) { 32 | $index += 1; 33 | 34 | continue; 35 | } 36 | 37 | break; 38 | } while ($tokens->get($index)->is([T_NAME_QUALIFIED, T_STRING])); 39 | 40 | return $usages; 41 | } 42 | } 43 | --------------------------------------------------------------------------------