Subversion Repositories phpLibraryV2

Rev

Rev 10 | Rev 15 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 rodolico 1
<?php
2
 
13 rodolico 3
   /*
4
    * Class DBQuery
5
    * 
6
    * PHP Class as a wrapper around the PDO class. Allows you to make
7
    * the connection and run queries and/or DDL's if needed with the
8
    * most common parameters (ie, not as flexible as PDO itself).
9
    * Enhancements include:
10
    *
11
    * Logs all queries with an optional username except select statements
12
    * ie, anything that will change the system ( insert, delete, update,
13
    * create, drop, alter, etc... ). Statement are logged with a date/time
14
    * stamp, the user who performed the function, and the query executed
15
    * 
16
    * Errors are simply trapped and the message is stored in the public
17
    * member $error. Functions which find an error return boolean false
18
    *
19
    * Public Functions are:
20
    *    __construct   -- creates connection
21
    *    doSQL         -- executes an SQL query or DDL
22
    *    errors        -- returns all errors not yet cleared
23
    *                     can also directly access via public member errors
24
    *
25
    *    public static function makeSafeSQLConstant ( $value, $type='S', $default='null', $falsetrue='10' ) {
1 rodolico 26
 
4 rodolico 27
 
13 rodolico 28
    * Author: R. W. Rodolico (rodo@dailydata.net)
29
    * Date: 2018-04-30
30
    * 
31
    */
32
 
