Passed
Push — main ( 8155a4...c1c15d )
by Chema
03:45
created

Gacela::createRecursiveIterator()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 10
c 1
b 1
f 0
dl 0
loc 21
rs 9.9332
ccs 0
cts 0
cp 0
cc 3
nc 1
nop 0
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Framework;
6
7
use Closure;
8
use Gacela\Framework\Bootstrap\GacelaConfig;
9
use Gacela\Framework\Bootstrap\SetupGacela;
10
use Gacela\Framework\Bootstrap\SetupGacelaInterface;
11
use Gacela\Framework\ClassResolver\AbstractClassResolver;
12
use Gacela\Framework\ClassResolver\Cache\GacelaFileCache;
13
use Gacela\Framework\ClassResolver\Cache\InMemoryCache;
14
use Gacela\Framework\ClassResolver\ClassResolverCache;
15
use Gacela\Framework\ClassResolver\GlobalInstance\AnonymousGlobal;
16
use Gacela\Framework\Config\Config;
17
use Gacela\Framework\Config\ConfigFactory;
18
use Gacela\Framework\Container\Container;
19
use Gacela\Framework\Container\Locator;
20
use Gacela\Framework\DocBlockResolver\DocBlockResolverCache;
21
use Gacela\Framework\Exception\GacelaNotBootstrappedException;
22
23
use RecursiveCallbackFilterIterator;
24
use RecursiveDirectoryIterator;
25
use RecursiveIteratorIterator;
26
27
use SplFileInfo;
28
29
use function is_string;
30
use function sprintf;
31
32
final class Gacela
33
{
34
    private const GACELA_PHP_FILENAME = 'gacela.php';
35
36
    private static ?Container $mainContainer = null;
37 106
38
    private static ?string $appRootDir = null;
39 106
40 106
    /**
41
     * Define the entry point of Gacela.
42 106
     *
43
     * @param null|Closure(GacelaConfig):void $configFn
44 106
     */
45 58
    public static function bootstrap(string $appRootDir, Closure $configFn = null): void
46
    {
47
        self::$appRootDir = $appRootDir;
48 106
        self::$mainContainer = null;
49 106
50 106
        $setup = self::processConfigFnIntoSetup($configFn);
51
52 106
        if ($setup->shouldResetInMemoryCache()) {
53
            self::resetCache();
54
        }
55
56
        self::addModuleBindingsToSetup($setup);
57
58
        $config = Config::createWithSetup($setup);
59
        $config->setAppRootDir($appRootDir)
60
            ->init();
61
62 7
        self::runPlugins($config);
63
    }
64 7
65
    /**
66
     * @template T
67
     *
68
     * @param class-string<T> $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
69
     *
70 38
     * @return T|null
71
     */
72 38
    public static function get(string $className): mixed
73 1
    {
74
        return Locator::getSingleton($className, self::$mainContainer);
75 37
    }
76
77
    /**
78
     * Get the application root dir set when bootstrapping gacela
79
     */
80
    public static function rootDir(): string
81 106
    {
82
        if (self::$appRootDir === null) {
83 106
            throw new GacelaNotBootstrappedException();
84 74
        }
85
86
        return self::$appRootDir;
87 32
    }
88 32
89 32
    /**
90 32
     * Add an anonymous class as 'Config', 'Factory' or 'Provider' as a global resource
91 32
     * bound to the context that it is passed as second argument.
92 32
     *
93
     * @param object|string $context It can be the string-key (file path) or the class/object itself.
94 32
     *                               If empty then the caller's file will be use
95
     */
96
    public static function addGlobal(object $resolvedClass, object|string $context = ''): void
97
    {
98 32
        if (is_string($context) && is_file($context)) {
99
            $context = basename($context, '.php');
100
        } elseif ($context === '') {
101 58
            // Use the caller's file as context
102
            $context = basename(debug_backtrace()[0]['file'] ?? __FILE__, '.php');
103 58
        }
104 58
105 58
        AnonymousGlobal::addGlobal($context, $resolvedClass);
106 58
    }
107 58
108 58
    public static function overrideExistingResolvedClass(string $className, object $resolvedClass): void
109 58
    {
110 58
        AnonymousGlobal::overrideExistingResolvedClass($className, $resolvedClass);
111 58
    }
112 58
113 58
    /**
114
     * @param null|Closure(GacelaConfig):void $configFn
115
     */
116 106
    private static function processConfigFnIntoSetup(Closure $configFn = null): SetupGacelaInterface
117
    {
118 106
        if ($configFn instanceof Closure) {
119
            return SetupGacela::fromCallable($configFn);
120 106
        }
121
122 106
        $gacelaFilePath = sprintf(
123
            '%s%s%s',
124 5
            self::rootDir(),
125 4
            DIRECTORY_SEPARATOR,
126 1
            self::GACELA_PHP_FILENAME,
127
        );
128 5
129
        if (is_file($gacelaFilePath)) {
130
            return SetupGacela::fromFile($gacelaFilePath);
131
        }
132
133
        return new SetupGacela();
134
    }
135
136
    private static function resetCache(): void
137
    {
138
        AnonymousGlobal::resetCache();
139
        AbstractFacade::resetCache();
140
        AbstractFactory::resetCache();
141
        AbstractClassResolver::resetCache();
142
        InMemoryCache::resetCache();
143
        GacelaFileCache::resetCache();
144
        DocBlockResolverCache::resetCache();
145
        ClassResolverCache::resetCache();
146
        ConfigFactory::resetCache();
147
        Config::resetInstance();
148
        Locator::resetInstance();
149
    }
150
151
    private static function runPlugins(Config $config): void
152
    {
153
        self::$mainContainer = Container::withConfig($config);
154
155
        $plugins = $config->getSetupGacela()->getPlugins();
156
157
        foreach ($plugins as $plugin) {
158
            /** @var callable $current */
159
            $current = is_string($plugin)
160
                ? self::$mainContainer->get($plugin)
161
                : $plugin;
162
163
            self::$mainContainer->resolve($current);
164
        }
165
    }
166
167
    private static function addModuleBindingsToSetup(SetupGacelaInterface $setup): void
168
    {
169
        $setup->combine(SetupGacela::fromCallable(static function (GacelaConfig $config): void {
170
            foreach (self::collectBindingsFromProviders() as $k => $v) {
171
                $config->addBinding($k, $v);
172
            }
173
        }));
174
    }
175
176
    /**
177
     * @return array<class-string, class-string|callable|object>
178
     */
179
    private static function collectBindingsFromProviders(): array
180
    {
181
        if (self::$appRootDir === null || !is_dir(self::$appRootDir)) {
182
            return [];
183
        }
184
185
        $result = [];
186
        /** @var SplFileInfo $file */
187
        foreach (self::createRecursiveIterator() as $file) {
188
            if ($file->getExtension() === 'php') {
189
                $fileContents = (string)file_get_contents($file->getPathname());
190
                if (preg_match('/namespace\s+([a-zA-Z0-9_\\\\]+)\s*;/', $fileContents, $matches) !== false) {
191
                    $namespace = $matches[1] ?? ''; // @phpstan-ignore-line
192
                } else {
193
                    $namespace = '';
194
                }
195
196
                /** @var string $className */
197
                $className = pathinfo($file->getFilename(), PATHINFO_FILENAME);
198
                $fullClassName = $namespace !== ''
199
                    ? $namespace . '\\' . $className
200
                    : $className;
201
202
                if (class_exists($fullClassName)) {
203
                    if (is_subclass_of($fullClassName, AbstractProvider::class)) {
204
                        $result = array_merge($result, (new $fullClassName())->bindings);
205
                    }
206
                }
207
            }
208
        }
209
        return $result;
210
    }
211
212
    /**
213
     * @return RecursiveIteratorIterator<RecursiveDirectoryIterator>
214
     */
215
    private static function createRecursiveIterator(): RecursiveIteratorIterator
216
    {
217
        $directoryIterator = new RecursiveDirectoryIterator(
218
            self::$appRootDir ?? '',
219
            RecursiveDirectoryIterator::SKIP_DOTS,
220
        );
221
222
        $filterIterator = new RecursiveCallbackFilterIterator(
223
            $directoryIterator,
224
            static function ($current, $key, $iterator) {
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type string expected by parameter $callback of RecursiveCallbackFilterIterator::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
            /** @scrutinizer ignore-type */ static function ($current, $key, $iterator) {
Loading history...
225
                /** @var SplFileInfo $current */
226
                if ($iterator->hasChildren() && $current->getFilename() === 'vendor') {
227
                    // Skip the vendor directory
228
                    return false;
229
                }
230
                return true;
231
            },
232
        );
233
234
        /** @var RecursiveIteratorIterator<RecursiveDirectoryIterator> */
235
        return new RecursiveIteratorIterator($filterIterator);
236
    }
237
}
238