| 1 | <?php |
||||||
| 2 | |||||||
| 3 | namespace Mpyw\EloquentHasByJoin; |
||||||
| 4 | |||||||
| 5 | use DomainException; |
||||||
| 6 | use Illuminate\Database\Eloquent\Builder; |
||||||
| 7 | use Illuminate\Database\Eloquent\Relations\BelongsTo; |
||||||
| 8 | use Illuminate\Database\Eloquent\Relations\HasOne; |
||||||
| 9 | use Illuminate\Database\Eloquent\Relations\MorphOne; |
||||||
| 10 | use Illuminate\Database\Eloquent\Relations\MorphTo; |
||||||
| 11 | use Illuminate\Database\Eloquent\Relations\Relation; |
||||||
| 12 | use Illuminate\Database\Query\JoinClause; |
||||||
| 13 | |||||||
| 14 | /** |
||||||
| 15 | * Class HasByJoinMacro |
||||||
| 16 | * |
||||||
| 17 | * Convert has() and whereHas() constraints to join() ones for single-result relations. |
||||||
| 18 | */ |
||||||
| 19 | class HasByJoinMacro |
||||||
| 20 | { |
||||||
| 21 | /** |
||||||
| 22 | * @var \Illuminate\Database\Eloquent\Builder |
||||||
| 23 | */ |
||||||
| 24 | protected $query; |
||||||
| 25 | |||||||
| 26 | /** |
||||||
| 27 | * HasByJoinMacro constructor. |
||||||
| 28 | * |
||||||
| 29 | * @param \Illuminate\Database\Eloquent\Builder $query |
||||||
| 30 | */ |
||||||
| 31 | 15 | public function __construct(Builder $query) |
|||||
| 32 | { |
||||||
| 33 | 15 | $this->query = $query; |
|||||
| 34 | } |
||||||
| 35 | |||||||
| 36 | /** |
||||||
| 37 | * Parse nested constraints and iterate them to apply. |
||||||
| 38 | * |
||||||
| 39 | * @param string|string[] $relationMethod |
||||||
| 40 | * @param callable[]|null[] $constraints |
||||||
| 41 | * @return \Illuminate\Database\Eloquent\Builder |
||||||
| 42 | */ |
||||||
| 43 | 15 | public function __invoke($relationMethod, ?callable ...$constraints): Builder |
|||||
| 44 | { |
||||||
| 45 | // Prepare a root Model |
||||||
| 46 | 15 | $root = $current = $this->query->getModel(); |
|||||
|
0 ignored issues
–
show
|
|||||||
| 47 | |||||||
| 48 | // Extract dot-chained expressions |
||||||
| 49 | 15 | $relationMethods = \is_string($relationMethod) ? \explode('.', $relationMethod) : \array_values($relationMethod); |
|||||
| 50 | |||||||
| 51 | 15 | foreach ($relationMethods as $i => $currentRelationMethod) { |
|||||
| 52 | // Extract an alias specified with "as" if exists |
||||||
| 53 | 15 | [$currentRelationMethod, $currentTableAlias] = \preg_split('/\s+as\s+/i', $currentRelationMethod, -1, PREG_SPLIT_NO_EMPTY) + [1 => null]; |
|||||
| 54 | |||||||
| 55 | // Create a Relation instance |
||||||
| 56 | 15 | $relation = $current->newModelQuery()->getRelation($currentRelationMethod); |
|||||
| 57 | |||||||
| 58 | // Convert Relation constraints to JOIN ones |
||||||
| 59 | 15 | $this->applyRelationAsJoin($relation, $constraints[$i] ?? null, $currentTableAlias); |
|||||
| 60 | |||||||
| 61 | // Prepare the next Model |
||||||
| 62 | 12 | $current = $relation->getRelated(); |
|||||
| 63 | } |
||||||
| 64 | |||||||
| 65 | // Prevent the original columns and JOIN target columns from being mixed |
||||||
| 66 | 12 | if (($this->query->getQuery()->columns ?: ['*']) === ['*']) { |
|||||
|
0 ignored issues
–
show
The method
getQuery() does not exist on Illuminate\Database\Eloquent\Builder.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 67 | 12 | $this->query->select("{$root->getTable()}.*"); |
|||||
|
0 ignored issues
–
show
The method
select() does not exist on Illuminate\Database\Eloquent\Builder.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 68 | } |
||||||
| 69 | |||||||
| 70 | 12 | return $this->query; |
|||||
| 71 | } |
||||||
| 72 | |||||||
| 73 | /** |
||||||
| 74 | * Convert Relation constraints to JOIN ones. |
||||||
| 75 | * |
||||||
| 76 | * @param \Illuminate\Database\Eloquent\Relations\Relation $relation |
||||||
| 77 | * @param null|callable $constraints |
||||||
| 78 | * @param null|string $tableAlias |
||||||
| 79 | */ |
||||||
| 80 | 15 | protected function applyRelationAsJoin(Relation $relation, ?callable $constraints, ?string $tableAlias): void |
|||||
| 81 | { |
||||||
| 82 | // Support BelongsTo and HasOne relations only |
||||||
| 83 | 15 | if ((!$relation instanceof BelongsTo || $relation instanceof MorphTo) && !$relation instanceof HasOne && !$relation instanceof MorphOne) { |
|||||
| 84 | 1 | throw new DomainException('Unsupported relation. Currently supported: BelongsTo, HasOne and MorphOne'); |
|||||
| 85 | } |
||||||
| 86 | |||||||
| 87 | // Generate the same subquery as has() method does |
||||||
| 88 | $relationExistenceQuery = $relation |
||||||
| 89 | 14 | ->getRelationExistenceQuery($this->overrideTableWithAlias($relation->getRelated()->newQuery(), $tableAlias), $this->query) |
|||||
| 90 | 14 | ->mergeConstraintsFrom($relationQuery = $this->overrideTableWithAlias($relation->getQuery(), $tableAlias)); |
|||||
| 91 | |||||||
| 92 | // Validate table alias availability |
||||||
| 93 | 14 | $this->ensureTableAliasAvailability($relationQuery, $tableAlias); |
|||||
| 94 | |||||||
| 95 | // Apply optional constraints |
||||||
| 96 | 12 | if ($constraints) { |
|||||
| 97 | 2 | $constraints($relationExistenceQuery); |
|||||
| 98 | } |
||||||
| 99 | |||||||
| 100 | // Convert Eloquent Builder to Query Builder and evaluate scope constraints |
||||||
| 101 | 12 | $relationExistenceQuery = $relationExistenceQuery->toBase(); |
|||||
| 102 | |||||||
| 103 | // Migrate has() constraints to join() |
||||||
| 104 | 12 | $this->query->join( |
|||||
|
0 ignored issues
–
show
The method
join() does not exist on Illuminate\Database\Eloquent\Builder.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 105 | 12 | $tableAlias ? "$relationExistenceQuery->from as $tableAlias" : $relationExistenceQuery->from, |
|||||
| 106 | 12 | function (JoinClause $join) use ($relationExistenceQuery) { |
|||||
| 107 | 12 | $join->mergeWheres($relationExistenceQuery->wheres, $relationExistenceQuery->bindings); |
|||||
| 108 | 12 | } |
|||||
| 109 | ); |
||||||
| 110 | |||||||
| 111 | // Migrate extra joins on has() constraints |
||||||
| 112 | 12 | $this->mergeJoins($this->query, $relationQuery); |
|||||
| 113 | } |
||||||
| 114 | |||||||
| 115 | /** |
||||||
| 116 | * Rewrite table name if its alias is provided. |
||||||
| 117 | * |
||||||
| 118 | * @param \Illuminate\Database\Eloquent\Builder $query |
||||||
| 119 | * @param null|string $tableAlias |
||||||
| 120 | * @return \Illuminate\Database\Eloquent\Builder |
||||||
| 121 | */ |
||||||
| 122 | 14 | protected function overrideTableWithAlias(Builder $query, ?string $tableAlias): Builder |
|||||
| 123 | { |
||||||
| 124 | 14 | if ($tableAlias) { |
|||||
| 125 | 3 | $query->getModel()->setTable($tableAlias); |
|||||
| 126 | } |
||||||
| 127 | |||||||
| 128 | 14 | return $query; |
|||||
| 129 | } |
||||||
| 130 | |||||||
| 131 | /** |
||||||
| 132 | * mergeJoins() is not provided by Laravel, |
||||||
| 133 | * so we need to implement it by ourselves. |
||||||
| 134 | * |
||||||
| 135 | * @param \Illuminate\Database\Eloquent\Builder $dst |
||||||
| 136 | * @param \Illuminate\Database\Eloquent\Builder $src |
||||||
| 137 | */ |
||||||
| 138 | 12 | protected function mergeJoins(Builder $dst, Builder $src): void |
|||||
| 139 | { |
||||||
| 140 | 12 | [$dst, $src] = [$dst->getQuery(), $src->getQuery()]; |
|||||
| 141 | |||||||
| 142 | 12 | if ($src->joins) { |
|||||
| 143 | 1 | $dst->joins = \array_merge((array)$dst->joins, $src->joins); |
|||||
| 144 | } |
||||||
| 145 | 12 | if ($src->bindings) { |
|||||
| 146 | 12 | $dst->bindings['join'] = \array_merge((array)$dst->bindings['join'], $src->bindings['join']); |
|||||
| 147 | } |
||||||
| 148 | } |
||||||
| 149 | |||||||
| 150 | /** |
||||||
| 151 | * Extra where() or join() constraints in relation are evaluated early, |
||||||
| 152 | * so we cannot apply table aliases to them. |
||||||
| 153 | * |
||||||
| 154 | * @param \Illuminate\Database\Eloquent\Builder $query |
||||||
| 155 | * @param null|string $tableAlias |
||||||
| 156 | */ |
||||||
| 157 | 14 | protected function ensureTableAliasAvailability(Builder $query, ?string $tableAlias): void |
|||||
| 158 | { |
||||||
| 159 | 14 | $query = $query->getQuery(); |
|||||
| 160 | |||||||
| 161 | 14 | if (($query->joins || $query->wheres) && $tableAlias) { |
|||||
| 162 | 2 | throw new DomainException('You cannot use table alias when your relation has extra joins or wheres.'); |
|||||
| 163 | } |
||||||
| 164 | } |
||||||
| 165 | } |
||||||
| 166 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.