soupmix /
sql
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Soupmix; |
||
| 4 | |||
| 5 | class SQLQueryBuilder extends AbstractQueryBuilder |
||
| 6 | { |
||
| 7 | |||
| 8 | private $queryBuilder; |
||
| 9 | |||
| 10 | 2 | public function run() |
|
| 11 | { |
||
| 12 | 2 | $this->queryBuilder = $this->getQueryBuilder(); |
|
| 13 | 2 | $this->setJoins(); |
|
| 14 | 2 | $numberOfRows = $this->getCount(); |
|
| 15 | 2 | if ($numberOfRows === 0) { |
|
| 16 | return ['total' => 0, 'data' => null]; |
||
| 17 | } |
||
| 18 | 2 | $this->setSortOrders(); |
|
| 19 | 2 | $this->setOffsetAndLimit(); |
|
| 20 | 2 | $this->setReturnFields(); |
|
| 21 | 2 | $stmt = $this->conn->executeQuery( |
|
| 22 | 2 | $this->queryBuilder->getSql(), |
|
| 23 | 2 | $this->queryBuilder->getParameters() |
|
| 24 | ); |
||
| 25 | 2 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); |
|
| 26 | 2 | if ($this->distinctFieldName !== null) { |
|
| 27 | 1 | $numberOfRows = count($result); |
|
| 28 | } |
||
| 29 | 2 | return ['total' => $numberOfRows, 'data' => $result]; |
|
| 30 | } |
||
| 31 | |||
| 32 | 2 | private function getQueryBuilder() |
|
| 33 | { |
||
| 34 | 2 | if ($this->orFilters !== null) { |
|
| 35 | 2 | $this->andFilters[] = $this->orFilters; |
|
| 36 | } |
||
| 37 | 2 | $this->filters = $this->andFilters; |
|
| 38 | 2 | return $this->buildQuery($this->collection, $this->filters); |
|
| 39 | } |
||
| 40 | |||
| 41 | 2 | private function getCount() |
|
| 42 | { |
||
| 43 | 2 | $queryBuilderCount = clone $this->queryBuilder; |
|
| 44 | 2 | $queryBuilderCount->select(' COUNT(*) AS total '); |
|
| 45 | 2 | $stmt = $this->conn->executeQuery($queryBuilderCount->getSql(), $queryBuilderCount->getParameters()); |
|
| 46 | 2 | return (int) $stmt->fetch(\PDO::FETCH_ASSOC)['total']; |
|
| 47 | } |
||
| 48 | |||
| 49 | 2 | private function setSortOrders() |
|
| 50 | { |
||
| 51 | 2 | if ($this->sortFields !== null) { |
|
| 52 | foreach ($this->addAlias($this->sortFields) as $sortKey => $sortDir) { |
||
|
0 ignored issues
–
show
|
|||
| 53 | $this->queryBuilder->addOrderBy($sortKey, $sortDir); |
||
| 54 | } |
||
| 55 | } |
||
| 56 | 2 | } |
|
| 57 | |||
| 58 | 2 | private function setJoins() |
|
| 59 | { |
||
| 60 | 2 | $this->setJoinsForType('innerJoin'); |
|
| 61 | 2 | $this->setJoinsForType('leftJoin'); |
|
| 62 | 2 | $this->setJoinsForType('rightJoin'); |
|
| 63 | 2 | $this->setJoinsForType('outerJoin'); |
|
| 64 | 2 | } |
|
| 65 | |||
| 66 | 2 | private function setJoinsForType($joinType) |
|
| 67 | { |
||
| 68 | 2 | if ($this->{$joinType} === null) { |
|
| 69 | 2 | return; |
|
| 70 | } |
||
| 71 | 1 | foreach ($this->{$joinType} as $collectionName => $collection) { |
|
| 72 | 1 | $fieldNames = $this->addAlias($collection['returnFields'], $collectionName); |
|
| 73 | 1 | $this->returnFieldsForJoin($fieldNames); |
|
|
0 ignored issues
–
show
It seems like
$fieldNames defined by $this->addAlias($collect...lds'], $collectionName) on line 72 can also be of type string; however, Soupmix\SQLQueryBuilder::returnFieldsForJoin() does only seem to accept null|array, maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. Loading history...
|
|||
| 74 | 1 | $joinCondition = ''; |
|
| 75 | 1 | foreach ($collection['relations'] as $relation) { |
|
| 76 | 1 | $joinCondition .= empty($joinCondition) ? '':' AND '; |
|
| 77 | 1 | $relationType = array_keys($relation)[0]; |
|
| 78 | 1 | $source = array_keys($relation[$relationType])[0]; |
|
| 79 | 1 | $condition = $collectionName . '.' . $source |
|
| 80 | 1 | . ' = ' . $this->collection . '.' . $relation[$relationType][$source]; |
|
| 81 | 1 | if ($relationType != 'field') { |
|
| 82 | 1 | $condition = $collectionName . '.' . $source . ' ' |
|
| 83 | 1 | . $relationType . ' '. $relation[$relationType][$source]; |
|
| 84 | } |
||
| 85 | 1 | $joinCondition .= $condition; |
|
| 86 | } |
||
| 87 | 1 | $this->queryBuilder->{$joinType}($this->collection, $collectionName, $collectionName, $joinCondition); |
|
| 88 | } |
||
| 89 | 1 | return $this; |
|
| 90 | } |
||
| 91 | 1 | public function returnFieldsForJoin(array $fieldNames = null) |
|
| 92 | { |
||
| 93 | 1 | if ($fieldNames !== null) { |
|
| 94 | 1 | foreach ($fieldNames as $fieldName) { |
|
| 95 | 1 | $this->fieldNames[] = $fieldName; |
|
| 96 | } |
||
| 97 | } |
||
| 98 | 1 | } |
|
| 99 | |||
| 100 | 2 | private function setReturnFields() |
|
| 101 | { |
||
| 102 | 2 | if ($this->distinctFieldName === null) { |
|
| 103 | 2 | $fieldNames = ($this->fieldNames === null) |
|
| 104 | 1 | ? $this->addAlias('*') |
|
| 105 | 2 | : $this->addAlias($this->fieldNames); |
|
| 106 | 2 | $this->queryBuilder->select($fieldNames); |
|
| 107 | 2 | return; |
|
| 108 | } |
||
| 109 | 1 | $this->queryBuilder->select('DISTINCT (' . $this->collection . '.' . $this->distinctFieldName . ')'); |
|
| 110 | 1 | } |
|
| 111 | |||
| 112 | 2 | private function setOffsetAndLimit() |
|
| 113 | { |
||
| 114 | 2 | $this->queryBuilder->setFirstResult($this->offset) |
|
| 115 | 2 | ->setMaxResults($this->limit); |
|
| 116 | 2 | } |
|
| 117 | |||
| 118 | 2 | private function addAlias($fields, $collection = null) |
|
| 119 | { |
||
| 120 | 2 | $collection = (null !== $collection) ? $collection : $this->collection; |
|
| 121 | 2 | if (!is_array($fields)) { |
|
| 122 | 1 | return $collection . '.' . $fields; |
|
| 123 | } |
||
| 124 | |||
| 125 | 1 | $newFields = []; |
|
| 126 | 1 | foreach ($fields as $field => $value) { |
|
| 127 | |||
| 128 | 1 | if (is_int($field)) { |
|
| 129 | 1 | if (strpos($value, '.') > 0) { |
|
| 130 | 1 | $newFields[] = $value; |
|
| 131 | 1 | continue; |
|
| 132 | } |
||
| 133 | 1 | $newFields[] = $collection . '.' . $value; |
|
| 134 | 1 | continue; |
|
| 135 | } |
||
| 136 | $newFields[$collection.'.'.$field] = $value; |
||
| 137 | } |
||
| 138 | 1 | return $newFields; |
|
| 139 | } |
||
| 140 | |||
| 141 | 2 | protected function buildQuery($collection, $filters) |
|
| 142 | { |
||
| 143 | 2 | $queryBuilder = $this->conn->createQueryBuilder(); |
|
| 144 | 2 | $queryBuilder->from($collection, $collection); |
|
| 145 | 2 | if ($filters === null) { |
|
| 146 | return $queryBuilder; |
||
| 147 | } |
||
| 148 | 2 | return $this->buildQueryFilters($queryBuilder, $filters); |
|
| 149 | } |
||
| 150 | |||
| 151 | 2 | protected function buildQueryFilters($queryBuilder, $filters) |
|
| 152 | { |
||
| 153 | 2 | foreach ($filters as $key => $value) { |
|
| 154 | 2 | if (is_array($value) && strpos($key, '__') === false) { |
|
| 155 | 2 | $queryBuilder = $this->buildQueryForOr($queryBuilder, $value); |
|
| 156 | 2 | continue; |
|
| 157 | } |
||
| 158 | 2 | $queryBuilder = $this->buildQueryForAnd($queryBuilder, $key, $value); |
|
| 159 | } |
||
| 160 | 2 | return $queryBuilder; |
|
| 161 | } |
||
| 162 | |||
| 163 | 2 | protected function buildQueryForAnd($queryBuilder, $key, $value) |
|
| 164 | { |
||
| 165 | 2 | $sqlOptions = self::buildFilter([$key => $value]); |
|
| 166 | 2 | View Code Duplication | if (in_array($sqlOptions['method'], ['in', 'notIn'], true)) { |
| 167 | $queryBuilder->andWhere( |
||
| 168 | $queryBuilder->expr()->{$sqlOptions['method']}( |
||
| 169 | $this->collection . '.' . $sqlOptions['key'], $sqlOptions['value'] |
||
| 170 | ) |
||
| 171 | ); |
||
| 172 | return $queryBuilder; |
||
| 173 | } |
||
| 174 | 2 | $queryBuilder->andWhere( |
|
| 175 | 2 | '' . $this->collection . '.' . $sqlOptions['key'].'' |
|
| 176 | 2 | . ' ' . $sqlOptions['operand'] |
|
| 177 | 2 | . ' ' . $queryBuilder->createNamedParameter($sqlOptions['value']) |
|
| 178 | ); |
||
| 179 | 2 | return $queryBuilder; |
|
| 180 | } |
||
| 181 | 2 | protected function buildQueryForOr($queryBuilder, $value) |
|
| 182 | { |
||
| 183 | 2 | $orQuery =[]; |
|
| 184 | 2 | foreach ($value as $orValue) { |
|
| 185 | 2 | $subKey = array_keys($orValue)[0]; |
|
| 186 | 2 | $subValue = $orValue[$subKey]; |
|
| 187 | 2 | $sqlOptions = self::buildFilter([$subKey => $subValue]); |
|
| 188 | 2 | View Code Duplication | if (in_array($sqlOptions['method'], ['in', 'notIn'], true)) { |
| 189 | 1 | $orQuery[] = $queryBuilder->expr()->{$sqlOptions['method']}( |
|
| 190 | 1 | $this->collection . '.' . $sqlOptions['key'], $sqlOptions['value'] |
|
| 191 | ); |
||
| 192 | 1 | continue; |
|
| 193 | } |
||
| 194 | 2 | $orQuery[] = |
|
| 195 | 2 | '' . $this->collection . '.' . $sqlOptions['key'].'' |
|
| 196 | 2 | . ' ' . $sqlOptions['operand'] |
|
| 197 | 2 | . ' ' . $queryBuilder->createNamedParameter($sqlOptions['value']); |
|
| 198 | } |
||
| 199 | 2 | $queryBuilder->andWhere( |
|
| 200 | 2 | '(' . implode(' OR ', $orQuery) . ')' |
|
| 201 | ); |
||
| 202 | 2 | return $queryBuilder; |
|
| 203 | } |
||
| 204 | |||
| 205 | |||
| 206 | 2 | public static function buildFilter($filter) |
|
| 207 | { |
||
| 208 | 2 | $key = array_keys($filter)[0]; |
|
| 209 | 2 | $value = $filter[$key]; |
|
| 210 | 2 | $operator = ' = '; |
|
| 211 | 2 | $method = 'eq'; |
|
| 212 | $options =[ |
||
| 213 | 2 | 'gte' => ['method' => 'gte', 'operand' => ' >= '], |
|
| 214 | 'gt' => ['method' => 'gt', 'operand' => ' > '], |
||
| 215 | 'lte' => ['method' => 'lte', 'operand' => ' <= '], |
||
| 216 | 'lt' => ['method' => 'lt', 'operand' => ' < '], |
||
| 217 | 'in' => ['method' => 'in', 'operand' => ' IN '], |
||
| 218 | '!in' => ['method' => 'notIn', 'operand' => ' NOT IN '], |
||
| 219 | 'not' => ['method' => 'not', 'operand' => ' != '], |
||
| 220 | 'wildcard' => ['method' => 'like', 'operand' => ' LIKE '], |
||
| 221 | 'prefix' => ['method' => 'like', 'operand' => ' LIKE '], |
||
| 222 | ]; |
||
| 223 | 2 | if (strpos($key, '__') !== false) { |
|
| 224 | 2 | preg_match('/__(.*?)$/', $key, $matches); |
|
| 225 | 2 | $key = str_replace($matches[0], '', $key); |
|
| 226 | 2 | $queryOperator = $matches[1]; |
|
| 227 | 2 | $method = $options[$queryOperator]['method']; |
|
| 228 | 2 | $operator = $options[$queryOperator]['operand']; |
|
| 229 | switch ($queryOperator) { |
||
| 230 | 2 | case 'wildcard': |
|
| 231 | 1 | $value = '%'.str_replace(array('?', '*'), array('_', '%'), $value).'%'; |
|
| 232 | 1 | break; |
|
| 233 | 2 | case 'prefix': |
|
| 234 | 1 | $value .= '%'; |
|
| 235 | 1 | break; |
|
| 236 | } |
||
| 237 | } |
||
| 238 | return [ |
||
| 239 | 2 | 'key' => $key, |
|
| 240 | 2 | 'operand' => $operator, |
|
| 241 | 2 | 'method' => $method, |
|
| 242 | 2 | 'value' => $value |
|
| 243 | ]; |
||
| 244 | } |
||
| 245 | } |
||
| 246 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.