blob: 2e11696b4421c56b56235265d8ed34a087b66c42 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Models;
4
5use ArrayAccess;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01006use Illuminate\Contracts\Support\Arrayable;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02007use InvalidArgumentException;
8use JsonSerializable;
9use LdapRecord\Connection;
10use LdapRecord\Container;
11use LdapRecord\EscapesValues;
12use LdapRecord\Models\Attributes\DistinguishedName;
13use LdapRecord\Models\Attributes\Guid;
14use LdapRecord\Models\Events\Renamed;
15use LdapRecord\Models\Events\Renaming;
16use LdapRecord\Query\Model\Builder;
17use LdapRecord\Support\Arr;
18use UnexpectedValueException;
19
20/** @mixin Builder */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010021abstract class Model implements ArrayAccess, Arrayable, JsonSerializable
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020022{
23 use EscapesValues;
24 use Concerns\HasEvents;
25 use Concerns\HasScopes;
26 use Concerns\HasAttributes;
27 use Concerns\HasGlobalScopes;
28 use Concerns\HidesAttributes;
29 use Concerns\HasRelationships;
30
31 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010032 * Indicates if the model exists in the directory.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020033 *
34 * @var bool
35 */
36 public $exists = false;
37
38 /**
39 * Indicates whether the model was created during the current request lifecycle.
40 *
41 * @var bool
42 */
43 public $wasRecentlyCreated = false;
44
45 /**
46 * Indicates whether the model was renamed during the current request lifecycle.
47 *
48 * @var bool
49 */
50 public $wasRecentlyRenamed = false;
51
52 /**
53 * The models distinguished name.
54 *
55 * @var string|null
56 */
57 protected $dn;
58
59 /**
60 * The base DN of where the model should be created in.
61 *
62 * @var string|null
63 */
64 protected $in;
65
66 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010067 * The object classes of the model.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020068 *
69 * @var array
70 */
71 public static $objectClasses = [];
72
73 /**
74 * The connection container instance.
75 *
76 * @var Container
77 */
78 protected static $container;
79
80 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010081 * The connection name for the model.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020082 *
83 * @var string|null
84 */
85 protected $connection;
86
87 /**
88 * The attribute key that contains the models object GUID.
89 *
90 * @var string
91 */
92 protected $guidKey = 'objectguid';
93
94 /**
95 * Contains the models modifications.
96 *
97 * @var array
98 */
99 protected $modifications = [];
100
101 /**
102 * The array of global scopes on the model.
103 *
104 * @var array
105 */
106 protected static $globalScopes = [];
107
108 /**
109 * The array of booted models.
110 *
111 * @var array
112 */
113 protected static $booted = [];
114
115 /**
116 * Constructor.
117 *
118 * @param array $attributes
119 */
120 public function __construct(array $attributes = [])
121 {
122 $this->bootIfNotBooted();
123
124 $this->fill($attributes);
125 }
126
127 /**
128 * Check if the model needs to be booted and if so, do it.
129 *
130 * @return void
131 */
132 protected function bootIfNotBooted()
133 {
134 if (! isset(static::$booted[static::class])) {
135 static::$booted[static::class] = true;
136
137 static::boot();
138 }
139 }
140
141 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100142 * The "boot" method of the model.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200143 *
144 * @return void
145 */
146 protected static function boot()
147 {
148 //
149 }
150
151 /**
152 * Clear the list of booted models so they will be re-booted.
153 *
154 * @return void
155 */
156 public static function clearBootedModels()
157 {
158 static::$booted = [];
159
160 static::$globalScopes = [];
161 }
162
163 /**
164 * Handle dynamic method calls into the model.
165 *
166 * @param string $method
167 * @param array $parameters
168 *
169 * @return mixed
170 */
171 public function __call($method, $parameters)
172 {
173 if (method_exists($this, $method)) {
174 return $this->$method(...$parameters);
175 }
176
177 return $this->newQuery()->$method(...$parameters);
178 }
179
180 /**
181 * Handle dynamic static method calls into the method.
182 *
183 * @param string $method
184 * @param array $parameters
185 *
186 * @return mixed
187 */
188 public static function __callStatic($method, $parameters)
189 {
190 return (new static())->$method(...$parameters);
191 }
192
193 /**
194 * Returns the models distinguished name.
195 *
196 * @return string|null
197 */
198 public function getDn()
199 {
200 return $this->dn;
201 }
202
203 /**
204 * Set the models distinguished name.
205 *
206 * @param string $dn
207 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100208 * @return $this
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200209 */
210 public function setDn($dn)
211 {
212 $this->dn = (string) $dn;
213
214 return $this;
215 }
216
217 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100218 * A mutator for setting the models distinguished name.
219 *
220 * @param string $dn
221 *
222 * @return $this
223 */
224 public function setDnAttribute($dn)
225 {
226 return $this->setRawAttribute('dn', $dn)->setDn($dn);
227 }
228
229 /**
230 * A mutator for setting the models distinguished name.
231 *
232 * @param string $dn
233 *
234 * @return $this
235 */
236 public function setDistinguishedNameAttribute($dn)
237 {
238 return $this->setRawAttribute('distinguishedname', $dn)->setDn($dn);
239 }
240
241 /**
242 * Get the connection for the model.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200243 *
244 * @return Connection
245 */
246 public function getConnection()
247 {
248 return static::resolveConnection($this->getConnectionName());
249 }
250
251 /**
252 * Get the current connection name for the model.
253 *
254 * @return string
255 */
256 public function getConnectionName()
257 {
258 return $this->connection;
259 }
260
261 /**
262 * Set the connection associated with the model.
263 *
264 * @param string $name
265 *
266 * @return $this
267 */
268 public function setConnection($name)
269 {
270 $this->connection = $name;
271
272 return $this;
273 }
274
275 /**
276 * Begin querying the model on a given connection.
277 *
278 * @param string|null $connection
279 *
280 * @return Builder
281 */
282 public static function on($connection = null)
283 {
284 $instance = new static();
285
286 $instance->setConnection($connection);
287
288 return $instance->newQuery();
289 }
290
291 /**
292 * Get all the models from the directory.
293 *
294 * @param array|mixed $attributes
295 *
296 * @return Collection|static[]
297 */
298 public static function all($attributes = ['*'])
299 {
300 return static::query()->select($attributes)->paginate();
301 }
302
303 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100304 * Make a new model instance.
305 *
306 * @param array $attributes
307 *
308 * @return static
309 */
310 public static function make($attributes = [])
311 {
312 return new static($attributes);
313 }
314
315 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200316 * Begin querying the model.
317 *
318 * @return Builder
319 */
320 public static function query()
321 {
322 return (new static())->newQuery();
323 }
324
325 /**
326 * Get a new query for builder filtered by the current models object classes.
327 *
328 * @return Builder
329 */
330 public function newQuery()
331 {
332 return $this->registerModelScopes(
333 $this->newQueryWithoutScopes()
334 );
335 }
336
337 /**
338 * Get a new query builder that doesn't have any global scopes.
339 *
340 * @return Builder
341 */
342 public function newQueryWithoutScopes()
343 {
344 return static::resolveConnection(
345 $this->getConnectionName()
346 )->query()->model($this);
347 }
348
349 /**
350 * Create a new query builder.
351 *
352 * @param Connection $connection
353 *
354 * @return Builder
355 */
356 public function newQueryBuilder(Connection $connection)
357 {
358 return new Builder($connection);
359 }
360
361 /**
362 * Create a new model instance.
363 *
364 * @param array $attributes
365 *
366 * @return static
367 */
368 public function newInstance(array $attributes = [])
369 {
370 return (new static($attributes))->setConnection($this->getConnectionName());
371 }
372
373 /**
374 * Resolve a connection instance.
375 *
376 * @param string|null $connection
377 *
378 * @return Connection
379 */
380 public static function resolveConnection($connection = null)
381 {
382 return static::getConnectionContainer()->get($connection);
383 }
384
385 /**
386 * Get the connection container.
387 *
388 * @return Container
389 */
390 public static function getConnectionContainer()
391 {
392 return static::$container ?? static::getDefaultConnectionContainer();
393 }
394
395 /**
396 * Get the default singleton container instance.
397 *
398 * @return Container
399 */
400 public static function getDefaultConnectionContainer()
401 {
402 return Container::getInstance();
403 }
404
405 /**
406 * Set the connection container.
407 *
408 * @param Container $container
409 *
410 * @return void
411 */
412 public static function setConnectionContainer(Container $container)
413 {
414 static::$container = $container;
415 }
416
417 /**
418 * Unset the connection container.
419 *
420 * @return void
421 */
422 public static function unsetConnectionContainer()
423 {
424 static::$container = null;
425 }
426
427 /**
428 * Register the query scopes for this builder instance.
429 *
430 * @param Builder $builder
431 *
432 * @return Builder
433 */
434 public function registerModelScopes($builder)
435 {
436 $this->applyObjectClassScopes($builder);
437
438 $this->registerGlobalScopes($builder);
439
440 return $builder;
441 }
442
443 /**
444 * Register the global model scopes.
445 *
446 * @param Builder $builder
447 *
448 * @return Builder
449 */
450 public function registerGlobalScopes($builder)
451 {
452 foreach ($this->getGlobalScopes() as $identifier => $scope) {
453 $builder->withGlobalScope($identifier, $scope);
454 }
455
456 return $builder;
457 }
458
459 /**
460 * Apply the model object class scopes to the given builder instance.
461 *
462 * @param Builder $query
463 *
464 * @return void
465 */
466 public function applyObjectClassScopes(Builder $query)
467 {
468 foreach (static::$objectClasses as $objectClass) {
469 $query->where('objectclass', '=', $objectClass);
470 }
471 }
472
473 /**
474 * Returns the models distinguished name when the model is converted to a string.
475 *
476 * @return null|string
477 */
478 public function __toString()
479 {
480 return $this->getDn();
481 }
482
483 /**
484 * Returns a new batch modification.
485 *
486 * @param string|null $attribute
487 * @param string|int|null $type
488 * @param array $values
489 *
490 * @return BatchModification
491 */
492 public function newBatchModification($attribute = null, $type = null, $values = [])
493 {
494 return new BatchModification($attribute, $type, $values);
495 }
496
497 /**
498 * Returns a new collection with the specified items.
499 *
500 * @param mixed $items
501 *
502 * @return Collection
503 */
504 public function newCollection($items = [])
505 {
506 return new Collection($items);
507 }
508
509 /**
510 * Dynamically retrieve attributes on the object.
511 *
512 * @param mixed $key
513 *
514 * @return bool
515 */
516 public function __get($key)
517 {
518 return $this->getAttribute($key);
519 }
520
521 /**
522 * Dynamically set attributes on the object.
523 *
524 * @param mixed $key
525 * @param mixed $value
526 *
527 * @return $this
528 */
529 public function __set($key, $value)
530 {
531 return $this->setAttribute($key, $value);
532 }
533
534 /**
535 * Determine if the given offset exists.
536 *
537 * @param string $offset
538 *
539 * @return bool
540 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100541 #[\ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200542 public function offsetExists($offset)
543 {
544 return ! is_null($this->getAttribute($offset));
545 }
546
547 /**
548 * Get the value for a given offset.
549 *
550 * @param string $offset
551 *
552 * @return mixed
553 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100554 #[\ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200555 public function offsetGet($offset)
556 {
557 return $this->getAttribute($offset);
558 }
559
560 /**
561 * Set the value at the given offset.
562 *
563 * @param string $offset
564 * @param mixed $value
565 *
566 * @return void
567 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100568 #[\ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200569 public function offsetSet($offset, $value)
570 {
571 $this->setAttribute($offset, $value);
572 }
573
574 /**
575 * Unset the value at the given offset.
576 *
577 * @param string $offset
578 *
579 * @return void
580 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100581 #[\ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200582 public function offsetUnset($offset)
583 {
584 unset($this->attributes[$offset]);
585 }
586
587 /**
588 * Determine if an attribute exists on the model.
589 *
590 * @param string $key
591 *
592 * @return bool
593 */
594 public function __isset($key)
595 {
596 return $this->offsetExists($key);
597 }
598
599 /**
600 * Unset an attribute on the model.
601 *
602 * @param string $key
603 *
604 * @return void
605 */
606 public function __unset($key)
607 {
608 $this->offsetUnset($key);
609 }
610
611 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100612 * Convert the model to its JSON encodeable array form.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200613 *
614 * @return array
615 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100616 public function toArray()
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200617 {
618 return $this->attributesToArray();
619 }
620
621 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100622 * Convert the model's attributes into JSON encodeable values.
623 *
624 * @return array
625 */
626 #[\ReturnTypeWillChange]
627 public function jsonSerialize()
628 {
629 return $this->toArray();
630 }
631
632 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200633 * Converts extra attributes for JSON serialization.
634 *
635 * @param array $attributes
636 *
637 * @return array
638 */
639 protected function convertAttributesForJson(array $attributes = [])
640 {
641 // If the model has a GUID set, we need to convert
642 // it due to it being in binary. Otherwise we'll
643 // receive a JSON serialization exception.
644 if ($this->hasAttribute($this->guidKey)) {
645 return array_replace($attributes, [
646 $this->guidKey => [$this->getConvertedGuid()],
647 ]);
648 }
649
650 return $attributes;
651 }
652
653 /**
654 * Reload a fresh model instance from the directory.
655 *
656 * @return static|false
657 */
658 public function fresh()
659 {
660 if (! $this->exists) {
661 return false;
662 }
663
664 return $this->newQuery()->find($this->dn);
665 }
666
667 /**
668 * Determine if two models have the same distinguished name and belong to the same connection.
669 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100670 * @param Model|null $model
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200671 *
672 * @return bool
673 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100674 public function is($model)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200675 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100676 return ! is_null($model)
677 && $this->dn == $model->getDn()
678 && $this->getConnectionName() == $model->getConnectionName();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200679 }
680
681 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100682 * Determine if two models are not the same.
683 *
684 * @param Model|null $model
685 *
686 * @return bool
687 */
688 public function isNot($model)
689 {
690 return ! $this->is($model);
691 }
692
693 /**
694 * Hydrate a new collection of models from search results.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200695 *
696 * @param array $records
697 *
698 * @return Collection
699 */
700 public function hydrate($records)
701 {
702 return $this->newCollection($records)->transform(function ($attributes) {
703 return $attributes instanceof static
704 ? $attributes
705 : static::newInstance()->setRawAttributes($attributes);
706 });
707 }
708
709 /**
710 * Converts the current model into the given model.
711 *
712 * @param Model $into
713 *
714 * @return Model
715 */
716 public function convert(self $into)
717 {
718 $into->setDn($this->getDn());
719 $into->setConnection($this->getConnectionName());
720
721 $this->exists
722 ? $into->setRawAttributes($this->getAttributes())
723 : $into->fill($this->getAttributes());
724
725 return $into;
726 }
727
728 /**
729 * Refreshes the current models attributes with the directory values.
730 *
731 * @return bool
732 */
733 public function refresh()
734 {
735 if ($model = $this->fresh()) {
736 $this->setRawAttributes($model->getAttributes());
737
738 return true;
739 }
740
741 return false;
742 }
743
744 /**
745 * Get the model's batch modifications to be processed.
746 *
747 * @return array
748 */
749 public function getModifications()
750 {
751 $builtModifications = [];
752
753 foreach ($this->buildModificationsFromDirty() as $modification) {
754 $builtModifications[] = $modification->get();
755 }
756
757 return array_merge($this->modifications, $builtModifications);
758 }
759
760 /**
761 * Set the models batch modifications.
762 *
763 * @param array $modifications
764 *
765 * @return $this
766 */
767 public function setModifications(array $modifications = [])
768 {
769 $this->modifications = [];
770
771 foreach ($modifications as $modification) {
772 $this->addModification($modification);
773 }
774
775 return $this;
776 }
777
778 /**
779 * Adds a batch modification to the model.
780 *
781 * @param array|BatchModification $mod
782 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200783 * @return $this
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100784 *
785 * @throws InvalidArgumentException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200786 */
787 public function addModification($mod = [])
788 {
789 if ($mod instanceof BatchModification) {
790 $mod = $mod->get();
791 }
792
793 if ($this->isValidModification($mod)) {
794 $this->modifications[] = $mod;
795
796 return $this;
797 }
798
799 throw new InvalidArgumentException(
800 "The batch modification array does not include the mandatory 'attrib' or 'modtype' keys."
801 );
802 }
803
804 /**
805 * Get the model's guid attribute key name.
806 *
807 * @return string
808 */
809 public function getGuidKey()
810 {
811 return $this->guidKey;
812 }
813
814 /**
815 * Get the model's ANR attributes for querying when incompatible with ANR.
816 *
817 * @return array
818 */
819 public function getAnrAttributes()
820 {
821 return ['cn', 'sn', 'uid', 'name', 'mail', 'givenname', 'displayname'];
822 }
823
824 /**
825 * Get the name of the model, or the given DN.
826 *
827 * @param string|null $dn
828 *
829 * @return string|null
830 */
831 public function getName($dn = null)
832 {
833 return $this->newDn($dn ?? $this->dn)->name();
834 }
835
836 /**
837 * Get the head attribute of the model, or the given DN.
838 *
839 * @param string|null $dn
840 *
841 * @return string|null
842 */
843 public function getHead($dn = null)
844 {
845 return $this->newDn($dn ?? $this->dn)->head();
846 }
847
848 /**
849 * Get the RDN of the model, of the given DN.
850 *
851 * @param string|null
852 *
853 * @return string|null
854 */
855 public function getRdn($dn = null)
856 {
857 return $this->newDn($dn ?? $this->dn)->relative();
858 }
859
860 /**
861 * Get the parent distinguished name of the model, or the given DN.
862 *
863 * @param string|null
864 *
865 * @return string|null
866 */
867 public function getParentDn($dn = null)
868 {
869 return $this->newDn($dn ?? $this->dn)->parent();
870 }
871
872 /**
873 * Create a new Distinguished Name object.
874 *
875 * @param string|null $dn
876 *
877 * @return DistinguishedName
878 */
879 public function newDn($dn = null)
880 {
881 return new DistinguishedName($dn);
882 }
883
884 /**
885 * Get the model's object GUID key.
886 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100887 * @return string
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200888 */
889 public function getObjectGuidKey()
890 {
891 return $this->guidKey;
892 }
893
894 /**
895 * Get the model's binary object GUID.
896 *
897 * @see https://msdn.microsoft.com/en-us/library/ms679021(v=vs.85).aspx
898 *
899 * @return string|null
900 */
901 public function getObjectGuid()
902 {
903 return $this->getFirstAttribute($this->guidKey);
904 }
905
906 /**
907 * Get the model's object classes.
908 *
909 * @return array
910 */
911 public function getObjectClasses()
912 {
913 return $this->getAttribute('objectclass') ?: [];
914 }
915
916 /**
917 * Get the model's string GUID.
918 *
919 * @return string|null
920 */
921 public function getConvertedGuid()
922 {
923 try {
924 return (string) new Guid($this->getObjectGuid());
925 } catch (InvalidArgumentException $e) {
926 return;
927 }
928 }
929
930 /**
931 * Determine if the current model is a direct descendant of the given.
932 *
933 * @param static|string $parent
934 *
935 * @return bool
936 */
937 public function isChildOf($parent)
938 {
939 return $this->newDn($this->getDn())->isChildOf(
940 $this->newDn((string) $parent)
941 );
942 }
943
944 /**
945 * Determine if the current model is a direct ascendant of the given.
946 *
947 * @param static|string $child
948 *
949 * @return bool
950 */
951 public function isParentOf($child)
952 {
953 return $this->newDn($this->getDn())->isParentOf(
954 $this->newDn((string) $child)
955 );
956 }
957
958 /**
959 * Determine if the current model is a descendant of the given.
960 *
961 * @param static|string $model
962 *
963 * @return bool
964 */
965 public function isDescendantOf($model)
966 {
967 return $this->dnIsInside($this->getDn(), $model);
968 }
969
970 /**
971 * Determine if the current model is a ancestor of the given.
972 *
973 * @param static|string $model
974 *
975 * @return bool
976 */
977 public function isAncestorOf($model)
978 {
979 return $this->dnIsInside($model, $this->getDn());
980 }
981
982 /**
983 * Determines if the DN is inside of the parent DN.
984 *
985 * @param static|string $dn
986 * @param static|string $parentDn
987 *
988 * @return bool
989 */
990 protected function dnIsInside($dn, $parentDn)
991 {
992 return $this->newDn((string) $dn)->isDescendantOf(
993 $this->newDn($parentDn)
994 );
995 }
996
997 /**
998 * Set the base DN of where the model should be created in.
999 *
1000 * @param static|string $dn
1001 *
1002 * @return $this
1003 */
1004 public function inside($dn)
1005 {
1006 $this->in = $dn instanceof self ? $dn->getDn() : $dn;
1007
1008 return $this;
1009 }
1010
1011 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001012 * Save the model to the directory without raising any events.
1013 *
1014 * @param array $attributes
1015 *
1016 * @return void
1017 *
1018 * @throws \LdapRecord\LdapRecordException
1019 */
1020 public function saveQuietly(array $attributes = [])
1021 {
1022 static::withoutEvents(function () use ($attributes) {
1023 $this->save($attributes);
1024 });
1025 }
1026
1027 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001028 * Save the model to the directory.
1029 *
1030 * @param array $attributes The attributes to update or create for the current entry.
1031 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001032 * @return void
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001033 *
1034 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001035 */
1036 public function save(array $attributes = [])
1037 {
1038 $this->fill($attributes);
1039
1040 $this->fireModelEvent(new Events\Saving($this));
1041
1042 $this->exists ? $this->performUpdate() : $this->performInsert();
1043
1044 $this->fireModelEvent(new Events\Saved($this));
1045
1046 $this->in = null;
1047 }
1048
1049 /**
1050 * Inserts the model into the directory.
1051 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001052 * @return void
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001053 *
1054 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001055 */
1056 protected function performInsert()
1057 {
1058 // Here we will populate the models object classes if it
1059 // does not already have any set. An LDAP object cannot
1060 // be successfully created in the server without them.
1061 if (! $this->hasAttribute('objectclass')) {
1062 $this->setAttribute('objectclass', static::$objectClasses);
1063 }
1064
1065 $query = $this->newQuery();
1066
1067 // If the model does not currently have a distinguished
1068 // name, we will attempt to generate one automatically
1069 // using the current query builder's DN as the base.
1070 if (empty($this->getDn())) {
1071 $this->setDn($this->getCreatableDn());
1072 }
1073
1074 $this->fireModelEvent(new Events\Creating($this));
1075
1076 // Here we perform the insert of new object in the directory,
1077 // but filter out any empty attributes before sending them
1078 // to the server. LDAP servers will throw an exception if
1079 // attributes have been given empty or null values.
1080 $query->insert($this->getDn(), array_filter($this->getAttributes()));
1081
1082 $this->fireModelEvent(new Events\Created($this));
1083
1084 $this->syncOriginal();
1085
1086 $this->exists = true;
1087
1088 $this->wasRecentlyCreated = true;
1089 }
1090
1091 /**
1092 * Updates the model in the directory.
1093 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001094 * @return void
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001095 *
1096 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001097 */
1098 protected function performUpdate()
1099 {
1100 if (! count($modifications = $this->getModifications())) {
1101 return;
1102 }
1103
1104 $this->fireModelEvent(new Events\Updating($this));
1105
1106 $this->newQuery()->update($this->dn, $modifications);
1107
1108 $this->fireModelEvent(new Events\Updated($this));
1109
1110 $this->syncOriginal();
1111
1112 $this->modifications = [];
1113 }
1114
1115 /**
1116 * Create the model in the directory.
1117 *
1118 * @param array $attributes The attributes for the new entry.
1119 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001120 * @return Model
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001121 *
1122 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001123 */
1124 public static function create(array $attributes = [])
1125 {
1126 $instance = new static($attributes);
1127
1128 $instance->save();
1129
1130 return $instance;
1131 }
1132
1133 /**
1134 * Create an attribute on the model.
1135 *
1136 * @param string $attribute The attribute to create
1137 * @param mixed $value The value of the new attribute
1138 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001139 * @return void
1140 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001141 * @throws ModelDoesNotExistException
1142 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001143 */
1144 public function createAttribute($attribute, $value)
1145 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001146 $this->requireExistence();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001147
1148 $this->newQuery()->insertAttributes($this->dn, [$attribute => (array) $value]);
1149
1150 $this->addAttributeValue($attribute, $value);
1151 }
1152
1153 /**
1154 * Update the model.
1155 *
1156 * @param array $attributes The attributes to update for the current entry.
1157 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001158 * @return void
1159 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001160 * @throws ModelDoesNotExistException
1161 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001162 */
1163 public function update(array $attributes = [])
1164 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001165 $this->requireExistence();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001166
1167 $this->save($attributes);
1168 }
1169
1170 /**
1171 * Update the model attribute with the specified value.
1172 *
1173 * @param string $attribute The attribute to modify
1174 * @param mixed $value The new value for the attribute
1175 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001176 * @return void
1177 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001178 * @throws ModelDoesNotExistException
1179 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001180 */
1181 public function updateAttribute($attribute, $value)
1182 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001183 $this->requireExistence();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001184
1185 $this->newQuery()->updateAttributes($this->dn, [$attribute => (array) $value]);
1186
1187 $this->addAttributeValue($attribute, $value);
1188 }
1189
1190 /**
1191 * Destroy the models for the given distinguished names.
1192 *
1193 * @param Collection|array|string $dns
1194 * @param bool $recursive
1195 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001196 * @return int
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001197 *
1198 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001199 */
1200 public static function destroy($dns, $recursive = false)
1201 {
1202 $count = 0;
1203
1204 $dns = is_string($dns) ? (array) $dns : $dns;
1205
1206 $instance = new static();
1207
1208 foreach ($dns as $dn) {
1209 if (! $model = $instance->find($dn)) {
1210 continue;
1211 }
1212
1213 $model->delete($recursive);
1214
1215 $count++;
1216 }
1217
1218 return $count;
1219 }
1220
1221 /**
1222 * Delete the model from the directory.
1223 *
1224 * Throws a ModelNotFoundException if the current model does
1225 * not exist or does not contain a distinguished name.
1226 *
1227 * @param bool $recursive Whether to recursively delete leaf nodes (models that are children).
1228 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001229 * @return void
1230 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001231 * @throws ModelDoesNotExistException
1232 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001233 */
1234 public function delete($recursive = false)
1235 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001236 $this->requireExistence();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001237
1238 $this->fireModelEvent(new Events\Deleting($this));
1239
1240 if ($recursive) {
1241 $this->deleteLeafNodes();
1242 }
1243
1244 $this->newQuery()->delete($this->dn);
1245
1246 // If the deletion is successful, we will mark the model
1247 // as non-existing, and then fire the deleted event so
1248 // developers can hook in and run further operations.
1249 $this->exists = false;
1250
1251 $this->fireModelEvent(new Events\Deleted($this));
1252 }
1253
1254 /**
1255 * Deletes leaf nodes that are attached to the model.
1256 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001257 * @return void
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001258 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001259 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001260 */
1261 protected function deleteLeafNodes()
1262 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001263 $this->newQueryWithoutScopes()
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001264 ->in($this->dn)
1265 ->listing()
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001266 ->chunk(250, function ($models) {
1267 $models->each->delete($recursive = true);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001268 });
1269 }
1270
1271 /**
1272 * Delete an attribute on the model.
1273 *
1274 * @param string|array $attributes The attribute(s) to delete
1275 *
1276 * Delete specific values in attributes:
1277 *
1278 * ["memberuid" => "jdoe"]
1279 *
1280 * Delete an entire attribute:
1281 *
1282 * ["memberuid" => []]
1283 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001284 * @return void
1285 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001286 * @throws ModelDoesNotExistException
1287 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001288 */
1289 public function deleteAttribute($attributes)
1290 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001291 $this->requireExistence();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001292
1293 $attributes = $this->makeDeletableAttributes($attributes);
1294
1295 $this->newQuery()->deleteAttributes($this->dn, $attributes);
1296
1297 foreach ($attributes as $attribute => $value) {
1298 // If the attribute value is empty, we can assume the
1299 // attribute was completely deleted from the model.
1300 // We will pull the attribute out and continue on.
1301 if (empty($value)) {
1302 unset($this->attributes[$attribute]);
1303 }
1304 // Otherwise, only specific attribute values have been
1305 // removed. We will determine which ones have been
1306 // removed and update the attributes value.
1307 elseif (Arr::exists($this->attributes, $attribute)) {
1308 $this->attributes[$attribute] = array_values(
1309 array_diff($this->attributes[$attribute], (array) $value)
1310 );
1311 }
1312 }
1313
1314 $this->syncOriginal();
1315 }
1316
1317 /**
1318 * Make a deletable attribute array.
1319 *
1320 * @param string|array $attributes
1321 *
1322 * @return array
1323 */
1324 protected function makeDeletableAttributes($attributes)
1325 {
1326 $delete = [];
1327
1328 foreach (Arr::wrap($attributes) as $key => $value) {
1329 is_int($key)
1330 ? $delete[$value] = []
1331 : $delete[$key] = Arr::wrap($value);
1332 }
1333
1334 return $delete;
1335 }
1336
1337 /**
1338 * Move the model into the given new parent.
1339 *
1340 * For example: $user->move($ou);
1341 *
1342 * @param static|string $newParentDn The new parent of the current model.
1343 * @param bool $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
1344 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001345 * @return void
1346 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001347 * @throws UnexpectedValueException
1348 * @throws ModelDoesNotExistException
1349 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001350 */
1351 public function move($newParentDn, $deleteOldRdn = true)
1352 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001353 $this->requireExistence();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001354
1355 if (! $rdn = $this->getRdn()) {
1356 throw new UnexpectedValueException('Current model does not contain an RDN to move.');
1357 }
1358
1359 $this->rename($rdn, $newParentDn, $deleteOldRdn);
1360 }
1361
1362 /**
1363 * Rename the model to a new RDN and new parent.
1364 *
1365 * @param string $rdn The models new relative distinguished name. Example: "cn=JohnDoe"
1366 * @param static|string|null $newParentDn The models new parent distinguished name (if moving). Leave this null if you are only renaming. Example: "ou=MovedUsers,dc=acme,dc=org"
1367 * @param bool|true $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
1368 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001369 * @return void
1370 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001371 * @throws ModelDoesNotExistException
1372 * @throws \LdapRecord\LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001373 */
1374 public function rename($rdn, $newParentDn = null, $deleteOldRdn = true)
1375 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001376 $this->requireExistence();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001377
1378 if ($newParentDn instanceof self) {
1379 $newParentDn = $newParentDn->getDn();
1380 }
1381
1382 if (is_null($newParentDn)) {
1383 $newParentDn = $this->getParentDn($this->dn);
1384 }
1385
1386 // If the RDN and the new parent DN are the same as the current,
1387 // we will simply return here to prevent a rename operation
1388 // being sent, which would fail anyway in such case.
1389 if (
1390 $rdn === $this->getRdn()
1391 && $newParentDn === $this->getParentDn()
1392 ) {
1393 return;
1394 }
1395
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001396 // If the RDN we have been given is empty when parsed, we must
1397 // have been given a string, with no attribute. In this case,
1398 // we will create a new RDN using the current DN's head.
1399 if ($this->newDn($rdn)->isEmpty()) {
1400 $rdn = $this->getUpdateableRdn($rdn);
1401 }
1402
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001403 $this->fireModelEvent(new Renaming($this, $rdn, $newParentDn));
1404
1405 $this->newQuery()->rename($this->dn, $rdn, $newParentDn, $deleteOldRdn);
1406
1407 // If the model was successfully renamed, we will set
1408 // its new DN so any further updates to the model
1409 // can be performed without any issues.
1410 $this->dn = implode(',', [$rdn, $newParentDn]);
1411
1412 $map = $this->newDn($this->dn)->assoc();
1413
1414 // Here we'll populate the models new primary
1415 // RDN attribute on the model so we do not
1416 // have to re-synchronize with the server.
1417 $modelNameAttribute = key($map);
1418
1419 $this->attributes[$modelNameAttribute]
1420 = $this->original[$modelNameAttribute]
1421 = [reset($map[$modelNameAttribute])];
1422
1423 $this->fireModelEvent(new Renamed($this));
1424
1425 $this->wasRecentlyRenamed = true;
1426 }
1427
1428 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001429 * Get an updateable RDN for the model.
1430 *
1431 * @param string $name
1432 *
1433 * @return string
1434 */
1435 public function getUpdateableRdn($name)
1436 {
1437 return $this->getCreatableRdn($name, $this->newDn($this->dn)->head());
1438 }
1439
1440 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001441 * Get a distinguished name that is creatable for the model.
1442 *
1443 * @param string|null $name
1444 * @param string|null $attribute
1445 *
1446 * @return string
1447 */
1448 public function getCreatableDn($name = null, $attribute = null)
1449 {
1450 return implode(',', [
1451 $this->getCreatableRdn($name, $attribute),
1452 $this->in ?? $this->newQuery()->getbaseDn(),
1453 ]);
1454 }
1455
1456 /**
1457 * Get a creatable (escaped) RDN for the model.
1458 *
1459 * @param string|null $name
1460 * @param string|null $attribute
1461 *
1462 * @return string
1463 */
1464 public function getCreatableRdn($name = null, $attribute = null)
1465 {
1466 $attribute = $attribute ?? $this->getCreatableRdnAttribute();
1467
1468 $name = $this->escape(
1469 $name ?? $this->getFirstAttribute($attribute)
1470 )->dn();
1471
1472 return "$attribute=$name";
1473 }
1474
1475 /**
1476 * Get the creatable RDN attribute name.
1477 *
1478 * @return string
1479 */
1480 protected function getCreatableRdnAttribute()
1481 {
1482 return 'cn';
1483 }
1484
1485 /**
1486 * Determines if the given modification is valid.
1487 *
1488 * @param mixed $mod
1489 *
1490 * @return bool
1491 */
1492 protected function isValidModification($mod)
1493 {
1494 return Arr::accessible($mod)
1495 && Arr::exists($mod, BatchModification::KEY_MODTYPE)
1496 && Arr::exists($mod, BatchModification::KEY_ATTRIB);
1497 }
1498
1499 /**
1500 * Builds the models modifications from its dirty attributes.
1501 *
1502 * @return BatchModification[]
1503 */
1504 protected function buildModificationsFromDirty()
1505 {
1506 $modifications = [];
1507
1508 foreach ($this->getDirty() as $attribute => $values) {
1509 $modification = $this->newBatchModification($attribute, null, (array) $values);
1510
1511 if (Arr::exists($this->original, $attribute)) {
1512 // If the attribute we're modifying has an original value, we will
1513 // give the BatchModification object its values to automatically
1514 // determine which type of LDAP operation we need to perform.
1515 $modification->setOriginal($this->original[$attribute]);
1516 }
1517
1518 if (! $modification->build()->isValid()) {
1519 continue;
1520 }
1521
1522 $modifications[] = $modification;
1523 }
1524
1525 return $modifications;
1526 }
1527
1528 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001529 * Throw an exception if the model does not exist.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001530 *
1531 * @return void
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001532 *
1533 * @throws ModelDoesNotExistException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001534 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001535 protected function requireExistence()
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001536 {
1537 if (! $this->exists || is_null($this->dn)) {
1538 throw ModelDoesNotExistException::forModel($this);
1539 }
1540 }
1541}