blob: d8dfa08c2fb450036bb41b8859affa808a662d5a [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Models\Relations;
4
5use Closure;
6use LdapRecord\DetectsErrors;
7use LdapRecord\LdapRecordException;
8use LdapRecord\Models\Model;
9use LdapRecord\Models\ModelNotFoundException;
10use LdapRecord\Query\Collection;
11
12class HasMany extends OneToMany
13{
14 use DetectsErrors;
15
16 /**
17 * The model to use for attaching / detaching.
18 *
19 * @var Model
20 */
21 protected $using;
22
23 /**
24 * The attribute key to use for attaching / detaching.
25 *
26 * @var string
27 */
28 protected $usingKey;
29
30 /**
31 * The pagination page size.
32 *
33 * @var int
34 */
35 protected $pageSize = 1000;
36
37 /**
38 * The exceptions to bypass for each relation operation.
39 *
40 * @var array
41 */
42 protected $bypass = [
43 'attach' => [
44 'Already exists', 'Type or value exists',
45 ],
46 'detach' => [
47 'No such attribute', 'Server is unwilling to perform',
48 ],
49 ];
50
51 /**
52 * Set the model and attribute to use for attaching / detaching.
53 *
54 * @param Model $using
55 * @param string $usingKey
56 *
57 * @return $this
58 */
59 public function using(Model $using, $usingKey)
60 {
61 $this->using = $using;
62 $this->usingKey = $usingKey;
63
64 return $this;
65 }
66
67 /**
68 * Set the pagination page size of the relation query.
69 *
70 * @param int $pageSize
71 *
72 * @return $this
73 */
74 public function setPageSize($pageSize)
75 {
76 $this->pageSize = $pageSize;
77
78 return $this;
79 }
80
81 /**
82 * Paginate the relation using the given page size.
83 *
84 * @param int $pageSize
85 *
86 * @return Collection
87 */
88 public function paginate($pageSize = 1000)
89 {
90 return $this->paginateOnceUsing($pageSize);
91 }
92
93 /**
94 * Paginate the relation using the page size once.
95 *
96 * @param int $pageSize
97 *
98 * @return Collection
99 */
100 protected function paginateOnceUsing($pageSize)
101 {
102 $size = $this->pageSize;
103
104 $result = $this->setPageSize($pageSize)->get();
105
106 $this->pageSize = $size;
107
108 return $result;
109 }
110
111 /**
112 * Chunk the relation results using the given callback.
113 *
114 * @param int $pageSize
115 * @param Closure $callback
116 *
117 * @return void
118 */
119 public function chunk($pageSize, Closure $callback)
120 {
121 $this->getRelationQuery()->chunk($pageSize, function ($entries) use ($callback) {
122 $callback($this->transformResults($entries));
123 });
124 }
125
126 /**
127 * Get the relationships results.
128 *
129 * @return Collection
130 */
131 public function getRelationResults()
132 {
133 return $this->transformResults(
134 $this->getRelationQuery()->paginate($this->pageSize)
135 );
136 }
137
138 /**
139 * Get the prepared relationship query.
140 *
141 * @return \LdapRecord\Query\Model\Builder
142 */
143 public function getRelationQuery()
144 {
145 $columns = $this->query->getSelects();
146
147 // We need to select the proper key to be able to retrieve its
148 // value from LDAP results. If we don't, we won't be able
149 // to properly attach / detach models from relation
150 // query results as the attribute will not exist.
151 $key = $this->using ? $this->usingKey : $this->relationKey;
152
153 // If the * character is missing from the attributes to select,
154 // we will add the key to the attributes to select and also
155 // validate that the key isn't already being selected
156 // to prevent stacking on multiple relation calls.
157 if (! in_array('*', $columns) && ! in_array($key, $columns)) {
158 $this->query->addSelect($key);
159 }
160
161 return $this->query->whereRaw(
162 $this->relationKey,
163 '=',
164 $this->getEscapedForeignValueFromModel($this->parent)
165 );
166 }
167
168 /**
169 * Attach a model to the relation.
170 *
171 * @param Model|string $model
172 *
173 * @return Model|string|false
174 */
175 public function attach($model)
176 {
177 return $this->attemptFailableOperation(
178 $this->buildAttachCallback($model),
179 $this->bypass['attach'],
180 $model
181 );
182 }
183
184 /**
185 * Build the attach callback.
186 *
187 * @param Model|string $model
188 *
189 * @return \Closure
190 */
191 protected function buildAttachCallback($model)
192 {
193 return function () use ($model) {
194 $foreign = $this->getAttachableForeignValue($model);
195
196 if ($this->using) {
197 return $this->using->createAttribute($this->usingKey, $foreign);
198 }
199
200 if (! $model instanceof Model) {
201 $model = $this->getForeignModelByValueOrFail($model);
202 }
203
204 return $model->createAttribute($this->relationKey, $foreign);
205 };
206 }
207
208 /**
209 * Attach a collection of models to the parent instance.
210 *
211 * @param iterable $models
212 *
213 * @return iterable
214 */
215 public function attachMany($models)
216 {
217 foreach ($models as $model) {
218 $this->attach($model);
219 }
220
221 return $models;
222 }
223
224 /**
225 * Detach the model from the relation.
226 *
227 * @param Model|string $model
228 *
229 * @return Model|string|false
230 */
231 public function detach($model)
232 {
233 return $this->attemptFailableOperation(
234 $this->buildDetachCallback($model),
235 $this->bypass['detach'],
236 $model
237 );
238 }
239
240 /**
241 * Build the detach callback.
242 *
243 * @param Model|string $model
244 *
245 * @return \Closure
246 */
247 protected function buildDetachCallback($model)
248 {
249 return function () use ($model) {
250 $foreign = $this->getAttachableForeignValue($model);
251
252 if ($this->using) {
253 return $this->using->deleteAttribute([$this->usingKey => $foreign]);
254 }
255
256 if (! $model instanceof Model) {
257 $model = $this->getForeignModelByValueOrFail($model);
258 }
259
260 return $model->deleteAttribute([$this->relationKey => $foreign]);
261 };
262 }
263
264 /**
265 * Get the attachable foreign value from the model.
266 *
267 * @param Model|string $model
268 *
269 * @return string
270 */
271 protected function getAttachableForeignValue($model)
272 {
273 if ($model instanceof Model) {
274 return $this->using
275 ? $this->getForeignValueFromModel($model)
276 : $this->getParentForeignValue();
277 }
278
279 return $this->using ? $model : $this->getParentForeignValue();
280 }
281
282 /**
283 * Get the foreign model by the given value, or fail.
284 *
285 * @param string $model
286 *
287 * @throws ModelNotFoundException
288 *
289 * @return Model
290 */
291 protected function getForeignModelByValueOrFail($model)
292 {
293 if (! is_null($model = $this->getForeignModelByValue($model))) {
294 return $model;
295 }
296
297 throw ModelNotFoundException::forQuery(
298 $this->query->getUnescapedQuery(),
299 $this->query->getDn()
300 );
301 }
302
303 /**
304 * Attempt a failable operation and return the value if successful.
305 *
306 * If a bypassable exception is encountered, the value will be returned.
307 *
308 * @param callable $operation
309 * @param string|array $bypass
310 * @param mixed $value
311 *
312 * @throws LdapRecordException
313 *
314 * @return mixed
315 */
316 protected function attemptFailableOperation($operation, $bypass, $value)
317 {
318 try {
319 $operation();
320
321 return $value;
322 } catch (LdapRecordException $e) {
323 if ($this->errorContainsMessage($e->getMessage(), $bypass)) {
324 return $value;
325 }
326
327 throw $e;
328 }
329 }
330
331 /**
332 * Detach all relation models.
333 *
334 * @return Collection
335 */
336 public function detachAll()
337 {
338 return $this->onceWithoutMerging(function () {
339 return $this->get()->each(function (Model $model) {
340 $this->detach($model);
341 });
342 });
343 }
344}