root/fUnbufferedResult.php

Revision 347, 12.7 kB (checked in by wbond, 2 years ago)

Added custom message support to fResult::tossIfNoResults() and fUnbufferedResult::tossIfNoResults()

LineHide Line Numbers
1 <?php
2 /**
3  * Representation of an unbuffered result from a query against the fDatabase class
4  *
5  * @copyright  Copyright (c) 2007-2008 William Bond
6  * @author     William Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fUnbufferedResult
11  *
12  * @version    1.0.0b
13  * @changes    1.0.0b  The initial implementation [wb, 2008-05-07]
14  */
15 class fUnbufferedResult implements Iterator
16 {
17     /**
18     * The character set to transcode from for MSSQL queries
19     *
20     * @var string
21     */
22     private $character_set = NULL;
23    
24     /**
25     * The current row of the result set
26     *
27     * @var array
28     */
29     private $current_row = NULL;
30    
31     /**
32     * The php extension used for database interaction
33     *
34     * @var string
35     */
36     private $extension = NULL;
37    
38     /**
39     * The position of the pointer in the result set
40     *
41     * @var integer
42     */
43     private $pointer;
44    
45     /**
46     * The result resource
47     *
48     * @var resource
49     */
50     private $result = NULL;
51    
52     /**
53     * The SQL query
54     *
55     * @var string
56     */
57     private $sql = '';
58    
59     /**
60     * The type of the database
61     *
62     * @var string
63     */
64     private $type = NULL;
65    
66     /**
67     * The SQL from before translation
68     *
69     * @var string
70     */
71     private $untranslated_sql = NULL;
72    
73    
74     /**
75     * Sets the PHP extension the query occured through
76     *
77     * @internal
78      *
79     * @param  string $type           The type of database: `'mssql'`, `'mysql'`, `'postgresql'`, `'sqlite'`
80     * @param  string $extension      The database extension used: `'mssql'`, `'mysql'`, `'mysqli'`, `'odbc'`, `'pdo'`, `'pgsql'`, `'sqlite', 'sqlsrv'`
81     * @param  string $character_set  MSSQL only: the character set to transcode from since MSSQL doesn't do UTF-8
82     * @return fUnbufferedResult
83     */
84     public function __construct($type, $extension, $character_set=NULL)
85     {
86         $valid_types = array('mssql', 'mysql', 'postgresql', 'sqlite');
87         if (!in_array($type, $valid_types)) {
88             fCore::toss(
89                 'fProgrammerException',
90                 fGrammar::compose(
91                     'The database type specified, %1$s, in invalid. Must be one of: %2$s.',
92                     fCore::dump($type),
93                     join(', ', $valid_types)
94                 )
95             );
96         }
97        
98         $valid_extensions = array('mssql', 'mysql', 'mysqli', 'odbc', 'pdo', 'pgsql', 'sqlite', 'sqlsrv');
99         if (!in_array($extension, $valid_extensions)) {
100             fCore::toss(
101                 'fProgrammerException',
102                 fGrammar::compose(
103                     'The database extension specified, %1$s, is invalid. Must be one of: %2$s.',
104                     fCore::dump($extension),
105                     join(', ', $valid_extensions)
106                 )
107             );
108         }
109        
110         $this->type          = $type;
111         $this->extension     = $extension;
112         $this->character_set = $character_set;
113     }
114    
115    
116     /**
117     * Frees up the result object
118     *
119     * @internal
120      *
121     * @return void
122     */
123     public function __destruct()
124     {
125         if (!is_resource($this->result) && !is_object($this->result)) {
126             return;
127         }
128        
129         if ($this->extension == 'mssql') {
130             mssql_free_result($this->result);
131         } elseif ($this->extension == 'mysql') {
132             mysql_free_result($this->result);
133         } elseif ($this->extension == 'mysqli') {
134             mysqli_free_result($this->result);
135         } elseif ($this->extension == 'odbc') {
136             odbc_free_result($this->result);
137         } elseif ($this->extension == 'pgsql') {
138             pg_free_result($this->result);
139         } elseif ($this->extension == 'sqlite') {
140             sqlite_fetch_all($this->result);
141         } elseif ($this->extension == 'sqlsrv') {
142             sqlsrv_free_stmt($this->result);
143         } elseif ($this->extension == 'pdo') {
144             $this->result->closeCursor();
145         }
146     }
147    
148    
149     /**
150     * All requests that hit this method should be requests for callbacks
151     *
152     * @param  string $method  The method to create a callback for
153     * @return callback  The callback for the method requested
154     */
155     public function __get($method)
156     {
157         return array($this, $method);       
158     }
159    
160    
161     /**
162     * Gets the next row from the result and assigns it to the current row
163     *
164     * @return void
165     */
166     private function advanceCurrentRow()
167     {
168         if ($this->extension == 'mssql') {
169             $row = mssql_fetch_assoc($this->result);
170             if (empty($row)) {
171                 mssql_fetch_batch($this->result);
172                 $row = mssql_fetch_assoc($this->result);
173             }
174             if (!empty($row)) {
175                 $row = $this->fixDblibMSSQLDriver($row);
176                
177                 // This is an unfortunate fix that required for databases that don't support limit
178                 // clauses with an offset. It prevents unrequested columns from being returned.
179                 if (!empty($row) && $this->untranslated_sql !== NULL && isset($row['__flourish_limit_offset_row_num'])) {
180                     unset($row['__flourish_limit_offset_row_num']);
181                 }
182             }
183                
184         } elseif ($this->extension == 'mysql') {
185             $row = mysql_fetch_assoc($this->result);
186         } elseif ($this->extension == 'mysqli') {
187             $row = mysqli_fetch_assoc($this->result);
188         } elseif ($this->extension == 'odbc') {
189             $row = odbc_fetch_array($this->result);
190         } elseif ($this->extension == 'pgsql') {
191             $row = pg_fetch_assoc($this->result);
192         } elseif ($this->extension == 'sqlite') {
193             $row = sqlite_fetch_array($this->result, SQLITE_ASSOC);
194         } elseif ($this->extension == 'sqlsrv') {
195             $row = sqlsrv_fetch_array($this->result, SQLSRV_FETCH_ASSOC);
196         } elseif ($this->extension == 'pdo') {
197             $row = $this->result->fetch(PDO::FETCH_ASSOC);
198         }
199        
200         // This decodes the data coming out of MSSQL into UTF-8
201         if ($row && $this->type == 'mssql') {
202             if ($this->character_set) {
203                 foreach ($row as $key => $value) {
204                     if (!is_string($value) || strpos($key, '__flourish_mssqln_') === 0) {
205                         continue;
206                     }         
207                     $row[$key] = iconv($this->character_set, 'UTF-8', $value);
208                 }
209             }
210             $row = $this->decodeMSSQLNationalColumns($row);
211         }
212        
213         $this->current_row = $row;
214     }
215    
216    
217     /**
218     * Returns the current row in the result set (required by iterator interface)
219     *
220     * @throws fNoResultsException
221     * @throws fNoRemainingException
222     * @internal
223      *
224     * @return array  The current row
225     */
226     public function current()
227     {
228         // Primes the result set
229         if ($this->pointer === NULL) {
230             $this->pointer = 0;
231             $this->advanceCurrentRow();
232         }
233        
234         if(!$this->current_row && $this->pointer == 0) {
235             fCore::toss(
236                 'fNoResultsException',
237                 fGrammar::compose('The query did not return any rows')
238             );
239            
240         } elseif (!$this->current_row) {
241             fCore::toss(
242                 'fNoRemainingException',
243                 fGrammar::compose('There are no remaining rows')
244             );
245         }
246        
247         return $this->current_row;
248     }
249    
250    
251     /**
252     * Decodes national (unicode) character data coming out of MSSQL into UTF-8
253     *
254     * @param  array $row  The row from the database
255     * @return array  The fixed row
256     */
257     private function decodeMSSQLNationalColumns($row)
258     {
259         if (strpos($this->sql, '__flourish_mssqln_') === FALSE) {
260             return $row;
261         }
262        
263         $columns = array_keys($row);
264        
265         foreach ($columns as $column) {
266             if (substr($column, 0, 18) != '__flourish_mssqln_') {
267                 continue;
268             }   
269            
270             $real_column = substr($column, 18);
271            
272             $row[$real_column] = iconv('ucs-2le', 'utf-8', $row[$column]);
273             unset($row[$column]);
274         }
275        
276         return $row;
277     }
278    
279    
280     /**
281     * Returns the row next row in the result set (where the pointer is currently assigned to)
282     *
283     * @throws fNoResultsException
284     * @throws fNoRemainingException
285     *
286     * @return array  The associative array of the row
287     */
288     public function fetchRow()
289     {
290         $row = $this->current();
291         $this->next();
292         return $row;
293     }
294    
295    
296     /**
297     * Warns the user about bugs in the DBLib driver for MSSQL, fixes some bugs
298     *
299     * @param  array $row  The row from the database
300     * @return array  The fixed row
301     */
302     private function fixDblibMSSQLDriver($row)
303     {
304         static $using_dblib = NULL;
305        
306         if ($using_dblib === NULL) {
307        
308             // If it is not a windows box we are definitely not using dblib
309             if (fCore::getOS() != 'windows') {
310                 $using_dblib = FALSE;
311            
312             // Check this windows box for dblib
313             } else {
314                 ob_start();
315                 phpinfo(INFO_MODULES);
316                 $module_info = ob_get_contents();
317                 ob_end_clean();
318                
319                 $using_dblib = preg_match('#FreeTDS#ims', $module_info, $match);
320             }
321         }
322        
323         if (!$using_dblib) {
324             return $row;
325         }
326        
327         foreach ($row as $key => $value) {
328             if ($value == ' ') {
329                 $row[$key] = '';
330                 fCore::trigger(
331                     'notice',
332                     fGrammar::compose(
333                         'A single space was detected coming out of the database and was converted into an empty string - see %s for more information',
334                         'http://bugs.php.net/bug.php?id=26315'
335                     )
336                 );
337             }
338             if (strlen($key) == 30) {
339                 fCore::trigger(
340                     'notice',
341                     fGrammar::compose(
342                         'A column name exactly 30 characters in length was detected coming out of the database - this column name may be truncated, see %s for more information.',
343                         'http://bugs.php.net/bug.php?id=23990'
344                     )
345                 );
346             }
347             if (strlen($value) == 256) {
348                 fCore::trigger(
349                     'notice',
350                     fGrammar::compose(
351                         'A value exactly 255 characters in length was detected coming out of the database - this value may be truncated, see %s for more information.',
352                         'http://bugs.php.net/bug.php?id=37757'
353                     )
354                 );
355             }
356         }
357        
358         return $row;
359     }
360    
361    
362     /**
363     * Returns the result
364     *
365     * @internal
366      *
367     * @return mixed  The result of the query
368     */
369     public function getResult()
370     {
371         return $this->result;
372     }
373    
374    
375     /**
376     * Returns the SQL used in the query
377     *
378     * @return string  The SQL used in the query
379     */
380     public function getSQL()
381     {
382         return $this->sql;
383     }
384    
385    
386     /**
387     * Returns the SQL as it was before translation
388     *
389     * @return string  The SQL from before translation
390     */
391     public function getUntranslatedSQL()
392     {
393         return $this->untranslated_sql;
394     }
395    
396    
397     /**
398     * Returns the current row number (required by iterator interface)
399     *
400     * @throws fNoResultsException
401     * @internal
402      *
403     * @return integer  The current row number
404     */
405     public function key()
406     {
407         if ($this->pointer === NULL) {
408             $this->current();
409         }
410        
411         return $this->pointer;
412     }
413    
414    
415     /**
416     * Advances to the next row in the result (required by iterator interface)
417     *
418     * @throws fNoResultsException
419     * @internal
420      *
421     * @return void
422     */
423     public function next()
424     {
425         if ($this->pointer === NULL) {
426             $this->current();
427         }
428        
429         $this->advanceCurrentRow();
430         $this->pointer++;
431     }
432    
433    
434     /**
435     * Rewinds the query (required by iterator interface)
436     *
437     * @internal
438      *
439     * @return void
440     */
441     public function rewind()
442     {
443         if (!empty($this->pointer)) {
444             fCore::toss(
445                 'fProgrammerException',
446                 fGrammar::compose(
447                     'Unbuffered database results can not be iterated through multiple times'
448                 )
449             );
450         }
451     }
452    
453    
454     /**
455     * Sets the result from the query
456     *
457     * @internal
458      *
459     * @param  mixed $result  The result from the query
460     * @return void
461     */
462     public function setResult($result)
463     {
464         $this->result = $result;
465     }
466    
467    
468     /**
469     * Sets the SQL used in the query
470     *
471     * @internal
472      *
473     * @param  string $sql  The SQL used in the query
474     * @return void
475     */
476     public function setSQL($sql)
477     {
478         $this->sql = $sql;
479     }
480    
481    
482     /**
483     * Sets the SQL from before translation
484     *
485     * @internal
486      *
487     * @param  string $untranslated_sql  The SQL from before translation
488     * @return void
489     */
490     public function setUntranslatedSQL($untranslated_sql)
491     {
492         $this->untranslated_sql = $untranslated_sql;
493     }
494    
495    
496     /**
497     * Throws an fNoResultException if the query did not return any rows
498     *
499     * @throws fNoResultsException
500     *
501     * @param  string $message  The message to use for the exception if there are no rows in this result set
502     * @return void
503     */
504     public function tossIfNoResults($message=NULL)
505     {
506         try {
507             $this->current();
508         } catch (fNoResultsException $e) {
509             if ($message !== NULL) {
510                 $e->getMessage($message);
511             }   
512             throw $e;
513         }
514     }
515    
516    
517     /**
518     * Returns if the query has any rows left
519     *
520     * @return boolean  If the iterator is still valid
521     */
522     public function valid()
523     {
524         if ($this->pointer === NULL) {
525             $this->advanceCurrentRow();
526             $this->pointer = 0;
527         }
528        
529         return !empty($this->current_row);
530     }
531 }
532  
533  
534  
535 /**
536  * Copyright (c) 2007-2008 William Bond <will@flourishlib.com>
537  *
538  * Permission is hereby granted, free of charge, to any person obtaining a copy
539  * of this software and associated documentation files (the "Software"), to deal
540  * in the Software without restriction, including without limitation the rights
541  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
542  * copies of the Software, and to permit persons to whom the Software is
543  * furnished to do so, subject to the following conditions:
544  *
545  * The above copyright notice and this permission notice shall be included in
546  * all copies or substantial portions of the Software.
547  *
548  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
549  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
550  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
551  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
552  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
553  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
554  * THE SOFTWARE.
555  */