1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace LaravelUi5\OData\Service\Builder;
6:
7: use LaravelUi5\OData\Edm\Contracts\Container\EntitySetInterface;
8: use LaravelUi5\OData\Edm\Contracts\Container\FunctionImportInterface;
9: use LaravelUi5\OData\Edm\Contracts\Container\SingletonInterface;
10: use LaravelUi5\OData\Edm\Contracts\EdmxInterface;
11: use LaravelUi5\OData\Driver\Sql\EloquentEntitySetResolver;
12: use LaravelUi5\OData\Service\Contracts\EntitySetResolverInterface;
13: use LaravelUi5\OData\Service\Contracts\FunctionResolverInterface;
14: use LaravelUi5\OData\Service\Contracts\RuntimeSchemaBuilderInterface;
15: use LaravelUi5\OData\Service\Contracts\RuntimeSchemaInterface;
16: use LaravelUi5\OData\Service\Contracts\SingletonResolverInterface;
17: use LaravelUi5\OData\Service\RuntimeSchema;
18:
19: /**
20: * Mutable accumulator that binds resolvers to a frozen EdmxInterface (Stage 2).
21: *
22: * Retrieve canonical EntitySetInterface and FunctionImportInterface instances
23: * via getEdmx() before binding — object identity (spl_object_id) is the map key.
24: *
25: * Example:
26: * $c = $builder->getEdmx()->getEntityContainer();
27: * $builder->bindEntitySet($c->getEntitySet('Partners'), new EloquentEntitySetResolver(Partner::class));
28: */
29: final class RuntimeSchemaBuilder implements RuntimeSchemaBuilderInterface
30: {
31: private bool $built = false;
32:
33: /**
34: * @var array<int, EntitySetResolverInterface> keyed by spl_object_id of EntitySetInterface
35: */
36: private array $entitySetResolvers = [];
37:
38: /**
39: * @var array<int, FunctionResolverInterface> keyed by spl_object_id of FunctionImportInterface
40: */
41: private array $functionResolvers = [];
42:
43: /**
44: * @var array<int, SingletonResolverInterface> keyed by spl_object_id of SingletonInterface
45: */
46: private array $singletonResolvers = [];
47:
48: public function __construct(private readonly EdmxInterface $edmx) {}
49:
50: public function getEdmx(): EdmxInterface
51: {
52: return $this->edmx;
53: }
54:
55: public function bindEntitySet(
56: EntitySetInterface $set,
57: EntitySetResolverInterface $resolver,
58: ): static {
59: $this->assertNotBuilt();
60: $this->assertEntitySetBelongs($set);
61:
62: $this->entitySetResolvers[spl_object_id($set)] = $resolver;
63: return $this;
64: }
65:
66: public function bindFunctionImport(
67: FunctionImportInterface $import,
68: FunctionResolverInterface $resolver,
69: ): static {
70: $this->assertNotBuilt();
71: $this->assertFunctionImportBelongs($import);
72:
73: $this->functionResolvers[spl_object_id($import)] = $resolver;
74: return $this;
75: }
76:
77: public function bindSingleton(
78: SingletonInterface $singleton,
79: SingletonResolverInterface $resolver,
80: ): static {
81: $this->assertNotBuilt();
82: $this->assertSingletonBelongs($singleton);
83:
84: $this->singletonResolvers[spl_object_id($singleton)] = $resolver;
85: return $this;
86: }
87:
88: public function build(): RuntimeSchemaInterface
89: {
90: $this->assertNotBuilt();
91: $this->assertAllEntitySetsBound();
92: $this->built = true;
93:
94: $schema = new RuntimeSchema(
95: edmx: $this->edmx,
96: resolvers: $this->entitySetResolvers,
97: functionResolvers: $this->functionResolvers,
98: singletonResolvers: $this->singletonResolvers,
99: );
100:
101: // Wire schema reference into Eloquent resolvers so they can
102: // delegate virtual expand resolution to custom resolvers.
103: foreach ($this->entitySetResolvers as $resolver) {
104: if ($resolver instanceof EloquentEntitySetResolver) {
105: $resolver->setSchema($schema);
106: }
107: }
108:
109: return $schema;
110: }
111:
112: // ── Guards ─────────────────────────────────────────────────────────────────
113:
114: private function assertNotBuilt(): void
115: {
116: if ($this->built) {
117: throw new \LogicException('RuntimeSchemaBuilder has already been built and must not be mutated.');
118: }
119: }
120:
121: private function assertEntitySetBelongs(EntitySetInterface $set): void
122: {
123: $id = spl_object_id($set);
124: foreach ($this->edmx->getEntityContainer()->getEntitySets() as $known) {
125: if (spl_object_id($known) === $id) {
126: return;
127: }
128: }
129: throw new \InvalidArgumentException(
130: sprintf('EntitySet "%s" is not part of this builder\'s EdmxInterface.', $set->getName())
131: );
132: }
133:
134: private function assertFunctionImportBelongs(FunctionImportInterface $import): void
135: {
136: $id = spl_object_id($import);
137: foreach ($this->edmx->getEntityContainer()->getFunctionImports() as $known) {
138: if (spl_object_id($known) === $id) {
139: return;
140: }
141: }
142: throw new \InvalidArgumentException(
143: sprintf('FunctionImport "%s" is not part of this builder\'s EdmxInterface.', $import->getName())
144: );
145: }
146:
147: private function assertSingletonBelongs(SingletonInterface $singleton): void
148: {
149: $id = spl_object_id($singleton);
150: foreach ($this->edmx->getEntityContainer()->getSingletons() as $known) {
151: if (spl_object_id($known) === $id) {
152: return;
153: }
154: }
155: throw new \InvalidArgumentException(
156: sprintf('Singleton "%s" is not part of this builder\'s EdmxInterface.', $singleton->getName())
157: );
158: }
159:
160: private function assertAllEntitySetsBound(): void
161: {
162: $unbound = [];
163: foreach ($this->edmx->getEntityContainer()->getEntitySets() as $set) {
164: if (!isset($this->entitySetResolvers[spl_object_id($set)])) {
165: $unbound[] = $set->getName();
166: }
167: }
168:
169: if ($unbound !== []) {
170: throw new \RuntimeException(
171: sprintf(
172: 'The following entity sets have no resolver bound: %s.',
173: implode(', ', $unbound)
174: )
175: );
176: }
177: }
178: }
179: