1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data;
6:
7: use Atk4\Core\DiContainerTrait;
8: use Atk4\Core\Factory;
9: use Atk4\Core\InitializerTrait;
10: use Atk4\Core\TrackableTrait;
11:
12: /**
13: * Reference implements a link between one model and another. The basic components for
14: * a reference is ability to generate the destination model, which is returned through
15: * getModel() and that's pretty much it.
16: *
17: * It's possible to extend the basic reference with more meaningful references.
18: *
19: * @method Model getOwner() our model
20: */
21: class Reference
22: {
23: use DiContainerTrait;
24: use InitializerTrait {
25: init as private _init;
26: }
27: use TrackableTrait {
28: setOwner as private _setOwner;
29: }
30:
31: /**
32: * Use this alias for related entity by default. This can help you
33: * if you create sub-queries or joins to separate this from main
34: * table. The tableAlias will be uniquely generated.
35: *
36: * @var string
37: */
38: protected $tableAlias;
39:
40: /**
41: * What should we pass into owner->ref() to get through to this reference.
42: * Each reference has a unique identifier, although it's stored
43: * in Model's elements as '#ref-xx'.
44: *
45: * @var string
46: */
47: public $link;
48:
49: /**
50: * Definition of the destination their model, that can be either an object, a
51: * callback or a string. This can be defined during initialization and
52: * then used inside getModel() to fully populate and associate with
53: * persistence.
54: *
55: * @var Model|\Closure(object, static, array<string, mixed>): Model|array<mixed>
56: */
57: public $model;
58:
59: /**
60: * This is an optional property which can be used by your implementation
61: * to store field-level relationship based on a common field matching.
62: */
63: protected ?string $ourField = null;
64:
65: /**
66: * This is an optional property which can be used by your implementation
67: * to store field-level relationship based on a common field matching.
68: */
69: protected ?string $theirField = null;
70:
71: /**
72: * Database our/their field types must always match, but DBAL types can be different in theory,
73: * set this to false when the DBAL types are intentionally different.
74: */
75: public bool $checkTheirType = true;
76:
77: /**
78: * Caption of the referenced model. Can be used in UI components, for example.
79: * Should be in plain English and ready for proper localization.
80: *
81: * @var string|null
82: */
83: public $caption;
84:
85: public function __construct(string $link)
86: {
87: $this->link = $link;
88: }
89:
90: /**
91: * @param Model $owner
92: *
93: * @return $this
94: */
95: public function setOwner(object $owner)
96: {
97: $owner->assertIsModel();
98:
99: return $this->_setOwner($owner);
100: }
101:
102: /**
103: * @param mixed $value
104: */
105: protected function assertReferenceValueNotNull($value): void
106: {
107: if ($value === null) {
108: throw (new Exception('Unable to traverse on null value'))
109: ->addMoreInfo('value', $value);
110: }
111: }
112:
113: public function getOurFieldName(): string
114: {
115: return $this->ourField ?? $this->getOurModel(null)->idField;
116: }
117:
118: final protected function getOurField(): Field
119: {
120: return $this->getOurModel(null)->getField($this->getOurFieldName());
121: }
122:
123: /**
124: * @return mixed
125: */
126: final protected function getOurFieldValue(Model $ourEntity)
127: {
128: return $this->getOurModel($ourEntity)->get($this->getOurFieldName());
129: }
130:
131: public function getTheirFieldName(Model $theirModel = null): string
132: {
133: return $this->theirField ?? ($theirModel ?? Model::assertInstanceOf($this->model))->idField;
134: }
135:
136: /**
137: * @template T of Model
138: *
139: * @param \Closure(T, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed): mixed $fx
140: * @param array<int, mixed> $args
141: */
142: protected function onHookToOurModel(Model $model, string $spot, \Closure $fx, array $args = [], int $priority = 5): int
143: {
144: $name = $this->shortName; // use static function to allow this object to be GCed
145:
146: return $model->onHookDynamic(
147: $spot,
148: static function (Model $model) use ($name): self {
149: return $model->getModel(true)->getElement($name);
150: },
151: $fx,
152: $args,
153: $priority
154: );
155: }
156:
157: /**
158: * @param \Closure(Model, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed): mixed $fx
159: * @param array<int, mixed> $args
160: */
161: protected function onHookToTheirModel(Model $model, string $spot, \Closure $fx, array $args = [], int $priority = 5): int
162: {
163: if ($model->ownerReference !== null && $model->ownerReference !== $this) {
164: throw new Exception('Model owner reference is unexpectedly already set');
165: }
166: $model->ownerReference = $this;
167: $getThisFx = static function (Model $model) {
168: return $model->ownerReference;
169: };
170:
171: return $model->onHookDynamic(
172: $spot,
173: $getThisFx,
174: $fx,
175: $args,
176: $priority
177: );
178: }
179:
180: protected function init(): void
181: {
182: $this->_init();
183:
184: $this->initTableAlias();
185: }
186:
187: /**
188: * Will use #ref-<link>.
189: */
190: public function getDesiredName(): string
191: {
192: return '#ref-' . $this->link;
193: }
194:
195: public function getOurModel(?Model $ourModel): Model
196: {
197: if ($ourModel === null) {
198: $ourModel = $this->getOwner();
199: }
200:
201: $this->getOwner()->assertIsModel($ourModel->getModel(true));
202:
203: return $ourModel;
204: }
205:
206: /**
207: * Create destination model that is linked through this reference. Will apply
208: * necessary conditions.
209: *
210: * IMPORTANT: the returned model must be a fresh clone or freshly built from a seed
211: *
212: * @param array<string, mixed> $defaults
213: */
214: public function createTheirModel(array $defaults = []): Model
215: {
216: // set tableAlias
217: $defaults['tableAlias'] ??= $this->tableAlias;
218:
219: // if model is Closure, then call the closure and it should return a model
220: if ($this->model instanceof \Closure) {
221: $m = ($this->model)($this->getOurModel(null), $this, $defaults);
222: } else {
223: $m = $this->model;
224: }
225:
226: if (is_object($m)) {
227: $theirModel = Factory::factory(clone $m, $defaults);
228: } else {
229: // add model from seed
230: $modelDefaults = $m;
231: $theirModelSeed = [$modelDefaults[0]];
232: unset($modelDefaults[0]);
233: $defaults = array_merge($modelDefaults, $defaults);
234:
235: $theirModel = Factory::factory($theirModelSeed, $defaults);
236: }
237:
238: $this->addToPersistence($theirModel, $defaults);
239:
240: if ($this->checkTheirType) {
241: $ourField = $this->getOurField();
242: $theirField = $theirModel->getField($this->getTheirFieldName($theirModel));
243: if ($theirField->type !== $ourField->type) {
244: throw (new Exception('Reference type mismatch'))
245: ->addMoreInfo('ourField', $ourField)
246: ->addMoreInfo('ourFieldType', $ourField->type)
247: ->addMoreInfo('theirField', $theirField)
248: ->addMoreInfo('theirFieldType', $theirField->type);
249: }
250: }
251:
252: return $theirModel;
253: }
254:
255: protected function initTableAlias(): void
256: {
257: if (!$this->tableAlias) {
258: $ourModel = $this->getOurModel(null);
259:
260: $aliasFull = $this->link;
261: $alias = preg_replace('~_(' . preg_quote($ourModel->idField !== false ? $ourModel->idField : '', '~') . '|id)$~', '', $aliasFull);
262: $alias = preg_replace('~([0-9a-z]?)[0-9a-z]*[^0-9a-z]*~i', '$1', $alias);
263: if ($ourModel->tableAlias !== null) {
264: $aliasFull = $ourModel->tableAlias . '_' . $aliasFull;
265: $alias = preg_replace('~^_(.+)_[0-9a-f]{12}$~', '$1', $ourModel->tableAlias) . '_' . $alias;
266: }
267: $this->tableAlias = '_' . $alias . '_' . substr(md5($aliasFull), 0, 12);
268: }
269: }
270:
271: /**
272: * @param array<string, mixed> $defaults
273: */
274: protected function addToPersistence(Model $theirModel, array $defaults = []): void
275: {
276: if (!$theirModel->issetPersistence()) {
277: $persistence = $this->getDefaultPersistence($theirModel);
278: if ($persistence !== false) {
279: $theirModel->setDefaults($defaults);
280: $theirModel->setPersistence($persistence);
281: }
282: } elseif ($defaults !== []) {
283: // TODO this seems dangerous
284: }
285:
286: // set model caption
287: if ($this->caption !== null) {
288: $theirModel->caption = $this->caption;
289: }
290: }
291:
292: /**
293: * Returns default persistence for theirModel.
294: *
295: * @return Persistence|false
296: */
297: protected function getDefaultPersistence(Model $theirModel)
298: {
299: $ourModel = $this->getOurModel(null);
300:
301: // this is useful for ContainsOne/Many implementation in case when you have
302: // SQL_Model->containsOne()->hasOne() structure to get back to SQL persistence
303: // from Array persistence used in ContainsOne model
304: if ($ourModel->containedInEntity && $ourModel->containedInEntity->getModel()->issetPersistence()) {
305: return $ourModel->containedInEntity->getModel()->getPersistence();
306: }
307:
308: return $ourModel->issetPersistence() ? $ourModel->getPersistence() : false;
309: }
310:
311: /**
312: * Returns referenced model without any extra conditions. However other
313: * relationship types may override this to imply conditions.
314: *
315: * @param array<string, mixed> $defaults
316: */
317: public function ref(Model $ourModel, array $defaults = []): Model
318: {
319: return $this->createTheirModel($defaults);
320: }
321:
322: /**
323: * Returns referenced model without any extra conditions. Ever when extended
324: * must always respond with Model that does not look into current record
325: * or scope.
326: *
327: * @param array<string, mixed> $defaults
328: */
329: public function refModel(Model $ourModel, array $defaults = []): Model
330: {
331: return $this->createTheirModel($defaults);
332: }
333:
334: /**
335: * @return array<string, mixed>
336: */
337: public function __debugInfo(): array
338: {
339: $res = [];
340: foreach (['link', 'model', 'ourField', 'theirField'] as $k) {
341: if ($this->{$k} !== null) {
342: $res[$k] = $this->{$k};
343: }
344: }
345:
346: return $res;
347: }
348: }
349: