Passed
Push — feat/add-provider-bindings ( 36306e )
by Chema
04:36
created

Gacela::collectBindingsFromProviders()   B

Complexity

Conditions 9
Paths 15

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
c 0
b 0
f 0
dl 0
loc 35
rs 8.0555
cc 9
nc 15
nop 0
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
    private static ?string $appRootDir = null;
38
39
    /**
40
     * Define the entry point of Gacela.
41
     *
42
     * @param null|Closure(GacelaConfig):void $configFn
43
     */
44
    public static function bootstrap(string $appRootDir, Closure $configFn = null): void
45
    {
46
        self::$appRootDir = $appRootDir;
47
        self::$mainContainer = null;
48
49
        $setup = self::processConfigFnIntoSetup($configFn);
50
51
        if ($setup->shouldResetInMemoryCache()) {
52
            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
    }
63
64
    /**
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
     */
71
    public static function get(string $className): mixed
72
    {
73
        return Locator::getSingleton($className, self::$mainContainer);
74
    }
75
76
    /**
77
     * Get the application root dir set when bootstrapping gacela
78
     */
79
    public static function rootDir(): string
80
    {
81
        if (self::$appRootDir === null) {
82
            throw new GacelaNotBootstrappedException();
83
        }
84
85
        return self::$appRootDir;
86
    }
87
88
    /**
89
     * Add an anonymous class as 'Config', 'Factory' or 'Provider' as a global resource
90
     * bound to the context that it is passed as second argument.
91
     *
92
     * @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
     */
95
    public static function addGlobal(object $resolvedClass, object|string $context = ''): void
96
    {
97
        if (is_string($context) && is_file($context)) {
98
            $context = basename($context, '.php');
99
        } elseif ($context === '') {
100
            // Use the caller's file as context
101
            $context = basename(debug_backtrace()[0]['file'] ?? __FILE__, '.php');
102
        }
103
104
        AnonymousGlobal::addGlobal($context, $resolvedClass);
105
    }
106
107
    public static function overrideExistingResolvedClass(string $className, object $resolvedClass): void
108
    {
109
        AnonymousGlobal::overrideExistingResolvedClass($className, $resolvedClass);
110
    }
111
112
    /**
113
     * @param null|Closure(GacelaConfig):void $configFn
114
     */
115
    private static function processConfigFnIntoSetup(Closure $configFn = null): SetupGacelaInterface
116
    {
117
        if ($configFn instanceof Closure) {
118
            return SetupGacela::fromCallable($configFn);
119
        }
120
121
        $gacelaFilePath = sprintf(
122
            '%s%s%s',
123
            self::rootDir(),
124
            DIRECTORY_SEPARATOR,
125
            self::GACELA_PHP_FILENAME,
126
        );
127
128
        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
            $bs = self::collectBindingsFromProviders();
170
171
            foreach ($bs as $k => $v) {
172
                $config->addBinding($k, $v);
173
            }
174
        }));
175
    }
176
177
    /**
178
     * @return array<class-string, class-string|callable|object>
179
     */
180
    private static function collectBindingsFromProviders(): array
181
    {
182
        if (self::$appRootDir === null || !is_dir(self::$appRootDir)) {
183
            return [];
184
        }
185
186
        $iterator = new RecursiveIteratorIterator(
187
            new RecursiveDirectoryIterator(self::$appRootDir),
188
        );
189
190
        $result = [];
191
        /** @var SplFileInfo $file */
192
        foreach ($iterator as $file) {
193
            if ($file->getExtension() === 'php') {
194
                $fileContents = (string)file_get_contents($file->getPathname());
195
                if (preg_match('/namespace\s+([a-zA-Z0-9_\\\\]+)\s*;/', $fileContents, $matches) !== false) {
196
                    $namespace = $matches[1] ?? ''; // @phpstan-ignore-line
197
                } else {
198
                    $namespace = '';
199
                }
200
201
                /** @var string $className */
202
                $className = pathinfo($file->getFilename(), PATHINFO_FILENAME);
203
                $fullClassName = $namespace !== ''
204
                    ? $namespace . '\\' . $className
205
                    : $className;
206
207
                if (class_exists($fullClassName)) {
208
                    if (is_subclass_of($fullClassName, AbstractProvider::class)) {
209
                        $result = array_merge($result, (new $fullClassName())->bindings);
210
                    }
211
                }
212
            }
213
        }
214
        return $result;
215
    }
216
}
217