1: <?php
2:
3: namespace LaravelUi5\Core\Ui5;
4:
5: use LaravelUi5\Core\Attributes\Setting;
6: use LaravelUi5\Core\Infrastructure\Contracts\Ui5SourceStrategyResolverInterface;
7: use LaravelUi5\Core\Ui5\Contracts\Ui5ArtifactInterface;
8: use LaravelUi5\Core\Ui5\Contracts\Ui5ModuleInterface;
9: use LaravelUi5\Core\Ui5\Contracts\Ui5RegistryInterface;
10: use LaravelUi5\Core\Ui5CoreServiceProvider;
11: use LogicException;
12: use ReflectionClass;
13: use ReflectionException;
14:
15: class Ui5Registry implements Ui5RegistryInterface
16: {
17: /**
18: * @var array<string, Ui5ModuleInterface>
19: */
20: protected array $modules = [];
21:
22: /**
23: * @var array<string, Ui5ArtifactInterface>
24: */
25: protected array $artifacts = [];
26:
27: /**
28: * @var array<string, string>
29: */
30: protected array $namespaceToModule = [];
31:
32: /**
33: * @var array<class-string<Ui5ArtifactInterface>, string>
34: */
35: protected array $artifactToModule = [];
36:
37: /**
38: * @var array<string, array<string, string[]>>
39: */
40: protected array $settings = [];
41:
42: protected Ui5SourceStrategyResolverInterface $sourceStrategyResolver;
43:
44: /**
45: * @throws ReflectionException
46: */
47: public function __construct(Ui5SourceStrategyResolverInterface $sourceStrategyResolver, ?array $config = null)
48: {
49: $this->sourceStrategyResolver = $sourceStrategyResolver;
50:
51: if ($config) {
52: $this->loadFromArray($config);
53: } else {
54: $this->loadFromArray(config('ui5'));
55: }
56: }
57:
58: /**
59: * @throws ReflectionException
60: */
61: public static function fromArray(array $config): self
62: {
63: return new self(app(Ui5SourceStrategyResolverInterface::class), $config);
64: }
65:
66: /**
67: * @throws ReflectionException
68: */
69: protected function loadFromArray(array $config): void
70: {
71: $modules = $config['modules'] ?? [];
72:
73: // Pass 1: Instantiate modules
74: foreach ($modules as $class) {
75:
76: if (!class_exists($class)) {
77: throw new LogicException("UI5 module class `{$class}` does not exist.");
78: }
79:
80: $strategy = $this->sourceStrategyResolver->resolve($class);
81:
82: /** @var Ui5ModuleInterface $module */
83: $module = new $class($strategy);
84:
85: $this->modules[$module->getName()] = $module;
86: }
87:
88: // Pass 2: Reflect everything else
89: foreach ($this->modules as $module) {
90: foreach ($module->getAllArtifacts() as $artifact) {
91: $this->registerArtifact($artifact);
92: }
93: }
94:
95: // Extension Hook
96: $this->afterLoad($config);
97: }
98:
99: protected function afterLoad(array $config): void
100: {
101: // extension hook (no-op by default)
102: }
103:
104: /**
105: * Registers a UI5 artifact within the registry.
106: *
107: * This method adds the artifact to the internal lookup maps by namespace
108: * and module context. If the artifact is sluggable (i.e., addressable via URI),
109: * it also registers the composed `urlKey` for reverse lookup.
110: *
111: * @param Ui5ArtifactInterface $artifact
112: */
113: protected function registerArtifact(Ui5ArtifactInterface $artifact): void
114: {
115: $namespace = $artifact->getNamespace();
116: $moduleNamespace = $artifact->getModule()->getName();
117: $this->artifacts[$namespace] = $artifact;
118: $this->namespaceToModule[$namespace] = $moduleNamespace;
119: $this->artifactToModule[get_class($artifact)] = $moduleNamespace;
120:
121: $this->discoverSettings($artifact);
122: }
123:
124: /**
125: * Discover and register Setting attributes defined on Ui5 artifacts.
126: *
127: * @param Ui5ArtifactInterface $artifact
128: */
129: protected function discoverSettings(Ui5ArtifactInterface $artifact): void
130: {
131: $ref = new ReflectionClass($artifact);
132: $attributes = $ref->getAttributes(Setting::class);
133:
134: if (empty($attributes)) {
135: return;
136: }
137:
138: $namespace = $artifact->getModule()->getArtifactRoot()->getNamespace();
139:
140: foreach ($attributes as $attr) {
141: /** @var Setting $setting */
142: $setting = $attr->newInstance();
143:
144: if (array_key_exists($setting->key, $this->settings[$namespace] ?? [])) {
145: throw new LogicException(sprintf(
146: 'Duplicate setting [%s] found in [%s].',
147: $setting->key,
148: get_class($artifact)
149: ));
150: }
151:
152: $this->settings[$namespace][$setting->key] = [
153: 'default' => $setting->default,
154: 'type' => $setting->type,
155: 'scope' => $setting->scope,
156: 'role' => $setting->role,
157: 'note' => $setting->note,
158: ];
159: }
160: }
161:
162: /** -- Lookup ---------------------------------------------------------- */
163:
164: public function modules(): array
165: {
166: return $this->modules;
167: }
168:
169: public function getModule(string $namespace): ?Ui5ModuleInterface
170: {
171: return $this->modules[$namespace] ?? null;
172: }
173:
174: public function artifacts(): array
175: {
176: return $this->artifacts;
177: }
178:
179: public function get(string $namespace): ?Ui5ArtifactInterface
180: {
181: return $this->artifacts[$namespace] ?? null;
182: }
183:
184: public function settings(?string $namespace = null): array
185: {
186: if (null === $namespace) {
187: return $this->settings;
188: }
189:
190: return $this->settings[$namespace] ?? [];
191: }
192:
193: /** -- Laravel routing ------------------------------------------------- */
194:
195: public function pathToNamespace(string $namespace): string
196: {
197: return str_replace('/', '.', trim($namespace, '/'));
198: }
199:
200: public function namespaceToPath(string $namespace): string
201: {
202: return str_replace('.', '/', trim($namespace, '.'));
203: }
204:
205: public function resolve(string $namespace): ?string
206: {
207: $artifact = $this->get($namespace);
208: if ($artifact) {
209: $prefix = Ui5CoreServiceProvider::UI5_ROUTE_PREFIX;
210: $typePrefix = $artifact->getType()->routePrefix();
211: $path = $this->namespaceToPath($namespace);
212: $version = $artifact->getVersion();
213:
214: return "/{$prefix}/{$typePrefix}/{$path}@{$version}";
215: }
216:
217: return null;
218: }
219:
220: public function resolveRoots(array $namespaces): array
221: {
222: $roots = [];
223: foreach ($namespaces as $ns) {
224: $roots[$ns] = $this->resolve($ns);
225: }
226: return $roots;
227: }
228:
229: /** -- Export ---------------------------------------------------------- */
230: public function exportToCache(): array
231: {
232: return [
233: 'modules' => $this->modules,
234: 'artifacts' => $this->artifacts,
235: 'namespaceToModule' => $this->namespaceToModule,
236: 'artifactToModule' => $this->artifactToModule,
237: 'settings' => $this->settings,
238: ];
239: }
240: }
241: