1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Reference;
6:
7: use Atk4\Data\Field;
8: use Atk4\Data\Model;
9: use Atk4\Data\Reference;
10:
11: class HasOne extends Reference
12: {
13: use Model\FieldPropertiesTrait;
14: use Model\JoinLinkTrait;
15:
16: #[\Override]
17: protected function init(): void
18: {
19: parent::init();
20:
21: if (!$this->ourField) {
22: $this->ourField = $this->link;
23: }
24:
25: // for references use "integer" as a default type
26: if (!(new \ReflectionProperty($this, 'type'))->isInitialized($this)) {
27: $this->type = 'integer';
28: }
29:
30: $this->referenceLink = $this->link; // named differently than in Model\FieldPropertiesTrait
31:
32: $fieldPropsRefl = (new \ReflectionClass(Model\FieldPropertiesTrait::class))->getProperties();
33: $fieldPropsRefl[] = (new \ReflectionClass(Model\JoinLinkTrait::class))->getProperty('joinName');
34:
35: $ourModel = $this->getOurModel(null);
36: if (!$ourModel->hasField($this->ourField)) {
37: $fieldSeed = [];
38: foreach ($fieldPropsRefl as $fieldPropRefl) {
39: $v = $this->{$fieldPropRefl->getName()};
40: $vDefault = \PHP_MAJOR_VERSION < 8
41: ? ($fieldPropRefl->getDeclaringClass()->getDefaultProperties()[$fieldPropRefl->getName()] ?? null)
42: : (null ?? $fieldPropRefl->getDefaultValue()); // @phpstan-ignore-line for PHP 7.x
43: if ($v !== $vDefault) {
44: $fieldSeed[$fieldPropRefl->getName()] = $v;
45: }
46: }
47:
48: $ourModel->addField($this->ourField, $fieldSeed);
49: }
50:
51: // TODO seeding thru Model\FieldPropertiesTrait is a hack, at least unset these properties for now
52: foreach ($fieldPropsRefl as $fieldPropRefl) {
53: if ($fieldPropRefl->getName() !== 'caption') { // "caption" is also defined in Reference class
54: unset($this->{$fieldPropRefl->getName()});
55: }
56: }
57: }
58:
59: /**
60: * Returns our field or id field.
61: */
62: protected function referenceOurValue(): Field
63: {
64: // TODO horrible hack to render the field with a table prefix,
65: // find a solution how to wrap the field inside custom Field (without owner?)
66: $ourModelCloned = clone $this->getOurModel(null);
67: $ourModelCloned->persistenceData['use_table_prefixes'] = true;
68:
69: return $ourModelCloned->getReference($this->link)->getOurField();
70: }
71:
72: /**
73: * If our model is loaded, then return their model with respective loaded entity.
74: *
75: * If our model is not loaded, then return their model with condition set.
76: * This can happen in case of deep traversal $model->ref('Many')->ref('one_id'), for example.
77: */
78: #[\Override]
79: public function ref(Model $ourModel, array $defaults = []): Model
80: {
81: $ourModel = $this->getOurModel($ourModel);
82: $theirModel = $this->createTheirModel($defaults);
83:
84: if ($ourModel->isEntity()) {
85: $ourValue = $this->getOurFieldValue($ourModel);
86:
87: if ($this->getOurFieldName() === $ourModel->idField) {
88: $this->assertReferenceValueNotNull($ourValue);
89: $tryLoad = true;
90: } else {
91: $tryLoad = false;
92: }
93:
94: $theirModelOrig = $theirModel;
95: if ($ourValue === null) {
96: $theirModel = null;
97: } else {
98: $theirModel->addCondition($this->getTheirFieldName($theirModel), $ourValue);
99:
100: $theirModel = $tryLoad
101: ? $theirModel->tryLoadOne()
102: : $theirModel->loadOne();
103: }
104:
105: if ($theirModel === null) {
106: $theirModel = $theirModelOrig->createEntity();
107: }
108: }
109:
110: // their model will be reloaded after saving our model to reflect changes in referenced fields
111: $theirModel->getModel(true)->reloadAfterSave = false;
112:
113: if ($ourModel->isEntity()) {
114: $this->onHookToTheirModel($theirModel, Model::HOOK_AFTER_SAVE, function (Model $theirModel) use ($ourModel) {
115: $theirValue = $this->theirField ? $theirModel->get($this->theirField) : $theirModel->getId();
116:
117: if (!$this->getOurField()->compare($this->getOurFieldValue($ourModel), $theirValue)) {
118: $ourModel->set($this->getOurFieldName(), $theirValue)->save();
119: }
120:
121: $theirModel->reload();
122: });
123:
124: // add hook to set our field = null when record of referenced model is deleted
125: $this->onHookToTheirModel($theirModel, Model::HOOK_AFTER_DELETE, function (Model $theirModel) use ($ourModel) {
126: $ourModel->setNull($this->getOurFieldName());
127: });
128: }
129:
130: return $theirModel;
131: }
132: }
133: