77use Countable ;
88use Iterator ;
99use Override ;
10+ use PhpDb \Sql \Part \AbstractPart ;
11+ use PhpDb \Sql \Part \JoinSpec ;
12+ use PhpDb \Sql \Part \JoinTableType ;
13+ use PhpDb \Sql \Part \SqlProcessor ;
1014use ReturnTypeWillChange ;
1115
1216use function array_shift ;
1317use function count ;
18+ use function explode ;
19+ use function implode ;
1420use function is_array ;
1521use function is_string ;
1622use function key ;
23+ use function preg_replace_callback ;
24+ use function spl_object_id ;
1725use function sprintf ;
1826
1927/**
2634 * - type: the type of JOIN being performed; see the `JOIN_*` constants;
2735 * defaults to `JOIN_INNER`
2836 */
29- class Join implements Iterator, Countable
37+ class Join extends AbstractPart implements Iterator, Countable
3038{
39+ private const IDENTIFIER_PATTERN = '/\b(?!(?:AS|AND|OR|BETWEEN)\b)([a-zA-Z_]\w*+(?:\.[a-zA-Z_]\w*+)*)(?!\s*\()/i ' ;
40+
3141 final public const JOIN_INNER = 'INNER ' ;
3242
3343 final public const JOIN_OUTER = 'OUTER ' ;
@@ -42,59 +52,45 @@ class Join implements Iterator, Countable
4252
4353 final public const JOIN_LEFT_OUTER = 'LEFT OUTER ' ;
4454
45- /**
46- * Current iterator position.
47- */
4855 private int $ position = 0 ;
4956
50- /**
51- * JOIN specifications
52- */
57+ /** @var array[] */
5358 protected array $ joins = [];
5459
55- /**
56- * Rewind iterator.
57- */
60+ /** @var JoinSpec[] */
61+ private array $ specs = [];
62+
63+ private ?string $ sqlCache = null ;
64+ private ?int $ sqlCachePlatformId = null ;
65+
5866 #[Override]
5967 #[ReturnTypeWillChange]
6068 public function rewind (): void
6169 {
6270 $ this ->position = 0 ;
6371 }
6472
65- /**
66- * Return current join specification.
67- */
6873 #[Override]
6974 #[ReturnTypeWillChange]
7075 public function current (): array
7176 {
7277 return $ this ->joins [$ this ->position ];
7378 }
7479
75- /**
76- * Return the current iterator index.
77- */
7880 #[Override]
7981 #[ReturnTypeWillChange]
8082 public function key (): int
8183 {
8284 return $ this ->position ;
8385 }
8486
85- /**
86- * Advance to the next JOIN specification.
87- */
8887 #[Override]
8988 #[ReturnTypeWillChange]
9089 public function next (): void
9190 {
9291 ++$ this ->position ;
9392 }
9493
95- /**
96- * Is the iterator at a valid position?
97- */
9894 #[Override]
9995 #[ReturnTypeWillChange]
10096 public function valid (): bool
@@ -134,13 +130,17 @@ public function join(
134130 $ columns = [$ columns ];
135131 }
136132
137- $ this -> joins [] = [
133+ $ raw = [
138134 'name ' => $ name ,
139135 'on ' => $ on ,
140136 'columns ' => $ columns ,
141137 'type ' => $ type ,
142138 ];
143139
140+ $ this ->joins [] = $ raw ;
141+ $ this ->specs [] = new JoinSpec ($ raw );
142+ $ this ->sqlCache = null ;
143+
144144 return $ this ;
145145 }
146146
@@ -149,17 +149,92 @@ public function join(
149149 */
150150 public function reset (): static
151151 {
152- $ this ->joins = [];
152+ $ this ->joins = [];
153+ $ this ->specs = [];
154+ $ this ->sqlCache = null ;
155+ $ this ->sqlCachePlatformId = null ;
153156 return $ this ;
154157 }
155158
156- /**
157- * Get count of attached predicates
158- */
159159 #[Override]
160160 #[ReturnTypeWillChange]
161161 public function count (): int
162162 {
163163 return count ($ this ->joins );
164164 }
165+
166+ #[Override]
167+ public function toSql (SqlProcessor $ processor ): ?string
168+ {
169+ if ($ this ->specs === []) {
170+ return null ;
171+ }
172+
173+ $ canCache = $ processor ->parameterContainer === null ;
174+ if ($ canCache ) {
175+ $ platformId = spl_object_id ($ processor ->platform );
176+ if ($ this ->sqlCachePlatformId === $ platformId ) {
177+ return $ this ->sqlCache ;
178+ }
179+ }
180+
181+ $ platform = $ processor ->platform ;
182+ $ joinSqlParts = [];
183+
184+ foreach ($ this ->specs as $ j => $ spec ) {
185+ $ joinName = match ($ spec ->tableType ) {
186+ JoinTableType::Expression => $ spec ->table ->getExpression (), // @phpstan-ignore method.nonObject
187+ JoinTableType::TableIdentifier => $ processor ->resolveTable ($ spec ->table ),
188+ JoinTableType::Select => '( ' . $ processor ->processSubSelect ($ spec ->table ) . ') ' ,
189+ JoinTableType::Identifier => $ platform ->quoteIdentifier ($ spec ->table ),
190+ };
191+ $ quotedAlias = $ spec ->alias !== null
192+ ? $ platform ->quoteIdentifier ($ spec ->alias )
193+ : null ;
194+
195+ $ renderedTable = $ processor ->renderTable ($ joinName , $ quotedAlias );
196+
197+ if ($ spec ->isExpressionOn ) {
198+ $ onClause = $ processor ->renderExpression (
199+ $ spec ->on ,
200+ 'join ' . ($ j + 1 ) . 'part '
201+ );
202+ } else {
203+ $ onClause = preg_replace_callback (
204+ self ::IDENTIFIER_PATTERN ,
205+ static fn ($ m ) => $ platform ->quoteIdentifier (...explode ('. ' , $ m [1 ], 2 )),
206+ $ spec ->on ,
207+ );
208+ }
209+
210+ $ joinSqlParts [] = "{$ spec ->type } JOIN {$ renderedTable } ON {$ onClause }" ;
211+ }
212+
213+ $ result = implode (' ' , $ joinSqlParts );
214+
215+ if ($ canCache ) {
216+ $ this ->sqlCache = $ result ;
217+ $ this ->sqlCachePlatformId = $ platformId ;
218+ }
219+
220+ return $ result ;
221+ }
222+
223+ #[Override]
224+ public function isEmpty (): bool
225+ {
226+ return $ this ->specs === [];
227+ }
228+
229+ /** @return JoinSpec[] */
230+ public function getSpecs (): array
231+ {
232+ return $ this ->specs ;
233+ }
234+
235+ public function __clone ()
236+ {
237+ $ this ->sqlCache = null ;
238+ $ this ->sqlCachePlatformId = null ;
239+ }
165240}
0 commit comments