Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"illuminate/database": "^10"
},
"require-dev": {
"phpunit/phpunit": "^10",
"friendsofphp/php-cs-fixer": "^3",
"nunomaduro/larastan": "^2.0",
"orchestra/testbench": "^8.9",
"phpunit/phpunit": "^10",
"squizlabs/php_codesniffer": "^3.5"
},
"autoload": {
Expand All @@ -31,10 +33,13 @@
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage",
"fix": "./vendor/bin/php-cs-fixer fix",
"lint": "./vendor/bin/phpcs --error-severity=1 --warning-severity=8 --extensions=php"
"test": "@php vendor/bin/phpunit",
"test-coverage": "@php vendor/bin/phpunit --coverage-html coverage",
"fix": "@php ./vendor/bin/php-cs-fixer fix",
"lint": "@php ./vendor/bin/phpcs --error-severity=1 --warning-severity=8 --extensions=php",
"analyse": [
"@php ./vendor/bin/phpstan analyse --memory-limit=2G"
]
},
"config": {
"sort-packages": true
Expand Down
11 changes: 11 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
includes:
- ./vendor/nunomaduro/larastan/extension.neon

parameters:

paths:
- src
- tests

# Level 9 is the highest level
level: 5
150 changes: 7 additions & 143 deletions src/HasManyMerged.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,17 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;

class HasManyMerged extends Relation
/**
* @template TRelatedModel of Model
* @extends HasOneOrManyMerged<TRelatedModel>
*/
class HasManyMerged extends HasOneOrManyMerged
{
/**
* The foreign keys of the parent model.
*
* @var string[]
*/
protected $foreignKeys;

/**
* The local key of the parent model.
*
* @var string
*/
protected $localKey;

/**
* Create a new has one or many relationship instance.
*
* @param Builder $query
* @param Builder<TRelatedModel> $query
* @param Model $parent
* @param array $foreignKeys
* @param string $localKey
Expand All @@ -42,28 +31,6 @@ public function __construct(Builder $query, Model $parent, array $foreignKeys, s
parent::__construct($query, $parent);
}

/**
* Set the base constraints on the relation query.
* Note: Used to load relations of one model.
*
* @return void
*/
public function addConstraints(): void
{
if (static::$constraints) {
$foreignKeys = $this->foreignKeys;

$this->query->where(function ($query) use ($foreignKeys): void {
foreach ($foreignKeys as $foreignKey) {
$query->orWhere(function ($query) use ($foreignKey): void {
$query->where($foreignKey, '=', $this->getParentKey())
->whereNotNull($foreignKey);
});
}
});
}
}

/**
* Get the key value of the parent's local key.
* Info: From HasOneOrMany class.
Expand All @@ -85,48 +52,6 @@ public function getQualifiedParentKeyName()
return $this->parent->qualifyColumn($this->localKey);
}

/**
* Set the constraints for an eager load of the relation.
* Note: Used to load relations of multiple models at once.
*
* @param array $models
*/
public function addEagerConstraints(array $models): void
{
$foreignKeys = $this->foreignKeys;
$orWhereIn = $this->orWhereInMethod($this->parent, $this->localKey);

$this->query->where(function ($query) use ($foreignKeys, $models, $orWhereIn): void {
foreach ($foreignKeys as $foreignKey) {
$query->{$orWhereIn}($foreignKey, $this->getKeys($models, $this->localKey));
}
});
}

/**
* Add the constraints for an internal relationship existence query.
*
* Essentially, these queries compare on column names like whereColumn.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$foreignKeys = $this->foreignKeys;

return $query->select($columns)->where(function ($query) use ($foreignKeys): void {
foreach ($foreignKeys as $foreignKey) {
$query->orWhere(function ($query) use ($foreignKey): void {
$query->whereColumn($this->getQualifiedParentKeyName(), '=', $foreignKey)
->whereNotNull($foreignKey);
});
}
});
}

/**
* Get the name of the "where in" method for eager loading.
* Note: Similar to whereInMethod of Relation class.
Expand Down Expand Up @@ -188,74 +113,13 @@ public function match(array $models, Collection $results, $relation)
return $models;
}

/**
* Build model dictionary keyed by the relation's foreign key.
* Note: Custom code.
*
* @param Collection $results
* @return array
*/
protected function buildDictionary(Collection $results): array
{
$dictionary = [];
$foreignKeyNames = $this->getForeignKeyNames();

foreach ($results as $result) {
foreach ($foreignKeyNames as $foreignKeyName) {
$foreignKeyValue = $result->{$foreignKeyName};
if (! isset($dictionary[$foreignKeyValue])) {
$dictionary[$foreignKeyValue] = [];
}

$dictionary[$foreignKeyValue][] = $result;
}
}

return $dictionary;
}

/**
* Get the plain foreign key.
*
* @return string[]
*/
public function getForeignKeyNames(): array
{
return array_map(function (string $qualifiedForeignKeyName) {
$segments = explode('.', $qualifiedForeignKeyName);

return end($segments);
}, $this->getQualifiedForeignKeyNames());
}

/**
* Get the foreign key for the relationship.
*
* @return string[]
*/
public function getQualifiedForeignKeyNames(): array
{
return $this->foreignKeys;
}

/**
* Get the results of the relationship.
*
* @return mixed
* @phpstan-return \Traversable<int, TRelatedModel>
*/
public function getResults()
{
return $this->get();
}

/**
* Execute the query as a "select" statement.
*
* @param array $columns
* @return \Illuminate\Database\Eloquent\Collection
*/
public function get($columns = ['*'])
{
return parent::get($columns);
}
}
114 changes: 114 additions & 0 deletions src/HasOneMerged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Korridor\LaravelHasManyMerged;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

/**
* @template TRelatedModel of Model
* @extends HasOneOrManyMerged<TRelatedModel>
*/
class HasOneMerged extends HasOneOrManyMerged
{
/**
* Create a new has one or many relationship instance.
*
* @param Builder<TRelatedModel> $query
* @param Model $parent
* @param array $foreignKeys
* @param string $localKey
* @return void
*/
public function __construct(Builder $query, Model $parent, array $foreignKeys, string $localKey)
{
$this->foreignKeys = $foreignKeys;
$this->localKey = $localKey;

parent::__construct($query, $parent);
}

/**
* Initialize the relation on a set of models.
*
* @param array $models
* @param string $relation
* @return array
*/
public function initRelation(array $models, $relation)
{
// TODO!!!

// Info: From HasMany class
foreach ($models as $model) {
$model->setRelation($relation, $this->related->newCollection());
}

return $models;
}

/**
* Match the eagerly loaded results to their parents.
* Info: From HasMany class.
*
* @param array $models
* @param Collection $results
* @param string $relation
* @return array
*/
public function match(array $models, Collection $results, $relation)
{
$dictionary = $this->buildDictionary($results);

// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model) {
if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) {
$model->setRelation(
$relation,
reset($dictionary[$key])
);
}
}

return $models;
}

