1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
/** |
5
|
|
|
* This file is part of the Happyr Doctrine Specification package. |
6
|
|
|
* |
7
|
|
|
* (c) Tobias Nyholm <[email protected]> |
8
|
|
|
* Kacper Gunia <[email protected]> |
9
|
|
|
* Peter Gribanov <[email protected]> |
10
|
|
|
* |
11
|
|
|
* For the full copyright and license information, please view the LICENSE |
12
|
|
|
* file that was distributed with this source code. |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace Happyr\DoctrineSpecification\Filter; |
16
|
|
|
|
17
|
|
|
use Doctrine\ORM\Query\Expr\Comparison as DoctrineComparison; |
18
|
|
|
use Doctrine\ORM\QueryBuilder; |
19
|
|
|
use Happyr\DoctrineSpecification\Operand\ArgumentToOperandConverter; |
20
|
|
|
use Happyr\DoctrineSpecification\Operand\LikePattern; |
21
|
|
|
use Happyr\DoctrineSpecification\Operand\Operand; |
22
|
|
|
|
23
|
|
|
final class Like implements Filter, Satisfiable |
24
|
|
|
{ |
25
|
|
|
public const CONTAINS = LikePattern::CONTAINS; |
26
|
|
|
|
27
|
|
|
public const ENDS_WITH = LikePattern::ENDS_WITH; |
28
|
|
|
|
29
|
|
|
public const STARTS_WITH = LikePattern::STARTS_WITH; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var Operand|string |
33
|
|
|
*/ |
34
|
|
|
private $field; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var LikePattern |
38
|
|
|
*/ |
39
|
|
|
private $value; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var string|null |
43
|
|
|
*/ |
44
|
|
|
private $context; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @param Operand|string $field |
48
|
|
|
* @param LikePattern|string $value |
49
|
|
|
* @param string $format |
50
|
|
|
* @param string|null $context |
51
|
|
|
*/ |
52
|
|
|
public function __construct($field, $value, string $format = LikePattern::CONTAINS, ?string $context = null) |
53
|
|
|
{ |
54
|
|
|
if (!($value instanceof LikePattern)) { |
55
|
|
|
$value = new LikePattern($value, $format); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
$this->field = $field; |
59
|
|
|
$this->value = $value; |
60
|
|
|
$this->context = $context; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @param QueryBuilder $qb |
65
|
|
|
* @param string $context |
66
|
|
|
* |
67
|
|
|
* @return string |
68
|
|
|
*/ |
69
|
|
|
public function getFilter(QueryBuilder $qb, string $context): string |
70
|
|
|
{ |
71
|
|
|
if (null !== $this->context) { |
72
|
|
|
$context = sprintf('%s.%s', $context, $this->context); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
$field = ArgumentToOperandConverter::toField($this->field); |
76
|
|
|
|
77
|
|
|
$field = $field->transform($qb, $context); |
78
|
|
|
$value = $this->value->transform($qb, $context); |
79
|
|
|
|
80
|
|
|
return (string) new DoctrineComparison($field, 'LIKE', $value); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* {@inheritdoc} |
85
|
|
|
*/ |
86
|
|
|
public function filterCollection(iterable $collection, ?string $context = null): iterable |
87
|
|
|
{ |
88
|
|
|
$context = $this->resolveContext($context); |
89
|
|
|
$field = ArgumentToOperandConverter::toField($this->field); |
90
|
|
|
$value = $this->getUnescapedValue(); |
91
|
|
|
|
92
|
|
|
foreach ($collection as $candidate) { |
93
|
|
|
if ($this->isMatch($field->execute($candidate, $context), $value)) { |
94
|
|
|
yield $candidate; |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* {@inheritdoc} |
101
|
|
|
*/ |
102
|
|
|
public function isSatisfiedBy($candidate, ?string $context = null): bool |
103
|
|
|
{ |
104
|
|
|
$context = $this->resolveContext($context); |
105
|
|
|
$field = ArgumentToOperandConverter::toField($this->field); |
106
|
|
|
$value = $this->getUnescapedValue(); |
107
|
|
|
|
108
|
|
|
return $this->isMatch($field->execute($candidate, $context), $value); |
|
|
|
|
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @param string|null $context |
113
|
|
|
* |
114
|
|
|
* @return string|null |
115
|
|
|
*/ |
116
|
|
|
private function resolveContext(?string $context): ?string |
117
|
|
|
{ |
118
|
|
|
if (null !== $this->context && null !== $context) { |
119
|
|
|
return sprintf('%s.%s', $context, $this->context); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
if (null !== $this->context) { |
123
|
|
|
return $this->context; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
return $context; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* @return string |
131
|
|
|
*/ |
132
|
|
|
private function getUnescapedValue(): string |
133
|
|
|
{ |
134
|
|
|
// remove escaping |
135
|
|
|
return str_replace('%%', '%', $this->value->getValue()); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @param string $haystack |
140
|
|
|
* @param string $needle |
141
|
|
|
* |
142
|
|
|
* @return bool |
143
|
|
|
*/ |
144
|
|
|
private function isMatch(string $haystack, string $needle): bool |
145
|
|
|
{ |
146
|
|
|
switch ($this->value->getFormat()) { |
147
|
|
|
case LikePattern::STARTS_WITH: |
148
|
|
|
return str_starts_with($haystack, $needle); |
149
|
|
|
|
150
|
|
|
case LikePattern::ENDS_WITH: |
151
|
|
|
return str_ends_with($haystack, $needle); |
152
|
|
|
|
153
|
|
|
default: |
154
|
|
|
return str_contains($haystack, $needle); |
155
|
|
|
} |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.