Skip to content

Commit

Permalink
Added HasOneMerged relation; Added larastan and generic typings
Browse files Browse the repository at this point in the history
  • Loading branch information
korridor committed Aug 25, 2023
1 parent 1cd6612 commit 774c21a
Show file tree
Hide file tree
Showing 9 changed files with 509 additions and 150 deletions.
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

0 comments on commit 774c21a

Please sign in to comment.