4
4
5
5
namespace Yiisoft \Db \QueryBuilder \Condition \Builder ;
6
6
7
+ use Traversable ;
7
8
use Yiisoft \Db \Command \Param ;
8
9
use Yiisoft \Db \Constant \DataType ;
9
10
use Yiisoft \Db \Exception \Exception ;
15
16
use Yiisoft \Db \QueryBuilder \Condition \Like ;
16
17
use Yiisoft \Db \QueryBuilder \Condition \LikeConjunction ;
17
18
use Yiisoft \Db \QueryBuilder \Condition \LikeMode ;
19
+ use Yiisoft \Db \QueryBuilder \Condition \NotLike ;
18
20
use Yiisoft \Db \QueryBuilder \QueryBuilderInterface ;
19
21
20
22
use function implode ;
21
- use function is_array ;
22
- use function preg_match ;
23
+ use function is_string ;
23
24
use function str_contains ;
24
- use function strtoupper ;
25
25
use function strtr ;
26
26
27
27
/**
28
- * Build an object of {@see Like} into SQL expressions.
28
+ * Build an object of {@see Like} or {@see NotLike} into SQL expressions.
29
29
*
30
- * @implements ExpressionBuilderInterface<Like>
30
+ * @implements ExpressionBuilderInterface<Like|NotLike >
31
31
*/
32
32
class LikeBuilder implements ExpressionBuilderInterface
33
33
{
34
- public function __construct (
35
- private readonly QueryBuilderInterface $ queryBuilder ,
36
- private readonly string |null $ escapeSql = null
37
- ) {
38
- }
34
+ /**
35
+ * @var string SQL fragment to append to the end of `LIKE` conditions.
36
+ */
37
+ protected const ESCAPE_SQL = '' ;
39
38
40
39
/**
41
40
* @var array Map of chars to their replacements in `LIKE` conditions. By default, it's configured to escape
@@ -47,10 +46,15 @@ public function __construct(
47
46
'\\' => '\\\\' ,
48
47
];
49
48
49
+ public function __construct (
50
+ private readonly QueryBuilderInterface $ queryBuilder ,
51
+ ) {
52
+ }
53
+
50
54
/**
51
- * Build SQL for {@see Like}.
55
+ * Build SQL for {@see Like} or {@see NotLike} .
52
56
*
53
- * @param Like $expression
57
+ * @param Like|NotLike $expression
54
58
*
55
59
* @throws Exception
56
60
* @throws InvalidArgumentException
@@ -61,24 +65,30 @@ public function build(ExpressionInterface $expression, array &$params = []): str
61
65
{
62
66
$ values = $ expression ->value ;
63
67
64
- [$ not , $ operator ] = $ this ->parseOperator ($ expression );
68
+ [$ not , $ operator ] = $ this ->getOperatorData ($ expression );
65
69
66
- if (! is_array ( $ values) ) {
67
- $ values = [ $ values ] ;
70
+ if ($ values === null ) {
71
+ return $ this -> buildForEmptyValue ( $ not ) ;
68
72
}
69
73
70
- if (empty ($ values )) {
71
- return $ not ? '' : '0=1 ' ;
74
+ if (is_iterable ($ values )) {
75
+ if ($ values instanceof Traversable) {
76
+ $ values = iterator_to_array ($ values );
77
+ }
78
+ if (empty ($ values )) {
79
+ return $ this ->buildForEmptyValue ($ not );
80
+ }
81
+ } else {
82
+ $ values = [$ values ];
72
83
}
73
84
74
85
$ column = $ this ->prepareColumn ($ expression , $ params );
75
86
76
87
$ parts = [];
77
-
78
- /** @psalm-var list<string|ExpressionInterface> $values */
79
88
foreach ($ values as $ value ) {
89
+ /** @var ExpressionInterface|int|string $value */
80
90
$ placeholderName = $ this ->preparePlaceholderName ($ value , $ expression , $ params );
81
- $ parts [] = "$ column $ operator $ placeholderName$ this -> escapeSql " ;
91
+ $ parts [] = "$ column $ operator $ placeholderName" . static :: ESCAPE_SQL ;
82
92
}
83
93
84
94
$ conjunction = match ($ expression ->conjunction ) {
@@ -97,7 +107,7 @@ public function build(ExpressionInterface $expression, array &$params = []): str
97
107
* @throws InvalidConfigException
98
108
* @throws NotSupportedException
99
109
*/
100
- protected function prepareColumn (Like $ condition , array &$ params ): string
110
+ protected function prepareColumn (Like | NotLike $ condition , array &$ params ): string
101
111
{
102
112
$ column = $ condition ->column ;
103
113
@@ -122,45 +132,43 @@ protected function prepareColumn(Like $condition, array &$params): string
122
132
* @return string
123
133
*/
124
134
protected function preparePlaceholderName (
125
- string |ExpressionInterface $ value ,
126
- Like $ condition ,
135
+ string |int | ExpressionInterface $ value ,
136
+ Like | NotLike $ condition ,
127
137
array &$ params ,
128
138
): string {
129
139
if ($ value instanceof ExpressionInterface) {
130
140
return $ this ->queryBuilder ->buildExpression ($ value , $ params );
131
141
}
132
142
133
- if ($ condition ->escape ) {
143
+ if (is_string ( $ value ) && $ condition ->escape ) {
134
144
$ value = strtr ($ value , $ this ->escapingReplacements );
135
145
}
136
146
137
147
$ value = match ($ condition ->mode ) {
138
148
LikeMode::Contains => '% ' . $ value . '% ' ,
139
149
LikeMode::StartsWith => $ value . '% ' ,
140
150
LikeMode::EndsWith => '% ' . $ value ,
141
- LikeMode::Custom => $ value ,
151
+ LikeMode::Custom => ( string ) $ value ,
142
152
};
143
153
144
154
return $ this ->queryBuilder ->bindParam (new Param ($ value , DataType::STRING ), $ params );
145
155
}
146
156
147
157
/**
148
- * Parses operator and returns its parts.
149
- *
150
- * @throws InvalidArgumentException
158
+ * Get operator and `not` flag for the given condition.
151
159
*
152
160
* @psalm-return array{0: bool, 1: string}
153
161
*/
154
- protected function parseOperator (Like $ condition ): array
162
+ protected function getOperatorData (Like | NotLike $ condition ): array
155
163
{
156
- $ operator = strtoupper ($ condition ->operator );
157
- if (!preg_match ('/^((NOT |)I?LIKE)/ ' , $ operator , $ matches )) {
158
- throw new InvalidArgumentException ("Invalid operator in like condition: \"$ operator \"" );
159
- }
160
-
161
- $ not = !empty ($ matches [2 ]);
162
- $ operator = $ matches [1 ];
164
+ return match ($ condition ::class) {
165
+ Like::class => [false , 'LIKE ' ],
166
+ NotLike::class => [true , 'NOT LIKE ' ],
167
+ };
168
+ }
163
169
164
- return [$ not , $ operator ];
170
+ private function buildForEmptyValue (bool $ not ): string
171
+ {
172
+ return $ not ? '' : '0=1 ' ;
165
173
}
166
174
}
0 commit comments