globalExtractionFolder = $tempDirectory; if (!strlen($this->globalExtractionFolder)) { throw new Exception("No Temp extraction directory specified!"); } // Set shared directory if (!file_exists($this->globalExtractionFolder)) { mkdir($this->globalExtractionFolder, 0777, true); } } /** * @param string $file * @return bool|null * @throws Exception */ public function loadFileForExtraction(string $file): ?bool { // Fail on invalid file if (!file_exists($file) || !is_file($file)) { throw new Exception("Base extraction file not found!"); } $this->activeSheetObject = null; $this->sharedStringsObject = null; // Set sheet ID $this->activeSheetId = $this->defaultActiveSheetId; // Create file temp folder $fileInfo = pathinfo($file); $this->extractionFolder = $this->globalExtractionFolder . '/' . $fileInfo['filename'] . '_data_'; $i = 1; while (file_exists($this->extractionFolder . $i)) { $i++; } $this->extractionFolder .= $i; // Copy base file as zip mkdir($this->extractionFolder, 0777, true); $newFile = $this->globalExtractionFolder . '/' . $fileInfo['filename'] . '.zip'; copy($file, $newFile); // Extract xls file data $zip = new ZipArchive(); $res = $zip->open($newFile); if ($res === true) { $zip->extractTo($this->extractionFolder); $zip->close(); } else { throw new Exception("Data extraction from base file has failed!"); } // Validate that we got base xml files if ( !file_exists($this->extractionFolder . $this->workbookXmlFile) || !file_exists($this->extractionFolder . $this->sharedStringsFile) || !file_exists(sprintf($this->extractionFolder . $this->SheetBaseFile, $this->activeSheetId)) ) { throw new Exception("Structure files are missing!"); } return true; } /** * @param string $sheetTitle * @return int * @throws Exception */ public function getSheetIdByTitle(string $sheetTitle): int { if ( strlen($sheetTitle) == 0 || !file_exists($this->extractionFolder . $this->workbookXmlFile) ) { throw new Exception("Sheet title or base workbook file is missing!"); } $xmlObject = simplexml_load_file($this->extractionFolder . $this->workbookXmlFile); if ($xmlObject instanceof SimpleXMLElement) { foreach ($xmlObject->sheets->sheet as $sheet) { if ($sheet['name'] == $sheetTitle) { return (int)$sheet['sheetId']; } } } throw new Exception("Excel '$sheetTitle' sheet not found!"); } /** * @param int $sheetId * @return bool|null * @throws Exception */ public function setActiveWorkingSheetById(int $sheetId): ?bool { if ( $sheetId <= 0 || !file_exists(sprintf($this->extractionFolder . $this->SheetBaseFile, $sheetId)) ) { throw new Exception("Sheet ID or sheet XML file is missing!"); } if ($this->activeSheetId != $sheetId) { $this->activeSheetId = $sheetId; $this->activeSheetObject = null; } return true; } /** * @param int $x * @param int $y * @return string|null * @throws Exception */ public function extractTextFromCell(int $x, int $y): ?string { if ($x <= 0 || $y <= 0) { throw new Exception("Invalid coordinates!"); } if ( $this->activeSheetId <= 0 || !file_exists(sprintf($this->extractionFolder . $this->SheetBaseFile, $this->activeSheetId)) ) { throw new Exception("Sheet ID or sheet XML file is missing!"); } $value = null; // Load sheet XMl if not loaded if (is_null($this->activeSheetObject)) { $this->activeSheetObject = simplexml_load_file( sprintf($this->extractionFolder . $this->SheetBaseFile, $this->activeSheetId) ); // Without namespace XPath queries won't work if ($this->activeSheetObject instanceof SimpleXMLElement) { $namespaces = $this->activeSheetObject->getDocNamespaces(); if (is_array($namespaces)) { foreach ($namespaces as $prefix => $namespace) { $this->activeSheetObject->registerXPathNamespace($prefix ?: 'default', $namespace); } } } } // Look for data if ($this->activeSheetObject instanceof SimpleXMLElement) { $reqCell = $this->activeSheetObject->xpath( '//default:sheetData/default:row[@r="' . $y . '"]/default:c[@r="' . self::getAddress($x, $y) . '"]' ); if (!is_array($reqCell) || count($reqCell) <= 0) { $message = "Cell " . self::getAddress($x, $y) . " not found in '" . basename(sprintf($this->SheetBaseFile, $this->activeSheetId)) . "'!"; throw new Exception($message, self::ERROR_NODE_NOT_FOUND); } $reqCell = $reqCell[0]; // Get value $value = $reqCell->{'v'}; // Check if formula exists - then return formula if (isset($reqCell->{'f'})) { $value = $reqCell->{'f'}; } // Check if we got rich text set in cell if (isset($reqCell['t']) && $reqCell['t'] == 'inlineStr') { $value = $reqCell->{'is'}->{'r'}->{'t'}; } // Check if we got a string from sharedStrings.xml and retrieve text if (isset($reqCell['t']) && $reqCell['t'] == 's') { // Load shared strings if not loaded if (is_null($this->sharedStringsObject)) { $this->sharedStringsObject = simplexml_load_file( $this->extractionFolder . $this->sharedStringsFile ); // Without namespace XPath queries won't work if ($this->sharedStringsObject instanceof SimpleXMLElement) { $namespaces = $this->sharedStringsObject->getDocNamespaces(); if (is_array($namespaces)) { foreach ($namespaces as $prefix => $namespace) { $this->sharedStringsObject->registerXPathNamespace($prefix ?: 'default', $namespace); } } } } // Get string by index if ($this->sharedStringsObject instanceof SimpleXMLElement) { $reqCell = $this->sharedStringsObject->xpath('//default:si[' . ((int)$value + 1) . ']/default:t'); if ($reqCell === false || !is_array($reqCell) || count($reqCell) <= 0) { throw new Exception("Shared string not found in '" . basename($this->sharedStringsFile) . "'!"); } $value = $reqCell[0]; } } } return $value; } /** * @param int $x * @param int $y * @return string */ public static function getAddress(int $x, int $y): string { $x = $x <= 0 ? 1 : $x; $y = $y <= 0 ? 1 : $y; $sk = 0; $iX = $x; $address = ''; while (1) { if ($iX > 26) { while ($iX > 26) { $iX -= 26; $sk++; } $address = Chr(64 + $iX) . $address; $iX = $sk; $sk = 0; } else { return chr(64 + $iX) . $address . $y; } } } /** * @param BasePhpUnitSeleniumCase $baseTestCase * @param string $file * @param array>> $excelValueCfg - * ['sheet_title' => ['row_index' => ['col_index 0 based' => 'assertion value']]] * @param bool $assertByStringContainsString * @return void * @throws Exception */ public function assertionsOnExcelSheetData( BasePhpUnitSeleniumCase $baseTestCase, string $file = '', array $excelValueCfg = [], bool $assertByStringContainsString = false ): void { if (count($excelValueCfg) == 0) { throw new Exception("Validation configuration is missing!"); } try { $this->loadFileForExtraction($file); // Loop cfg & assert values within Excel sheets foreach ($excelValueCfg as $sheetTitle => $rows) { $this->setActiveWorkingSheetById( $this->getSheetIdByTitle($sheetTitle) ); foreach ($rows as $rowExactIndex => $values) { foreach ($values as $k => $val) { // Check is formula - remove = sign since formulas are stored without it if (mb_substr($val, 0, 1) == '=') { $val = mb_substr($val, 1); } // Find cell in xml & do the assertion try { $rawActual = (string)$this->extractTextFromCell($k + 1, $rowExactIndex); $rawExpected = (string)$val; $errorMessage = "Error within '$sheetTitle' sheet cell '" . self::getAddress($k + 1, $rowExactIndex) . "'!" . "\nexpected: $val \n actual: $rawActual"; if ($assertByStringContainsString) { $baseTestCase->assertStringContainsString($val, $rawActual, $errorMessage); } else { if (is_numeric($rawActual) && is_numeric($rawExpected)) { if (!$this->excelNumbersEqual($rawActual, $rawExpected, 3)) { $baseTestCase->fail($errorMessage); } } else { $baseTestCase->assertEquals($rawExpected, $rawActual, $errorMessage); } } } catch (Exception $e) { // Expect empty string assertions to fail on xml lookup - since empty cells are not stored if ( strlen($val) == 0 && $e->getCode() == self::ERROR_NODE_NOT_FOUND ) { continue; } else { throw $e; } } } } } } catch (Exception $e) { $baseTestCase->fail($e->getMessage()); } $baseTestCase->clearTempFolder($this->globalExtractionFolder); } private function excelNumbersEqual($a, $b, int $precision = 3): bool { if (!is_numeric($a) || !is_numeric($b)) { return false; } // Round precision $a = round((float)$a, $precision); $b = round((float)$b, $precision); return bccomp((string)$a, (string)$b, $precision) === 0; } /** * Generate a PHP expected array file directly from Excel XML (1:1) * * @param string $excelFilePath Path to a source Excel file * @param string $outputPhpFile Path where a PHP file will be written * @param string $sheetTitle Excel sheet title * @param int $startRow First row index (Excel-based) * @param int $endRow Last row index (inclusive) * @param int $startCol First column index (Excel-based) * @param int $endCol Last column index (inclusive) * @return void * @throws Exception */ public function generateExpectedPhpArrayFile( string $excelFilePath, string $outputPhpFile, string $sheetTitle, int $startRow, int $endRow, int $startCol, int $endCol ): void { if ($startRow <= 0 || $endRow < $startRow) { throw new Exception('Invalid row range'); } if ($startCol <= 0 || $endCol < $startCol) { throw new Exception('Invalid column range'); } $this->loadFileForExtraction($excelFilePath); $sheetId = $this->getSheetIdByTitle($sheetTitle); $this->setActiveWorkingSheetById($sheetId); $result = []; for ($row = $startRow; $row <= $endRow; $row++) { $rowData = []; for ($col = $startCol; $col <= $endCol; $col++) { try { $rowData[] = (string)$this->extractTextFromCell($col, $row); } catch (Exception $e) { // Empty cells are not stored in XML if ($e->getCode() === self::ERROR_NODE_NOT_FOUND) { $rowData[] = ''; } else { throw $e; } } } $result[$row] = $rowData; } $php = "