Total Complexity | 48 |
Total Lines | 492 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like SSP 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.
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 SSP, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
26 | class SSP { |
||
27 | /** |
||
28 | * Create the data output array for the DataTables rows |
||
29 | * |
||
30 | * @param array $columns Column information array |
||
31 | * @param array $data Data from the SQL get |
||
32 | * @return array Formatted data in a row based format |
||
33 | */ |
||
34 | static function data_output ( $columns, $data ) |
||
|
|||
35 | { |
||
36 | $out = array(); |
||
37 | |||
38 | for ( $i=0, $ien=count($data) ; $i<$ien ; $i++ ) { |
||
39 | $row = array(); |
||
40 | |||
41 | for ( $j=0, $jen=count($columns) ; $j<$jen ; $j++ ) { |
||
42 | $column = $columns[$j]; |
||
43 | |||
44 | // Is there a formatter? |
||
45 | if ( isset( $column['formatter'] ) ) { |
||
46 | $row[ $column['dt'] ] = $column['formatter']( $data[$i][ $column['db'] ], $data[$i] ); |
||
47 | } |
||
48 | else { |
||
49 | $row[ $column['dt'] ] = $data[$i][ $columns[$j]['db'] ]; |
||
50 | } |
||
51 | } |
||
52 | |||
53 | $out[] = $row; |
||
54 | } |
||
55 | |||
56 | return $out; |
||
57 | } |
||
58 | |||
59 | |||
60 | /** |
||
61 | * Database connection |
||
62 | * |
||
63 | * Obtain an PHP PDO connection from a connection details array |
||
64 | * |
||
65 | * @param array $conn SQL connection details. The array should have |
||
66 | * the following properties |
||
67 | * * host - host name |
||
68 | * * db - database name |
||
69 | * * user - user name |
||
70 | * * pass - user password |
||
71 | * @return resource PDO connection |
||
72 | */ |
||
73 | static function db ( $conn ) |
||
74 | { |
||
75 | if ( is_array( $conn ) ) { |
||
76 | return self::sql_connect( $conn ); |
||
77 | } |
||
78 | |||
79 | return $conn; |
||
80 | } |
||
81 | |||
82 | |||
83 | /** |
||
84 | * Paging |
||
85 | * |
||
86 | * Construct the LIMIT clause for server-side processing SQL query |
||
87 | * |
||
88 | * @param array $request Data sent to server by DataTables |
||
89 | * @param array $columns Column information array |
||
90 | * @return string SQL limit clause |
||
91 | */ |
||
92 | static function limit ( $request, $columns ) |
||
93 | { |
||
94 | $limit = ''; |
||
95 | |||
96 | if ( isset($request['start']) && $request['length'] != -1 ) { |
||
97 | $limit = "LIMIT ".intval($request['start']).", ".intval($request['length']); |
||
98 | } |
||
99 | |||
100 | return $limit; |
||
101 | } |
||
102 | |||
103 | |||
104 | /** |
||
105 | * Ordering |
||
106 | * |
||
107 | * Construct the ORDER BY clause for server-side processing SQL query |
||
108 | * |
||
109 | * @param array $request Data sent to server by DataTables |
||
110 | * @param array $columns Column information array |
||
111 | * @return string SQL order by clause |
||
112 | */ |
||
113 | static function order ( $request, $columns ) |
||
114 | { |
||
115 | $order = ''; |
||
116 | |||
117 | if ( isset($request['order']) && count($request['order']) ) { |
||
118 | $orderBy = array(); |
||
119 | $dtColumns = self::pluck( $columns, 'dt' ); |
||
120 | |||
121 | for ( $i=0, $ien=count($request['order']) ; $i<$ien ; $i++ ) { |
||
122 | // Convert the column index into the column data property |
||
123 | $columnIdx = intval($request['order'][$i]['column']); |
||
124 | $requestColumn = $request['columns'][$columnIdx]; |
||
125 | |||
126 | $columnIdx = array_search( $requestColumn['data'], $dtColumns ); |
||
127 | $column = $columns[ $columnIdx ]; |
||
128 | |||
129 | if ( $requestColumn['orderable'] == 'true' ) { |
||
130 | $dir = $request['order'][$i]['dir'] === 'asc' ? |
||
131 | 'ASC' : |
||
132 | 'DESC'; |
||
133 | |||
134 | $orderBy[] = '`'.$column['db'].'` '.$dir; |
||
135 | } |
||
136 | } |
||
137 | |||
138 | $order = 'ORDER BY '.implode(', ', $orderBy); |
||
139 | } |
||
140 | |||
141 | return $order; |
||
142 | } |
||
143 | |||
144 | |||
145 | /** |
||
146 | * Searching / Filtering |
||
147 | * |
||
148 | * Construct the WHERE clause for server-side processing SQL query. |
||
149 | * |
||
150 | * NOTE this does not match the built-in DataTables filtering which does it |
||
151 | * word by word on any field. It's possible to do here performance on large |
||
152 | * databases would be very poor |
||
153 | * |
||
154 | * @param array $request Data sent to server by DataTables |
||
155 | * @param array $columns Column information array |
||
156 | * @param array $bindings Array of values for PDO bindings, used in the |
||
157 | * sql_exec() function |
||
158 | * @return string SQL where clause |
||
159 | */ |
||
160 | static function filter ( $request, $columns, &$bindings ) |
||
161 | { |
||
162 | $globalSearch = array(); |
||
163 | $columnSearch = array(); |
||
164 | $dtColumns = self::pluck( $columns, 'dt' ); |
||
165 | |||
166 | if ( isset($request['search']) && $request['search']['value'] != '' ) { |
||
167 | $str = $request['search']['value']; |
||
168 | |||
169 | for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) { |
||
170 | $requestColumn = $request['columns'][$i]; |
||
171 | $columnIdx = array_search( $requestColumn['data'], $dtColumns ); |
||
172 | $column = $columns[ $columnIdx ]; |
||
173 | |||
174 | if ( $requestColumn['searchable'] == 'true' ) { |
||
175 | $binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR ); |
||
176 | $globalSearch[] = "`".$column['db']."` LIKE ".$binding; |
||
177 | } |
||
178 | } |
||
179 | } |
||
180 | |||
181 | // Individual column filtering |
||
182 | for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) { |
||
183 | $requestColumn = $request['columns'][$i]; |
||
184 | $columnIdx = array_search( $requestColumn['data'], $dtColumns ); |
||
185 | $column = $columns[ $columnIdx ]; |
||
186 | |||
187 | $str = $requestColumn['search']['value']; |
||
188 | |||
189 | if ( $requestColumn['searchable'] == 'true' && |
||
190 | $str != '' ) { |
||
191 | $binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR ); |
||
192 | $columnSearch[] = "`".$column['db']."` LIKE ".$binding; |
||
193 | } |
||
194 | } |
||
195 | |||
196 | // Combine the filters into a single string |
||
197 | $where = ''; |
||
198 | |||
199 | if ( count( $globalSearch ) ) { |
||
200 | $where = '('.implode(' OR ', $globalSearch).')'; |
||
201 | } |
||
202 | |||
203 | if ( count( $columnSearch ) ) { |
||
204 | $where = $where === '' ? |
||
205 | implode(' AND ', $columnSearch) : |
||
206 | $where .' AND '. implode(' AND ', $columnSearch); |
||
207 | } |
||
208 | |||
209 | if ( $where !== '' ) { |
||
210 | $where = 'WHERE '.$where; |
||
211 | } |
||
212 | |||
213 | return $where; |
||
214 | } |
||
215 | |||
216 | |||
217 | /** |
||
218 | * Perform the SQL queries needed for an server-side processing requested, |
||
219 | * utilising the helper functions of this class, limit(), order() and |
||
220 | * filter() among others. The returned array is ready to be encoded as JSON |
||
221 | * in response to an SSP request, or can be modified if needed before |
||
222 | * sending back to the client. |
||
223 | * |
||
224 | * @param array $request Data sent to server by DataTables |
||
225 | * @param array|PDO $conn PDO connection resource or connection parameters array |
||
226 | * @param string $table SQL table to query |
||
227 | * @param string $primaryKey Primary key of the table |
||
228 | * @param array $columns Column information array |
||
229 | * @return array Server-side processing response array |
||
230 | */ |
||
231 | static function simple ( $request, $conn, $table, $primaryKey, $columns ) |
||
232 | { |
||
233 | $bindings = array(); |
||
234 | $db = self::db( $conn ); |
||
235 | |||
236 | // Build the SQL query string from the request |
||
237 | $limit = self::limit( $request, $columns ); |
||
238 | $order = self::order( $request, $columns ); |
||
239 | $where = self::filter( $request, $columns, $bindings ); |
||
240 | |||
241 | // Main query to actually get the data |
||
242 | $data = self::sql_exec( $db, $bindings, |
||
243 | "SELECT SQL_CALC_FOUND_ROWS `".implode("`, `", self::pluck($columns, 'db'))."` |
||
244 | FROM `$table` |
||
245 | $where |
||
246 | $order |
||
247 | $limit" |
||
248 | ); |
||
249 | |||
250 | // Data set length after filtering |
||
251 | $resFilterLength = self::sql_exec( $db, |
||
252 | "SELECT FOUND_ROWS()" |
||
253 | ); |
||
254 | $recordsFiltered = $resFilterLength[0][0]; |
||
255 | |||
256 | // Total data set length |
||
257 | $resTotalLength = self::sql_exec( $db, |
||
258 | "SELECT COUNT(`{$primaryKey}`) |
||
259 | FROM `$table`" |
||
260 | ); |
||
261 | $recordsTotal = $resTotalLength[0][0]; |
||
262 | |||
263 | |||
264 | /* |
||
265 | * Output |
||
266 | */ |
||
267 | return array( |
||
268 | "draw" => intval( $request['draw'] ), |
||
269 | "recordsTotal" => intval( $recordsTotal ), |
||
270 | "recordsFiltered" => intval( $recordsFiltered ), |
||
271 | "data" => self::data_output( $columns, $data ) |
||
272 | ); |
||
273 | } |
||
274 | |||
275 | |||
276 | /** |
||
277 | * The difference between this method and the `simple` one, is that you can |
||
278 | * apply additional `where` conditions to the SQL queries. These can be in |
||
279 | * one of two forms: |
||
280 | * |
||
281 | * * 'Result condition' - This is applied to the result set, but not the |
||
282 | * overall paging information query - i.e. it will not effect the number |
||
283 | * of records that a user sees they can have access to. This should be |
||
284 | * used when you want apply a filtering condition that the user has sent. |
||
285 | * * 'All condition' - This is applied to all queries that are made and |
||
286 | * reduces the number of records that the user can access. This should be |
||
287 | * used in conditions where you don't want the user to ever have access to |
||
288 | * particular records (for example, restricting by a login id). |
||
289 | * |
||
290 | * @param array $request Data sent to server by DataTables |
||
291 | * @param array|PDO $conn PDO connection resource or connection parameters array |
||
292 | * @param string $table SQL table to query |
||
293 | * @param string $primaryKey Primary key of the table |
||
294 | * @param array $columns Column information array |
||
295 | * @param string $whereResult WHERE condition to apply to the result set |
||
296 | * @param string $whereAll WHERE condition to apply to all queries |
||
297 | * @return array Server-side processing response array |
||
298 | */ |
||
299 | static function complex ( $request, $conn, $table, $primaryKey, $columns, $whereResult=null, $whereAll=null ) |
||
300 | { |
||
301 | $bindings = array(); |
||
302 | $db = self::db( $conn ); |
||
303 | $localWhereResult = array(); |
||
304 | $localWhereAll = array(); |
||
305 | $whereAllSql = ''; |
||
306 | |||
307 | // Build the SQL query string from the request |
||
308 | $limit = self::limit( $request, $columns ); |
||
309 | $order = self::order( $request, $columns ); |
||
310 | $where = self::filter( $request, $columns, $bindings ); |
||
311 | |||
312 | $whereResult = self::_flatten( $whereResult ); |
||
313 | $whereAll = self::_flatten( $whereAll ); |
||
314 | |||
315 | if ( $whereResult ) { |
||
316 | $where = $where ? |
||
317 | $where .' AND '.$whereResult : |
||
318 | 'WHERE '.$whereResult; |
||
319 | } |
||
320 | |||
321 | if ( $whereAll ) { |
||
322 | $where = $where ? |
||
323 | $where .' AND '.$whereAll : |
||
324 | 'WHERE '.$whereAll; |
||
325 | |||
326 | $whereAllSql = 'WHERE '.$whereAll; |
||
327 | } |
||
328 | |||
329 | // Main query to actually get the data |
||
330 | $data = self::sql_exec( $db, $bindings, |
||
331 | "SELECT SQL_CALC_FOUND_ROWS `".implode("`, `", self::pluck($columns, 'db'))."` |
||
332 | FROM `$table` |
||
333 | $where |
||
334 | $order |
||
335 | $limit" |
||
336 | ); |
||
337 | |||
338 | // Data set length after filtering |
||
339 | $resFilterLength = self::sql_exec( $db, |
||
340 | "SELECT FOUND_ROWS()" |
||
341 | ); |
||
342 | $recordsFiltered = $resFilterLength[0][0]; |
||
343 | |||
344 | // Total data set length |
||
345 | $resTotalLength = self::sql_exec( $db, $bindings, |
||
346 | "SELECT COUNT(`{$primaryKey}`) |
||
347 | FROM `$table` ". |
||
348 | $whereAllSql |
||
349 | ); |
||
350 | $recordsTotal = $resTotalLength[0][0]; |
||
351 | |||
352 | /* |
||
353 | * Output |
||
354 | */ |
||
355 | return array( |
||
356 | "draw" => intval( $request['draw'] ), |
||
357 | "recordsTotal" => intval( $recordsTotal ), |
||
358 | "recordsFiltered" => intval( $recordsFiltered ), |
||
359 | "data" => self::data_output( $columns, $data ) |
||
360 | ); |
||
361 | } |
||
362 | |||
363 | |||
364 | /** |
||
365 | * Connect to the database |
||
366 | * |
||
367 | * @param array $sql_details SQL server connection details array, with the |
||
368 | * properties: |
||
369 | * * host - host name |
||
370 | * * db - database name |
||
371 | * * user - user name |
||
372 | * * pass - user password |
||
373 | * @return resource Database connection handle |
||
374 | */ |
||
375 | static function sql_connect ( $sql_details ) |
||
393 | } |
||
394 | |||
395 | |||
396 | /** |
||
397 | * Execute an SQL query on the database |
||
398 | * |
||
399 | * @param resource $db Database handler |
||
400 | * @param array $bindings Array of PDO binding values from bind() to be |
||
401 | * used for safely escaping strings. Note that this can be given as the |
||
402 | * SQL query string if no bindings are required. |
||
403 | * @param string $sql SQL query to execute. |
||
404 | * @return array Result from the query (all rows) |
||
405 | */ |
||
406 | static function sql_exec ( $db, $bindings, $sql=null ) |
||
407 | { |
||
408 | // Argument shifting |
||
409 | if ( $sql === null ) { |
||
410 | $sql = $bindings; |
||
411 | } |
||
412 | |||
413 | $stmt = $db->prepare( $sql ); |
||
414 | //echo $sql; |
||
415 | |||
416 | // Bind parameters |
||
417 | if ( is_array( $bindings ) ) { |
||
418 | for ( $i=0, $ien=count($bindings) ; $i<$ien ; $i++ ) { |
||
419 | $binding = $bindings[$i]; |
||
420 | $stmt->bindValue( $binding['key'], $binding['val'], $binding['type'] ); |
||
421 | } |
||
422 | } |
||
423 | |||
424 | // Execute |
||
425 | try { |
||
426 | $stmt->execute(); |
||
427 | } |
||
428 | catch (PDOException $e) { |
||
429 | self::fatal( "An SQL error occurred: ".$e->getMessage() ); |
||
430 | } |
||
431 | |||
432 | // Return all |
||
433 | return $stmt->fetchAll(); |
||
434 | } |
||
435 | |||
436 | |||
437 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * |
||
438 | * Internal methods |
||
439 | */ |
||
440 | |||
441 | /** |
||
442 | * Throw a fatal error. |
||
443 | * |
||
444 | * This writes out an error message in a JSON string which DataTables will |
||
445 | * see and show to the user in the browser. |
||
446 | * |
||
447 | * @param string $msg Message to send to the client |
||
448 | */ |
||
449 | static function fatal ( $msg ) |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * Create a PDO binding key which can be used for escaping variables safely |
||
460 | * when executing a query with sql_exec() |
||
461 | * |
||
462 | * @param array &$a Array of bindings |
||
463 | * @param * $val Value to bind |
||
464 | * @param int $type PDO field type |
||
465 | * @return string Bound key to be used in the SQL where this parameter |
||
466 | * would be used. |
||
467 | */ |
||
468 | static function bind ( &$a, $val, $type ) |
||
469 | { |
||
470 | $key = ':binding_'.count( $a ); |
||
471 | |||
472 | $a[] = array( |
||
473 | 'key' => $key, |
||
474 | 'val' => $val, |
||
475 | 'type' => $type |
||
476 | ); |
||
477 | |||
478 | return $key; |
||
479 | } |
||
480 | |||
481 | |||
482 | /** |
||
483 | * Pull a particular property from each assoc. array in a numeric array, |
||
484 | * returning and array of the property values from each item. |
||
485 | * |
||
486 | * @param array $a Array to get data from |
||
487 | * @param string $prop Property to read |
||
488 | * @return array Array of property values |
||
489 | */ |
||
490 | static function pluck ( $a, $prop ) |
||
491 | { |
||
492 | $out = array(); |
||
493 | |||
494 | for ( $i=0, $len=count($a) ; $i<$len ; $i++ ) { |
||
495 | $out[] = $a[$i][$prop]; |
||
496 | } |
||
497 | |||
498 | return $out; |
||
499 | } |
||
500 | |||
501 | |||
502 | /** |
||
503 | * Return a string from an array or a string |
||
504 | * |
||
505 | * @param array|string $a Array to join |
||
506 | * @param string $join Glue for the concatenation |
||
507 | * @return string Joined string |
||
508 | */ |
||
509 | static function _flatten ( $a, $join = ' AND ' ) |
||
518 | } |
||
519 | } |
||
520 | |||
521 |
Adding explicit visibility (
private
,protected
, orpublic
) is generally recommend to communicate to other developers how, and from where this method is intended to be used.