blob: eed5e91e4ca439c09d0aa185e5118bb783555694 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Query\Model;
4
5use Closure;
6use DateTime;
7use LdapRecord\Models\Model;
8use LdapRecord\Models\ModelNotFoundException;
9use LdapRecord\Models\Scope;
10use LdapRecord\Models\Types\ActiveDirectory;
11use LdapRecord\Query\Builder as BaseBuilder;
12use LdapRecord\Utilities;
13
14class Builder extends BaseBuilder
15{
16 /**
17 * The model being queried.
18 *
19 * @var Model
20 */
21 protected $model;
22
23 /**
24 * The global scopes to be applied.
25 *
26 * @var array
27 */
28 protected $scopes = [];
29
30 /**
31 * The removed global scopes.
32 *
33 * @var array
34 */
35 protected $removedScopes = [];
36
37 /**
38 * The applied global scopes.
39 *
40 * @var array
41 */
42 protected $appliedScopes = [];
43
44 /**
45 * Dynamically handle calls into the query instance.
46 *
47 * @param string $method
48 * @param array $parameters
49 *
50 * @return mixed
51 */
52 public function __call($method, $parameters)
53 {
54 if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
55 return $this->callScope([$this->model, $scope], $parameters);
56 }
57
58 return parent::__call($method, $parameters);
59 }
60
61 /**
62 * Apply the given scope on the current builder instance.
63 *
64 * @param callable $scope
65 * @param array $parameters
66 *
67 * @return mixed
68 */
69 protected function callScope(callable $scope, $parameters = [])
70 {
71 array_unshift($parameters, $this);
72
73 return $scope(...array_values($parameters)) ?? $this;
74 }
75
76 /**
77 * Get the attributes to select on the search.
78 *
79 * @return array
80 */
81 public function getSelects()
82 {
83 // Here we will ensure the models GUID attribute is always
84 // selected. In some LDAP directories, the attribute is
85 // virtual and must be requested for specifically.
86 return array_values(array_unique(
87 array_merge([$this->model->getGuidKey()], parent::getSelects())
88 ));
89 }
90
91 /**
92 * Set the model instance for the model being queried.
93 *
94 * @param Model $model
95 *
96 * @return $this
97 */
98 public function setModel(Model $model)
99 {
100 $this->model = $model;
101
102 return $this;
103 }
104
105 /**
106 * Returns the model being queried for.
107 *
108 * @return Model
109 */
110 public function getModel()
111 {
112 return $this->model;
113 }
114
115 /**
116 * Get a new model query builder instance.
117 *
118 * @param string|null $baseDn
119 *
120 * @return static
121 */
122 public function newInstance($baseDn = null)
123 {
124 return parent::newInstance($baseDn)->model($this->model);
125 }
126
127 /**
128 * Finds a model by its distinguished name.
129 *
130 * @param array|string $dn
131 * @param array|string|string[] $columns
132 *
133 * @return Model|\LdapRecord\Query\Collection|static|null
134 */
135 public function find($dn, $columns = ['*'])
136 {
137 return $this->afterScopes(function () use ($dn, $columns) {
138 return parent::find($dn, $columns);
139 });
140 }
141
142 /**
143 * Finds a record using ambiguous name resolution.
144 *
145 * @param string|array $value
146 * @param array|string $columns
147 *
148 * @return Model|\LdapRecord\Query\Collection|static|null
149 */
150 public function findByAnr($value, $columns = ['*'])
151 {
152 if (is_array($value)) {
153 return $this->findManyByAnr($value, $columns);
154 }
155
156 // If the model is not compatible with ANR filters,
157 // we must construct an equivalent filter that
158 // the current LDAP server does support.
159 if (! $this->modelIsCompatibleWithAnr()) {
160 return $this->prepareAnrEquivalentQuery($value)->first($columns);
161 }
162
163 return $this->findBy('anr', $value, $columns);
164 }
165
166 /**
167 * Determine if the current model is compatible with ANR filters.
168 *
169 * @return bool
170 */
171 protected function modelIsCompatibleWithAnr()
172 {
173 return $this->model instanceof ActiveDirectory;
174 }
175
176 /**
177 * Finds a record using ambiguous name resolution.
178 *
179 * If a record is not found, an exception is thrown.
180 *
181 * @param string $value
182 * @param array|string $columns
183 *
184 * @throws ModelNotFoundException
185 *
186 * @return Model
187 */
188 public function findByAnrOrFail($value, $columns = ['*'])
189 {
190 if (! $entry = $this->findByAnr($value, $columns)) {
191 $this->throwNotFoundException($this->getUnescapedQuery(), $this->dn);
192 }
193
194 return $entry;
195 }
196
197 /**
198 * Throws a not found exception.
199 *
200 * @param string $query
201 * @param string $dn
202 *
203 * @throws ModelNotFoundException
204 */
205 protected function throwNotFoundException($query, $dn)
206 {
207 throw ModelNotFoundException::forQuery($query, $dn);
208 }
209
210 /**
211 * Finds multiple records using ambiguous name resolution.
212 *
213 * @param array $values
214 * @param array $columns
215 *
216 * @return \LdapRecord\Query\Collection
217 */
218 public function findManyByAnr(array $values = [], $columns = ['*'])
219 {
220 $this->select($columns);
221
222 if (! $this->modelIsCompatibleWithAnr()) {
223 foreach ($values as $value) {
224 $this->prepareAnrEquivalentQuery($value);
225 }
226
227 return $this->get($columns);
228 }
229
230 return $this->findManyBy('anr', $values);
231 }
232
233 /**
234 * Creates an ANR equivalent query for LDAP distributions that do not support ANR.
235 *
236 * @param string $value
237 *
238 * @return $this
239 */
240 protected function prepareAnrEquivalentQuery($value)
241 {
242 return $this->orFilter(function (self $query) use ($value) {
243 foreach ($this->model->getAnrAttributes() as $attribute) {
244 $query->whereEquals($attribute, $value);
245 }
246 });
247 }
248
249 /**
250 * Finds a record by its string GUID.
251 *
252 * @param string $guid
253 * @param array|string $columns
254 *
255 * @return Model|static|null
256 */
257 public function findByGuid($guid, $columns = ['*'])
258 {
259 try {
260 return $this->findByGuidOrFail($guid, $columns);
261 } catch (ModelNotFoundException $e) {
262 return;
263 }
264 }
265
266 /**
267 * Finds a record by its string GUID.
268 *
269 * Fails upon no records returned.
270 *
271 * @param string $guid
272 * @param array|string $columns
273 *
274 * @throws ModelNotFoundException
275 *
276 * @return Model|static
277 */
278 public function findByGuidOrFail($guid, $columns = ['*'])
279 {
280 if ($this->model instanceof ActiveDirectory) {
281 $guid = Utilities::stringGuidToHex($guid);
282 }
283
284 return $this->whereRaw([
285 $this->model->getGuidKey() => $guid,
286 ])->firstOrFail($columns);
287 }
288
289 /**
290 * @inheritdoc
291 */
292 public function getQuery()
293 {
294 return $this->afterScopes(function () {
295 return parent::getQuery();
296 });
297 }
298
299 /**
300 * Apply the query scopes and execute the callback.
301 *
302 * @param Closure $callback
303 *
304 * @return mixed
305 */
306 protected function afterScopes(Closure $callback)
307 {
308 $this->applyScopes();
309
310 return $callback();
311 }
312
313 /**
314 * Apply the global query scopes.
315 *
316 * @return $this
317 */
318 public function applyScopes()
319 {
320 if (! $this->scopes) {
321 return $this;
322 }
323
324 foreach ($this->scopes as $identifier => $scope) {
325 if (isset($this->appliedScopes[$identifier])) {
326 continue;
327 }
328
329 $scope instanceof Scope
330 ? $scope->apply($this, $this->getModel())
331 : $scope($this);
332
333 $this->appliedScopes[$identifier] = $scope;
334 }
335
336 return $this;
337 }
338
339 /**
340 * Register a new global scope.
341 *
342 * @param string $identifier
343 * @param Scope|\Closure $scope
344 *
345 * @return $this
346 */
347 public function withGlobalScope($identifier, $scope)
348 {
349 $this->scopes[$identifier] = $scope;
350
351 return $this;
352 }
353
354 /**
355 * Remove a registered global scope.
356 *
357 * @param Scope|string $scope
358 *
359 * @return $this
360 */
361 public function withoutGlobalScope($scope)
362 {
363 if (! is_string($scope)) {
364 $scope = get_class($scope);
365 }
366
367 unset($this->scopes[$scope]);
368
369 $this->removedScopes[] = $scope;
370
371 return $this;
372 }
373
374 /**
375 * Remove all or passed registered global scopes.
376 *
377 * @param array|null $scopes
378 *
379 * @return $this
380 */
381 public function withoutGlobalScopes(array $scopes = null)
382 {
383 if (! is_array($scopes)) {
384 $scopes = array_keys($this->scopes);
385 }
386
387 foreach ($scopes as $scope) {
388 $this->withoutGlobalScope($scope);
389 }
390
391 return $this;
392 }
393
394 /**
395 * Get an array of global scopes that were removed from the query.
396 *
397 * @return array
398 */
399 public function removedScopes()
400 {
401 return $this->removedScopes;
402 }
403
404 /**
405 * Get an array of the global scopes that were applied to the query.
406 *
407 * @return array
408 */
409 public function appliedScopes()
410 {
411 return $this->appliedScopes;
412 }
413
414 /**
415 * Processes and converts the given LDAP results into models.
416 *
417 * @param array $results
418 *
419 * @return \LdapRecord\Query\Collection
420 */
421 protected function process(array $results)
422 {
423 return $this->model->hydrate(parent::process($results));
424 }
425
426 /**
427 * @inheritdoc
428 */
429 protected function prepareWhereValue($field, $value, $raw = false)
430 {
431 if ($value instanceof DateTime) {
432 $field = $this->model->normalizeAttributeKey($field);
433
434 if (! $this->model->isDateAttribute($field)) {
435 throw new \UnexpectedValueException(
436 "Cannot convert field [$field] to an LDAP timestamp. You must add this field as a model date."
437 .' Refer to https://ldaprecord.com/docs/model-mutators/#date-mutators'
438 );
439 }
440
441 $value = $this->model->fromDateTime($this->model->getDates()[$field], $value);
442 }
443
444 return parent::prepareWhereValue($field, $value, $raw);
445 }
446}