1: <?php
2:
3: namespace LaravelUi5\Core\Ui5;
4:
5: use LaravelUi5\Core\Attributes\Setting;
6: use LaravelUi5\Core\Enums\ArtifactType;
7: use LaravelUi5\Core\Infrastructure\Contracts\Ui5SourceStrategyResolverInterface;
8: use LaravelUi5\Core\Ui5\Contracts\Ui5ArtifactInterface;
9: use LaravelUi5\Core\Ui5\Contracts\Ui5ModuleInterface;
10: use LaravelUi5\Core\Ui5\Contracts\Ui5RegistryInterface;
11: use LaravelUi5\Core\Ui5CoreServiceProvider;
12: use LogicException;
13: use ReflectionClass;
14: use ReflectionException;
15:
16: class Ui5Registry implements Ui5RegistryInterface
17: {
18: /**
19: * @var array<string, Ui5ModuleInterface>
20: */
21: protected array $modules = [];
22:
23: /**
24: * @var array<string, Ui5ArtifactInterface>
25: */
26: protected array $artifacts = [];
27:
28: /**
29: * @var array<string, string>
30: */
31: protected array $namespaceToModule = [];
32:
33: /**
34: * @var array<class-string<Ui5ArtifactInterface>, string>
35: */
36: protected array $artifactToModule = [];
37:
38: /**
39: * @var array<string, Ui5ArtifactInterface>
40: */
41: protected array $slugs = [];
42:
43: /**
44: * @var array<string, array<string, string[]>>
45: */
46: protected array $settings = [];
47:
48: protected Ui5SourceStrategyResolverInterface $sourceStrategyResolver;
49:
50: /**
51: * @throws ReflectionException
52: */
53: public function __construct(Ui5SourceStrategyResolverInterface $sourceStrategyResolver, ?array $config = null)
54: {
55: $this->sourceStrategyResolver = $sourceStrategyResolver;
56:
57: if ($config) {
58: $this->loadFromArray($config);
59: } else {
60: $this->loadFromArray(config('ui5'));
61: }
62: }
63:
64: /**
65: * @throws ReflectionException
66: */
67: public static function fromArray(array $config): self
68: {
69: return new self(app(Ui5SourceStrategyResolverInterface::class), $config);
70: }
71:
72: /**
73: * @throws ReflectionException
74: */
75: protected function loadFromArray(array $config): void
76: {
77: $modules = $config['modules'] ?? [];
78:
79: // Pass 1: Instantiate modules
80: foreach ($modules as $slug => $moduleClass) {
81:
82: if (!class_exists($moduleClass)) {
83: throw new LogicException("UI5 module class [{$moduleClass}] does not exist.");
84: }
85:
86: $strategy = $this->sourceStrategyResolver->resolve($moduleClass);
87:
88: /** @var Ui5ModuleInterface $module */
89: $module = new $moduleClass($slug, $strategy);
90:
91: $this->modules[$slug] = $module;
92: }
93:
94: // Pass 2: Reflect everything else
95: $dashboards = $config['dashboards'] ?? [];
96: $reports = $config['reports'] ?? [];
97: $dialogs = $config['dialogs'] ?? [];
98:
99: foreach ($this->modules as $slug => $module) {
100:
101: if ($module->hasApp() && ($app = $module->getApp())) {
102: $this->registerArtifact($app, $slug);
103: }
104: if ($module->hasLibrary() && ($lib = $module->getLibrary())) {
105: $this->registerArtifact($lib, $slug);
106: }
107: foreach ($module->getCards() as $card) {
108: $this->registerArtifact($card, $slug);
109: }
110: foreach ($module->getKpis() as $kpi) {
111: $this->registerArtifact($kpi, $slug);
112: }
113: foreach ($module->getTiles() as $tile) {
114: $this->registerArtifact($tile, $slug);
115: }
116: foreach ($module->getActions() as $action) {
117: $this->registerArtifact($action, $slug);
118: }
119: foreach ($module->getResources() as $resource) {
120: $this->registerArtifact($resource, $slug);
121: }
122: foreach ($module->getReports() as $report) {
123: $key = get_class($report);
124: if (array_key_exists($key, $reports)) {
125: $report->setSlug($reports[$key]);
126: $this->registerArtifact($report, $slug);
127: }
128: }
129: foreach ($module->getDashboards() as $dashboard) {
130: $key = get_class($dashboard);
131: if (array_key_exists($key, $dashboards)) {
132: $dashboard->setSlug($dashboards[$key]);
133: $this->registerArtifact($dashboard, $slug);
134: }
135: }
136: foreach ($module->getDialogs() as $dialog) {
137: $key = get_class($dialog);
138: if (array_key_exists($key, $dialogs)) {
139: $dialog->setSlug($dialogs[$key]);
140: $this->registerArtifact($dialog, $slug);
141: }
142: }
143: }
144:
145: // Extension Hook
146: $this->afterLoad($config);
147: }
148:
149: protected function afterLoad(array $config): void
150: {
151: // extension hook (no-op by default)
152: }
153:
154: /**
155: * Registers a UI5 artifact within the registry.
156: *
157: * This method adds the artifact to the internal lookup maps by namespace
158: * and module context. If the artifact is sluggable (i.e., addressable via URI),
159: * it also registers the composed `urlKey` for reverse lookup.
160: *
161: * @param Ui5ArtifactInterface $artifact
162: * @param string $moduleSlug
163: */
164: protected function registerArtifact(Ui5ArtifactInterface $artifact, string $moduleSlug): void
165: {
166: $namespace = $artifact->getNamespace();
167: $this->artifacts[$namespace] = $artifact;
168: $this->namespaceToModule[$namespace] = $moduleSlug;
169: $this->artifactToModule[get_class($artifact)] = $moduleSlug;
170:
171: $this->discoverSettings($artifact);
172:
173: $urlKey = ArtifactType::urlKeyFromArtifact($artifact);
174: if (null !== $urlKey) {
175: $this->slugs[$urlKey] = $artifact;
176: }
177: }
178:
179: /**
180: * Discover and register Setting attributes defined on Ui5 artifacts.
181: *
182: * @param Ui5ArtifactInterface $artifact
183: */
184: protected function discoverSettings(Ui5ArtifactInterface $artifact): void
185: {
186: $ref = new ReflectionClass($artifact);
187: $attributes = $ref->getAttributes(Setting::class);
188:
189: if (empty($attributes)) {
190: return;
191: }
192:
193: $namespace = $artifact->getModule()->getArtifactRoot()->getNamespace();
194:
195: foreach ($attributes as $attr) {
196: /** @var Setting $setting */
197: $setting = $attr->newInstance();
198:
199: if (array_key_exists($setting->setting, $this->settings[$namespace] ?? [])) {
200: throw new LogicException(sprintf(
201: 'Duplicate setting [%s] found in [%s].',
202: $setting->setting,
203: get_class($artifact)
204: ));
205: }
206:
207: $this->settings[$namespace][$setting->setting] = [
208: 'default' => $setting->default,
209: 'type' => $setting->type,
210: 'scope' => $setting->scope,
211: 'role' => $setting->role,
212: 'note' => $setting->note,
213: ];
214: }
215: }
216:
217: /** -- Lookup ---------------------------------------------------------- */
218:
219: public function modules(): array
220: {
221: return $this->modules;
222: }
223:
224: public function hasModule(string $slug): bool
225: {
226: return isset($this->modules[$slug]);
227: }
228:
229: public function getModule(string $slug): ?Ui5ModuleInterface
230: {
231: if (isset($this->modules[$slug])) {
232: return $this->modules[$slug];
233: }
234:
235: return null;
236: }
237:
238: public function artifacts(): array
239: {
240: return $this->artifacts;
241: }
242:
243: public function has(string $namespace): bool
244: {
245: return isset($this->artifacts[$namespace]);
246: }
247:
248: public function get(string $namespace): ?Ui5ArtifactInterface
249: {
250: if (isset($this->artifacts[$namespace])) {
251: return $this->artifacts[$namespace];
252: }
253:
254: return null;
255: }
256:
257: public function settings(?string $namespace = null): array
258: {
259: if (null === $namespace) {
260: return $this->settings;
261: }
262:
263: return $this->settings[$namespace] ?? [];
264: }
265:
266: /** -- Laravel routing ------------------------------------------------- */
267:
268: public function fromSlug(string $slug): ?Ui5ArtifactInterface
269: {
270: return $this->slugs[$slug] ?? null;
271: }
272:
273: public function resolve(string $namespace): ?string
274: {
275: $artifact = $this->get($namespace);
276: if ($artifact) {
277: $slug = ArtifactType::urlKeyFromArtifact($artifact);
278:
279: $prefix = Ui5CoreServiceProvider::UI5_ROUTE_PREFIX;
280:
281: return "/{$prefix}/{$slug}/{$artifact->getVersion()}";
282: }
283:
284: return null;
285: }
286:
287: public function resolveRoots(array $namespaces): array
288: {
289: return collect($namespaces)->mapWithKeys(fn($ns) => [$ns => $this->resolve($ns)])->all();
290: }
291:
292: /** -- Export ---------------------------------------------------------- */
293: public function exportToCache(): array
294: {
295: return [
296: 'modules' => $this->modules,
297: 'artifacts' => $this->artifacts,
298: 'namespaceToModule' => $this->namespaceToModule,
299: 'artifactToModule' => $this->artifactToModule,
300: 'slugs' => $this->slugs,
301: 'settings' => $this->settings,
302: ];
303: }
304: }
305: