Passed
Push — main ( 1a17f8...8155a4 )
by Chema
10:50 queued 09:35
created

Gacela::addModuleBindingsToSetup()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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