33
   class DBQuery extends mysqli {
34
 
35
      /* everything is in the one $parameters array, which can then
36
       * be set/saved/loaded as needed
37
       */
6 rodolico 38
      protected $parameters = array(
13 rodolico 39
             // query(s) to be run
40
             'query' => '',
41
             // additional clause to be added to where on a statement
42
             'whereClause' => '',
43
             // additional order by clause to be added
44
             'orderBy' => '',
45
             // the number of rows 1) affectd by an insert, update or delete or 2) returned by a select
46
             'rowsAffected' => '',
47
             // the value of the last row inserted
48
             'lastInsertKey' => '',
49
             // an array that contains the data retrieved by a query (select only)
50
             'returnData' => '',
51
             // an array that contains the meta data from the query for each column
52
             'columnMetaData' => '',
53
             // number of columns (fields) returned by query (select only)
54
             'numfields' => 0,
55
             // type of data returned, array of array, array of hash, or both
56
             'fetchType' => MYSQLI_ASSOC,
57
             // human readable form of fetchType
58
             'returntype' => 'associative',
59
             // array of any errors which occurred
60
             'error' => array(),
61
             // if set to a table name, all modifying queries are written to it
62
             // must contain, at a minimum, columns whenrun timestamp, whoran varchar and query text
63
             // can be created with $this->buildAuditTable
64
             'auditTable' => '_activity_log'
65
           );
66
 
6 rodolico 67
 
4 rodolico 68
 
13 rodolico 69
      /*
70
       * static function which simply parses out an error and returns
71
       * a string suitable for printing. NOTE: it includes line returns
72
      */
73
      public static function error2String( $error ) {
74
         $return = array();
75
         for ( $i = 0; $i < count( $error ); $i++ ) {
76
            $return[] = implode( "\n", array( 'Error ' . $error[$i]['id'] . ' - ' . $error[$i]['message'], 'while executing query', $error[$i]['query'] ) );
6 rodolico 77
         }
13 rodolico 78
         return implode( "\n\n", $return ) . "\n";
79
      } // error2String
6 rodolico 80
 
13 rodolico 81
      // simply returns parameters for saving and reloading later
6 rodolico 82
      public function save() {
83
         return $this->parameters;
84
      } // function save
13 rodolico 85
 
86
      // loads parameters from $this->save(), or hand built
6 rodolico 87
      public function load( $parameters = array() ) {
88
         $this->parameters = $parameters;
89
      } // function load
90
 
13 rodolico 91
      // sets a key/value pair in $this->parameters
92
      // returns $value
1 rodolico 93
      public function __set( $name, $value ) {
6 rodolico 94
         $this->parameters[$name] = $value;
4 rodolico 95
         return $value;
1 rodolico 96
      }
13 rodolico 97
 
98
      // gets the current value of $this->parameters[$name]
1 rodolico 99
      public function __get( $name ) {
6 rodolico 100
         return isset( $this->parameters[$name] ) ? $this->parameters[$name] : null;
1 rodolico 101
      }
13 rodolico 102
 
103
      // returns true if $parameters[$name] is set
1 rodolico 104
      public function __isset( $name ) {
6 rodolico 105
         return isset( $this->parameters[$name] );
1 rodolico 106
      }
13 rodolico 107
 
108
 
109
      /*
110
       * function: buildAuditTable
111
       * parameters:
112
       *       $tablename - name of table to be built
113
       *       $createStatement - SQL DDL to build the table
114
       *
115
       * If $tablename is set, will use that, otherwise will use
116
       * $parameters[auditTable]. In either case, $parameters[auditTable]
117
       * is set to the value of the table used
118
       * 
119
       * if $createStatement is set, will be run WITHOUT MODIFICATION, and
120
       * $parameters[auditTable] is not set to anything (unless $tablename
121
       * is set)
122
       *
123
       * If $createStatement is not set, will use a default to build
124
       * a table from $parameters[auditTable].
125
       *
126
       * Can definitely blow up if the table name is not set both places
127
       * or if $createStatement is fubar
128
       */
129
 
130
      public function buildAuditTable( $tablename = '', $createStatement = '' ) {
131
         if ( $tablename ) // they sent us one, so set it
132
            $this->parameters[ 'auditTable' ] = $tablename;
133
         if ( ! $createStatement ) { // they did not set createStatement, so use our default
134
            $createStatement = "
135
               create table $this->parameters['auditTable'] (
136
                  _activity_log_id int unsigned not null auto_increment,
137
                  timestamp  timestamp,
138
                  user       varchar(64),
139
                  query      text,
140
                  primary key(_activity_log_id)
141
               ) comment 'tracks queries which modify data'";
142
         } // if
143
         if ( parent::query( $createStatement ) === false ) {
144
            // on error, die
145
            print "Can not create audit table with query<br />\n$createStatement<br />\n";
146
            die ( printf("Errormessage: %d - %s\n", $this->errno, $this->error ) );
147
         } // if error, die
148
      } // buildAuditTable
149
 
150
      /*
151
       * log queries to a table, file, or nothing
152
       * log contains date/time, username and query
153
       * to turn off logginging:
154
       * unset( $parameters['auditTable'] )
155
       */
156
      private function logIt( $username, $query, $recursion = false ) {
157
         if ( ! isset( $this->parameters['auditTable'] ) )
158
            return; 
159
         $username = $this->real_escape_string( $username );
160
         $query = $this->real_escape_string( $query );
161
         $logEntry = "insert into $this->parameters['auditTable'] (user, query) values ( '$username', '$query')";
162
         //print "Loggging\n$logEntry\n";
163
         if ( parent::query( $logEntry ) !== false ) { // good
164
            return;
165
         } else { // we had an error
166
            if ( ! $recursion && $this->errno == 1146 ) { // table doesn't exist, so let's create it
167
               $result = parent::query( "show tables like '$this->parameters['auditTable']'" );
168
               if ( $result->num_rows == 0 ) {
169
                  $this->buildAuditTable( );
170
                  return $this->logIt( $username, $query, true );
171
               }
6 rodolico 172
            } else {
13 rodolico 173
               print "Trying to log transaction with query<br />\n$logEntry<br />\n";
174
               die ( printf("Errormessage: %d - %s\n", $this->errno, $this->error ) );
175
            } // if..else
1 rodolico 176
         } // if
13 rodolico 177
      } // function logIt
178
 
179
 
180
      /*
181
       * doSQL
182
       * Parameters: $query - string or array of strings to be executed
183
       *             $parameters - hash used to pass additional parameters, to include
184
       *                           $parameters['username'] = 'fred'; // username for logging
185
       *                           $parameters['returntype'] = 'hash'; or array or both
186
       * 
187
       * executes one or more queries
188
       *
189
       * If the query is one of select, show, describe or explain, it must
190
       * be a single string. It will return the data results in a hash
191
       * containing
192
       *    'data'  - an array of array/hash/both depending on what you asked for
193
       *    'count' - number of results (ie, count(data)
194
       *    'meta'  - metadata for each column returned
195
       *    'numfields' - number of columns in result (ie, count(meta))
196
       *    'errors' - normally empty array of errors
197
       * 
198
       * if the query modifies data (ie, NOT above), query may be an array
199
       * which will be surrounded by a transaction and rolled back if
200
       * anything causes an error.
201
       *
202
       * These will return a hash containing
203
       *    'count' l- number of rows affected by last statement
204
       *    'last_insert_id' - last insert id created by BLOCK of queries
205
       *    'errors' - normally empty array of errors which occurred (caused a rollback)
206
       * 
207
       */
208
      public function doSQL( $query, $parameters = array() ) {
209
         $errors = array();
210
         // if it is a "selectstatement" it doesn't modify data
211
         // if query is an array, assume it modifies something
212
         // if it is a single statement, look for the regex
213
         $selectStatement = is_array( $query ) ?
214
            false :
215
            ( preg_match( '/^\s*(select)|(show)|(describe)|(explain)/i', $query ) === 1 );
216
 
217
         // different actions based on whether it modifies data or not
218
         if ( $selectStatement ) { // if a select, simply return the rows
219
            // dataset is small enough, we just read it into memory all at one time.
220
            // NOTE: fetch_all is nice, but tied to mysqlnd, which has reports of problems, so we do it the old way
221
            if ( $sth = parent::query( $query, MYSQLI_USE_RESULT  ) ) {
222
               if ( $sth === false ) {
223
                  $errors[] = array( 'id' => $this->errno, 'message' => $this->error );
224
               } else {
225
                  $this->parameters['columnMetaData'] = $sth->fetch_fields(); // get metadata
226
                  $this->parameters['returnData'] = array(); // we'll put all the results in an array
227
                  // $fetchtype returns either an array of array, array of hash, or both. Default is array of hash
228
                  if ( isset( $this->parameters['returntype'] ) )
229
                     $this->parameters[ 'fetchType' ] = $this->parameters['returntype'] == 'array' ? MYSQLI_NUM : (
230
                           ( $parameters['both'] == 'both' ) ? MYSQLI_BOTH : MYSQLI_ASSOC
231
                           );
232
                  } else { // default is associative array (hash)
233
                     $this->parameters[ 'fetchType' ] =  MYSQLI_ASSOC;
234
                     $this->parameters['returntype'] = 'associative';
235
                  }
236
                  // slurp all the stuff in
237
                  while ( $values =  $sth->fetch_array( $this->parameters[ 'fetchType' ] ) ) {
238
                     $this->parameters[ 'returnData' ][] = $values;
239
                  }
240
                  $sth->free(); // cleanup memory, don't need two copies
241
               } // if we had no errors
1 rodolico 242
            }
13 rodolico 243
            $this->parameters[ 'rowsAffected' ] = count( $this->parameters[ 'returnData' ] );
244
            $this->parameters[ 'numfields' ] = count( $this->parameters['columnMetaData'] );
245
            $this->parameters[ 'lastInsertKey' ] = 0;
246
            $this->parameters[ 'error' ] = $errors;
247
         } else {
248
            if ( ! is_array( $this->parameters['query'] ) ) { // not an array, so make it one
249
               $temp = $this->parameters['query'];
250
               $this->parameters['query'] = array( $temp );
1 rodolico 251
            }
13 rodolico 252
            // do it in a transaction so we can back out on failure
253
            $this->autocommit(false);
254
            $allOk = true;
255
            for ( $i = 0; $i < count( $this->parameters['query'] ); $i++ ) {
256
               $this->logIt( isset( $parameters['username'] ) ? $parameters['username'] : 'unknown', $this->parameters['query'][$i] );
257
               if ( parent::query( $this->parameters['query'][$i] ) === false ) { // we had an erorr
258
                  // record it
259
                  $errors[] = array( 'id' => $this->errno, 'message' => $this->error, 'query' => $this->parameters['query'][$i] );
260
                  $allOk = false;
261
                  // and bail
262
                  break;
263
               }
264
            }
265
            // if we made it through ok, commit, otherwise rollback
266
            $allOk ? $this->commit() : $this->rollback();
267
            // reset autocommit to true
268
            $this->autocommit(true);
269
            $this->parameters['query'] = $query;
270
            $this->parameters[ 'rowsAffected' ] = $this->affected_rows;
271
            $this->parameters[ 'lastInsertKey' ] = $this->insert_id;
272
            $this->parameters[ 'error' ] = $errors;
273
         } // if select .. else
274
         return $this->parameters;
275
      }  // function doSQL
