Subversion-Projekte lars-tiefland.ci

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
875 lars 1
<?php
2
 
3
/*
4
 * Helper functions for building a DataTables server-side processing SQL query
5
 *
6
 * The static functions in this class are just helper functions to help build
7
 * the SQL used in the DataTables demo server-side processing scripts. These
8
 * functions obviously do not represent all that can be done with server-side
9
 * processing, they are intentionally simple to show how it works. More complex
10
 * server-side processing operations will likely require a custom script.
11
 *
12
 * See http://datatables.net/usage/server-side for full details on the server-
13
 * side processing requirements of DataTables.
14
 *
15
 * @license MIT - http://datatables.net/license_mit
16
 */
17
 
18
 
19
// REMOVE THIS BLOCK - used for DataTables test environment only!
20
$file = $_SERVER['DOCUMENT_ROOT'].'/datatables/mysql.php';
21
if ( is_file( $file ) ) {
22
	include( $file );
23
}
24
 
25
 
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 )
376
	{
377
		try {
378
			$db = @new PDO(
379
				"mysql:host={$sql_details['host']};dbname={$sql_details['db']}",
380
				$sql_details['user'],
381
				$sql_details['pass'],
382
				array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION )
383
			);
384
		}
385
		catch (PDOException $e) {
386
			self::fatal(
387
				"An error occurred while connecting to the database. ".
388
				"The error reported by the server was: ".$e->getMessage()
389
			);
390
		}
391
 
392
		return $db;
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 )
450
	{
451
		echo json_encode( array(
452
			"error" => $msg
453
		) );
454
 
455
		exit(0);
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 ' )
510
	{
511
		if ( ! $a ) {
512
			return '';
513
		}
514
		else if ( $a && is_array($a) ) {
515
			return implode( $join, $a );
516
		}
517
		return $a;
518
	}
519
}
520