spreadsheetreader_ods.php (7689B)
1 <?php 2 /** 3 * Class for parsing ODS files 4 * 5 * @author Martins Pilsetnieks 6 */ 7 class SpreadsheetReader_ODS implements Iterator, Countable { 8 private $Options = array( 9 'TempDir' => '', 10 'ReturnDateTimeObjects' => false 11 ); 12 13 /** 14 * @var string Path to temporary content file 15 */ 16 private $ContentPath = ''; 17 /** 18 * @var XMLReader XML reader object 19 */ 20 private $Content = false; 21 22 /** 23 * @var array Data about separate sheets in the file 24 */ 25 private $Sheets = false; 26 27 private $CurrentRow = null; 28 29 /** 30 * @var int Number of the sheet we're currently reading 31 */ 32 private $CurrentSheet = 0; 33 34 private $Index = 0; 35 36 private $TableOpen = false; 37 private $RowOpen = false; 38 39 /** 40 * @param string Path to file 41 * @param array Options: 42 * TempDir => string Temporary directory path 43 * ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects, false => as strings 44 */ 45 public function __construct($Filepath, array $Options = null) 46 { 47 if (!is_readable($Filepath)) 48 { 49 throw new Exception('SpreadsheetReader_ODS: File not readable ('.$Filepath.')'); 50 } 51 52 $this -> TempDir = isset($Options['TempDir']) && is_writable($Options['TempDir']) ? 53 $Options['TempDir'] : 54 sys_get_temp_dir(); 55 56 $this -> TempDir = rtrim($this -> TempDir, DIRECTORY_SEPARATOR); 57 $this -> TempDir = $this -> TempDir.DIRECTORY_SEPARATOR.uniqid().DIRECTORY_SEPARATOR; 58 59 $Zip = new ZipArchive; 60 $Status = $Zip -> open($Filepath); 61 62 if ($Status !== true) 63 { 64 throw new Exception('SpreadsheetReader_ODS: File not readable ('.$Filepath.') (Error '.$Status.')'); 65 } 66 67 if ($Zip -> locateName('content.xml') !== false) 68 { 69 $Zip -> extractTo($this -> TempDir, 'content.xml'); 70 $this -> ContentPath = $this -> TempDir.'content.xml'; 71 } 72 73 $Zip -> close(); 74 75 if ($this -> ContentPath && is_readable($this -> ContentPath)) 76 { 77 $this -> Content = new XMLReader; 78 $this -> Content -> open($this -> ContentPath); 79 $this -> Valid = true; 80 } 81 } 82 83 /** 84 * Destructor, destroys all that remains (closes and deletes temp files) 85 */ 86 public function __destruct() 87 { 88 if ($this -> Content && $this -> Content instanceof XMLReader) 89 { 90 $this -> Content -> close(); 91 unset($this -> Content); 92 } 93 if (file_exists($this -> ContentPath)) 94 { 95 @unlink($this -> ContentPath); 96 unset($this -> ContentPath); 97 } 98 } 99 100 /** 101 * Retrieves an array with information about sheets in the current file 102 * 103 * @return array List of sheets (key is sheet index, value is name) 104 */ 105 public function Sheets() 106 { 107 if ($this -> Sheets === false) 108 { 109 $this -> Sheets = array(); 110 111 if ($this -> Valid) 112 { 113 $this -> SheetReader = new XMLReader; 114 $this -> SheetReader -> open($this -> ContentPath); 115 116 while ($this -> SheetReader -> read()) 117 { 118 if ($this -> SheetReader -> name == 'table:table') 119 { 120 $this -> Sheets[] = $this -> SheetReader -> getAttribute('table:name'); 121 $this -> SheetReader -> next(); 122 } 123 } 124 125 $this -> SheetReader -> close(); 126 } 127 } 128 return $this -> Sheets; 129 } 130 131 /** 132 * Changes the current sheet in the file to another 133 * 134 * @param int Sheet index 135 * 136 * @return bool True if sheet was successfully changed, false otherwise. 137 */ 138 public function ChangeSheet($Index) 139 { 140 $Index = (int)$Index; 141 142 $Sheets = $this -> Sheets(); 143 if (isset($Sheets[$Index])) 144 { 145 $this -> CurrentSheet = $Index; 146 $this -> rewind(); 147 148 return true; 149 } 150 151 return false; 152 } 153 154 // !Iterator interface methods 155 /** 156 * Rewind the Iterator to the first element. 157 * Similar to the reset() function for arrays in PHP 158 */ 159 public function rewind() 160 { 161 if ($this -> Index > 0) 162 { 163 // If the worksheet was already iterated, XML file is reopened. 164 // Otherwise it should be at the beginning anyway 165 $this -> Content -> close(); 166 $this -> Content -> open($this -> ContentPath); 167 $this -> Valid = true; 168 169 $this -> TableOpen = false; 170 $this -> RowOpen = false; 171 172 $this -> CurrentRow = null; 173 } 174 175 $this -> Index = 0; 176 } 177 178 /** 179 * Return the current element. 180 * Similar to the current() function for arrays in PHP 181 * 182 * @return mixed current element from the collection 183 */ 184 public function current() 185 { 186 if ($this -> Index == 0 && is_null($this -> CurrentRow)) 187 { 188 $this -> next(); 189 $this -> Index--; 190 } 191 return $this -> CurrentRow; 192 } 193 194 /** 195 * Move forward to next element. 196 * Similar to the next() function for arrays in PHP 197 */ 198 public function next() 199 { 200 $this -> Index++; 201 202 $this -> CurrentRow = array(); 203 204 if (!$this -> TableOpen) 205 { 206 $TableCounter = 0; 207 $SkipRead = false; 208 209 while ($this -> Valid = ($SkipRead || $this -> Content -> read())) 210 { 211 if ($SkipRead) 212 { 213 $SkipRead = false; 214 } 215 216 if ($this -> Content -> name == 'table:table' && $this -> Content -> nodeType != XMLReader::END_ELEMENT) 217 { 218 if ($TableCounter == $this -> CurrentSheet) 219 { 220 $this -> TableOpen = true; 221 break; 222 } 223 224 $TableCounter++; 225 $this -> Content -> next(); 226 $SkipRead = true; 227 } 228 } 229 } 230 231 if ($this -> TableOpen && !$this -> RowOpen) 232 { 233 while ($this -> Valid = $this -> Content -> read()) 234 { 235 switch ($this -> Content -> name) 236 { 237 case 'table:table': 238 $this -> TableOpen = false; 239 $this -> Content -> next('office:document-content'); 240 $this -> Valid = false; 241 break 2; 242 case 'table:table-row': 243 if ($this -> Content -> nodeType != XMLReader::END_ELEMENT) 244 { 245 $this -> RowOpen = true; 246 break 2; 247 } 248 break; 249 } 250 } 251 } 252 253 if ($this -> RowOpen) 254 { 255 $LastCellContent = ''; 256 257 while ($this -> Valid = $this -> Content -> read()) 258 { 259 switch ($this -> Content -> name) 260 { 261 case 'table:table-cell': 262 if ($this -> Content -> nodeType == XMLReader::END_ELEMENT || $this -> Content -> isEmptyElement) 263 { 264 if ($this -> Content -> nodeType == XMLReader::END_ELEMENT) 265 { 266 $CellValue = $LastCellContent; 267 } 268 elseif ($this -> Content -> isEmptyElement) 269 { 270 $LastCellContent = ''; 271 $CellValue = $LastCellContent; 272 } 273 274 $this -> CurrentRow[] = $LastCellContent; 275 276 if ($this -> Content -> getAttribute('table:number-columns-repeated') !== null) 277 { 278 $RepeatedColumnCount = $this -> Content -> getAttribute('table:number-columns-repeated'); 279 // Checking if larger than one because the value is already added to the row once before 280 if ($RepeatedColumnCount > 1) 281 { 282 $this -> CurrentRow = array_pad($this -> CurrentRow, count($this -> CurrentRow) + $RepeatedColumnCount - 1, $LastCellContent); 283 } 284 } 285 } 286 else 287 { 288 $LastCellContent = ''; 289 } 290 case 'text:p': 291 if ($this -> Content -> nodeType != XMLReader::END_ELEMENT) 292 { 293 $LastCellContent = $this -> Content -> readString(); 294 } 295 break; 296 case 'table:table-row': 297 $this -> RowOpen = false; 298 break 2; 299 } 300 } 301 } 302 303 return $this -> CurrentRow; 304 } 305 306 /** 307 * Return the identifying key of the current element. 308 * Similar to the key() function for arrays in PHP 309 * 310 * @return mixed either an integer or a string 311 */ 312 public function key() 313 { 314 return $this -> Index; 315 } 316 317 /** 318 * Check if there is a current element after calls to rewind() or next(). 319 * Used to check if we've iterated to the end of the collection 320 * 321 * @return boolean FALSE if there's nothing more to iterate over 322 */ 323 public function valid() 324 { 325 return $this -> Valid; 326 } 327 328 // !Countable interface method 329 /** 330 * Ostensibly should return the count of the contained items but this just returns the number 331 * of rows read so far. It's not really correct but at least coherent. 332 */ 333 public function count() 334 { 335 return $this -> Index + 1; 336 } 337 } 338 339 ?>