├── .php_cs.dist.php ├── LICENSE.md ├── README.md ├── composer.json └── src ├── ClassnameLazyCollection.php ├── FileLazyCollection.php ├── Lody.php ├── LodyManager.php ├── LodyServiceProvider.php └── Psr4Resolver.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 | '@PSR2' => 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 | 'method_argument_space' => [ 30 | 'on_multiline' => 'ensure_fully_multiline', 31 | 'keep_multiple_spaces_after_comma' => true, 32 | ], 33 | 'single_trait_insert_per_statement' => true, 34 | ]) 35 | ->setFinder($finder); 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Loris Leiva 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 | # 🗄 Lody 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/lorisleiva/lody.svg)](https://packagist.org/packages/lorisleiva/lody) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/lorisleiva/lody/run-tests.yml?branch=main)](https://github.com/lorisleiva/lody/actions?query=workflow%3ATests+branch%3Amain) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/lorisleiva/lody.svg)](https://packagist.org/packages/lorisleiva/lody) 6 | 7 | Load files and classes as lazy collections in Laravel. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | composer require lorisleiva/lody 13 | ``` 14 | 15 | ## Usage 16 | 17 | Lody enables you to fetch all existing PHP classes of a provided path (or array of paths) that are relative to your application's base path. It returns a custom `LazyCollection` with helpful methods so that you can filter classes even further based on your own requirement. For example, the code below will fetch all non-abstract instances of `Node` within the given path recursively and register each of them. 18 | 19 | ``` php 20 | use Lorisleiva\Lody\Lody; 21 | 22 | Lody::classes('app/Workflow/Nodes') 23 | ->isNotAbstract() 24 | ->isInstanceOf(Node::class) 25 | ->each(fn (string $classname) => $this->register($classname)); 26 | ``` 27 | 28 | If you want all files instead of existing PHP classes, you may use `Lody::files` instead. 29 | 30 | ``` php 31 | use Lorisleiva\Lody\Lody; 32 | 33 | Lody::files('app/Workflow/Nodes') 34 | ->each(fn (SplFileInfo $file) => $this->register($file)); 35 | ``` 36 | 37 | ## Configuration 38 | 39 | ### Resolving paths 40 | 41 | When providing paths to the `Lody::files` or `Lody::classes` methods, Lody will automatically assume these paths are within the root of your application unless they start with a slash in which case they are left untouched. 42 | 43 | You may configure this logic by calling the `Lody::resolvePathUsing` method on one of your service providers. The example below provides the default logic. 44 | 45 | ```php 46 | Lody::resolvePathUsing(function (string $path) { 47 | return Str::startsWith($path, DIRECTORY_SEPARATOR) ? $path : base_path($path); 48 | }); 49 | ``` 50 | 51 | ### Resolving classnames 52 | 53 | When using the `Lody::classes` method, Lody will transform your filenames into classnames **by following PSR-4 conventions**. For example, if your filename is `app/Models/User.php` and you have mapped the `App` namespace to the `app` directory in your `composer.json` file, the it will be resolved to `App\Models\User`. 54 | 55 | By default, the classname resolution takes into account every single PSR-4 mapping as defined in your `vendor/composer/autoload_psr4.php` file. This means, it will even resolves classes that live in your vendor directory properly. 56 | 57 | If your PSR-4 autoload file is located elsewhere, you may configure it by calling the `Lody::setAutoloadPath` method on one of your service providers. 58 | 59 | ```php 60 | Lody::setAutoloadPath('my/custom/autoload_psr4.php'); 61 | ``` 62 | 63 | Alternatively, you may override this logic entirely by calling the `Lody::resolveClassnameUsing` method. The example below provides a useful example for Laravel applications. 64 | 65 | ```php 66 | Lody::resolveClassnameUsing(function (SplFileInfo $file) { 67 | $classnameFromAppPath = str_replace( 68 | ['/', '.php'], 69 | ['\\', ''], 70 | Str::after($file->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR) 71 | ); 72 | 73 | return app()->getNamespace() . $classnameFromAppPath; 74 | }); 75 | ``` 76 | 77 | ### Using Lody without Laravel 78 | 79 | Lody works out of the box with Laravel because we can use the `base_path` method to access the root of your project. 80 | 81 | However, if you wish to use Lody without Laravel, you may simply provide the base path of your application explicitely using the `Lody::setBasePath` method. 82 | 83 | ```php 84 | // Assuming this is executed at the root of your project. 85 | Lody::setBasePath(__DIR__); 86 | ``` 87 | 88 | ## References 89 | 90 | ### Lody 91 | 92 | ```php 93 | // All return an instance of FileLazyCollection (see below). 94 | Lody::files('app/Actions'); 95 | Lody::files(['app/Auth/Actions', 'app/Billing/Actions']); 96 | Lody::files('app/Actions', recursive: false); // Non-recursively. 97 | Lody::files('app/Actions', hidden: true); // Includes dot files. 98 | Lody::filesFromFinder(Finder::create()->files()->in(app_path('Actions'))->depth(1)); // With custom finder. 99 | 100 | // All return an instance of ClassnameLazyCollection (see below). 101 | Lody::classes('app/Actions'); 102 | Lody::classes(['app/Auth/Actions', 'app/Billing/Actions']); 103 | Lody::classes('app/Actions', recursive: false); // Non-recursively. 104 | Lody::classesFromFinder(Finder::create()->files()->in(app_path('Actions'))->depth(1)); // With custom finder. 105 | 106 | // Registering custom resolvers. 107 | Lody::resolvePathUsing(fn(string $path) => ...); 108 | Lody::resolveClassnameUsing(fn(SplFileInfo $file) => ...); 109 | ``` 110 | 111 | ### FileLazyCollection 112 | 113 | ```php 114 | // Transforms files into classnames and returns a `ClassnameLazyCollection`. 115 | // Note that these can still be invalid classes. See `classExists` below. 116 | Lody::files('...')->getClassnames(); 117 | ``` 118 | 119 | ### ClassnameLazyCollection 120 | 121 | ```php 122 | // The `classExists` rejects all classnames that do not reference a valid PHP class. 123 | Lody::files('...')->getClassnames()->classExists(); 124 | 125 | // Note that this is equivalent to the line above. 126 | Lody::classes('...'); 127 | 128 | // Filter abstract classes. 129 | Lody::classes('...')->isAbstract(); 130 | Lody::classes('...')->isNotAbstract(); 131 | 132 | // Filter classes based on inheritance. 133 | Lody::classes('...')->isInstanceOf(SomeClassOrInterface::class); 134 | Lody::classes('...')->isNotInstanceOf(SomeClassOrInterface::class); 135 | 136 | // Filter classes based on traits. 137 | Lody::classes('...')->hasTrait(SomeTrait::class); 138 | Lody::classes('...')->hasTrait(SomeTrait::class, recursive: false); // Don't include recursive traits. 139 | Lody::classes('...')->doesNotHaveTrait(SomeTrait::class); 140 | Lody::classes('...')->doesNotHaveTrait(SomeTrait::class, recursive: false); // Don't include recursive traits. 141 | 142 | // Filter classes based on method it contains or not. 143 | Lody::classes('...')->hasMethod('someMethod'); 144 | Lody::classes('...')->hasStaticMethod('someMethod'); // Ensures the method is static. 145 | Lody::classes('...')->hasNonStaticMethod('someMethod'); // Ensures the method is non-static. 146 | Lody::classes('...')->doesNotHaveMethod('someMethod'); 147 | ``` 148 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lorisleiva/lody", 3 | "description": "Load files and classes as lazy collections in Laravel.", 4 | "keywords": [ 5 | "laravel", 6 | "load", 7 | "files", 8 | "classes", 9 | "collection" 10 | ], 11 | "homepage": "https://github.com/lorisleiva/lody", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Loris Leiva", 16 | "email": "loris.leiva@gmail.com", 17 | "homepage": "https://lorisleiva.com", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.1", 23 | "illuminate/contracts": "^10.0|^11.0|^12.0" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^10.0", 27 | "pestphp/pest": "^2.34|^3.0", 28 | "phpunit/phpunit": "^10.5|^11.5" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Lorisleiva\\Lody\\": "src" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Lorisleiva\\Lody\\Tests\\": "tests" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "vendor/bin/phpunit --colors=always", 42 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 43 | }, 44 | "config": { 45 | "sort-packages": true, 46 | "allow-plugins": { 47 | "pestphp/pest-plugin": true 48 | } 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "Lorisleiva\\Lody\\LodyServiceProvider" 54 | ], 55 | "aliases": { 56 | "Lody": "Lorisleiva\\Lody\\Lody" 57 | } 58 | } 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true, 62 | "funding": [ 63 | { 64 | "type": "github", 65 | "url": "https://github.com/sponsors/lorisleiva" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /src/ClassnameLazyCollection.php: -------------------------------------------------------------------------------- 1 | filter( 14 | fn (string $classname) => class_exists($classname) 15 | )->values(); 16 | } 17 | 18 | public function isAbstract(bool $expected = true): static 19 | { 20 | return $this->filter( 21 | fn (string $classname) => (new ReflectionClass($classname))->isAbstract() === $expected 22 | )->values(); 23 | } 24 | 25 | public function isNotAbstract(): static 26 | { 27 | return $this->isAbstract(expected: false); 28 | } 29 | 30 | public function isInstanceOf(string $superclass, bool $expected = true): static 31 | { 32 | return $this->filter( 33 | fn (string $classname) => is_subclass_of($classname, $superclass) === $expected 34 | )->values(); 35 | } 36 | 37 | public function isNotInstanceOf(string $superclass): static 38 | { 39 | return $this->isInstanceOf($superclass, expected: false); 40 | } 41 | 42 | public function hasTrait(string $trait, bool $recursive = true, bool $expected = true): static 43 | { 44 | return $this->filter(function (string $classname) use ($trait, $recursive, $expected) { 45 | $usedTraits = $recursive ? class_uses_recursive($classname) : class_uses($classname); 46 | 47 | return in_array($trait, $usedTraits) === $expected; 48 | })->values(); 49 | } 50 | 51 | public function doesNotHaveTrait($trait, bool $recursive = true): static 52 | { 53 | return $this->hasTrait($trait, $recursive, expected: false); 54 | } 55 | 56 | public function hasMethod(string $method, ?bool $static = null, bool $expected = true): static 57 | { 58 | return $this->filter(function (string $classname) use ($method, $static, $expected) { 59 | if (! method_exists($classname, $method)) { 60 | return ! $expected; 61 | } 62 | 63 | $staticConstraint = is_null($static) || (new ReflectionMethod($classname, $method))->isStatic() === $static; 64 | 65 | return $expected && $staticConstraint; 66 | })->values(); 67 | } 68 | 69 | public function hasStaticMethod($method): static 70 | { 71 | return $this->hasMethod($method, static: true); 72 | } 73 | 74 | public function hasNonStaticMethod($method): static 75 | { 76 | return $this->hasMethod($method, static: false); 77 | } 78 | 79 | public function doesNotHaveMethod(string $method): static 80 | { 81 | return $this->hasMethod($method, expected: false); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/FileLazyCollection.php: -------------------------------------------------------------------------------- 1 | map( 13 | fn (SplFileInfo $file) => Lody::resolveClassname($file) 14 | ); 15 | 16 | return ClassnameLazyCollection::make($source); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lody.php: -------------------------------------------------------------------------------- 1 | files($paths, $recursive) 21 | ->getClassnames() 22 | ->classExists(); 23 | } 24 | 25 | public function classesFromFinder(Finder $finder): ClassnameLazyCollection 26 | { 27 | return $this->filesFromFinder($finder) 28 | ->getClassnames() 29 | ->classExists(); 30 | } 31 | 32 | public function files(array | string $paths, bool $recursive = true, bool $hidden = false): FileLazyCollection 33 | { 34 | $finder = Finder::create() 35 | ->in($this->resolvePaths($paths)) 36 | ->ignoreDotFiles(! $hidden) 37 | ->sortByName() 38 | ->files(); 39 | 40 | if (! $recursive) { 41 | $finder->depth(0); 42 | } 43 | 44 | return $this->filesFromFinder($finder); 45 | } 46 | 47 | public function filesFromFinder(Finder $finder): FileLazyCollection 48 | { 49 | return FileLazyCollection::make(function () use ($finder) { 50 | foreach ($finder as $file) { 51 | yield $file; 52 | } 53 | }); 54 | } 55 | 56 | public function resolvePathUsing(Closure $callback): static 57 | { 58 | $this->pathResolver = $callback; 59 | 60 | return $this; 61 | } 62 | 63 | public function resolvePath(string $path): string 64 | { 65 | if ($resolver = $this->pathResolver) { 66 | return $resolver($path); 67 | } 68 | 69 | $startsWithDirectorySeparator = Str::startsWith($path, ['/', '\\']); 70 | $startsWithWindowsDisk = (bool) preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path); 71 | 72 | if ($startsWithDirectorySeparator || $startsWithWindowsDisk) { 73 | return $path; 74 | } 75 | 76 | return $this->getBasePath($path); 77 | } 78 | 79 | public function resolveClassnameUsing(Closure $callback): static 80 | { 81 | $this->classnameResolver = $callback; 82 | 83 | return $this; 84 | } 85 | 86 | public function resolveClassname(SplFileInfo $file): string 87 | { 88 | if ($resolver = $this->classnameResolver) { 89 | return $resolver($file); 90 | } 91 | 92 | /** @var Psr4Resolver $psr4Resolver */ 93 | $psr4Resolver = app(Psr4Resolver::class); 94 | 95 | return $psr4Resolver->resolve($file->getRealPath()); 96 | } 97 | 98 | public function setBasePath(string $basePath): static 99 | { 100 | $this->basePath = $basePath; 101 | 102 | return $this; 103 | } 104 | 105 | public function getBasePath(string $path = ''): string 106 | { 107 | $basePath = $this->basePath ?? base_path(); 108 | 109 | return $basePath.($path ? DIRECTORY_SEPARATOR.$path : $path); 110 | } 111 | 112 | public function setAutoloadPath(string $autoloadPath): static 113 | { 114 | $this->autoloadPath = $autoloadPath; 115 | 116 | return $this; 117 | } 118 | 119 | public function getAutoloadPath(): string 120 | { 121 | return $this->autoloadPath 122 | ?? $this->getBasePath('vendor/composer/autoload_psr4.php'); 123 | } 124 | 125 | protected function resolvePaths(array | string $paths): array 126 | { 127 | return Collection::wrap($paths) 128 | ->map(fn (string $path) => $this->resolvePath($path)) 129 | ->unique() 130 | ->filter(fn (string $path) => is_dir($path)) 131 | ->values() 132 | ->toArray(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/LodyServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(LodyManager::class); 12 | $this->app->singleton(Psr4Resolver::class); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Psr4Resolver.php: -------------------------------------------------------------------------------- 1 | lody = $lody; 17 | } 18 | 19 | public function resolve(string $filename): string 20 | { 21 | [$pathPrefix, $classPrefix] = $this->findPrefixes($filename); 22 | 23 | return Str::of($filename) 24 | ->after($pathPrefix) 25 | ->beforeLast('.php') 26 | ->replace('/', '\\') 27 | ->prepend($classPrefix); 28 | } 29 | 30 | public function findPrefixes(string $filename): array 31 | { 32 | $directory = $this->getPsr4Dictionary(); 33 | $fragments = array_reverse(explode(DIRECTORY_SEPARATOR, $filename)); 34 | $accumulator = $filename; 35 | 36 | foreach ($fragments as $fragment) { 37 | $accumulator = Str::beforeLast($accumulator, DIRECTORY_SEPARATOR.$fragment); 38 | 39 | if ($classPrefix = ($directory[$accumulator] ?? false)) { 40 | return [realpath($accumulator).DIRECTORY_SEPARATOR, $classPrefix]; 41 | } 42 | } 43 | 44 | return ['', '']; 45 | } 46 | 47 | public function getPsr4Namespaces(): array 48 | { 49 | return require($this->lody->getAutoloadPath()); 50 | } 51 | 52 | public function getPsr4Dictionary(): array 53 | { 54 | if (! $this->psr4Loaded) { 55 | $this->loadPsr4Dictionary(); 56 | } 57 | 58 | return $this->psr4Dictionary; 59 | } 60 | 61 | public function loadPsr4Dictionary(): void 62 | { 63 | foreach ($this->getPsr4Namespaces() as $classPrefix => $paths) { 64 | $this->add($classPrefix, $paths); 65 | } 66 | 67 | $this->psr4Loaded = true; 68 | } 69 | 70 | public function add(string $classPrefix, string | array $paths): void 71 | { 72 | foreach (Arr::wrap($paths) as $path) { 73 | $this->psr4Dictionary[$this->unifyDirectorySeparator($path)] = $classPrefix; 74 | } 75 | } 76 | 77 | protected function unifyDirectorySeparator(string $path): string 78 | { 79 | if (DIRECTORY_SEPARATOR !== '\\') { 80 | return $path; 81 | } 82 | 83 | return str_replace('/', '\\', $path); 84 | } 85 | } 86 | --------------------------------------------------------------------------------