spreadsheetreader.php (8488B)
1 <?php 2 /** 3 * Main class for spreadsheet reading 4 * 5 * @version 0.5.10 6 * @author Martins Pilsetnieks 7 */ 8 9 namespace d_excel_reader_writer; 10 11 use SpreadsheetReader_CSV; 12 use SpreadsheetReader_ODS; 13 use SpreadsheetReader_XLS; 14 use SpreadsheetReader_XLSX; 15 use SeekableIterator; 16 use Countable; 17 use OutOfBoundsException; 18 use Exception; 19 20 class SpreadsheetReader implements SeekableIterator, Countable { 21 const TYPE_XLSX = 'XLSX'; 22 const TYPE_XLS = 'XLS'; 23 const TYPE_CSV = 'CSV'; 24 const TYPE_ODS = 'ODS'; 25 26 private $Options = array( 27 'Delimiter' => '', 28 'Enclosure' => '"' 29 ); 30 31 /** 32 * @var int Current row in the file 33 */ 34 private $Index = 0; 35 36 /** 37 * @var SpreadsheetReader_* Handle for the reader object 38 */ 39 private $Handle = array(); 40 41 /** 42 * @var TYPE_* Type of the contained spreadsheet 43 */ 44 private $Type = false; 45 46 /** 47 * @param string Path to file 48 * @param string Original filename (in case of an uploaded file), used to determine file type, optional 49 * @param string MIME type from an upload, used to determine file type, optional 50 */ 51 public function __construct($Filepath, $OriginalFilename = false, $MimeType = false) 52 { 53 if (!is_readable($Filepath)) 54 { 55 throw new Exception('SpreadsheetReader: File ('.$Filepath.') not readable'); 56 } 57 58 // To avoid timezone warnings and exceptions for formatting dates retrieved from files 59 $DefaultTZ = @date_default_timezone_get(); 60 if ($DefaultTZ) 61 { 62 date_default_timezone_set($DefaultTZ); 63 } 64 65 // Checking the other parameters for correctness 66 67 // This should be a check for string but we're lenient 68 if (!empty($OriginalFilename) && !is_scalar($OriginalFilename)) 69 { 70 throw new Exception('SpreadsheetReader: Original file (2nd parameter) path is not a string or a scalar value.'); 71 } 72 if (!empty($MimeType) && !is_scalar($MimeType)) 73 { 74 throw new Exception('SpreadsheetReader: Mime type (3nd parameter) path is not a string or a scalar value.'); 75 } 76 77 // 1. Determine type 78 if (!$OriginalFilename) 79 { 80 $OriginalFilename = $Filepath; 81 } 82 83 $Extension = strtolower(pathinfo($OriginalFilename, PATHINFO_EXTENSION)); 84 85 switch ($MimeType) 86 { 87 case 'text/csv': 88 case 'text/comma-separated-values': 89 case 'text/plain': 90 $this -> Type = self::TYPE_CSV; 91 break; 92 case 'application/vnd.ms-excel': 93 case 'application/msexcel': 94 case 'application/x-msexcel': 95 case 'application/x-ms-excel': 96 case 'application/vnd.ms-excel': 97 case 'application/x-excel': 98 case 'application/x-dos_ms_excel': 99 case 'application/xls': 100 case 'application/xlt': 101 case 'application/x-xls': 102 // Excel does weird stuff 103 if (in_array($Extension, array('csv', 'tsv', 'txt'))) 104 { 105 $this -> Type = self::TYPE_CSV; 106 } 107 else 108 { 109 $this -> Type = self::TYPE_XLS; 110 } 111 break; 112 case 'application/vnd.oasis.opendocument.spreadsheet': 113 case 'application/vnd.oasis.opendocument.spreadsheet-template': 114 $this -> Type = self::TYPE_ODS; 115 break; 116 case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 117 case 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': 118 case 'application/xlsx': 119 case 'application/xltx': 120 $this -> Type = self::TYPE_XLSX; 121 break; 122 case 'application/xml': 123 // Excel 2004 xml format uses this 124 break; 125 } 126 127 if (!$this -> Type) 128 { 129 switch ($Extension) 130 { 131 case 'xlsx': 132 case 'xltx': // XLSX template 133 case 'xlsm': // Macro-enabled XLSX 134 case 'xltm': // Macro-enabled XLSX template 135 $this -> Type = self::TYPE_XLSX; 136 break; 137 case 'xls': 138 case 'xlt': 139 $this -> Type = self::TYPE_XLS; 140 break; 141 case 'ods': 142 case 'odt': 143 $this -> Type = self::TYPE_ODS; 144 break; 145 default: 146 $this -> Type = self::TYPE_CSV; 147 break; 148 } 149 } 150 151 // Pre-checking XLS files, in case they are renamed CSV or XLSX files 152 if ($this -> Type == self::TYPE_XLS) 153 { 154 self::Load(self::TYPE_XLS); 155 $this -> Handle = new SpreadsheetReader_XLS($Filepath); 156 if ($this -> Handle -> Error) 157 { 158 $this -> Handle -> __destruct(); 159 160 if (is_resource($ZipHandle = zip_open($Filepath))) 161 { 162 $this -> Type = self::TYPE_XLSX; 163 zip_close($ZipHandle); 164 } 165 else 166 { 167 $this -> Type = self::TYPE_CSV; 168 } 169 } 170 } 171 172 // 2. Create handle 173 switch ($this -> Type) 174 { 175 case self::TYPE_XLSX: 176 self::Load(self::TYPE_XLSX); 177 $this -> Handle = new SpreadsheetReader_XLSX($Filepath); 178 break; 179 case self::TYPE_CSV: 180 self::Load(self::TYPE_CSV); 181 $this -> Handle = new SpreadsheetReader_CSV($Filepath, $this -> Options); 182 break; 183 case self::TYPE_XLS: 184 // Everything already happens above 185 break; 186 case self::TYPE_ODS: 187 self::Load(self::TYPE_ODS); 188 $this -> Handle = new SpreadsheetReader_ODS($Filepath, $this -> Options); 189 break; 190 } 191 } 192 193 /** 194 * Gets information about separate sheets in the given file 195 * 196 * @return array Associative array where key is sheet index and value is sheet name 197 */ 198 public function Sheets() 199 { 200 return $this -> Handle -> Sheets(); 201 } 202 203 /** 204 * Changes the current sheet to another from the file. 205 * Note that changing the sheet will rewind the file to the beginning, even if 206 * the current sheet index is provided. 207 * 208 * @param int Sheet index 209 * 210 * @return bool True if sheet could be changed to the specified one, 211 * false if not (for example, if incorrect index was provided. 212 */ 213 public function ChangeSheet($Index) 214 { 215 return $this -> Handle -> ChangeSheet($Index); 216 } 217 218 /** 219 * Autoloads the required class for the particular spreadsheet type 220 * 221 * @param TYPE_* Spreadsheet type, one of TYPE_* constants of this class 222 */ 223 private static function Load($Type) 224 { 225 if (!in_array($Type, array(self::TYPE_XLSX, self::TYPE_XLS, self::TYPE_CSV, self::TYPE_ODS))) 226 { 227 throw new Exception('SpreadsheetReader: Invalid type ('.$Type.')'); 228 } 229 230 // 2nd parameter is to prevent autoloading for the class. 231 // If autoload works, the require line is unnecessary, if it doesn't, it ends badly. 232 if (!class_exists('SpreadsheetReader_'.$Type, false)) 233 { 234 require(dirname(__FILE__).DIRECTORY_SEPARATOR.'spreadsheetreader/spreadsheetreader_'.strtolower($Type).'.php'); 235 } 236 } 237 238 // !Iterator interface methods 239 240 /** 241 * Rewind the Iterator to the first element. 242 * Similar to the reset() function for arrays in PHP 243 */ 244 public function rewind() 245 { 246 $this -> Index = 0; 247 if ($this -> Handle) 248 { 249 $this -> Handle -> rewind(); 250 } 251 } 252 253 /** 254 * Return the current element. 255 * Similar to the current() function for arrays in PHP 256 * 257 * @return mixed current element from the collection 258 */ 259 public function current() 260 { 261 if ($this -> Handle) 262 { 263 return $this -> Handle -> current(); 264 } 265 return null; 266 } 267 268 /** 269 * Move forward to next element. 270 * Similar to the next() function for arrays in PHP 271 */ 272 public function next() 273 { 274 if ($this -> Handle) 275 { 276 $this -> Index++; 277 278 return $this -> Handle -> next(); 279 } 280 return null; 281 } 282 283 /** 284 * Return the identifying key of the current element. 285 * Similar to the key() function for arrays in PHP 286 * 287 * @return mixed either an integer or a string 288 */ 289 public function key() 290 { 291 if ($this -> Handle) 292 { 293 return $this -> Handle -> key(); 294 } 295 return null; 296 } 297 298 public function setKey($index) 299 { 300 if ($this -> Handle) 301 { 302 return $this -> Handle -> setKey($index); 303 } 304 return null; 305 } 306 307 /** 308 * Check if there is a current element after calls to rewind() or next(). 309 * Used to check if we've iterated to the end of the collection 310 * 311 * @return boolean FALSE if there's nothing more to iterate over 312 */ 313 public function valid() 314 { 315 if ($this -> Handle) 316 { 317 return $this -> Handle -> valid(); 318 } 319 return false; 320 } 321 322 // !Countable interface method 323 public function count() 324 { 325 if ($this -> Handle) 326 { 327 return $this -> Handle -> count(); 328 } 329 return 0; 330 } 331 332 /** 333 * Method for SeekableIterator interface. Takes a posiiton and traverses the file to that position 334 * The value can be retrieved with a `current()` call afterwards. 335 * 336 * @param int Position in file 337 */ 338 public function seek($Position) 339 { 340 if (!$this -> Handle) 341 { 342 throw new OutOfBoundsException('SpreadsheetReader: No file opened'); 343 } 344 345 $CurrentIndex = $this -> Handle -> key(); 346 347 if ($CurrentIndex != $Position) 348 { 349 if ($Position < $CurrentIndex || is_null($CurrentIndex) || $Position == 0) 350 { 351 $this -> rewind(); 352 } 353 354 while ($this -> Handle -> valid() && ($Position > $this -> Handle -> key())) 355 { 356 $this -> Handle -> next(); 357 } 358 359 if (!$this -> Handle -> valid()) 360 { 361 throw new OutOfBoundsException('SpreadsheetError: Position '.$Position.' not found'); 362 } 363 } 364 365 return null; 366 } 367 } 368 369 ?>