1 rodolico 276
 
277
      /*
278
       * function will return one and only one row, NOT as an array of array
279
       * but as a single row array
280
       * if more than one row is returned by query, error is set and function
281
       * returns false.
282
       * Otherwise, function returns true
283
       */
284
      public function getOneRow( $sql = null ) {
285
         if ( isset( $sql ) )
6 rodolico 286
            $this->parameters[ 'query' ] = $sql;
287
         $save = $this->parameters[ 'useAssociativeArray' ];
1 rodolico 288
         $useAssociativeArray = true;
289
         $this->run();
290
         $useAssociativeArray = $save;
6 rodolico 291
         if ( $this->parameters[ 'rowsAffected' ] == 1 ) {
292
            $this->parameters[ 'returnData' ] = $this->parameters[ 'returnData' ][0];
1 rodolico 293
            return true;
294
         } else
6 rodolico 295
            $this->parameters['error'] = "$this->parameters[query] did not return a unique row in getOneRow";
1 rodolico 296
         return false;
297
      } // getOneRow
298
 
299
 
300
      // function returns the first column of the first row of data returned from query
301
      // or null no value returned
6 rodolico 302
      public function getOneDBValue( $sql = null ) {
1 rodolico 303
         if ( isset( $sql ) )
6 rodolico 304
            $this->parameters[ 'query' ] = $sql;
305
         $save = $this->parameters[ 'useAssociativeArray' ];
1 rodolico 306
         $useAssociativeArray = false;
307
         $this->run();
308
         $useAssociativeArray = $save;
6 rodolico 309
         return $this->parameters[ 'rowsAffected' ] ? $this->parameters[ 'returnData' ][0][0] : null;
1 rodolico 310
      }
