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