Skip to content

Commit

Permalink
Relation apply logic is moved from the ModelQuery class to the relati…
Browse files Browse the repository at this point in the history
…ons classes. ModelQuery now doesn't know about the relations implementations.
  • Loading branch information
Finesse committed Nov 8, 2017
1 parent 5609ac4 commit e339f5b
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 290 deletions.
166 changes: 18 additions & 148 deletions src/ModelQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
use Finesse\Wired\Exceptions\IncorrectQueryException;
use Finesse\Wired\Exceptions\InvalidArgumentException;
use Finesse\Wired\Exceptions\RelationException;
use Finesse\Wired\Relations\BelongsTo;
use Finesse\Wired\Relations\HasMany;

/**
* Query builder for targeting a model.
Expand All @@ -36,8 +34,8 @@ class ModelQuery extends QueryProxy
*/
public function __construct(Query $baseQuery, string $modelClass = null)
{
$this->modelClass = $modelClass;
parent::__construct($baseQuery);
$this->setModelClass($modelClass);
}

/**
Expand All @@ -49,6 +47,16 @@ public function getBaseQuery(): QSQuery
return parent::getBaseQuery();
}

/**
* FOR INNER USAGE ONLY!
*
* @param string|null $modelClass Target model class name (already checked)
*/
public function setModelClass(string $modelClass = null)
{
$this->modelClass = $modelClass;
}

/**
* Gets the model by the identifier.
*
Expand Down Expand Up @@ -109,19 +117,15 @@ public function whereRelation(
));
}

$relation = $this->modelClass::$relationName();
$applyRelation = function (self $query) use ($relation, $target) {
$relation->applyToQueryWhere($query, [$target]);
};

if ($relation instanceof BelongsTo) {
return $this->whereBelongsToRelation($relation, $target, $not, $appendRule);
}
if ($relation instanceof HasMany) {
return $this->whereHasManyRelation($relation, $target, $not, $appendRule);
if ($not) {
return $this->whereNot($applyRelation, $appendRule);
} else {
return $this->where($applyRelation, null, null, $appendRule);
}

throw new RelationException(sprintf(
'The given relation %s is unknown',
is_object($relation) ? get_class($relation) : '('.gettype($relation).')'
));
}

/**
Expand Down Expand Up @@ -198,138 +202,4 @@ protected function handleBaseQueryException(\Throwable $exception)

return parent::handleBaseQueryException($exception);
}

/**
* Adds a BelongsTo relation clause.
*
* @param BelongsTo $relation Relation
* @param ModelInterface|\Closure|null $target Relation target. ModelInterface means "must belong to the specified
* model". Closure means "must belong to models that fit the clause in the closure". Null means "must belong to
* anything".
* @param bool $not
* @param int $appendRule
* @return $this
* @throws RelationException
* @throws InvalidArgumentException
*/
protected function whereBelongsToRelation(
BelongsTo $relation,
$target = null,
bool $not = false,
int $appendRule = Criterion::APPEND_RULE_AND
): self {
return $this->whereRelatedModel(
$relation->foreignField,
$relation->identifierField ?? $relation->modelClass::getIdentifierField(),
$relation->modelClass,
$target,
$not,
$appendRule
);
}

/**
* Adds a HasMany relation clause.
*
* @param BelongsTo $relation Relation
* @param ModelInterface|\Closure|null $target Relation target. ModelInterface means "must have the specified
* model". Closure means "must have a model that fit the clause in the closure". Null means "must have
* anything".
* @param bool $not
* @param int $appendRule
* @return $this
* @throws RelationException
* @throws InvalidArgumentException
*/
protected function whereHasManyRelation(
HasMany $relation,
$target = null,
bool $not = false,
int $appendRule = Criterion::APPEND_RULE_AND
): self {
return $this->whereRelatedModel(
$relation->identifierField ?? $relation->modelClass::getIdentifierField(),
$relation->foreignField,
$relation->modelClass,
$target,
$not,
$appendRule
);
}

