OrmStatement   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 361
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 50
lcom 1
cbo 2
dl 0
loc 361
ccs 126
cts 126
cp 1
rs 8.6206
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A createUpdateStatement() 0 17 3
A createQuery() 0 16 3
A escapeCriterion() 0 12 3
B parseCriteria() 0 26 6
B whereConditionsAsString() 0 18 5
A parseLimits() 0 16 4
C persistenceQueryParams() 0 28 8
B getAnnotatedCriterion() 0 22 4
C getAnnotatedQuery() 0 54 12
A getSimpleCriterionName() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like OrmStatement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OrmStatement, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Nkey\Caribu\Orm;
3
4
/**
5
 * Statement provider for Caribu Orm
6
 *
7
 * This class is part of Caribu package
8
 *
9
 * @author Maik Greubel <[email protected]>
10
 */
11
trait OrmStatement
12
{
13
    /**
14
     * Include entity analyzing related functionality
15
     */
16
    use OrmEntityAnalyzer;
17
18
    /**
19
     * Create a query for selection
20
     *
21
     * @param string $class
22
     *            The class for which the query will be created
23
     * @param array $criteria
24
     *            Array of criterias in form of "property" => "value"
25
     * @param array $columns
26
     *            The columns to retrieve
27
     * @param string $orderBy
28
     *            An order-by statement in form of "property ASC|DESC"
29
     * @param int $limit
30
     *            The maximum amount of results
31
     * @param int $startFrom
32
     *            The offset where to get results of
33
     * @param string $escapeSign
34
     *            The string which represents the escape character
35
     *            
36
     * @return string The query as sql statement
37
     *        
38
     * @throws OrmException
39
     */
40 28
    private static function createQuery(string $class, string $tableName, array &$criteria, array $columns, string $orderBy = '', int $limit = 0, int $startFrom = 0, string $escapeSign = ""): string
41
    {
42 28
        $joins = self::getAnnotatedQuery($class, $tableName, $criteria, $columns, $escapeSign);
43
        
44 28
        $wheres = self::parseCriteria($criteria, $escapeSign);
45
        
46 27
        $limits = self::parseLimits($limit, $startFrom);
47
        
48 27
        if ($orderBy && ! stristr($orderBy, 'ORDER BY ')) {
49 2
            $orderBy = sprintf("ORDER BY %s%s%s", $escapeSign, $orderBy, $escapeSign);
50
        }
51
        
52 27
        $query = sprintf("SELECT %s FROM %s%s%s %s %s %s %s", implode(',', $columns), $escapeSign, $tableName, $escapeSign, $joins, $wheres, $orderBy, $limits);
53
        
54 27
        return $query;
55
    }
56
57
    /**
58
     * Create a insert or update statement
59
     *
60
     * @param string $class
61
     *            The class of entity
62
     * @param array $pairs
63
     *            The pairs of columns and its corresponding values
64
     * @param string $primaryKeyCol
65
     *            The name of column which represents the primary key
66
     * @param mixed $primaryKeyValue
67
     *            The primary key value
68
     * @param string $escapeSign
69
     *            The string which represents the escape character
70
     *            
71
     * @return string The update statement as SQL string
72
     */
73 12
    private static function createUpdateStatement(string $class, array $pairs, string $primaryKeyCol, $primaryKeyValue, string $escapeSign): string
74
    {
75 12
        $tableName = self::getTableName($class);
76
        
77 12
        $query = sprintf("INSERT INTO %s%s%s ", $escapeSign, $tableName, $escapeSign);
78 12
        if ($primaryKeyValue) {
79 6
            $query = sprintf("UPDATE %s%s%s ", $escapeSign, $tableName, $escapeSign);
80
        }
81
        
82 12
        $query .= self::persistenceQueryParams($pairs, $primaryKeyCol, is_null($primaryKeyValue), $escapeSign);
83
        
84 12
        if ($primaryKeyValue) {
85 6
            $query .= sprintf(" WHERE %s%s%s = :%s", $escapeSign, $primaryKeyCol, $escapeSign, $primaryKeyCol);
86
        }
87
        
88 12
        return $query;
89
    }
90
91
    /**
92
     * Escape all parts of a criterion
93
     *
94
     * @param string $criterion
95
     *            The criterion pattern
96
     * @param string $escapeSign
97
     *            The escape sign
98
     *            
99
     * @return string The escaped criterion
100
     */
101 25
    private static function escapeCriterion(string $criterion, string $escapeSign)
102
    {
103 25
        $criterionEscaped = '';
104 25
        $criterionParts = explode('.', $criterion);
105
        
106 25
        foreach ($criterionParts as $part) {
107 25
            $criterionEscaped .= $criterionEscaped ? '.' : '';
108 25
            $criterionEscaped .= sprintf("%s%s%s", $escapeSign, $part, $escapeSign);
109
        }
110
        
111 25
        return $criterionEscaped;
112
    }
113
114
    /**
115
     * Parse criteria into where conditions
116
     *
117
     * @param array $criteria
118
     *            The criteria to parse
119
     * @param string $escapeSign
120
     *            The escape sign
121
     *            
122
     * @return string The parsed criterias as where condition string
123
     *        
124
     * @throws OrmException
125
     */
126 28
    private static function parseCriteria(array &$criteria, string $escapeSign): string
127
    {
128 28
        $wheres = array();
129
        
130 28
        $criterias = array_keys($criteria);
131
        
132 28
        foreach ($criterias as $criterion) {
133 26
            $placeHolder = str_replace('.', '_', $criterion);
134 26
            $placeHolder = str_replace('OR ', 'OR_', $placeHolder);
135 26
            if (strtoupper(substr($criteria[$criterion], 0, 4)) == 'LIKE') {
136 2
                $wheres[] = sprintf("%s LIKE :%s", self::escapeCriterion($criterion, $escapeSign), $placeHolder);
137 24
            } elseif (strtoupper(substr($criteria[$criterion], 0, 7)) == 'BETWEEN') {
138 2
                $start = $end = null;
139 2
                sscanf(strtoupper($criteria[$criterion]), "BETWEEN %s AND %s", $start, $end);
140 2
                if (! $start || ! $end) {
141 1
                    throw new OrmException("Invalid range for between");
142
                }
143 1
                $wheres[] = sprintf("%s BETWEEN %s AND %s", self::escapeCriterion($criterion, $escapeSign), $start, $end);
144 1
                unset($criteria[$criterion]);
145
            } else {
146 25
                $wheres[] = sprintf("%s = :%s", self::escapeCriterion($criterion, $escapeSign), $placeHolder);
147
            }
148
        }
149
        
150 27
        return self::whereConditionsAsString($wheres);
151
    }
152
153
    /**
154
     * Loops over all where conditions and create a string of it
155
     *
156
     * @param array $wheres            
157
     *
158
     * @return string The where conditions as string
159
     */
160 27
    private static function whereConditionsAsString(array $wheres): string
161
    {
162 27
        if (count($wheres)) {
163 25
            $conditions = "";
164 25
            foreach ($wheres as $where) {
165 25
                $and = "";
166 25
                if ($conditions) {
167 2
                    $and = substr($where, 0, 3) == 'OR ' ? " " : " AND ";
168
                }
169 25
                $conditions .= $and . $where;
170
            }
171 25
            $wheres = sprintf("WHERE %s", $conditions);
172
        } else {
173 2
            $wheres = '';
174
        }
175
        
176 27
        return $wheres;
177
    }
178
179
    /**
180
     * Prepare the limit and offset modifier
181
     *
182
     * @param int $limit            
183
     * @param int $startFrom            
184
     *
185
     * @return string The limit modifier or empty string
186
     */
187 27
    private static function parseLimits(int $limit = 0, int $startFrom = 0): string
188
    {
189 27
        $limits = "";
190 27
        if ($startFrom > 0) {
191 1
            $limits = sprintf("%d,", $startFrom);
192
        }
193 27
        if ($limit > 0) {
194 3
            $limits .= $limit;
195
        }
196
        
197 27
        if ($limits) {
198 3
            $limits = sprintf("LIMIT %s", $limits);
199
        }
200
        
201 27
        return $limits;
202
    }
203
204
    /**
205
     * Retrieve the persistence parameters via reflection
206
     *
207
     * @param array $pairs
208
     *            The pairs of column names => values
209
     * @param string $primaryKeyCol
210
     *            The name of primary key column
211
     * @param bool $insert
212
     *            Whether to insert or update the data
213
     * @param string $escapeSign
214
     *            The escape sign
215
     *            
216
     * @return string The prepared statement parameters for persistence
217
     *        
218
     * @throws OrmException
219
     */
220 12
    private static function persistenceQueryParams(array $pairs, string $primaryKeyCol, bool $insert, string $escapeSign): string
221
    {
222 12
        $query = "";
223
        
224 12
        $columns = array_keys($pairs);
225
        
226 12
        if ($insert) {
227 11
            $cols = "";
228 11
            $vals = "";
229 11
            foreach ($columns as $column) {
230 11
                $cols .= ($cols ? ',' : '');
231 11
                $cols .= sprintf("%s%s%s", $escapeSign, $column, $escapeSign);
232 11
                $vals .= ($vals ? ',' : '');
233 11
                $vals .= sprintf(':%s', $column);
234
            }
235 11
            $query = sprintf("(%s) VALUES (%s)", $cols, $vals);
236
        } else {
237 6
            foreach ($columns as $column) {
238 6
                if ($column == $primaryKeyCol) {
239 6
                    continue;
240
                }
241 6
                $query .= ($query ? ", " : "SET ");
242 6
                $query .= sprintf("%s%s%s = :%s", $escapeSign, $column, $escapeSign, $column);
243
            }
244
        }
245
        
246 12
        return $query;
247
    }
248
249
    /**
250
     * When criterion is a property but annotated column name differs, we take the column name
251
     *
252
     * @param string $className
253
     *            The entity class name
254
     * @param string $criterion
255
     *            The criterion
256
     *            
257
     * @return string The criterion
258
     */
259 26
    private static function getAnnotatedCriterion(string $className, string $criterion): string
260
    {
261 26
        $class = new \ReflectionClass($className);
262 26
        $simpleCriterion = self::getSimpleCriterionName($criterion);
263 26
        if (! $class->hasProperty($simpleCriterion)) {
264 12
            return $criterion;
265
        }
266
        
267 19
        $property = $class->getProperty($simpleCriterion);
268
        
269 19
        if (strcmp($criterion, $simpleCriterion) != 0) {
270 5
            return $criterion;
271
        }
272
        
273 17
        $column = self::getAnnotatedColumn($property->getDocComment());
274
        
275 17
        if ("" !== $column) {
276 3
            $criterion = str_replace($simpleCriterion, $column, $criterion);
277
        }
278
        
279 17
        return $criterion;
280
    }
281
282
    /**
283
     * Get the annotated join query
284
     *
285
     * @param string $class
286
     *            The name of class to use as left class
287
     * @param string $table
288
     *            The name of table
289
     * @param array $criteria
290
     *            (by reference) List of criterias
291
     * @param array $columns
292
     *            (by reference) List of columns
293
     * @param string $escapeSign
294
     *            The character which escapes special literals
295
     *            
296
     * @return string The join query sql statment
297
     *        
298
     * @throws OrmException
299
     */
300 28
    private static function getAnnotatedQuery(string $class, string $table, array &$criteria, array &$columns, string $escapeSign): string
301
    {
302 28
        $joinQuery = "";
303
        
304 28
        $rf = new \ReflectionClass($class);
305
        
306 28
        $replacedCriteria = array();
307
        
308
        // Example criterion: user.name => 'john'
309 28
        foreach (array_keys($criteria) as $criterion) {
310 26
            $replacedCriteria[self::getAnnotatedCriterion($class, $criterion)] = $criteria[$criterion];
311
            // from example criterionProperty will be 'name', criterion will now be 'user'
312 26
            if (strpos($criterion, '.') !== false) {
313 3
                list ($criterion) = explode('.', $criterion);
314
            }
315
            
316
            // class must have a property named by criterion
317 26
            $rfProperty = $rf->hasProperty($criterion) ? $rf->getProperty($criterion) : null;
318 26
            if ($rfProperty == null) {
319 14
                continue;
320
            }
321
            
322
            // check annotations
323 19
            $propertyClass = "";
324
            // search the type of property value
325 19
            if ("" !== ($type = self::getAnnotatedType($rfProperty->getDocComment(), $rf->getNamespaceName()))) {
326 14
                if (! self::isPrimitive($type) && class_exists($type)) {
327 3
                    $propertyClass = $type;
328
                }
329
            }
330 19
            $inverseTable = $propertyClass ? self::getAnnotatedTableName($propertyClass, $criterion) : $criterion;
331
            
332
            // search the table mapping conditions
333 19
            if ("" !== ($parameters = self::getAnnotatedMappedByParameters($rfProperty->getDocComment())) && !empty($propertyClass)) {
334 2
                $mappedBy = self::parseMappedBy($parameters);
335
                
336 2
                $pkCol = self::getAnnotatedPrimaryKeyColumn($class);
337 2
                $inversePkCol = self::getAnnotatedPrimaryKeyColumn($propertyClass);
338
                
339 2
                $joinQuery = sprintf("JOIN %s%s%s ON %s%s%s.%s%s%s = %s%s%s.%s%s%s ", $escapeSign, $mappedBy['table'], $escapeSign, $escapeSign, $mappedBy['table'], $escapeSign, $escapeSign, $mappedBy['inverseColumn'], $escapeSign, $escapeSign, $table, $escapeSign, $escapeSign, $pkCol, $escapeSign);
340 2
                $joinQuery .= sprintf("JOIN %s%s%s ON %s%s%s.%s%s%s = %s%s%s.%s%s%s", $escapeSign, $inverseTable, $escapeSign, $escapeSign, $inverseTable, $escapeSign, $escapeSign, $inversePkCol, $escapeSign, $escapeSign, $mappedBy['table'], $escapeSign, $escapeSign, $mappedBy['column'], $escapeSign);
341
                
342 2
                $columns[] = sprintf("%s%s%s.%s%s%s AS '%s.%s'", $escapeSign, $inverseTable, $escapeSign, $escapeSign, $inversePkCol, $escapeSign, $inverseTable, $inversePkCol);
343 17
            } elseif ($propertyClass != "") {
344 1
                $inversePkCol = self::getAnnotatedPrimaryKeyColumn($propertyClass);
345 1
                $column = self::getAnnotatedColumnFromProperty($class, $rfProperty->getName());
346 1
                $joinQuery = sprintf("JOIN %s%s%s AS %s%s%s ON %s%s%s.%s%s%s = %s%s%s.%s%s%s", $escapeSign, $inverseTable, $escapeSign, $escapeSign, $criterion, $escapeSign, $escapeSign, $criterion, $escapeSign, $escapeSign, $inversePkCol, $escapeSign, $escapeSign, $table, $escapeSign, $escapeSign, $column, $escapeSign);
347 19
                $columns[] = sprintf("%s%s%s.%s%s%s AS '%s.%s'", $escapeSign, $criterion, $escapeSign, $escapeSign, $inversePkCol, $escapeSign, $criterion, $inversePkCol);
348
            }
349
        }
350 28
        $criteria = $replacedCriteria;
351
        
352 28
        return $joinQuery;
353
    }
354
355
    /**
356
     * Parse a complex crition into simple criterion
357
     *
358
     * @param string $criterion
359
     *            The full criterion pattern
360
     *            
361
     * @return string The simple criterion name
362
     */
363 26
    private static function getSimpleCriterionName(string $criterion): string
364
    {
365 26
        $criterion = str_ireplace('OR ', '', $criterion);
366 26
        if (strpos($criterion, '.')) {
367 3
            list ($criterion) = explode('.', $criterion);
368
        }
369 26
        return $criterion;
370
    }
371
}
372