1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace LaravelUi5\OData\Edm\Vocabularies;
6:
7: /**
8: * Runtime registry that resolves alias-qualified OData term names to their
9: * fully qualified form.
10: *
11: * The registry is initialised once from a VocabularyCatalogInterface and is
12: * immutable thereafter. The alias map is built eagerly in the constructor.
13: *
14: * The static singleton is lazily initialised from VocabularyCatalog::default()
15: * on first call to getInstance(). Annotatable model objects use the singleton
16: * to resolve aliases in getAnnotation() without carrying a registry reference
17: * in their constructors.
18: *
19: * NOTE: This class is not declared readonly because the PHP readonly-class
20: * modifier makes ALL typed properties readonly, which includes static
21: * properties and prevents the mutable static $instance field required for
22: * the lazy-singleton pattern. Instance immutability is preserved by
23: * declaring every instance property readonly explicitly.
24: *
25: * @see VocabularyRegistryInterface
26: * @see OData CSDL XML v4.01 ยง4.1 (Namespace and Alias)
27: */
28: final class VocabularyRegistry implements VocabularyRegistryInterface
29: {
30: /** Lazily initialised singleton, populated on first call to getInstance(). */
31: private static ?self $instance = null;
32:
33: /** @var array<string, string> alias => namespace */
34: private readonly array $aliasMap;
35:
36: /** @var list<string> all known namespaces, for already-qualified detection */
37: private readonly array $namespaces;
38:
39: public function __construct(VocabularyCatalogInterface $catalog)
40: {
41: $aliasMap = [];
42: $namespaces = [];
43: foreach ($catalog->getEntries() as $entry) {
44: $aliasMap[$entry->getAlias()] = $entry->getNamespace();
45: $namespaces[] = $entry->getNamespace();
46: }
47: $this->aliasMap = $aliasMap;
48: $this->namespaces = $namespaces;
49: }
50:
51: /**
52: * Returns the shared singleton instance, lazily initialised from
53: * VocabularyCatalog::default() on first call.
54: */
55: public static function getInstance(): self
56: {
57: return self::$instance ??= new self(VocabularyCatalog::default());
58: }
59:
60: public function resolveAlias(string $alias, string $termName): ?string
61: {
62: if (!isset($this->aliasMap[$alias])) {
63: return null;
64: }
65: return $this->aliasMap[$alias] . '.' . $termName;
66: }
67:
68: public function fullyQualify(string $term): ?string
69: {
70: // Already fully qualified when its prefix matches a known namespace.
71: foreach ($this->namespaces as $ns) {
72: if (str_starts_with($term, $ns . '.')) {
73: return $term;
74: }
75: }
76:
77: // Alias-qualified: split on the first dot.
78: $dotPos = strpos($term, '.');
79: if ($dotPos === false) {
80: return null;
81: }
82: $alias = substr($term, 0, $dotPos);
83: $remainder = substr($term, $dotPos + 1);
84:
85: if (!isset($this->aliasMap[$alias])) {
86: return null;
87: }
88:
89: return $this->aliasMap[$alias] . '.' . $remainder;
90: }
91:
92: /** @return array<string, string> */
93: public function getAliasMap(): array
94: {
95: return $this->aliasMap;
96: }
97:
98: public function hasAlias(string $alias): bool
99: {
100: return isset($this->aliasMap[$alias]);
101: }
102: }
103: