Skip to content

Commit

Permalink
Make tree relationships concatenable
Browse files Browse the repository at this point in the history
  • Loading branch information
staudenmeir committed Nov 15, 2022
1 parent 2be340a commit cdee59e
Show file tree
Hide file tree
Showing 11 changed files with 696 additions and 4 deletions.
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
"require": {
"php": "^8.0.2",
"illuminate/database": "^9.0",
"staudenmeir/laravel-cte": "^1.6"
"staudenmeir/laravel-cte": "^1.6",
"staudenmeir/eloquent-has-many-deep-contracts": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"doctrine/dbal": "^3.3",
"barryvdh/laravel-ide-helper": "^2.12",
"mockery/mockery": "^1.5",
"nesbot/carbon": "^2.62.1"
"nesbot/carbon": "^2.62.1",
"staudenmeir/eloquent-has-many-deep": "^1.17"
},
"suggest": {
"barryvdh/laravel-ide-helper": "Provide type hints for attributes and relations."
Expand Down
5 changes: 4 additions & 1 deletion src/Eloquent/Relations/Ancestors.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations;

use Illuminate\Database\Eloquent\Relations\HasMany;
use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits\Concatenation\IsConcatenableAncestorsRelation;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits\IsAncestorRelation;

class Ancestors extends HasMany
class Ancestors extends HasMany implements ConcatenableRelation
{
use IsAncestorRelation;
use IsConcatenableAncestorsRelation;
}
5 changes: 4 additions & 1 deletion src/Eloquent/Relations/Descendants.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Query\Expression;
use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits\Concatenation\IsConcatenableDescendantsRelation;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Traits\IsRecursiveRelation;

class Descendants extends HasMany
class Descendants extends HasMany implements ConcatenableRelation
{
use IsConcatenableDescendantsRelation;
use IsRecursiveRelation {
buildDictionary as baseBuildDictionary;
}
Expand Down
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();
}
}
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 src/Eloquent/Relations/Traits/Concatenation/IsConcatenableRelation.php
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'
);
}
}
Loading

0 comments on commit cdee59e

Please sign in to comment.