blob: d8dfa08c2fb450036bb41b8859affa808a662d5a [file] [log] [blame]
<?php
namespace LdapRecord\Models\Relations;
use Closure;
use LdapRecord\DetectsErrors;
use LdapRecord\LdapRecordException;
use LdapRecord\Models\Model;
use LdapRecord\Models\ModelNotFoundException;
use LdapRecord\Query\Collection;
class HasMany extends OneToMany
{
use DetectsErrors;
/**
* The model to use for attaching / detaching.
*
* @var Model
*/
protected $using;
/**
* The attribute key to use for attaching / detaching.
*
* @var string
*/
protected $usingKey;
/**
* The pagination page size.
*
* @var int
*/
protected $pageSize = 1000;
/**
* The exceptions to bypass for each relation operation.
*
* @var array
*/
protected $bypass = [
'attach' => [
'Already exists', 'Type or value exists',
],
'detach' => [
'No such attribute', 'Server is unwilling to perform',
],
];
/**
* Set the model and attribute to use for attaching / detaching.
*
* @param Model $using
* @param string $usingKey
*
* @return $this
*/
public function using(Model $using, $usingKey)
{
$this->using = $using;
$this->usingKey = $usingKey;
return $this;
}
/**
* Set the pagination page size of the relation query.
*
* @param int $pageSize
*
* @return $this
*/
public function setPageSize($pageSize)
{
$this->pageSize = $pageSize;
return $this;
}
/**
* Paginate the relation using the given page size.
*
* @param int $pageSize
*
* @return Collection
*/
public function paginate($pageSize = 1000)
{
return $this->paginateOnceUsing($pageSize);
}
/**
* Paginate the relation using the page size once.
*
* @param int $pageSize
*
* @return Collection
*/
protected function paginateOnceUsing($pageSize)
{
$size = $this->pageSize;
$result = $this->setPageSize($pageSize)->get();
$this->pageSize = $size;
return $result;
}
/**
* Chunk the relation results using the given callback.
*
* @param int $pageSize
* @param Closure $callback
*
* @return void
*/
public function chunk($pageSize, Closure $callback)
{
$this->getRelationQuery()->chunk($pageSize, function ($entries) use ($callback) {
$callback($this->transformResults($entries));
});
}
/**
* Get the relationships results.
*
* @return Collection
*/
public function getRelationResults()
{
return $this->transformResults(
$this->getRelationQuery()->paginate($this->pageSize)
);
}
/**
* Get the prepared relationship query.
*
* @return \LdapRecord\Query\Model\Builder
*/
public function getRelationQuery()
{
$columns = $this->query->getSelects();
// We need to select the proper key to be able to retrieve its
// value from LDAP results. If we don't, we won't be able
// to properly attach / detach models from relation
// query results as the attribute will not exist.
$key = $this->using ? $this->usingKey : $this->relationKey;
// If the * character is missing from the attributes to select,
// we will add the key to the attributes to select and also
// validate that the key isn't already being selected
// to prevent stacking on multiple relation calls.
if (! in_array('*', $columns) && ! in_array($key, $columns)) {
$this->query->addSelect($key);
}
return $this->query->whereRaw(
$this->relationKey,
'=',
$this->getEscapedForeignValueFromModel($this->parent)
);
}
/**
* Attach a model to the relation.
*
* @param Model|string $model
*
* @return Model|string|false
*/
public function attach($model)
{
return $this->attemptFailableOperation(
$this->buildAttachCallback($model),
$this->bypass['attach'],
$model
);
}
/**
* Build the attach callback.
*
* @param Model|string $model
*
* @return \Closure
*/
protected function buildAttachCallback($model)
{
return function () use ($model) {
$foreign = $this->getAttachableForeignValue($model);
if ($this->using) {
return $this->using->createAttribute($this->usingKey, $foreign);
}
if (! $model instanceof Model) {
$model = $this->getForeignModelByValueOrFail($model);
}
return $model->createAttribute($this->relationKey, $foreign);
};
}
/**
* Attach a collection of models to the parent instance.
*
* @param iterable $models
*
* @return iterable
*/
public function attachMany($models)
{
foreach ($models as $model) {
$this->attach($model);
}
return $models;
}
/**
* Detach the model from the relation.
*
* @param Model|string $model
*
* @return Model|string|false
*/
public function detach($model)
{
return $this->attemptFailableOperation(
$this->buildDetachCallback($model),
$this->bypass['detach'],
$model
);
}
/**
* Build the detach callback.
*
* @param Model|string $model
*
* @return \Closure
*/
protected function buildDetachCallback($model)
{
return function () use ($model) {
$foreign = $this->getAttachableForeignValue($model);
if ($this->using) {
return $this->using->deleteAttribute([$this->usingKey => $foreign]);
}
if (! $model instanceof Model) {
$model = $this->getForeignModelByValueOrFail($model);
}
return $model->deleteAttribute([$this->relationKey => $foreign]);
};
}
/**
* Get the attachable foreign value from the model.
*
* @param Model|string $model
*
* @return string
*/
protected function getAttachableForeignValue($model)
{
if ($model instanceof Model) {
return $this->using
? $this->getForeignValueFromModel($model)
: $this->getParentForeignValue();
}
return $this->using ? $model : $this->getParentForeignValue();
}
/**
* Get the foreign model by the given value, or fail.
*
* @param string $model
*
* @throws ModelNotFoundException
*
* @return Model
*/
protected function getForeignModelByValueOrFail($model)
{
if (! is_null($model = $this->getForeignModelByValue($model))) {
return $model;
}
throw ModelNotFoundException::forQuery(
$this->query->getUnescapedQuery(),
$this->query->getDn()
);
}
/**
* Attempt a failable operation and return the value if successful.
*
* If a bypassable exception is encountered, the value will be returned.
*
* @param callable $operation
* @param string|array $bypass
* @param mixed $value
*
* @throws LdapRecordException
*
* @return mixed
*/
protected function attemptFailableOperation($operation, $bypass, $value)
{
try {
$operation();
return $value;
} catch (LdapRecordException $e) {
if ($this->errorContainsMessage($e->getMessage(), $bypass)) {
return $value;
}
throw $e;
}
}
/**
* Detach all relation models.
*
* @return Collection
*/
public function detachAll()
{
return $this->onceWithoutMerging(function () {
return $this->get()->each(function (Model $model) {
$this->detach($model);
});
});
}
}