-
-
Save MatteoOreficeIT/d3f66e90436dd5b9c90fbe144118e667 to your computer and use it in GitHub Desktop.
| <?php | |
| /** | |
| * User: matteo.orefice | |
| * Date: 16/02/2018 | |
| * Time: 16:57 | |
| */ | |
| namespace MatteoOrefice\Illuminate\Database\Eloquent\Concerns; | |
| use Illuminate\Support\Str; | |
| use Illuminate\Database\Eloquent\Builder; | |
| use Illuminate\Database\Eloquent\Model; | |
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | |
| use Illuminate\Database\Eloquent\Relations\HasOneOrMany; | |
| use Illuminate\Database\Query\JoinClause; | |
| use Illuminate\Support\Facades\DB; | |
| /** | |
| * \App\MyModel::query()->select('S.*')->withJoin('relationShip1','S'); | |
| * \App\MyModel::query()->select('P.*')->withJoin(['relationShip1','relationShip2'],'P'); | |
| * \App\MyModel::query()->select('P.*','S.*')->withJoin(['relationShip1','relationShip2'],['P','S']); | |
| * \App\MyModel::query()->from(\App\MyModel::query()->getModel()->getTable().' as U')->select('U.*','S.*')->withJoin(['relationShip1','relationShip2'],['P','S']); | |
| * \App\MyModel::query()->from(\App\MyModel::query()->getModel()->getTable().' as U')->select('U.*','S.*')->withJoin('relationShip1.relationShip2',['P','S']); | |
| * \App\MyModel::query()->select('P.*','S.*')->withSelect('relationShip1.'relationShip2',['alias'=>'farcolumn']); | |
| * \App\MyModel::query()->select('P.*','S.*')->withSelect('relationShip1.'relationShip2',['alias'=>DB::Expr('farcolumn_expression')]); | |
| * \App\MyModel::query()->select('P.*','S.*')->withSelect('relationShip1.'relationShip2',[DB::Expr('farcolumn_expression')]); | |
| **/ | |
| trait JoinRelationShip | |
| { | |
| /** | |
| * @param Builder $builder | |
| * @param $relationSegments | |
| * @param string|string[]|null $rightAlias se non fornito si genera a caso e vendono appesi progressivi | |
| * se fornito come stringa diventa alias di quella piu a dx e le precedenti avranno un suffisso numerico N+1 | |
| * se fornito come array, elemento zero viene usato per la relazione piu lontana e cosi via | |
| * @param string $operator | |
| * @return $this | |
| * @throws \Exception | |
| */ | |
| public function scopeWithJoin(Builder $builder, $relationSegments, $rightAlias=null, $decorators=null, $join='join', $operator='=') | |
| { | |
| if(is_string($decorators)) { | |
| if($join === 'join') { | |
| $operator = '='; | |
| } else { | |
| $operator = $join; | |
| } | |
| $join = $decorators; | |
| $decorators = null; | |
| } | |
| else if($decorators) { | |
| $decorators = array_wrap($decorators); | |
| } | |
| // recupera il nome della tabella con eventuale alias dal from oppure dal nome della table corrispondente nel model | |
| $aliasSegments = preg_split('/\s+/i', $previousTableAlias = $builder->getQuery()->from ?: $builder->getModel()->getTable()); | |
| // il terzo conterrebbe l'alias | |
| if(is_array($aliasSegments) && isset($aliasSegments[2])) { | |
| $previousTableAlias = $aliasSegments[2]; | |
| } | |
| $this->getJoinRelationShipSubQuery($this,$builder,$relationSegments,$previousTableAlias,$rightAlias,false,$decorators,$join,$operator); | |
| return $builder; | |
| } | |
| public function scopeWithSelect(Builder $builder, $relationSegments, $columns, $rightAlias=null, $decorators=null, $join='join', $operator='=') | |
| { | |
| if(is_string($decorators)) { | |
| if($join === 'join') { | |
| $operator = '='; | |
| } else { | |
| $operator = $join; | |
| } | |
| $join = $decorators; | |
| $decorators = null; | |
| } | |
| else if($decorators) { | |
| $decorators = array_wrap($decorators); | |
| } | |
| // recupera il nome della tabella con eventuale alias dal from oppure dal nome della table corrispondente nel model | |
| $aliasSegments = preg_split('/\s+/i', $previousTableAlias = $builder->getQuery()->from ?: $builder->getModel()->getTable()); | |
| // il terzo conterrebbe l'alias | |
| if(is_array($aliasSegments) && isset($aliasSegments[2])) { | |
| $previousTableAlias = $aliasSegments[2]; | |
| } | |
| $this->getJoinRelationShipSubQuery($this,$subQuery=DB::query(),$relationSegments,$previousTableAlias,$rightAlias,true,$decorators,$join,$operator); | |
| foreach ($columns as $alias => $column) { | |
| $subQuery->addSelect($column); | |
| $builder->selectSub($subQuery , is_string($alias) ? $alias : $this->wrapColumnDefinition($column)); | |
| } | |
| return $builder; | |
| } | |
| protected function wrapColumnDefinition($columnDefinition) | |
| { | |
| return urldecode( | |
| $this->getQuery()->getGrammar()->wrap( | |
| preg_replace_callback('/[^a-zA-Z]+/',function($value) { | |
| $value = $value[0]; | |
| $out = ''; | |
| for ($i = 0; isset($value[$i]); $i++) { | |
| $c = $value[$i]; | |
| if (!ctype_alnum($c)) $c = '%' . sprintf('%02X', ord($c)); | |
| $out .= $c; | |
| } | |
| return $out; | |
| },$columnDefinition) | |
| ) | |
| ); | |
| } | |
| /** | |
| * A partire da un model crea join multiple sfruttando le relazioni | |
| * | |
| * | |
| * | |
| * @param Model $model | |
| * @param $relationSegments | |
| * @param null $builder | |
| * @param null $previousTableAlias | |
| * @return null | |
| * @throws \Exception | |
| */ | |
| protected function getJoinRelationShipSubQuery( | |
| Model $model, | |
| $builder, | |
| $relationSegments, | |
| $previousTableAlias=null, | |
| $rightAlias=null, | |
| $sub=false, | |
| $decorators=null, | |
| $join='join', | |
| $operator='=') { | |
| $currentModel = $model; | |
| $relatedModel = null; | |
| $relatedTableAlias = null; | |
| $tableAliases = []; | |
| $relatedTableAndAlias = null; | |
| $relationSegments = array_wrap($relationSegments); | |
| if(count($relationSegments)==1 && Str::contains($relationSegments[0],'.')) { | |
| $relationSegments = preg_split('/\./',$relationSegments[0]); | |
| } | |
| if(($relationIndex=count($relationSegments))==0) { | |
| throw new \Exception('Relation path cannot be empty'); | |
| } | |
| /** | |
| * Il prefisso per le tabelle unite in JOIN viene generato a caso se non fornito | |
| */ | |
| $randomPrefix = Str::randomStringAlpha(3); | |
| /** | |
| * Per ogni segmento aggiungo una join | |
| */ | |
| foreach ($relationSegments as $segment) { | |
| if (!method_exists($currentModel,$segment)) { | |
| throw new \BadMethodCallException("Relationship $segment does not exist, cannot join."); | |
| } | |
| $decorator = $this->getDecorator($decorators,$relationIndex); | |
| $relation = $currentModel->$segment(); | |
| $relatedModel = $relation->getRelated(); | |
| $relatedTableAlias = $this->makeTableAlias($randomPrefix,$rightAlias,$relationIndex); | |
| if(!is_null($relatedTableAlias)) { | |
| $tableAlias = ' AS ' . $relatedTableAlias; | |
| $relatedTableAndAlias = $relatedModel->getTable() . $tableAlias; | |
| } | |
| else { | |
| $relatedTableAndAlias = $relatedTableAlias = $relatedModel->getTable(); | |
| } | |
| $tableAliases []= $relatedTableAlias; | |
| /** | |
| * Nelle BelongsTo definiamo : | |
| * - CHILD TABLE(SX) : quella dal lato con cardinalita N | |
| * - FOREIGN KEY : la colonna chiave esterna sulla tabella CHILD ovvero il lato con cardinalita (N) | |
| * - PARENT TABLE(DX) : quella dal lato con cardinalita 1 | |
| * - OWNER KEY : la colonna chiave sulla tabella PARENT ovvero il lato con cardinalita (1) | |
| */ | |
| if ($relation instanceof BelongsTo) { | |
| if($sub) { | |
| if(!$previousTableAlias) { | |
| throw new \RuntimeException('$previousTableAlias is required for sub'); | |
| } | |
| $sub = false; | |
| $builder | |
| ->from($relatedTableAndAlias) | |
| ->whereColumn( | |
| $previousTableAlias.'.'. $relation->getForeignKey(), | |
| $operator , | |
| $relatedTableAlias . '.' . $relation->getOwnerKey() | |
| ); | |
| } else { | |
| $builder | |
| ->$join( | |
| $relatedTableAndAlias, | |
| function (JoinClause $joinClause) use($decorator,$previousTableAlias,$relation,$operator,$relatedTableAlias) { | |
| $joinClause->on( | |
| $previousTableAlias ? $previousTableAlias.'.'. $relation->getForeignKey() : $relation->getQualifiedForeignKey(), | |
| $operator , | |
| $relatedTableAlias . '.' . $relation->getOwnerKey() | |
| ); | |
| if($decorator instanceof \Closure) { | |
| $decorator($joinClause); | |
| } | |
| } | |
| ); | |
| } | |
| } | |
| // endif | |
| /** | |
| * Nelle HasOneOrMany definiamo : | |
| * - PARENT TABLE(SX) : quella dal lato con cardinalita 1 | |
| * - CHILD TABLE(DX) : quella dal lato con cardinalita N | |
| */ | |
| elseif ($relation instanceof HasOneOrMany) { | |
| if($sub) { | |
| if(!$previousTableAlias) { | |
| throw new \RuntimeException('$previousTableAlias is required for sub'); | |
| } | |
| $sub = false; | |
| $builder | |
| ->from($relatedTableAndAlias) | |
| ->whereColumn( | |
| $previousTableAlias.'.'. $relation->getParent()->getKeyName(), | |
| $operator , | |
| $relatedTableAlias . '.' . $relation->getForeignKeyName() | |
| ); | |
| } else { | |
| $builder | |
| ->$join( | |
| $relatedTableAndAlias, | |
| function(JoinClause $joinClause) use($decorator,$previousTableAlias,$relation,$operator,$relatedTableAlias) { | |
| $joinClause->on( | |
| $previousTableAlias ? $previousTableAlias.'.'. $relation->getParent()->getKeyName() : $relation->getQualifiedParentKeyName(), | |
| $operator , | |
| $relatedTableAlias . '.' . $relation->getForeignKeyName() | |
| ); | |
| if($decorator instanceof \Closure) { | |
| $decorator($joinClause); | |
| } | |
| } | |
| ); | |
| } | |
| } // endif | |
| else { | |
| throw new \InvalidArgumentException( | |
| sprintf("Relation $segment of type %s is not supported" , get_class($relation)) | |
| ); | |
| } // else | |
| /** | |
| * Avanza i puntatori | |
| */ | |
| $currentModel = $relatedModel; | |
| $previousTableAlias = $relatedTableAlias; | |
| $relationIndex--; | |
| } // end foreach <RELATIONs> | |
| return $tableAliases; | |
| } | |
| public function scopeWithJoinLeft(Builder $builder, $relationSegments, $rightAlias=null, $decorators=null, $operator='=') { | |
| return $this->scopeWithJoin($builder,$relationSegments,$rightAlias,$decorators,'leftJoin',$operator); | |
| } | |
| /** | |
| * Crea un alias con un prefisso numerico oppure lo recupera da un array | |
| * | |
| * Se indice non e' presente prende elemento piu prossimo | |
| * | |
| * @param $string $prefix | |
| * @param string|array $alias | |
| * @return array|mixed|null|string | |
| */ | |
| public function makeTableAlias($prefix,$alias,$index) | |
| { | |
| $index -=1 ; | |
| if(is_array($alias)) { | |
| if(isset($alias[$index])) | |
| return $alias[$index]; | |
| if($index>0) | |
| return array_last($alias).'_'.($index); | |
| return array_last($alias); | |
| } | |
| else if($index==0) { | |
| if(is_null($alias)) | |
| return null; | |
| return $alias; | |
| } | |
| return $prefix.'_'.($index); | |
| } | |
| public function getDecorator($decorators,$index) | |
| { | |
| $index -=1 ; | |
| if(!is_array($decorators)) { | |
| return null; | |
| } | |
| if(isset($decorators[$index])) | |
| return $decorators[$index]; | |
| return null; | |
| } | |
| } |
Hi @koxu1996 , if you mean fetching nested/consecutive relations from a model through its and other model relations the answer is YES
For example if we have :
class A extends Model {
public function relatesB() { return $this->belongsTo( B::class ); }
}
class B extends Model {
public function relatesC() { return $this->belongsTo( C::class ); }
}
class C extends Model {
public function relatesD() { return $this->belongsTo( D::class ); }
}
class C extends Model { }you can do this :
\App\A::query()
// include every available aliases from different join clause
->select('A.*','B.*','C.*','D.*')
->joinWith(
// specify relations join order/path for consecutive / nested relations
['relatesB','relatesC','relatesD'],
// place an alias for every related model, remember in reverse order
// you can also use only ['D'] and so the other aliases will be generated appendind a number for example D_1,D_2, ..., D_N
['D','C','B','A']
);Keep in mind that to simulate the eager loading behaviour you can place in the select clause many alias containing in the name a dot , for example :
$query->select('A.field1 as relationB.field1') /// you will find in result a key [ 'relationB.field1' => 'valueOfField1OnModelB' ]and after you can group these values with a code like the following :
$relationValues = [];
collect($model->getAttributes())->each(function($value,$name)use(&$relationValues,$model){
if(Str::contains($name,'.')) {
Arr::set($relationValues,$name,$value);
unset($model[$name]);
}
});
collect($relationValues)->each(function($value, $name)use(&$model){
$model[$name] = $value;
});The resulting model will have the related model fields grouped by relation name of your choice.
UPDATE: now you can also use withSelect()
Thanks for the snippet. Just small things I found while inspecting:
->joinWith(
// specify relations join order/path for consecutive / nested relations
['relatesB','relatesC','relatesD'],
// place an alias for every related model, remember in reverse order
// you can also use only ['D'] and so the other aliases will be generated appendind a number for example D_1,D_2, ..., D_N
['D','C','B','A']
);
should be
->withJoin(...
and $randomPrefix = Str::randomStringAlpha(3); is not available, it's just Str::random(3)
https://laravel.com/api/5.6/Illuminate/Support/Str.html#method_random
Thanks @mludi , it was a custom function we added.
Does this support nested relations?