/**
* Get the plain foreign key.
*
* @return string[]
*/
public function getForeignKeyNames(): array
{
return array_map(function (string $qualifiedForeignKeyName) {
$segments = explode('.', $qualifiedForeignKeyName);

return end($segments);
}, $this->getQualifiedForeignKeyNames());
}

/**
* Get the foreign key for the relationship.
*
* @return string[]
*/
public function getQualifiedForeignKeyNames(): array
{
return $this->foreignKeys;
}

/**
* Get the results of the relationship.
*
* @phpstan-return ?TRelatedModel
*/
public function getResults()
{
return $this->first();
}
}
34 changes: 34 additions & 0 deletions src/HasOneMergedRelation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Korridor\LaravelHasManyMerged;

trait HasOneMergedRelation
{
/**
* @param string $related
* @param string[]|null $foreignKeys
* @param string|null $localKey
* @return HasOneMerged
*/
public function hasOneMerged(string $related, ?array $foreignKeys = null, ?string $localKey = null): HasOneMerged
{
$instance = new $related();

$localKey = $localKey ?: $this->getKeyName();

$foreignKeys = array_map(function ($foreignKey) use ($instance) {
return $instance->getTable() . '.' . $foreignKey;
}, $foreignKeys);

return new HasOneMerged($instance->newQuery(), $this, $foreignKeys, $localKey);
}

/**
* Get the primary key for the model.
*
* @return string
*/
abstract public function getKeyName();
}
Loading