Skip to content

Commit

Permalink
Support existing relationships with constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
staudenmeir committed Jul 2, 2022
1 parent 5af9c35 commit d58289d
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 56 deletions.
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ class Country extends Model

public function comments()
{
return $this->hasManyDeepFromRelations($this->posts(), (new Post)->comments());
return $this->hasManyDeepFromRelations($this->posts(), (new Post())->comments());
}

public function posts()
Expand All @@ -322,6 +322,41 @@ class Post extends Model
}
```

Use `hasOneDeepFromRelations()` to define a `HasOneDeep` relationship.

#### <a name="existing-relationships-constraints">Constraints</a>

By default, constraints from the concatenated relationships are not transferred to the new deep relationship.
Use `hasManyDeepFromRelationsWithConstraints()` to apply these constraints and pass the relationships as callables:

```php
class Country extends Model
{
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

public function comments()
{
return $this->hasManyDeepFromRelationsWithConstraints([$this, 'posts'], [new Post(), 'comments']);
}

public function posts()
{
return $this->hasManyThrough(Post::class, User::class)->where('posts.published', true);
}
}

class Post extends Model
{
public function comments()
{
return $this->hasMany(Comment::class)->withTrashed();
}
}
```

Make sure to qualify the constraints' column names if they appear in multiple tables:
`->where('posts.published', true)` instead of `->where('published', true)`

### HasOneDeep

Define a `HasOneDeep` relationship if you only want to retrieve a single related instance:
Expand Down Expand Up @@ -499,7 +534,7 @@ class Post extends Model
{
return $this->hasManyDeepFromRelations(
$this->comments(),
(new Comment)->setAlias('alias')->replies()
(new Comment())->setAlias('alias')->replies()
);
}

Expand Down
22 changes: 0 additions & 22 deletions src/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,6 @@ public function hasManyDeep($related, array $through, array $foreignKeys = [], a
return $this->newHasManyDeep(...$this->hasOneOrManyDeep($related, $through, $foreignKeys, $localKeys));
}

/**
* Define a has-many-deep relationship from existing relationships.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation ...$relations
* @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
*/
public function hasManyDeepFromRelations(...$relations)
{
return $this->hasManyDeep(...$this->hasOneOrManyDeepFromRelations($relations));
}

/**
* Define a has-one-deep relationship.
*
Expand All @@ -52,17 +41,6 @@ public function hasOneDeep($related, array $through, array $foreignKeys = [], ar
return $this->newHasOneDeep(...$this->hasOneOrManyDeep($related, $through, $foreignKeys, $localKeys));
}

/**
* Define a has-one-deep relationship from existing relationships.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation ...$relations
* @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
*/
public function hasOneDeepFromRelations(...$relations)
{
return $this->hasOneDeep(...$this->hasOneOrManyDeepFromRelations($relations));
}

/**
* Prepare a has-one-deep or has-many-deep relationship.
*
Expand Down
142 changes: 139 additions & 3 deletions src/Traits/ConcatenatesRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,49 @@
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use RuntimeException;
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
use Staudenmeir\EloquentHasManyDeep\HasOneDeep;

trait ConcatenatesRelationships
{
/**
* Define a has-many-deep relationship from existing relationships.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
* @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
*/
public function hasManyDeepFromRelations(...$relations)
{
return $this->hasManyDeep(...$this->hasOneOrManyDeepFromRelations($relations));
}

/**
* Define a has-one-deep relationship from existing relationships.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation|callable ...$relations
* @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
*/
public function hasOneDeepFromRelations(...$relations)
{
return $this->hasOneDeep(...$this->hasOneOrManyDeepFromRelations($relations));
}

/**
* Prepare a has-one-deep or has-many-deep relationship from existing relationships.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation[] $relations
* @param \Illuminate\Database\Eloquent\Relations\Relation[]|callable[] $relations
* @return array
*/
protected function hasOneOrManyDeepFromRelations(array $relations)
{
if (is_array($relations[0])) {
$relations = $relations[0];
$relations = $this->normalizeVariadicRelations($relations);

foreach ($relations as $i => $relation) {
if (is_callable($relation)) {
$relations[$i] = $relation();
}
}

$related = null;
Expand Down Expand Up @@ -265,4 +293,112 @@ protected function hasOneOrManyThroughParent(Relation $relation, Relation $succe

return $through;
}

/**
* Define a has-many-deep relationship with constraints from existing relationships.
*
* @param callable ...$relations
* @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
*/
public function hasManyDeepFromRelationsWithConstraints(...$relations): HasManyDeep
{
$hasManyDeep = $this->hasManyDeepFromRelations(...$relations);

return $this->addConstraintsToHasOneOrManyDeepRelationship($hasManyDeep, $relations);
}

/**
* Define a has-one-deep relationship with constraints from existing relationships.
*
* @param callable ...$relations
* @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep
*/
public function hasOneDeepFromRelationsWithConstraints(...$relations): HasOneDeep
{
$hasOneDeep = $this->hasOneDeepFromRelations(...$relations);

return $this->addConstraintsToHasOneOrManyDeepRelationship($hasOneDeep, $relations);
}

/**
* Add the constraints from existing relationships to a has-one-deep or has-many-deep relationship.
*
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
* @param callable[] $relations
* @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep|\Staudenmeir\EloquentHasManyDeep\HasOneDeep
*/
protected function addConstraintsToHasOneOrManyDeepRelationship(
HasManyDeep $deepRelation,
array $relations
): HasManyDeep|HasOneDeep {
$relations = $this->normalizeVariadicRelations($relations);

foreach ($relations as $i => $relation) {
$relationWithoutConstraints = Relation::noConstraints(function () use ($relation) {
return $relation();
});

$deepRelation->getQuery()->mergeWheres(
$relationWithoutConstraints->getQuery()->getQuery()->wheres,
$relationWithoutConstraints->getQuery()->getQuery()->getRawBindings()['where'] ?? []
);

$isLast = $i === count($relations) - 1;

$this->addRemovedScopesToHasOneOrManyDeepRelationship($deepRelation, $relationWithoutConstraints, $isLast);
}

return $deepRelation;
}

/**
* Add the removed scopes from an existing relationship to a has-one-deep or has-many-deep relationship.
*
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $deepRelation
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
* @param bool $isLastRelation
* @return void
*/
protected function addRemovedScopesToHasOneOrManyDeepRelationship(
HasManyDeep $deepRelation,
Relation $relation,
bool $isLastRelation
): void {
$removedScopes = $relation->getQuery()->removedScopes();

foreach ($removedScopes as $scope) {
if ($scope === SoftDeletingScope::class) {
if ($isLastRelation) {
$deepRelation->withTrashed();
} else {
$deletedAtColumn = $relation->getRelated()->getQualifiedDeletedAtColumn();

$deepRelation->withTrashed($deletedAtColumn);
}
}

if ($scope === 'SoftDeletableHasManyThrough') {
$deletedAtColumn = $relation->getParent()->getQualifiedDeletedAtColumn();

$deepRelation->withTrashed($deletedAtColumn);
}

if (str_starts_with($scope, HasManyDeep::class . ':')) {
$deletedAtColumn = explode(':', $scope)[1];

$deepRelation->withTrashed($deletedAtColumn);
}
}
}

/**
* Normalize the relations from a variadic parameter.
*
* @param array $relations
* @return array
*/
protected function normalizeVariadicRelations(array $relations): array
{
return is_array($relations[0]) && !is_callable($relations[0]) ? $relations[0] : $relations;
}
}
Loading

0 comments on commit d58289d

Please sign in to comment.