Skip to content

Commit

Permalink
Has many (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
fico7489 authored Oct 31, 2018
1 parent d584006 commit 0525825
Show file tree
Hide file tree
Showing 34 changed files with 1,158 additions and 591 deletions.
380 changes: 282 additions & 98 deletions README.md

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
{
"name": "fico7489/laravel-eloquent-join",
"description": "This package introduces the join capability for sorting and filtering on eloquent relations.",
"description": "This package introduces the join magic for eloquent models and relations.",
"keywords": [
"laravel eloquent join",
"laravel join",
"laravel eloquent join",
"laravel sort join",
"laravel where join",
"laravel join relation",
"laravel join relations",
"laravel"
"laravel join relation"
],
"homepage": "https://github.com/fico7489/laravel-eloquent-join",
"support": {
Expand Down
145 changes: 120 additions & 25 deletions src/EloquentJoinBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,42 @@

namespace Fico7489\Laravel\EloquentJoin;

use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidAggregateMethod;
use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelation;
use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationClause;
use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationGlobalScope;
use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationWhere;
use Fico7489\Laravel\EloquentJoin\Relations\HasManyJoin;
use Illuminate\Database\Eloquent\Builder;
use Fico7489\Laravel\EloquentJoin\Relations\BelongsToJoin;
use Fico7489\Laravel\EloquentJoin\Relations\HasOneJoin;
use Fico7489\Laravel\EloquentJoin\Relations\HasManyJoin;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletingScope;

class EloquentJoinBuilder extends Builder
{
//base builder
public $baseBuilder;
//constants
const AGGREGATE_SUM = 'SUM';
const AGGREGATE_AVG = 'AVG';
const AGGREGATE_MAX = 'MAX';
const AGGREGATE_MIN = 'MIN';
const AGGREGATE_COUNT = 'COUNT';

//use table alias for join (real table name or uniqid())
private $useTableAlias = false;

//appendRelationsCount
private $appendRelationsCount = false;

//leftJoin
private $leftJoin = true;

//aggregate method
private $aggregateMethod = self::AGGREGATE_MAX;

//base builder
public $baseBuilder;

//store if ->select(...) is already called on builder (we want only one groupBy())
private $selected = false;

Expand Down Expand Up @@ -63,18 +80,41 @@ public function orWhereJoin($column, $operator, $value)
return $this->orWhere($column, $operator, $value);
}

public function orderByJoin($column, $direction = 'asc', $leftJoin = true)
public function orderByJoin($column, $direction = 'asc', $aggregateMethod = null)
{
$dotPos = strrpos($column, '.');

$query = $this->baseBuilder ? $this->baseBuilder : $this;
$column = $query->performJoin($column, $leftJoin);
$column = $query->performJoin($column);
if (false !== $dotPos) {
$aggregateMethod = $aggregateMethod ? $aggregateMethod : $this->aggregateMethod;
$this->checkAggregateMethod($aggregateMethod);
$query->selectRaw($aggregateMethod.'('.$column.') as sort');

return $this->orderByRaw('sort '.$direction);
}

return $this->orderBy($column, $direction);
}

public function performJoin($relations, $leftJoin = true)
public function joinRelations($relations, $leftJoin = null)
{
$relations = explode('.', $relations);
$leftJoin = null !== $leftJoin ? $leftJoin : $this->leftJoin;

$query = $this->baseBuilder ? $this->baseBuilder : $this;
$column = $query->performJoin($relations.'.FAKE_FIELD', $leftJoin);

return $this;
}

private function performJoin($relations, $leftJoin = null)
{
//detect join method
$leftJoin = null !== $leftJoin ? $leftJoin : $this->leftJoin;
$joinMethod = $leftJoin ? 'leftJoin' : 'join';

//detect current model data
$relations = explode('.', $relations);
$column = end($relations);
$baseModel = $this->getModel();
$baseTable = $baseModel->getTable();
Expand All @@ -84,7 +124,6 @@ public function performJoin($relations, $leftJoin = true)
$currentPrimaryKey = $baseModel->getKeyName();

$relationsAccumulated = [];

foreach ($relations as $relation) {
if ($relation == $column) {
//last item in $relations argument is sort|where column
Expand All @@ -99,9 +138,13 @@ public function performJoin($relations, $leftJoin = true)
$relatedTableAlias = $this->useTableAlias ? uniqid() : $relatedTable;

$relationsAccumulated[] = $relatedTableAlias;
$relationAccumulatedString = implode('.', $relationsAccumulated);
$relationAccumulatedString = implode('_', $relationsAccumulated);

//relations count
if ($this->appendRelationsCount) {
$this->selectRaw('COUNT('.$relatedTable.'.'.$relatedPrimaryKey.') as '.$relationAccumulatedString.'_count');
}

$joinMethod = $leftJoin ? 'leftJoin' : 'join';
if (!in_array($relationAccumulatedString, $this->joinedTables)) {
$joinQuery = $relatedTable.($this->useTableAlias ? ' as '.$relatedTableAlias : '');
if ($relatedRelation instanceof BelongsToJoin) {
Expand All @@ -112,26 +155,14 @@ public function performJoin($relations, $leftJoin = true)

$this->joinQuery($join, $relatedRelation, $relatedTableAlias);
});
} elseif ($relatedRelation instanceof HasOneJoin || $relatedRelation instanceof HasManyJoin) {
} elseif ($relatedRelation instanceof HasOneJoin || $relatedRelation instanceof HasManyJoin) {
$relatedKey = $relatedRelation->getQualifiedForeignKeyName();
$relatedKey = last(explode('.', $relatedKey));

$this->$joinMethod($joinQuery, function ($join) use ($relatedRelation, $relatedTableAlias, $relatedPrimaryKey, $currentTableAlias, $relatedKey, $currentPrimaryKey) {
$join->on($relatedTableAlias.'.'.$relatedKey, '=', $currentTableAlias.'.'.$currentPrimaryKey);

$this->joinQuery($join, $relatedRelation, $relatedTableAlias);

if ($relatedRelation instanceof HasOneJoin) {

$join->whereRaw(
$relatedTableAlias.'.'.$relatedPrimaryKey.' = (
SELECT '.$relatedPrimaryKey.'
FROM '.$relatedTableAlias.'
WHERE '.$relatedTableAlias.'.'.$relatedKey.' = '.$currentTableAlias.'.'.$currentPrimaryKey.'
LIMIT 1
)
');
}
});
} else {
throw new InvalidRelation();
Expand All @@ -142,12 +173,13 @@ public function performJoin($relations, $leftJoin = true)
$currentTableAlias = $relatedTableAlias;
$currentPrimaryKey = $relatedPrimaryKey;

$this->joinedTables[] = implode('.', $relationsAccumulated);
$this->joinedTables[] = implode('_', $relationsAccumulated);
}

if (!$this->selected && count($relations) > 1) {
$this->selected = true;
$this->select($baseTable.'.*');
$this->selectRaw($baseTable.'.*');
$this->groupBy($baseTable.'.id');
}

return $currentTableAlias.'.'.$column;
Expand Down Expand Up @@ -206,4 +238,67 @@ private function applyClauseOnRelation($join, $method, $params, $relatedTableAli
throw new InvalidRelationClause();
}
}

private function checkAggregateMethod($aggregateMethod)
{
if (!in_array($aggregateMethod, [
self::AGGREGATE_SUM,
self::AGGREGATE_AVG,
self::AGGREGATE_MAX,
self::AGGREGATE_MIN,
self::AGGREGATE_COUNT,
])) {
throw new InvalidAggregateMethod();
}
}

//getters and setters
public function isUseTableAlias(): bool
{
return $this->useTableAlias;
}

public function setUseTableAlias(bool $useTableAlias)
{
$this->useTableAlias = $useTableAlias;

return $this;
}

public function isLeftJoin(): bool
{
return $this->leftJoin;
}

public function setLeftJoin(bool $leftJoin)
{
$this->leftJoin = $leftJoin;

return $this;
}

public function isAppendRelationsCount(): bool
{
return $this->appendRelationsCount;
}

public function setAppendRelationsCount(bool $appendRelationsCount)
{
$this->appendRelationsCount = $appendRelationsCount;

return $this;
}

public function getAggregateMethod(): string
{
return $this->aggregateMethod;
}

public function setAggregateMethod(string $aggregateMethod)
{
$this->checkAggregateMethod($aggregateMethod);
$this->aggregateMethod = $aggregateMethod;

return $this;
}
}
8 changes: 8 additions & 0 deletions src/Exceptions/InvalidAggregateMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Fico7489\Laravel\EloquentJoin\Exceptions;

class InvalidAggregateMethod extends \Exception
{
public $message = 'Invalid aggregate method';
}
2 changes: 1 addition & 1 deletion src/Exceptions/InvalidRelation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

class InvalidRelation extends \Exception
{
public $message = 'Package allows only following relations : BelongsToJoin and HasOneJoin.';
public $message = 'Package allows only following relations : BelongsToJoin, HasOneJoin and HasMany.';
}
20 changes: 19 additions & 1 deletion src/Traits/EloquentJoin.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ trait EloquentJoin

public function newEloquentBuilder($query)
{
return new EloquentJoinBuilder($query);
$newEloquentBuilder = new EloquentJoinBuilder($query);

if (isset($this->useTableAlias)) {
$newEloquentBuilder->setUseTableAlias($this->useTableAlias);
}

if (isset($this->appendRelationsCount)) {
$newEloquentBuilder->setAppendRelationsCount($this->appendRelationsCount);
}

if (isset($this->leftJoin)) {
$newEloquentBuilder->setLeftJoin($this->leftJoin);
}

if (isset($this->aggregateMethod)) {
$newEloquentBuilder->setAggregateMethod($this->aggregateMethod);
}

return $newEloquentBuilder;
}
}
65 changes: 10 additions & 55 deletions src/Traits/ExtendRelationsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,69 +5,24 @@
use Fico7489\Laravel\EloquentJoin\Relations\BelongsToJoin;
use Fico7489\Laravel\EloquentJoin\Relations\HasManyJoin;
use Fico7489\Laravel\EloquentJoin\Relations\HasOneJoin;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

/**
* Add new realations BelongsTo and HasOne.
*/
trait ExtendRelationsTrait
{
/**
* Define an inverse one-to-one or many relationship.
*
* @param string $related
* @param string $foreignKey
* @param string $ownerKey
* @param string $relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation)
{
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relationships.
if (is_null($relation)) {
$relation = $this->guessBelongsToRelation();
}

$instance = $this->newRelatedInstance($related);

// If no foreign key was supplied, we can use a backtrace to guess the proper
// foreign key name by using the name of the relationship function, which
// when combined with an "_id" should conventionally match the columns.
if (is_null($foreignKey)) {
$foreignKey = Str::snake($relation).'_'.$instance->getKeyName();
}

// Once we have the foreign key names, we'll just create a new Eloquent query
// for the related models and returns the relationship instance which will
// actually be responsible for retrieving and hydrating every relations.
$ownerKey = $ownerKey ?: $instance->getKeyName();

return new BelongsToJoin(
$instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
);
return new BelongsToJoin($query, $child, $foreignKey, $ownerKey, $relation);
}

/**
* Define a one-to-one relationship.
*
* @param string $related
* @param string $foreignKey
* @param string $localKey
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function hasOne($related, $foreignKey = null, $localKey = null)
protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey)
{
$instance = $this->newRelatedInstance($related);

$foreignKey = $foreignKey ?: $this->getForeignKey();

$localKey = $localKey ?: $this->getKeyName();
return new HasOneJoin($query, $parent, $foreignKey, $localKey);
}

return new HasOneJoin($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey)
{
return new HasManyJoin($query, $parent, $foreignKey, $localKey);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions tests/Models/City.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ public function zipCodePrimary()
return $this->hasOne(ZipCode::class)
->where('is_primary', '=', 1);
}

public function sellers()
{
return $this->belongsToMany(Seller::class, 'locations', 'seller_id', 'city_id');
}

public function zipCodes()
{
return $this->hasMany(ZipCode::class);
}
}
14 changes: 14 additions & 0 deletions tests/Models/Integration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Fico7489\Laravel\EloquentJoin\Tests\Models;

use Illuminate\Database\Eloquent\SoftDeletes;

class Integration extends BaseModel
{
use SoftDeletes;

protected $table = 'integrations';

protected $fillable = ['name'];
}
Loading

0 comments on commit 0525825

Please sign in to comment.