| <?php |
| |
| namespace LdapRecord\Models; |
| |
| use ArrayAccess; |
| use InvalidArgumentException; |
| use JsonSerializable; |
| use LdapRecord\Connection; |
| use LdapRecord\Container; |
| use LdapRecord\EscapesValues; |
| use LdapRecord\Models\Attributes\DistinguishedName; |
| use LdapRecord\Models\Attributes\Guid; |
| use LdapRecord\Models\Events\Renamed; |
| use LdapRecord\Models\Events\Renaming; |
| use LdapRecord\Query\Model\Builder; |
| use LdapRecord\Support\Arr; |
| use UnexpectedValueException; |
| |
| /** @mixin Builder */ |
| abstract class Model implements ArrayAccess, JsonSerializable |
| { |
| use EscapesValues; |
| use Concerns\HasEvents; |
| use Concerns\HasScopes; |
| use Concerns\HasAttributes; |
| use Concerns\HasGlobalScopes; |
| use Concerns\HidesAttributes; |
| use Concerns\HasRelationships; |
| |
| /** |
| * Indicates if the model exists in the LDAP directory. |
| * |
| * @var bool |
| */ |
| public $exists = false; |
| |
| /** |
| * Indicates whether the model was created during the current request lifecycle. |
| * |
| * @var bool |
| */ |
| public $wasRecentlyCreated = false; |
| |
| /** |
| * Indicates whether the model was renamed during the current request lifecycle. |
| * |
| * @var bool |
| */ |
| public $wasRecentlyRenamed = false; |
| |
| /** |
| * The models distinguished name. |
| * |
| * @var string|null |
| */ |
| protected $dn; |
| |
| /** |
| * The base DN of where the model should be created in. |
| * |
| * @var string|null |
| */ |
| protected $in; |
| |
| /** |
| * The object classes of the LDAP model. |
| * |
| * @var array |
| */ |
| public static $objectClasses = []; |
| |
| /** |
| * The connection container instance. |
| * |
| * @var Container |
| */ |
| protected static $container; |
| |
| /** |
| * The LDAP connection name for the model. |
| * |
| * @var string|null |
| */ |
| protected $connection; |
| |
| /** |
| * The attribute key that contains the models object GUID. |
| * |
| * @var string |
| */ |
| protected $guidKey = 'objectguid'; |
| |
| /** |
| * Contains the models modifications. |
| * |
| * @var array |
| */ |
| protected $modifications = []; |
| |
| /** |
| * The array of global scopes on the model. |
| * |
| * @var array |
| */ |
| protected static $globalScopes = []; |
| |
| /** |
| * The array of booted models. |
| * |
| * @var array |
| */ |
| protected static $booted = []; |
| |
| /** |
| * Constructor. |
| * |
| * @param array $attributes |
| */ |
| public function __construct(array $attributes = []) |
| { |
| $this->bootIfNotBooted(); |
| |
| $this->fill($attributes); |
| } |
| |
| /** |
| * Check if the model needs to be booted and if so, do it. |
| * |
| * @return void |
| */ |
| protected function bootIfNotBooted() |
| { |
| if (! isset(static::$booted[static::class])) { |
| static::$booted[static::class] = true; |
| |
| static::boot(); |
| } |
| } |
| |
| /** |
| * The "booting" method of the model. |
| * |
| * @return void |
| */ |
| protected static function boot() |
| { |
| // |
| } |
| |
| /** |
| * Clear the list of booted models so they will be re-booted. |
| * |
| * @return void |
| */ |
| public static function clearBootedModels() |
| { |
| static::$booted = []; |
| |
| static::$globalScopes = []; |
| } |
| |
| /** |
| * Handle dynamic method calls into the model. |
| * |
| * @param string $method |
| * @param array $parameters |
| * |
| * @return mixed |
| */ |
| public function __call($method, $parameters) |
| { |
| if (method_exists($this, $method)) { |
| return $this->$method(...$parameters); |
| } |
| |
| return $this->newQuery()->$method(...$parameters); |
| } |
| |
| /** |
| * Handle dynamic static method calls into the method. |
| * |
| * @param string $method |
| * @param array $parameters |
| * |
| * @return mixed |
| */ |
| public static function __callStatic($method, $parameters) |
| { |
| return (new static())->$method(...$parameters); |
| } |
| |
| /** |
| * Returns the models distinguished name. |
| * |
| * @return string|null |
| */ |
| public function getDn() |
| { |
| return $this->dn; |
| } |
| |
| /** |
| * Set the models distinguished name. |
| * |
| * @param string $dn |
| * |
| * @return static |
| */ |
| public function setDn($dn) |
| { |
| $this->dn = (string) $dn; |
| |
| return $this; |
| } |
| |
| /** |
| * Get the LDAP connection for the model. |
| * |
| * @return Connection |
| */ |
| public function getConnection() |
| { |
| return static::resolveConnection($this->getConnectionName()); |
| } |
| |
| /** |
| * Get the current connection name for the model. |
| * |
| * @return string |
| */ |
| public function getConnectionName() |
| { |
| return $this->connection; |
| } |
| |
| /** |
| * Set the connection associated with the model. |
| * |
| * @param string $name |
| * |
| * @return $this |
| */ |
| public function setConnection($name) |
| { |
| $this->connection = $name; |
| |
| return $this; |
| } |
| |
| /** |
| * Begin querying the model on a given connection. |
| * |
| * @param string|null $connection |
| * |
| * @return Builder |
| */ |
| public static function on($connection = null) |
| { |
| $instance = new static(); |
| |
| $instance->setConnection($connection); |
| |
| return $instance->newQuery(); |
| } |
| |
| /** |
| * Get all the models from the directory. |
| * |
| * @param array|mixed $attributes |
| * |
| * @return Collection|static[] |
| */ |
| public static function all($attributes = ['*']) |
| { |
| return static::query()->select($attributes)->paginate(); |
| } |
| |
| /** |
| * Begin querying the model. |
| * |
| * @return Builder |
| */ |
| public static function query() |
| { |
| return (new static())->newQuery(); |
| } |
| |
| /** |
| * Get a new query for builder filtered by the current models object classes. |
| * |
| * @return Builder |
| */ |
| public function newQuery() |
| { |
| return $this->registerModelScopes( |
| $this->newQueryWithoutScopes() |
| ); |
| } |
| |
| /** |
| * Get a new query builder that doesn't have any global scopes. |
| * |
| * @return Builder |
| */ |
| public function newQueryWithoutScopes() |
| { |
| return static::resolveConnection( |
| $this->getConnectionName() |
| )->query()->model($this); |
| } |
| |
| /** |
| * Create a new query builder. |
| * |
| * @param Connection $connection |
| * |
| * @return Builder |
| */ |
| public function newQueryBuilder(Connection $connection) |
| { |
| return new Builder($connection); |
| } |
| |
| /** |
| * Create a new model instance. |
| * |
| * @param array $attributes |
| * |
| * @return static |
| */ |
| public function newInstance(array $attributes = []) |
| { |
| return (new static($attributes))->setConnection($this->getConnectionName()); |
| } |
| |
| /** |
| * Resolve a connection instance. |
| * |
| * @param string|null $connection |
| * |
| * @return Connection |
| */ |
| public static function resolveConnection($connection = null) |
| { |
| return static::getConnectionContainer()->get($connection); |
| } |
| |
| /** |
| * Get the connection container. |
| * |
| * @return Container |
| */ |
| public static function getConnectionContainer() |
| { |
| return static::$container ?? static::getDefaultConnectionContainer(); |
| } |
| |
| /** |
| * Get the default singleton container instance. |
| * |
| * @return Container |
| */ |
| public static function getDefaultConnectionContainer() |
| { |
| return Container::getInstance(); |
| } |
| |
| /** |
| * Set the connection container. |
| * |
| * @param Container $container |
| * |
| * @return void |
| */ |
| public static function setConnectionContainer(Container $container) |
| { |
| static::$container = $container; |
| } |
| |
| /** |
| * Unset the connection container. |
| * |
| * @return void |
| */ |
| public static function unsetConnectionContainer() |
| { |
| static::$container = null; |
| } |
| |
| /** |
| * Register the query scopes for this builder instance. |
| * |
| * @param Builder $builder |
| * |
| * @return Builder |
| */ |
| public function registerModelScopes($builder) |
| { |
| $this->applyObjectClassScopes($builder); |
| |
| $this->registerGlobalScopes($builder); |
| |
| return $builder; |
| } |
| |
| /** |
| * Register the global model scopes. |
| * |
| * @param Builder $builder |
| * |
| * @return Builder |
| */ |
| public function registerGlobalScopes($builder) |
| { |
| foreach ($this->getGlobalScopes() as $identifier => $scope) { |
| $builder->withGlobalScope($identifier, $scope); |
| } |
| |
| return $builder; |
| } |
| |
| /** |
| * Apply the model object class scopes to the given builder instance. |
| * |
| * @param Builder $query |
| * |
| * @return void |
| */ |
| public function applyObjectClassScopes(Builder $query) |
| { |
| foreach (static::$objectClasses as $objectClass) { |
| $query->where('objectclass', '=', $objectClass); |
| } |
| } |
| |
| /** |
| * Returns the models distinguished name when the model is converted to a string. |
| * |
| * @return null|string |
| */ |
| public function __toString() |
| { |
| return $this->getDn(); |
| } |
| |
| /** |
| * Returns a new batch modification. |
| * |
| * @param string|null $attribute |
| * @param string|int|null $type |
| * @param array $values |
| * |
| * @return BatchModification |
| */ |
| public function newBatchModification($attribute = null, $type = null, $values = []) |
| { |
| return new BatchModification($attribute, $type, $values); |
| } |
| |
| /** |
| * Returns a new collection with the specified items. |
| * |
| * @param mixed $items |
| * |
| * @return Collection |
| */ |
| public function newCollection($items = []) |
| { |
| return new Collection($items); |
| } |
| |
| /** |
| * Dynamically retrieve attributes on the object. |
| * |
| * @param mixed $key |
| * |
| * @return bool |
| */ |
| public function __get($key) |
| { |
| return $this->getAttribute($key); |
| } |
| |
| /** |
| * Dynamically set attributes on the object. |
| * |
| * @param mixed $key |
| * @param mixed $value |
| * |
| * @return $this |
| */ |
| public function __set($key, $value) |
| { |
| return $this->setAttribute($key, $value); |
| } |
| |
| /** |
| * Determine if the given offset exists. |
| * |
| * @param string $offset |
| * |
| * @return bool |
| */ |
| public function offsetExists($offset) |
| { |
| return ! is_null($this->getAttribute($offset)); |
| } |
| |
| /** |
| * Get the value for a given offset. |
| * |
| * @param string $offset |
| * |
| * @return mixed |
| */ |
| public function offsetGet($offset) |
| { |
| return $this->getAttribute($offset); |
| } |
| |
| /** |
| * Set the value at the given offset. |
| * |
| * @param string $offset |
| * @param mixed $value |
| * |
| * @return void |
| */ |
| public function offsetSet($offset, $value) |
| { |
| $this->setAttribute($offset, $value); |
| } |
| |
| /** |
| * Unset the value at the given offset. |
| * |
| * @param string $offset |
| * |
| * @return void |
| */ |
| public function offsetUnset($offset) |
| { |
| unset($this->attributes[$offset]); |
| } |
| |
| /** |
| * Determine if an attribute exists on the model. |
| * |
| * @param string $key |
| * |
| * @return bool |
| */ |
| public function __isset($key) |
| { |
| return $this->offsetExists($key); |
| } |
| |
| /** |
| * Unset an attribute on the model. |
| * |
| * @param string $key |
| * |
| * @return void |
| */ |
| public function __unset($key) |
| { |
| $this->offsetUnset($key); |
| } |
| |
| /** |
| * Convert the object into something JSON serializable. |
| * |
| * @return array |
| */ |
| public function jsonSerialize() |
| { |
| return $this->attributesToArray(); |
| } |
| |
| /** |
| * Converts extra attributes for JSON serialization. |
| * |
| * @param array $attributes |
| * |
| * @return array |
| */ |
| protected function convertAttributesForJson(array $attributes = []) |
| { |
| // If the model has a GUID set, we need to convert |
| // it due to it being in binary. Otherwise we'll |
| // receive a JSON serialization exception. |
| if ($this->hasAttribute($this->guidKey)) { |
| return array_replace($attributes, [ |
| $this->guidKey => [$this->getConvertedGuid()], |
| ]); |
| } |
| |
| return $attributes; |
| } |
| |
| /** |
| * Reload a fresh model instance from the directory. |
| * |
| * @return static|false |
| */ |
| public function fresh() |
| { |
| if (! $this->exists) { |
| return false; |
| } |
| |
| return $this->newQuery()->find($this->dn); |
| } |
| |
| /** |
| * Determine if two models have the same distinguished name and belong to the same connection. |
| * |
| * @param static $model |
| * |
| * @return bool |
| */ |
| public function is(self $model) |
| { |
| return $this->dn == $model->getDn() && $this->getConnectionName() == $model->getConnectionName(); |
| } |
| |
| /** |
| * Hydrate a new collection of models from LDAP search results. |
| * |
| * @param array $records |
| * |
| * @return Collection |
| */ |
| public function hydrate($records) |
| { |
| return $this->newCollection($records)->transform(function ($attributes) { |
| return $attributes instanceof static |
| ? $attributes |
| : static::newInstance()->setRawAttributes($attributes); |
| }); |
| } |
| |
| /** |
| * Converts the current model into the given model. |
| * |
| * @param Model $into |
| * |
| * @return Model |
| */ |
| public function convert(self $into) |
| { |
| $into->setDn($this->getDn()); |
| $into->setConnection($this->getConnectionName()); |
| |
| $this->exists |
| ? $into->setRawAttributes($this->getAttributes()) |
| : $into->fill($this->getAttributes()); |
| |
| return $into; |
| } |
| |
| /** |
| * Refreshes the current models attributes with the directory values. |
| * |
| * @return bool |
| */ |
| public function refresh() |
| { |
| if ($model = $this->fresh()) { |
| $this->setRawAttributes($model->getAttributes()); |
| |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Get the model's batch modifications to be processed. |
| * |
| * @return array |
| */ |
| public function getModifications() |
| { |
| $builtModifications = []; |
| |
| foreach ($this->buildModificationsFromDirty() as $modification) { |
| $builtModifications[] = $modification->get(); |
| } |
| |
| return array_merge($this->modifications, $builtModifications); |
| } |
| |
| /** |
| * Set the models batch modifications. |
| * |
| * @param array $modifications |
| * |
| * @return $this |
| */ |
| public function setModifications(array $modifications = []) |
| { |
| $this->modifications = []; |
| |
| foreach ($modifications as $modification) { |
| $this->addModification($modification); |
| } |
| |
| return $this; |
| } |
| |
| /** |
| * Adds a batch modification to the model. |
| * |
| * @param array|BatchModification $mod |
| * |
| * @throws InvalidArgumentException |
| * |
| * @return $this |
| */ |
| public function addModification($mod = []) |
| { |
| if ($mod instanceof BatchModification) { |
| $mod = $mod->get(); |
| } |
| |
| if ($this->isValidModification($mod)) { |
| $this->modifications[] = $mod; |
| |
| return $this; |
| } |
| |
| throw new InvalidArgumentException( |
| "The batch modification array does not include the mandatory 'attrib' or 'modtype' keys." |
| ); |
| } |
| |
| /** |
| * Get the model's guid attribute key name. |
| * |
| * @return string |
| */ |
| public function getGuidKey() |
| { |
| return $this->guidKey; |
| } |
| |
| /** |
| * Get the model's ANR attributes for querying when incompatible with ANR. |
| * |
| * @return array |
| */ |
| public function getAnrAttributes() |
| { |
| return ['cn', 'sn', 'uid', 'name', 'mail', 'givenname', 'displayname']; |
| } |
| |
| /** |
| * Get the name of the model, or the given DN. |
| * |
| * @param string|null $dn |
| * |
| * @return string|null |
| */ |
| public function getName($dn = null) |
| { |
| return $this->newDn($dn ?? $this->dn)->name(); |
| } |
| |
| /** |
| * Get the head attribute of the model, or the given DN. |
| * |
| * @param string|null $dn |
| * |
| * @return string|null |
| */ |
| public function getHead($dn = null) |
| { |
| return $this->newDn($dn ?? $this->dn)->head(); |
| } |
| |
| /** |
| * Get the RDN of the model, of the given DN. |
| * |
| * @param string|null |
| * |
| * @return string|null |
| */ |
| public function getRdn($dn = null) |
| { |
| return $this->newDn($dn ?? $this->dn)->relative(); |
| } |
| |
| /** |
| * Get the parent distinguished name of the model, or the given DN. |
| * |
| * @param string|null |
| * |
| * @return string|null |
| */ |
| public function getParentDn($dn = null) |
| { |
| return $this->newDn($dn ?? $this->dn)->parent(); |
| } |
| |
| /** |
| * Create a new Distinguished Name object. |
| * |
| * @param string|null $dn |
| * |
| * @return DistinguishedName |
| */ |
| public function newDn($dn = null) |
| { |
| return new DistinguishedName($dn); |
| } |
| |
| /** |
| * Get the model's object GUID key. |
| * |
| * @return void |
| */ |
| public function getObjectGuidKey() |
| { |
| return $this->guidKey; |
| } |
| |
| /** |
| * Get the model's binary object GUID. |
| * |
| * @see https://msdn.microsoft.com/en-us/library/ms679021(v=vs.85).aspx |
| * |
| * @return string|null |
| */ |
| public function getObjectGuid() |
| { |
| return $this->getFirstAttribute($this->guidKey); |
| } |
| |
| /** |
| * Get the model's object classes. |
| * |
| * @return array |
| */ |
| public function getObjectClasses() |
| { |
| return $this->getAttribute('objectclass') ?: []; |
| } |
| |
| /** |
| * Get the model's string GUID. |
| * |
| * @return string|null |
| */ |
| public function getConvertedGuid() |
| { |
| try { |
| return (string) new Guid($this->getObjectGuid()); |
| } catch (InvalidArgumentException $e) { |
| return; |
| } |
| } |
| |
| /** |
| * Determine if the current model is a direct descendant of the given. |
| * |
| * @param static|string $parent |
| * |
| * @return bool |
| */ |
| public function isChildOf($parent) |
| { |
| return $this->newDn($this->getDn())->isChildOf( |
| $this->newDn((string) $parent) |
| ); |
| } |
| |
| /** |
| * Determine if the current model is a direct ascendant of the given. |
| * |
| * @param static|string $child |
| * |
| * @return bool |
| */ |
| public function isParentOf($child) |
| { |
| return $this->newDn($this->getDn())->isParentOf( |
| $this->newDn((string) $child) |
| ); |
| } |
| |
| /** |
| * Determine if the current model is a descendant of the given. |
| * |
| * @param static|string $model |
| * |
| * @return bool |
| */ |
| public function isDescendantOf($model) |
| { |
| return $this->dnIsInside($this->getDn(), $model); |
| } |
| |
| /** |
| * Determine if the current model is a ancestor of the given. |
| * |
| * @param static|string $model |
| * |
| * @return bool |
| */ |
| public function isAncestorOf($model) |
| { |
| return $this->dnIsInside($model, $this->getDn()); |
| } |
| |
| /** |
| * Determines if the DN is inside of the parent DN. |
| * |
| * @param static|string $dn |
| * @param static|string $parentDn |
| * |
| * @return bool |
| */ |
| protected function dnIsInside($dn, $parentDn) |
| { |
| return $this->newDn((string) $dn)->isDescendantOf( |
| $this->newDn($parentDn) |
| ); |
| } |
| |
| /** |
| * Set the base DN of where the model should be created in. |
| * |
| * @param static|string $dn |
| * |
| * @return $this |
| */ |
| public function inside($dn) |
| { |
| $this->in = $dn instanceof self ? $dn->getDn() : $dn; |
| |
| return $this; |
| } |
| |
| /** |
| * Save the model to the directory. |
| * |
| * @param array $attributes The attributes to update or create for the current entry. |
| * |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function save(array $attributes = []) |
| { |
| $this->fill($attributes); |
| |
| $this->fireModelEvent(new Events\Saving($this)); |
| |
| $this->exists ? $this->performUpdate() : $this->performInsert(); |
| |
| $this->fireModelEvent(new Events\Saved($this)); |
| |
| $this->in = null; |
| } |
| |
| /** |
| * Inserts the model into the directory. |
| * |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| protected function performInsert() |
| { |
| // Here we will populate the models object classes if it |
| // does not already have any set. An LDAP object cannot |
| // be successfully created in the server without them. |
| if (! $this->hasAttribute('objectclass')) { |
| $this->setAttribute('objectclass', static::$objectClasses); |
| } |
| |
| $query = $this->newQuery(); |
| |
| // If the model does not currently have a distinguished |
| // name, we will attempt to generate one automatically |
| // using the current query builder's DN as the base. |
| if (empty($this->getDn())) { |
| $this->setDn($this->getCreatableDn()); |
| } |
| |
| $this->fireModelEvent(new Events\Creating($this)); |
| |
| // Here we perform the insert of new object in the directory, |
| // but filter out any empty attributes before sending them |
| // to the server. LDAP servers will throw an exception if |
| // attributes have been given empty or null values. |
| $query->insert($this->getDn(), array_filter($this->getAttributes())); |
| |
| $this->fireModelEvent(new Events\Created($this)); |
| |
| $this->syncOriginal(); |
| |
| $this->exists = true; |
| |
| $this->wasRecentlyCreated = true; |
| } |
| |
| /** |
| * Updates the model in the directory. |
| * |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| protected function performUpdate() |
| { |
| if (! count($modifications = $this->getModifications())) { |
| return; |
| } |
| |
| $this->fireModelEvent(new Events\Updating($this)); |
| |
| $this->newQuery()->update($this->dn, $modifications); |
| |
| $this->fireModelEvent(new Events\Updated($this)); |
| |
| $this->syncOriginal(); |
| |
| $this->modifications = []; |
| } |
| |
| /** |
| * Create the model in the directory. |
| * |
| * @param array $attributes The attributes for the new entry. |
| * |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return Model |
| */ |
| public static function create(array $attributes = []) |
| { |
| $instance = new static($attributes); |
| |
| $instance->save(); |
| |
| return $instance; |
| } |
| |
| /** |
| * Create an attribute on the model. |
| * |
| * @param string $attribute The attribute to create |
| * @param mixed $value The value of the new attribute |
| * |
| * @throws ModelDoesNotExistException |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function createAttribute($attribute, $value) |
| { |
| $this->validateExistence(); |
| |
| $this->newQuery()->insertAttributes($this->dn, [$attribute => (array) $value]); |
| |
| $this->addAttributeValue($attribute, $value); |
| } |
| |
| /** |
| * Update the model. |
| * |
| * @param array $attributes The attributes to update for the current entry. |
| * |
| * @throws ModelDoesNotExistException |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function update(array $attributes = []) |
| { |
| $this->validateExistence(); |
| |
| $this->save($attributes); |
| } |
| |
| /** |
| * Update the model attribute with the specified value. |
| * |
| * @param string $attribute The attribute to modify |
| * @param mixed $value The new value for the attribute |
| * |
| * @throws ModelDoesNotExistException |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function updateAttribute($attribute, $value) |
| { |
| $this->validateExistence(); |
| |
| $this->newQuery()->updateAttributes($this->dn, [$attribute => (array) $value]); |
| |
| $this->addAttributeValue($attribute, $value); |
| } |
| |
| /** |
| * Destroy the models for the given distinguished names. |
| * |
| * @param Collection|array|string $dns |
| * @param bool $recursive |
| * |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return int |
| */ |
| public static function destroy($dns, $recursive = false) |
| { |
| $count = 0; |
| |
| $dns = is_string($dns) ? (array) $dns : $dns; |
| |
| $instance = new static(); |
| |
| foreach ($dns as $dn) { |
| if (! $model = $instance->find($dn)) { |
| continue; |
| } |
| |
| $model->delete($recursive); |
| |
| $count++; |
| } |
| |
| return $count; |
| } |
| |
| /** |
| * Delete the model from the directory. |
| * |
| * Throws a ModelNotFoundException if the current model does |
| * not exist or does not contain a distinguished name. |
| * |
| * @param bool $recursive Whether to recursively delete leaf nodes (models that are children). |
| * |
| * @throws ModelDoesNotExistException |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function delete($recursive = false) |
| { |
| $this->validateExistence(); |
| |
| $this->fireModelEvent(new Events\Deleting($this)); |
| |
| if ($recursive) { |
| $this->deleteLeafNodes(); |
| } |
| |
| $this->newQuery()->delete($this->dn); |
| |
| // If the deletion is successful, we will mark the model |
| // as non-existing, and then fire the deleted event so |
| // developers can hook in and run further operations. |
| $this->exists = false; |
| |
| $this->fireModelEvent(new Events\Deleted($this)); |
| } |
| |
| /** |
| * Deletes leaf nodes that are attached to the model. |
| * |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return Collection |
| */ |
| protected function deleteLeafNodes() |
| { |
| return $this->newQueryWithoutScopes() |
| ->in($this->dn) |
| ->listing() |
| ->paginate() |
| ->each(function (self $model) { |
| $model->delete($recursive = true); |
| }); |
| } |
| |
| /** |
| * Delete an attribute on the model. |
| * |
| * @param string|array $attributes The attribute(s) to delete |
| * |
| * Delete specific values in attributes: |
| * |
| * ["memberuid" => "jdoe"] |
| * |
| * Delete an entire attribute: |
| * |
| * ["memberuid" => []] |
| * |
| * @throws ModelDoesNotExistException |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function deleteAttribute($attributes) |
| { |
| $this->validateExistence(); |
| |
| $attributes = $this->makeDeletableAttributes($attributes); |
| |
| $this->newQuery()->deleteAttributes($this->dn, $attributes); |
| |
| foreach ($attributes as $attribute => $value) { |
| // If the attribute value is empty, we can assume the |
| // attribute was completely deleted from the model. |
| // We will pull the attribute out and continue on. |
| if (empty($value)) { |
| unset($this->attributes[$attribute]); |
| } |
| // Otherwise, only specific attribute values have been |
| // removed. We will determine which ones have been |
| // removed and update the attributes value. |
| elseif (Arr::exists($this->attributes, $attribute)) { |
| $this->attributes[$attribute] = array_values( |
| array_diff($this->attributes[$attribute], (array) $value) |
| ); |
| } |
| } |
| |
| $this->syncOriginal(); |
| } |
| |
| /** |
| * Make a deletable attribute array. |
| * |
| * @param string|array $attributes |
| * |
| * @return array |
| */ |
| protected function makeDeletableAttributes($attributes) |
| { |
| $delete = []; |
| |
| foreach (Arr::wrap($attributes) as $key => $value) { |
| is_int($key) |
| ? $delete[$value] = [] |
| : $delete[$key] = Arr::wrap($value); |
| } |
| |
| return $delete; |
| } |
| |
| /** |
| * Move the model into the given new parent. |
| * |
| * For example: $user->move($ou); |
| * |
| * @param static|string $newParentDn The new parent of the current model. |
| * @param bool $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved. |
| * |
| * @throws UnexpectedValueException |
| * @throws ModelDoesNotExistException |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function move($newParentDn, $deleteOldRdn = true) |
| { |
| $this->validateExistence(); |
| |
| if (! $rdn = $this->getRdn()) { |
| throw new UnexpectedValueException('Current model does not contain an RDN to move.'); |
| } |
| |
| $this->rename($rdn, $newParentDn, $deleteOldRdn); |
| } |
| |
| /** |
| * Rename the model to a new RDN and new parent. |
| * |
| * @param string $rdn The models new relative distinguished name. Example: "cn=JohnDoe" |
| * @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" |
| * @param bool|true $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved. |
| * |
| * @throws ModelDoesNotExistException |
| * @throws \LdapRecord\LdapRecordException |
| * |
| * @return void |
| */ |
| public function rename($rdn, $newParentDn = null, $deleteOldRdn = true) |
| { |
| $this->validateExistence(); |
| |
| if ($newParentDn instanceof self) { |
| $newParentDn = $newParentDn->getDn(); |
| } |
| |
| if (is_null($newParentDn)) { |
| $newParentDn = $this->getParentDn($this->dn); |
| } |
| |
| // If the RDN and the new parent DN are the same as the current, |
| // we will simply return here to prevent a rename operation |
| // being sent, which would fail anyway in such case. |
| if ( |
| $rdn === $this->getRdn() |
| && $newParentDn === $this->getParentDn() |
| ) { |
| return; |
| } |
| |
| $this->fireModelEvent(new Renaming($this, $rdn, $newParentDn)); |
| |
| $this->newQuery()->rename($this->dn, $rdn, $newParentDn, $deleteOldRdn); |
| |
| // If the model was successfully renamed, we will set |
| // its new DN so any further updates to the model |
| // can be performed without any issues. |
| $this->dn = implode(',', [$rdn, $newParentDn]); |
| |
| $map = $this->newDn($this->dn)->assoc(); |
| |
| // Here we'll populate the models new primary |
| // RDN attribute on the model so we do not |
| // have to re-synchronize with the server. |
| $modelNameAttribute = key($map); |
| |
| $this->attributes[$modelNameAttribute] |
| = $this->original[$modelNameAttribute] |
| = [reset($map[$modelNameAttribute])]; |
| |
| $this->fireModelEvent(new Renamed($this)); |
| |
| $this->wasRecentlyRenamed = true; |
| } |
| |
| /** |
| * Get a distinguished name that is creatable for the model. |
| * |
| * @param string|null $name |
| * @param string|null $attribute |
| * |
| * @return string |
| */ |
| public function getCreatableDn($name = null, $attribute = null) |
| { |
| return implode(',', [ |
| $this->getCreatableRdn($name, $attribute), |
| $this->in ?? $this->newQuery()->getbaseDn(), |
| ]); |
| } |
| |
| /** |
| * Get a creatable (escaped) RDN for the model. |
| * |
| * @param string|null $name |
| * @param string|null $attribute |
| * |
| * @return string |
| */ |
| public function getCreatableRdn($name = null, $attribute = null) |
| { |
| $attribute = $attribute ?? $this->getCreatableRdnAttribute(); |
| |
| $name = $this->escape( |
| $name ?? $this->getFirstAttribute($attribute) |
| )->dn(); |
| |
| return "$attribute=$name"; |
| } |
| |
| /** |
| * Get the creatable RDN attribute name. |
| * |
| * @return string |
| */ |
| protected function getCreatableRdnAttribute() |
| { |
| return 'cn'; |
| } |
| |
| /** |
| * Determines if the given modification is valid. |
| * |
| * @param mixed $mod |
| * |
| * @return bool |
| */ |
| protected function isValidModification($mod) |
| { |
| return Arr::accessible($mod) |
| && Arr::exists($mod, BatchModification::KEY_MODTYPE) |
| && Arr::exists($mod, BatchModification::KEY_ATTRIB); |
| } |
| |
| /** |
| * Builds the models modifications from its dirty attributes. |
| * |
| * @return BatchModification[] |
| */ |
| protected function buildModificationsFromDirty() |
| { |
| $modifications = []; |
| |
| foreach ($this->getDirty() as $attribute => $values) { |
| $modification = $this->newBatchModification($attribute, null, (array) $values); |
| |
| if (Arr::exists($this->original, $attribute)) { |
| // If the attribute we're modifying has an original value, we will |
| // give the BatchModification object its values to automatically |
| // determine which type of LDAP operation we need to perform. |
| $modification->setOriginal($this->original[$attribute]); |
| } |
| |
| if (! $modification->build()->isValid()) { |
| continue; |
| } |
| |
| $modifications[] = $modification; |
| } |
| |
| return $modifications; |
| } |
| |
| /** |
| * Validates that the current model exists. |
| * |
| * @throws ModelDoesNotExistException |
| * |
| * @return void |
| */ |
| protected function validateExistence() |
| { |
| if (! $this->exists || is_null($this->dn)) { |
| throw ModelDoesNotExistException::forModel($this); |
| } |
| } |
| } |