| 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.