1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Reference;
6:
7: use Atk4\Core\Factory;
8: use Atk4\Data\Exception;
9: use Atk4\Data\Field;
10: use Atk4\Data\Model;
11: use Atk4\Data\Reference;
12:
13: class HasMany extends Reference
14: {
15: private function getModelTableString(Model $model): string
16: {
17: if (is_object($model->table)) {
18: return $this->getModelTableString($model->table);
19: }
20:
21: return $model->table;
22: }
23:
24: #[\Override]
25: public function getTheirFieldName(Model $theirModel = null): string
26: {
27: if ($this->theirField) {
28: return $this->theirField;
29: }
30:
31: // this is pure guess, verify if such field exist, otherwise throw
32: // TODO probably remove completely in the future
33: $ourModel = $this->getOurModel(null);
34: $theirFieldName = preg_replace('~^.+\.~s', '', $this->getModelTableString($ourModel)) . '_' . $ourModel->idField;
35: if (!($theirModel ?? $this->createTheirModel())->hasField($theirFieldName)) {
36: throw (new Exception('Their model does not contain fallback field'))
37: ->addMoreInfo('their_fallback_field', $theirFieldName);
38: }
39:
40: return $theirFieldName;
41: }
42:
43: /**
44: * Returns our field value or id.
45: *
46: * @return mixed
47: */
48: protected function getOurFieldValueForRefCondition(Model $ourModel)
49: {
50: $ourModel = $this->getOurModel($ourModel);
51:
52: if ($ourModel->isEntity()) {
53: $res = $this->ourField
54: ? $ourModel->get($this->ourField)
55: : $ourModel->getId();
56: $this->assertReferenceValueNotNull($res);
57:
58: return $res;
59: }
60:
61: // create expression based on existing conditions
62: return $ourModel->action('field', [
63: $this->getOurFieldName(),
64: ]);
65: }
66:
67: /**
68: * Returns our field or id field.
69: */
70: protected function referenceOurValue(): Field
71: {
72: // TODO horrible hack to render the field with a table prefix,
73: // find a solution how to wrap the field inside custom Field (without owner?)
74: $ourModelCloned = clone $this->getOurModel(null);
75: $ourModelCloned->persistenceData['use_table_prefixes'] = true;
76:
77: return $ourModelCloned->getReference($this->link)->getOurField();
78: }
79:
80: /**
81: * Returns referenced model with condition set.
82: */
83: #[\Override]
84: public function ref(Model $ourModel, array $defaults = []): Model
85: {
86: $ourModel = $this->getOurModel($ourModel);
87:
88: return $this->createTheirModel($defaults)->addCondition(
89: $this->getTheirFieldName(),
90: $this->getOurFieldValueForRefCondition($ourModel)
91: );
92: }
93:
94: /**
95: * Creates model that can be used for generating sub-query actions.
96: *
97: * @param array<string, mixed> $defaults
98: */
99: public function refLink(?Model $ourModel, array $defaults = []): Model
100: {
101: $ourModel = $this->getOurModel($ourModel);
102:
103: $theirModelLinked = $this->createTheirModel($defaults)->addCondition(
104: $this->getTheirFieldName(),
105: $this->referenceOurValue()
106: );
107:
108: return $theirModelLinked;
109: }
110:
111: /**
112: * Adds field as expression to our model. Used in aggregate strategy.
113: *
114: * @param array<string, mixed> $defaults
115: */
116: public function addField(string $fieldName, array $defaults = []): Field
117: {
118: if (!isset($defaults['aggregate']) && !isset($defaults['concat']) && !isset($defaults['expr'])) {
119: throw (new Exception('Aggregate field requires "aggregate", "concat" or "expr" specified to hasMany()->addField()'))
120: ->addMoreInfo('field', $fieldName)
121: ->addMoreInfo('defaults', $defaults);
122: }
123:
124: $defaults['aggregateRelation'] = $this;
125:
126: $alias = $defaults['field'] ?? null;
127: $field = $alias ?? $fieldName;
128:
129: if (isset($defaults['concat'])) {
130: $defaults['aggregate'] = 'concat';
131: $defaults['concatSeparator'] = $defaults['concat'];
132: unset($defaults['concat']);
133: }
134:
135: if (isset($defaults['expr'])) {
136: $fx = function () use ($defaults, $alias) {
137: $theirModelLinked = $this->refLink(null);
138:
139: return $theirModelLinked->action('field', [$theirModelLinked->expr(
140: $defaults['expr'],
141: $defaults['args'] ?? []
142: ), 'alias' => $alias]);
143: };
144: unset($defaults['args']);
145: } elseif (is_object($defaults['aggregate'])) {
146: $fx = function () use ($defaults, $alias) {
147: return $this->refLink(null)->action('field', [$defaults['aggregate'], 'alias' => $alias]);
148: };
149: } elseif ($defaults['aggregate'] === 'count' && !isset($defaults['field'])) {
150: $fx = function () use ($alias) {
151: return $this->refLink(null)->action('count', ['alias' => $alias]);
152: };
153: } elseif (in_array($defaults['aggregate'], ['sum', 'avg', 'min', 'max', 'count'], true)) {
154: $fx = function () use ($defaults, $field) {
155: return $this->refLink(null)->action('fx0', [$defaults['aggregate'], $field]);
156: };
157: } else {
158: $fx = function () use ($defaults, $field) {
159: $args = [$defaults['aggregate'], $field];
160: if ($defaults['aggregate'] === 'concat') {
161: $args['concatSeparator'] = $defaults['concatSeparator'];
162: }
163:
164: return $this->refLink(null)->action('fx', $args);
165: };
166: }
167:
168: return $this->getOurModel(null)->addExpression($fieldName, array_merge($defaults, ['expr' => $fx]));
169: }
170:
171: /**
172: * Adds multiple fields.
173: *
174: * @param array<string, array<mixed>>|array<int, string> $fields
175: * @param array<mixed> $defaults
176: *
177: * @return $this
178: */
179: public function addFields(array $fields = [], array $defaults = [])
180: {
181: foreach ($fields as $k => $v) {
182: if (is_int($k)) {
183: $k = $v;
184: $v = [];
185: }
186:
187: $this->addField($k, Factory::mergeSeeds($v, $defaults));
188: }
189:
190: return $this;
191: }
192: }
193: