-
-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make tree relationships concatenable
- Loading branch information
1 parent
2be340a
commit cdee59e
Showing
11 changed files
with
696 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
src/Eloquent/Relations/Traits/Concatenation/IsConcatenableAncestorsRelation.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<?php | ||
|
||
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits\Concatenation; | ||
|
||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Database\Eloquent\Collection; | ||
use Illuminate\Database\Eloquent\Model; | ||
|
||
trait IsConcatenableAncestorsRelation | ||
{ | ||
use IsConcatenableRelation; | ||
|
||
/** | ||
* Set the constraints for an eager load of the deep relation. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Builder $query | ||
* @param array $models | ||
* @return void | ||
*/ | ||
public function addEagerConstraintsToDeepRelationship(Builder $query, array $models): void | ||
{ | ||
$this->addEagerConstraints($models); | ||
|
||
$this->mergeExpressions($query, $this->query); | ||
} | ||
|
||
/** | ||
* Match the eagerly loaded results for a deep relationship to their parents. | ||
* | ||
* @param array $models | ||
* @param \Illuminate\Database\Eloquent\Collection $results | ||
* @param string $relation | ||
* @return array | ||
*/ | ||
public function matchResultsForDeepRelationship(array $models, Collection $results, string $relation): array | ||
{ | ||
$dictionary = $this->buildDictionaryForDeepRelationship($results); | ||
|
||
$attribute = $this->andSelf ? $this->localKey : $this->getForeignKeyName(); | ||
|
||
foreach ($models as $model) { | ||
$key = $model->$attribute; | ||
|
||
if (isset($dictionary[$key])) { | ||
$value = $this->related->newCollection($dictionary[$key]); | ||
|
||
$model->setRelation($relation, $value); | ||
} | ||
} | ||
|
||
return $models; | ||
} | ||
|
||
/** | ||
* Build the model dictionary for a deep relation. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Collection $results | ||
* @return array | ||
*/ | ||
protected function buildDictionaryForDeepRelationship(Collection $results): array | ||
{ | ||
$pathSeparator = $this->related->getPathSeparator(); | ||
|
||
return $results->mapToDictionary(function (Model $result) use ($pathSeparator) { | ||
$key = strtok($result->laravel_through_key, $pathSeparator); | ||
|
||
return [$key => $result]; | ||
})->all(); | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
src/Eloquent/Relations/Traits/Concatenation/IsConcatenableDescendantsRelation.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php | ||
|
||
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits\Concatenation; | ||
|
||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Database\Eloquent\Collection; | ||
use Illuminate\Database\Eloquent\Model; | ||
|
||
trait IsConcatenableDescendantsRelation | ||
{ | ||
use IsConcatenableRelation; | ||
|
||
/** | ||
* Set the constraints for an eager load of the deep relation. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Builder $query | ||
* @param array $models | ||
* @return void | ||
*/ | ||
public function addEagerConstraintsToDeepRelationship(Builder $query, array $models): void | ||
{ | ||
$andSelf = $this->andSelf; | ||
|
||
$this->andSelf = true; | ||
|
||
$this->addEagerConstraints($models); | ||
|
||
$this->andSelf = $andSelf; | ||
|
||
$this->mergeExpressions($query, $this->query); | ||
} | ||
|
||
/** | ||
* Match the eagerly loaded results for a deep relationship to their parents. | ||
* | ||
* @param array $models | ||
* @param \Illuminate\Database\Eloquent\Collection $results | ||
* @param string $relation | ||
* @return array | ||
*/ | ||
public function matchResultsForDeepRelationship(array $models, Collection $results, string $relation): array | ||
{ | ||
$dictionary = $this->buildDictionaryForDeepRelationship($results); | ||
|
||
foreach ($models as $model) { | ||
$key = $model->{$this->localKey}; | ||
|
||
if (isset($dictionary[$key])) { | ||
$value = $this->related->newCollection($dictionary[$key]); | ||
|
||
$model->setRelation($relation, $value); | ||
} | ||
} | ||
|
||
return $models; | ||
} | ||
|
||
/** | ||
* Build the model dictionary for a deep relation. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Collection $results | ||
* @return array | ||
*/ | ||
protected function buildDictionaryForDeepRelationship(Collection $results): array | ||
{ | ||
$pathSeparator = $this->related->getPathSeparator(); | ||
|
||
if (!$this->andSelf) { | ||
$results = $results->filter( | ||
fn (Model $result) => str_contains($result->laravel_through_key, $pathSeparator) | ||
); | ||
} | ||
|
||
return $results->mapToDictionary(function (Model $result) use ($pathSeparator) { | ||
$key = strtok($result->laravel_through_key, $pathSeparator); | ||
|
||
return [$key => $result]; | ||
})->all(); | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
src/Eloquent/Relations/Traits/Concatenation/IsConcatenableRelation.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?php | ||
|
||
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits\Concatenation; | ||
|
||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Database\Eloquent\Collection; | ||
use Illuminate\Database\PostgresConnection; | ||
use RuntimeException; | ||
|
||
trait IsConcatenableRelation | ||
{ | ||
/** | ||
* Append the relation's through parents, foreign and local keys to a deep relationship. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Model[] $through | ||
* @param array $foreignKeys | ||
* @param array $localKeys | ||
* @param int $position | ||
* @return array | ||
*/ | ||
public function appendToDeepRelationship(array $through, array $foreignKeys, array $localKeys, int $position): array | ||
{ | ||
if ($position === 0) { | ||
$foreignKeys[] = function (Builder $query, Builder $parentQuery = null) { | ||
if ($parentQuery) { | ||
$this->getRelationExistenceQuery($this->query, $parentQuery); | ||
} | ||
|
||
$this->mergeExpressions($query, $this->query); | ||
}; | ||
|
||
$localKeys[] = null; | ||
} else { | ||
throw new RuntimeException( | ||
sprintf( | ||
'%s can only be at the beginning of deep relationships at the moment.', | ||
class_basename($this) | ||
) | ||
); | ||
} | ||
|
||
return [$through, $foreignKeys, $localKeys]; | ||
} | ||
|
||
/** | ||
* Get the related table name for a deep relationship. | ||
* | ||
* @return string | ||
*/ | ||
public function getTableForDeepRelationship(): string | ||
{ | ||
return $this->related->getExpressionName(); | ||
} | ||
|
||
/** | ||
* The custom callback to run at the end of the get() method. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Collection $models | ||
* @return void | ||
*/ | ||
public function postGetCallback(Collection $models): void | ||
{ | ||
if (!$this->query->getConnection() instanceof PostgresConnection) { | ||
return; | ||
} | ||
|
||
if (!isset($models[0]->laravel_through_key)) { | ||
return; | ||
} | ||
|
||
$this->replacePathSeparator( | ||
$models, | ||
'laravel_through_key', | ||
$this->related->getPathSeparator() | ||
); | ||
} | ||
|
||
/** | ||
* Replace the separator in a PostgreSQL path column. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Collection $models | ||
* @param string $column | ||
* @param string $separator | ||
* @return void | ||
*/ | ||
protected function replacePathSeparator(Collection $models, string $column, string $separator): void | ||
{ | ||
foreach ($models as $model) { | ||
$model->$column = str_replace( | ||
',', | ||
$separator, | ||
substr($model->$column, 1, -1) | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Get the custom through key for an eager load of the relation. | ||
* | ||
* @param string $alias | ||
* @return string | ||
*/ | ||
public function getThroughKeyForDeepRelationships(string $alias): string | ||
{ | ||
$throughKey = $this->related->qualifyColumn( | ||
$this->related->getPathName() | ||
); | ||
|
||
return "$throughKey as $alias"; | ||
} | ||
|
||
/** | ||
* Merge the common table expressions from one query into another. | ||
* | ||
* @param \Illuminate\Database\Eloquent\Builder $query | ||
* @param \Illuminate\Database\Eloquent\Builder $from | ||
* @return \Illuminate\Database\Eloquent\Builder | ||
*/ | ||
protected function mergeExpressions(Builder $query, Builder $from): Builder | ||
{ | ||
$query->getQuery()->expressions = array_merge( | ||
$query->getQuery()->expressions, | ||
$from->getQuery()->expressions | ||
); | ||
|
||
return $query->addBinding( | ||
$from->getQuery()->getRawBindings()['expressions'], | ||
'expressions' | ||
); | ||
} | ||
} |
Oops, something went wrong.