/**
* Adds a related model clause.
*
* @param string $parentField The field name of the current query model
* @param string $childField The field name of the related model
* @param string|ModelInterface $childModelClass The related model class name
* @param ModelInterface|\Closure|null $target Relation target. ModelInterface means "must be related to the
* specified model". Closure means "must be related to a model that fit the clause in the closure". Null means
* "must be related to anything".
* @param bool $not
* @param int $appendRule
* @return $this
* @throws RelationException
* @throws InvalidArgumentException
*/
protected function whereRelatedModel(
string $parentField,
string $childField,
string $childModelClass,
$child = null,
bool $not = false,
int $appendRule = Criterion::APPEND_RULE_AND
): self {
if ($child instanceof ModelInterface) {
if (!$child instanceof $childModelClass) {
throw new RelationException(sprintf(
'The given model %s is not %s model',
get_class($child),
$childModelClass
));
}

return $this->where(
$parentField,
$not ? '!=' : '=',
$child->$childField,
$appendRule
);
}

if ($child === null || $child instanceof \Closure) {
return $this->whereExists(function (self $query) use ($parentField, $childField, $childModelClass, $child) {
$parentTableName = $this->baseQuery->tableAlias ?? $this->baseQuery->table;
$childTable = $childModelClass::getTable();
$childTableAlias = null;

if ($childTable === $parentTableName) {
for ($i = 0;; ++$i) {
$childTableAlias = '__wired_reserved_alias_'.$i;
if ($childTableAlias !== $parentTableName) {
break;
}
}
}

$childTableName = $childTableAlias ?? $childTable;

$query->modelClass = $childModelClass;
$query->table($childTable, $childTableAlias);
$query->whereColumn(
$parentTableName.'.'.$parentField,
$childTableName.'.'.$childField
);

return $child ? $child($query) : $query;
}, $not, $appendRule);
}

throw new InvalidArgumentException(sprintf(
'The second argument expected to be %s, %s or null, %s given',
ModelInterface::class,
\Closure::class,
is_object($child) ? get_class($child) : gettype($child)
));
}
}
16 changes: 15 additions & 1 deletion src/RelationInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@

namespace Finesse\Wired;

use Finesse\Wired\Exceptions\InvalidArgumentException;
use Finesse\Wired\Exceptions\RelationException;

/**
* Describes a relation between models.
*
* @author Surgie
*/
interface RelationInterface {}
interface RelationInterface
{
/**
* Applies itself to the where part of a query.
*
* @param ModelQuery $query Where to apply
* @param array $arguments Parameters for application
* @throws RelationException
* @throws InvalidArgumentException
*/
public function applyToQueryWhere(ModelQuery $query, array $arguments);
}
68 changes: 65 additions & 3 deletions src/Relations/BelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace Finesse\Wired\Relations;

use Finesse\Wired\Exceptions\InvalidArgumentException;
use Finesse\Wired\Exceptions\NotModelException;
use Finesse\Wired\Exceptions\RelationException;
use Finesse\Wired\ModelInterface;
use Finesse\Wired\ModelQuery;
use Finesse\Wired\RelationInterface;

/**
Expand All @@ -16,18 +19,18 @@ class BelongsTo implements RelationInterface
/**
* @var string|ModelInterface The object model class name (checked)
*/
public $modelClass;
protected $modelClass;

/**
* @var string The name of the subject model field which contains an identifier of an object model
*/
public $foreignField;
protected $foreignField;

/**
* @var string|null The name of the object model field which contains a value which the foreign key targets. Null
* means that the default object model identifier field should be used.
*/
public $identifierField;
protected $identifierField;

/**
* @param string $modelClass The object model class name
Expand All @@ -44,4 +47,63 @@ public function __construct(string $modelClass, string $foreignField, string $id
$this->foreignField = $foreignField;
$this->identifierField = $identifierField;
}

/**
* {@inheritDoc}
*/
public function applyToQueryWhere(ModelQuery $query, array $arguments)
{
$target = $arguments[0] ?? null;

if ($target instanceof ModelInterface) {
if (!$target instanceof $this->modelClass) {
throw new RelationException(sprintf(
'The given model %s is not a %s model',
get_class($target),
$this->modelClass
));
}

$query->where(
$this->foreignField,
$target->{$this->identifierField ?? $target::getIdentifierField()}
);
return;
}

if ($target === null || $target instanceof \Closure) {
$query->whereExists(function (ModelQuery $subQuery) use ($query, $target) {
$baseQuery = $query->getBaseQuery();
$queryTableName = $baseQuery->tableAlias ?? $baseQuery->table;
$subQueryTable = $this->modelClass::getTable();
$subQueryTableAlias = null;

if ($subQueryTable === $queryTableName) {
$counter = 0;
do {
$subQueryTableAlias = '__wired_reserved_alias_'.$counter++;
} while ($subQueryTableAlias === $queryTableName);
}

$subQueryTableName = $subQueryTableAlias ?? $subQueryTable;

$subQuery->setModelClass($this->modelClass);
$subQuery->table($subQueryTable, $subQueryTableAlias);
$subQuery->whereColumn(
$queryTableName.'.'.$this->foreignField,
$subQueryTableName.'.'.($this->identifierField ?? $this->modelClass::getIdentifierField())
);

return $target ? $target($subQuery) : $subQuery;
});
return;
}

throw new InvalidArgumentException(sprintf(
'The relation argument expected to be %s, %s or null, %s given',
ModelInterface::class,
\Closure::class,
is_object($target) ? get_class($target) : gettype($target)
));
}
}
Loading

0 comments on commit e339f5b

Please sign in to comment.