6 rodolico 311
 
4 rodolico 312
      /*
313
       * function will attempt to make a constant ($value) safe for SQL depending on the type.
314
       * 
315
       * if $value is empty, $default is returned, as will happen if any of the
316
       * conversions (date, datetime, etc...) fail.
317
       * 
318
       * First, it will pass it through get_magic_quotes_gpc, 
319
       * then will run through mysql_real_escape_string
320
       * 
321
       * For strings, will encapsulate in quotes
322
       * Dates will attempt a conversion, then change to YYYY-MM-DD and encapsulate in quotes
323
       * DateTime will perform the same, but change to YYYY-MM-DD HH:MM:SS
324
       * Integer and Floats are passed through builtins intval and floatval
325
       * Boolean only checks the first character, a '0', 'f' and 'n' denoting false
326
       *    all else denoting true. The result is converted based on the variable
327
       *    $falsetrue, with the first char denoting false and the second denoting true
328
       */
329
      public static function makeSafeSQLConstant ( $value, $type='S', $default='null', $falsetrue='10' ) {
330
         if (strlen($value) == 0) // simply set any empty values to null
331
            return $default;
332
         // print "Processing $value as $type with default $default<br>\n";
333
         switch ( strtolower( $type ) ) {
334
            case 'string' :
335
            case 's' : 
336
                     if ( get_magic_quotes_gpc() ) 
337
                        $value = stripslashes($value);
13 rodolico 338
                     $value = $this->real_escape_string( $value );
4 rodolico 339
                     $value = strlen( $value ) > 0 ? "'$value'" : $default;
340
                     break;
341
            case 'date' :
342
            case 'd' :
343
                     if ( $value != 'null' ) {
344
                        $result = strtotime( $value );
345
                        $value = ( $result === false ) ? $default : "'" . Date( 'Y-m-d', $result) . "'";
346
                     }
347
                     break;
348
            case 'datetime':
349
            case 'timestamp':
350
            case 'dt': 
351
                     if ( $value != 'null' ) {
352
                        $result = strtotime( $value );
353
                        $value = ( $result === false ) ? $default : "'" . Date( 'Y-m-d H:i:s', $result) . "'";
354
                     }
355
                     break;
356
            case 'integer':
357
            case 'i' :  
358
                     $value = intval( $value );
359
                     break;
360
            case 'float':
361
            case 'f' :  
362
                     $value = floatval( $value );
363
                     break;
364
            case 'bool':
365
            case 'boolean':
366
            case 'b' :  // note, because of the way strpos works, you can not
367
                        // simply set $value based on the output; you MUST do
368
                        // as below; specifically check for false, then set the result
369
                        $value =  strpos( '0fn', strtolower(substr( $value, 0, 1 )) ) === false ? 0 : 1;
370
                        $value = substr( $falsetrue, $value, 0, 1 );
371
                        break;
372
         } // switch
373
         return $value;
374
      }
1 rodolico 375
 
4 rodolico 376
 
1 rodolico 377
   } // class DBQuery
378
 
13 rodolico 379
   $db = new DBQuery( '127.0.0.1', 'camp', 'camp', 'camp' );
380
 
381
   if ($db->connect_error) {
382
       die('Connect Error (' . $db->connect_errno . ') '  . $db->connect_error);
383
   }
384
   $result = $db->doSQL(
385
               array(
386
                     'drop table if exists temp',
387
                     'create table temp ( col1 int unsigned )',
388
                     "insert into temp values ('mike')"
389
                     )
390
         );
391
   if ( $result['errors'] ) {
392
      print_r ( $result );
393
      die ( DBQuery::error2String( $result['errors'] ) );
394
   } else {
395
      print "running select\n";
396
      $result = $db->doSQL( 'select * from temp' );
397
      print_r( $result );
398
   }
399
//   $return = $db->doSQL( "select device.device_id 'id',device.name 'name', device_type.name 'type' from device join device_type using (device_type_id) where device.removed_date is null and device_type.show_as_system = 'Y'" );
400
//   print_r( $return );
401
//   print_r( $db );
1 rodolico 402
?>
13 rodolico 403
 
404
 
405
 
406
 
407
 
408
 
409
 
410
 
411
 
412