ElixirVault/tests/ExcelDataExtractForTestingV...

462 lines
15 KiB
PHP

<?php
namespace LoretaAudit\Tests;
use Exception;
use SimpleXMLElement;
use ZipArchive;
class ExcelDataExtractForTestingValues
{
/**
* Global Temp folder
*/
private $globalExtractionFolder;
/**
* Temp folder for single file
*/
private $extractionFolder;
/**
* Active sheet file
*/
private $activeSheetId;
/**
* Default sheet file
*/
private $defaultActiveSheetId = 1;
/**
* Workbook XML file path within zip
*/
private $workbookXmlFile = '/xl/workbook.xml';
/**
* SharedStrings XML file path within zip
*/
private $sharedStringsFile = '/xl/sharedStrings.xml';
/**
* Sheet XML file path within zip
*/
private $SheetBaseFile = '/xl/worksheets/sheet%d.xml';
/**
* SharedStrings XML object
* SimpleXMLElement $sharedStringsObject
*/
private $sharedStringsObject = null;
/**
* SharedStrings XML object
* SimpleXMLElement $activeSheetObject
*/
private $activeSheetObject = null;
private const ERROR_NODE_NOT_FOUND = 10;
public function __construct(string $tempDirectory)
{
$this->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<string, array<int, array<int, string>>> $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 = "<?php\n\n";
$php .= "/**\n";
$php .= " * AUTO-GENERATED FILE\n";
$php .= " * Source: " . basename($excelFilePath) . "\n";
$php .= " */\n\n";
$php .= "return " . var_export($result, true) . ";\n";
if (file_put_contents($outputPhpFile, $php) === false) {
throw new Exception("Failed to write output file: $outputPhpFile");
}
}
}