invoiceninja/tests/Feature/Import/XLS/OfficeParseTest.php

1772 lines
70 KiB
PHP

<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\Import\XLS;
use App\Import\Providers\Csv;
use App\Import\Transformer\BaseTransformer;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Vendor;
use App\Utils\Traits\MakesHash;
use App\Utils\TruthSource;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Tests\MockAccountData;
use Tests\TestCase;
class OfficeParseTest extends TestCase
{
use MakesHash;
use MockAccountData;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(ThrottleRequests::class);
config(['database.default' => config('ninja.db.default')]);
$this->makeTestData();
$this->withoutExceptionHandling();
}
public function test_parse_excel_file()
{
$inputFileType = 'Xlsx';
$inputFileName = base_path('tests/Feature/Import/clients.xlsx');
// Test 1: Verify file exists
$this->assertFileExists($inputFileName, 'Excel file should exist');
// Test 2: Create reader and verify it's valid
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType);
$this->assertInstanceOf(\PhpOffice\PhpSpreadsheet\Reader\Xlsx::class, $reader, 'Reader should be Xlsx type');
// Test 3: Configure reader
$reader->setIgnoreRowsWithNoCells(true);
$reader->setReadDataOnly(true);
// Test 4: Load spreadsheet
$spreadsheet = $reader->load($inputFileName);
$this->assertInstanceOf(\PhpOffice\PhpSpreadsheet\Spreadsheet::class, $spreadsheet, 'Should load as Spreadsheet object');
// Test 5: Verify spreadsheet has content
$this->assertGreaterThan(0, $spreadsheet->getSheetCount(), 'Spreadsheet should have at least one sheet');
// Test 6: Get first worksheet
$worksheet = $spreadsheet->getActiveSheet();
$this->assertInstanceOf(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::class, $worksheet, 'Should have active worksheet');
// Test 7: Verify worksheet has data
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$this->assertGreaterThan(0, $highestRow, 'Worksheet should have at least one row');
$this->assertNotEmpty($highestColumn, 'Worksheet should have at least one column');
// Test 8: Read and validate header row
$headers = [];
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$cellValue = $worksheet->getCell($colLetter . '1')->getValue();
if (!empty($cellValue)) {
$headers[] = $cellValue;
}
}
$this->assertNotEmpty($headers, 'Should have header row');
$this->assertContains('Name', $headers, 'Should contain Name column');
$this->assertContains('Email', $headers, 'Should contain Email column');
// Test 9: Read and validate data rows
$dataRows = [];
for ($row = 2; $row <= $highestRow; $row++) {
$rowData = [];
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$cellValue = $worksheet->getCell($colLetter . $row)->getValue();
$rowData[] = $cellValue;
}
// Only add rows that have at least one non-empty cell
if (array_filter($rowData, function($value) { return !empty($value); })) {
$dataRows[] = $rowData;
}
}
$this->assertNotEmpty($dataRows, 'Should have data rows');
$this->assertGreaterThan(0, count($dataRows), 'Should have at least one data row');
// Test 10: Validate specific data structure
foreach ($dataRows as $index => $rowData) {
$this->assertIsArray($rowData, "Row {$index} should be an array");
// Check that we have at least the name field
$nameIndex = array_search('Name', $headers);
if ($nameIndex !== false && isset($rowData[$nameIndex])) {
$this->assertNotEmpty($rowData[$nameIndex], "Row {$index} should have a name");
}
}
// Test 11: Verify spreadsheet properties
$properties = $spreadsheet->getProperties();
$this->assertInstanceOf(\PhpOffice\PhpSpreadsheet\Document\Properties::class, $properties, 'Should have document properties');
// Test 12: Test cell access methods
$firstCell = $worksheet->getCell('A1');
$this->assertInstanceOf(\PhpOffice\PhpSpreadsheet\Cell\Cell::class, $firstCell, 'Should be able to access individual cells');
// Test 13: Test range access
$range = $worksheet->rangeToArray('A1:' . $highestColumn . '1');
$this->assertIsArray($range, 'Should be able to get range as array');
$this->assertNotEmpty($range, 'Range should not be empty');
// Clean up
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
public function test_parse_excel_file_error_handling()
{
// Test with non-existent file
$nonExistentFile = base_path('tests/Feature/Import/non_existent.xlsx');
$this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class);
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$reader->load($nonExistentFile);
}
public function test_parse_excel_file_invalid_format()
{
// Test with wrong file type
$csvFile = base_path('tests/Feature/Import/clients.csv');
$this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class);
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$reader->load($csvFile);
}
public function test_parse_excel_file_empty_sheet()
{
$inputFileName = base_path('tests/Feature/Import/clients.xlsx');
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$reader->setIgnoreRowsWithNoCells(true);
$spreadsheet = $reader->load($inputFileName);
$worksheet = $spreadsheet->getActiveSheet();
// Test that we can handle empty cells gracefully
$emptyCell = $worksheet->getCell('Z999');
$this->assertInstanceOf(\PhpOffice\PhpSpreadsheet\Cell\Cell::class, $emptyCell);
$this->assertNull($emptyCell->getValue());
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
public function test_parse_excel_file_with_format_detection()
{
$inputFileName = base_path('tests/Feature/Import/clients.xlsx');
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$reader->setReadDataOnly(false); // Important: Keep formatting info
$spreadsheet = $reader->load($inputFileName);
$worksheet = $spreadsheet->getActiveSheet();
// Test cell format detection and data extraction
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
for ($row = 1; $row <= min(5, $highestRow); $row++) {
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$cell = $worksheet->getCell($colLetter . $row);
// Get the raw value (unformatted)
$rawValue = $cell->getValue();
// Get the formatted value (as displayed in Excel)
$formattedValue = $cell->getFormattedValue();
// Get the calculated value (for formulas)
$calculatedValue = $cell->getCalculatedValue();
// Get the data type
$dataType = $cell->getDataType();
// Get the number format
$numberFormat = $cell->getStyle()->getNumberFormat()->getFormatCode();
// Get the cell format type
$formatType = $cell->getStyle()->getNumberFormat()->getBuiltInFormatCode();
// Log cell information for analysis
if (!empty($rawValue)) {
nlog("Cell {$colLetter}{$row}:");
nlog(" Raw Value: " . var_export($rawValue, true));
nlog(" Formatted Value: " . var_export($formattedValue, true));
nlog(" Calculated Value: " . var_export($calculatedValue, true));
nlog(" Data Type: " . $dataType);
nlog(" Number Format: " . $numberFormat);
nlog(" Format Type: " . $formatType);
// Test specific format detection
$this->assertIsString($dataType, 'Data type should be a string');
$this->assertIsString($numberFormat, 'Number format should be a string');
// Test data type constants
$validDataTypes = [
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING,
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC,
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_BOOL,
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NULL,
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_INLINE,
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_ERROR,
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_FORMULA,
];
$this->assertContains($dataType, $validDataTypes, 'Data type should be valid');
}
}
}
// Test specific format detection methods
$this->test_specific_format_detection($worksheet);
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
private function test_specific_format_detection($worksheet)
{
// Test date detection
$dateCell = $worksheet->getCell('A1'); // Assuming first cell might be a date
if ($dateCell->getDataType() === \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC) {
$numberFormat = $dateCell->getStyle()->getNumberFormat()->getFormatCode();
// Check if it's a date format
$isDate = $this->isDateTimeFormat($numberFormat);
if ($isDate) {
$rawValue = $dateCell->getValue();
$formattedDate = $dateCell->getFormattedValue();
nlog("Date Cell Found:");
nlog(" Raw Value (Excel timestamp): " . $rawValue);
nlog(" Formatted Date: " . $formattedDate);
nlog(" Number Format: " . $numberFormat);
// Convert Excel timestamp to PHP DateTime
if (is_numeric($rawValue)) {
$phpDate = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($rawValue);
nlog(" PHP DateTime: " . $phpDate->format('Y-m-d H:i:s'));
$this->assertInstanceOf(\DateTime::class, $phpDate, 'Should convert Excel date to PHP DateTime');
}
}
}
// Test currency detection
$currencyCell = $worksheet->getCell('B1'); // Assuming second cell might be currency
if ($currencyCell->getDataType() === \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC) {
$numberFormat = $currencyCell->getStyle()->getNumberFormat()->getFormatCode();
// Check if it's a currency format
$isCurrency = strpos($numberFormat, '$') !== false ||
strpos($numberFormat, '€') !== false ||
strpos($numberFormat, '£') !== false ||
strpos($numberFormat, '[$') !== false;
if ($isCurrency) {
$rawValue = $currencyCell->getValue();
$formattedCurrency = $currencyCell->getFormattedValue();
nlog("Currency Cell Found:");
nlog(" Raw Value: " . $rawValue);
nlog(" Formatted Currency: " . $formattedCurrency);
nlog(" Number Format: " . $numberFormat);
$this->assertIsNumeric($rawValue, 'Currency raw value should be numeric');
}
}
// Test percentage detection
$percentageCell = $worksheet->getCell('C1'); // Assuming third cell might be percentage
if ($percentageCell->getDataType() === \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC) {
$numberFormat = $percentageCell->getStyle()->getNumberFormat()->getFormatCode();
// Check if it's a percentage format
$isPercentage = strpos($numberFormat, '%') !== false;
if ($isPercentage) {
$rawValue = $percentageCell->getValue();
$formattedPercentage = $percentageCell->getFormattedValue();
nlog("Percentage Cell Found:");
nlog(" Raw Value (decimal): " . $rawValue);
nlog(" Formatted Percentage: " . $formattedPercentage);
nlog(" Number Format: " . $numberFormat);
$this->assertIsNumeric($rawValue, 'Percentage raw value should be numeric');
}
}
}
/**
* Practical helper method showing how to extract and process different data types
*/
public function test_practical_format_extraction()
{
$inputFileName = base_path('tests/Feature/Import/clients.xlsx');
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$reader->setReadDataOnly(false);
$spreadsheet = $reader->load($inputFileName);
$worksheet = $spreadsheet->getActiveSheet();
// Example: Extract and process data with format awareness
$processedData = $this->extractFormattedData($worksheet);
nlog("Processed Data:");
nlog($processedData);
$this->assertIsArray($processedData, 'Should return processed data array');
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Extract data from Excel with format awareness
*/
private function extractFormattedData($worksheet)
{
$data = [];
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
// Get headers first
$headers = [];
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$cellValue = $worksheet->getCell($colLetter . '1')->getValue();
if (!empty($cellValue)) {
$headers[] = $cellValue;
}
}
// Process data rows
for ($row = 2; $row <= $highestRow; $row++) {
$rowData = [];
$hasData = false;
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$cell = $worksheet->getCell($colLetter . $row);
$processedValue = $this->processCellValue($cell);
if ($processedValue !== null) {
$hasData = true;
}
$rowData[] = $processedValue;
}
if ($hasData) {
$data[] = array_combine($headers, $rowData);
}
}
return $data;
}
/**
* Process individual cell value based on its format
*/
private function processCellValue($cell)
{
$rawValue = $cell->getValue();
if (empty($rawValue)) {
return null;
}
$dataType = $cell->getDataType();
$numberFormat = $cell->getStyle()->getNumberFormat()->getFormatCode();
$builtInFormat = $cell->getStyle()->getNumberFormat()->getBuiltInFormatCode();
// Log format information for debugging
nlog("Processing cell with:");
nlog(" Data Type: " . $dataType);
nlog(" Number Format: " . $numberFormat);
nlog(" Built-in Format: " . $builtInFormat);
nlog(" Raw Value: " . var_export($rawValue, true));
switch ($dataType) {
case \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC:
return $this->processNumericValue($cell, $rawValue, $numberFormat);
case \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING:
return $this->processStringValue($cell, $rawValue, $numberFormat);
case \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_BOOL:
return (bool) $rawValue;
case \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_FORMULA:
return $this->processFormulaValue($cell, $rawValue, $numberFormat);
default:
return $rawValue;
}
}
/**
* Process numeric values (dates, currency, percentages, etc.)
*/
private function processNumericValue($cell, $rawValue, $numberFormat)
{
// Check if it's a date
if ($this->isDateTimeFormat($numberFormat)) {
$phpDate = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($rawValue);
return [
'type' => 'date',
'raw' => $rawValue,
'formatted' => $cell->getFormattedValue(),
'php_date' => $phpDate->format('Y-m-d H:i:s'),
'timestamp' => $phpDate->getTimestamp()
];
}
// Check if it's currency
if (strpos($numberFormat, '$') !== false ||
strpos($numberFormat, '€') !== false ||
strpos($numberFormat, '£') !== false ||
strpos($numberFormat, '[$') !== false) {
return [
'type' => 'currency',
'raw' => (float) $rawValue,
'formatted' => $cell->getFormattedValue(),
'decimal' => (float) $rawValue
];
}
// Check if it's percentage
if (strpos($numberFormat, '%') !== false) {
return [
'type' => 'percentage',
'raw' => (float) $rawValue,
'formatted' => $cell->getFormattedValue(),
'decimal' => (float) $rawValue,
'percentage' => (float) $rawValue * 100
];
}
// Regular number
return [
'type' => 'number',
'raw' => (float) $rawValue,
'formatted' => $cell->getFormattedValue(),
'decimal' => (float) $rawValue
];
}
/**
* Process string values with Excel format hints
*/
private function processStringValue($cell, $rawValue, $numberFormat)
{
// Check if Excel suggests this should be numeric despite being stored as string
$shouldBeNumeric = $this->shouldStringBeNumeric($numberFormat, $rawValue);
if ($shouldBeNumeric) {
$extractedValue = $this->extractCurrencyFromString($rawValue);
if ($extractedValue !== null) {
return [
'type' => 'currency_string',
'raw' => (string) $rawValue,
'formatted' => $cell->getFormattedValue(),
'extracted_decimal' => $extractedValue,
'original_string' => (string) $rawValue,
'excel_hint' => $numberFormat
];
}
}
return [
'type' => 'string',
'raw' => (string) $rawValue,
'formatted' => $cell->getFormattedValue(),
'trimmed' => trim((string) $rawValue)
];
}
/**
* Process formula values
*/
private function processFormulaValue($cell, $rawValue, $numberFormat)
{
$calculatedValue = $cell->getCalculatedValue();
return [
'type' => 'formula',
'raw' => $rawValue,
'calculated' => $calculatedValue,
'formatted' => $cell->getFormattedValue(),
'processed' => $this->processNumericValue($cell, $calculatedValue, $numberFormat)
];
}
/**
* Check if a number format code represents a date/time format
*/
private function isDateTimeFormat($formatCode)
{
// Common date/time format patterns
$datePatterns = [
'dd', 'mm', 'yy', 'yyyy', 'm', 'd', 'h', 'hh', 'ss', 'am/pm', 'a/p',
'dd/mm', 'mm/dd', 'dd-mm', 'mm-dd', 'dd/mm/yy', 'dd/mm/yyyy',
'mm/dd/yy', 'mm/dd/yyyy', 'yyyy-mm-dd', 'dd-mm-yyyy',
'h:mm', 'hh:mm', 'h:mm:ss', 'hh:mm:ss', 'm/d/yy h:mm',
'dd/mm/yyyy hh:mm', 'yyyy-mm-dd hh:mm:ss'
];
$formatCode = strtolower($formatCode);
foreach ($datePatterns as $pattern) {
if (strpos($formatCode, strtolower($pattern)) !== false) {
return true;
}
}
return false;
}
/**
* Extract currency value from string using Excel's formatting hints
*/
private function extractCurrencyFromString($value)
{
if (!is_string($value)) {
return null;
}
// Handle different currency formats
$value = trim($value);
// Remove common currency symbols
$currencySymbols = ['$', '€', '£', '¥', '₹', '₽', '₩', '₪', '₦', '₡', '₱', '₲', '₴', '₸', '₺', '₼', '₾', '₿'];
foreach ($currencySymbols as $symbol) {
$value = str_replace($symbol, '', $value);
}
$value = trim($value);
// Check if it's a valid number after removing currency symbols
if (is_numeric($value)) {
return (float) $value;
}
// Handle comma-separated thousands
if (strpos($value, ',') !== false) {
// Check if it's US format (comma as thousands separator)
if (preg_match('/^[0-9,]+\.?[0-9]*$/', $value)) {
$cleaned = str_replace(',', '', $value);
if (is_numeric($cleaned)) {
return (float) $cleaned;
}
}
// Check if it's European format (dot as thousands separator, comma as decimal)
if (preg_match('/^[0-9]+\.[0-9]{3},[0-9]{2}$/', $value)) {
$cleaned = str_replace('.', '', $value);
$cleaned = str_replace(',', '.', $cleaned);
if (is_numeric($cleaned)) {
return (float) $cleaned;
}
}
}
return null;
}
/**
* Check if a string should be treated as numeric based on Excel formatting
*/
private function shouldStringBeNumeric($numberFormat, $rawValue)
{
// If Excel has a numeric format but the value is stored as string
if ($numberFormat !== '@' && is_string($rawValue)) {
// Check if the string looks like a number
return $this->extractCurrencyFromString($rawValue) !== null;
}
return false;
}
/**
* Test currency string extraction specifically
*/
public function test_currency_string_extraction()
{
// Test the currency extraction method directly
$testCases = [
'$313.71' => 313.71,
'$1,234.56' => 1234.56,
'€500.00' => 500.00,
'£250.75' => 250.75,
'¥1000' => 1000.0,
'100.50' => 100.50, // No currency symbol
'Invalid' => null, // Invalid string
'' => null, // Empty string
];
foreach ($testCases as $input => $expected) {
$result = $this->extractCurrencyFromString($input);
$this->assertEquals($expected, $result, "Failed to extract currency from: {$input}");
}
// Test with actual Excel file
$inputFileName = base_path('tests/Feature/Import/clients.xlsx');
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$reader->setReadDataOnly(false);
$spreadsheet = $reader->load($inputFileName);
$worksheet = $spreadsheet->getActiveSheet();
// Look for the problematic cell D3
$cell = $worksheet->getCell('E3');
$rawValue = $cell->getValue();
$formattedValue = $cell->getFormattedValue();
$dataType = $cell->getDataType();
nlog("Cell D3 Analysis:");
nlog(" Raw Value: " . var_export($rawValue, true));
nlog(" Formatted Value: " . var_export($formattedValue, true));
nlog(" Data Type: " . $dataType);
if ($dataType === \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING) {
$extractedCurrency = $this->extractCurrencyFromString($rawValue);
nlog(" Extracted Currency: " . var_export($extractedCurrency, true));
if ($extractedCurrency !== null) {
$this->assertIsFloat($extractedCurrency, 'Should extract numeric value from currency string');
$this->assertEquals(313.71, $extractedCurrency, 'Should extract correct value from "$313.71"');
}
}
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Test currency column extraction and type analysis
*/
public function test_currency_column_analysis()
{
$inputFileName = base_path('tests/Feature/Import/clients.xlsx');
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$reader->setReadDataOnly(false);
$spreadsheet = $reader->load($inputFileName);
$worksheet = $spreadsheet->getActiveSheet();
// Find the Currency column
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
$currencyColumnIndex = null;
$currencyColumnLetter = null;
// Find the Currency column header
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$cellValue = $worksheet->getCell($colLetter . '1')->getValue();
if (strtolower(trim($cellValue)) === 'currency') {
$currencyColumnIndex = $colIndex;
$currencyColumnLetter = $colLetter;
break;
}
}
$this->assertNotNull($currencyColumnIndex, 'Currency column should be found');
nlog("Currency Column Found:");
nlog(" Column Letter: " . $currencyColumnLetter);
nlog(" Column Index: " . $currencyColumnIndex);
// Analyze the Currency column data
$highestRow = $worksheet->getHighestRow();
$currencyData = [];
for ($row = 2; $row <= $highestRow; $row++) {
$cell = $worksheet->getCell($currencyColumnLetter . $row);
$rawValue = $cell->getValue();
$formattedValue = $cell->getFormattedValue();
$dataType = $cell->getDataType();
$numberFormat = $cell->getStyle()->getNumberFormat()->getFormatCode();
$builtInFormat = $cell->getStyle()->getNumberFormat()->getBuiltInFormatCode();
if (!empty($rawValue)) {
$cellInfo = [
'row' => $row,
'raw_value' => $rawValue,
'formatted_value' => $formattedValue,
'data_type' => $dataType,
'number_format' => $numberFormat,
'built_in_format' => $builtInFormat,
'data_type_name' => $this->getDataTypeName($dataType)
];
$currencyData[] = $cellInfo;
nlog("Row {$row}:");
nlog(" Raw Value: " . var_export($rawValue, true));
nlog(" Formatted Value: " . var_export($formattedValue, true));
nlog(" Data Type: " . $dataType . " (" . $this->getDataTypeName($dataType) . ")");
nlog(" Number Format: " . $numberFormat);
nlog(" Built-in Format: " . $builtInFormat);
}
}
// Analyze the data types found
$dataTypes = array_unique(array_column($currencyData, 'data_type'));
$numberFormats = array_unique(array_column($currencyData, 'number_format'));
nlog("Currency Column Analysis:");
nlog(" Total non-empty cells: " . count($currencyData));
nlog(" Data types found: " . implode(', ', $dataTypes));
nlog(" Number formats found: " . implode(', ', $numberFormats));
// Test specific expectations
$this->assertNotEmpty($currencyData, 'Currency column should have data');
// Check if all values are of the same type
if (count($dataTypes) === 1) {
nlog(" All values are of type: " . $dataTypes[0]);
} else {
nlog(" Mixed data types detected: " . implode(', ', $dataTypes));
}
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Get human-readable data type name
*/
private function getDataTypeName($dataType)
{
$typeMap = [
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING => 'String',
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC => 'Numeric',
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_BOOL => 'Boolean',
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NULL => 'Null',
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_INLINE => 'Inline',
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_ERROR => 'Error',
\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_FORMULA => 'Formula',
];
return $typeMap[$dataType] ?? 'Unknown';
}
/**
* Test creating Excel file with localization settings
*/
public function test_create_excel_with_localization()
{
// Create a new spreadsheet
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
// Set up different localization scenarios
$testCases = [
'US_Dollar' => [
'locale' => 'en_US',
'currency_code' => 'USD',
'currency_symbol' => '$',
'number_format' => '#,##0.00',
'date_format' => 'm/d/yyyy',
'values' => [
['Name', 'Amount', 'Date', 'Percentage'],
['John Doe', 1234.56, '2023-12-25', 0.15],
['Jane Smith', -567.89, '2023-12-26', 0.25],
['Bob Wilson', 9999.99, '2023-12-27', 0.75],
]
],
'European_Euro' => [
'locale' => 'de_DE',
'currency_code' => 'EUR',
'currency_symbol' => '€',
'number_format' => '#,##0.00',
'date_format' => 'dd.mm.yyyy',
'values' => [
['Name', 'Amount', 'Date', 'Percentage'],
['Hans Müller', 1234.56, '2023-12-25', 0.15],
['Anna Schmidt', -567.89, '2023-12-26', 0.25],
['Klaus Weber', 9999.99, '2023-12-27', 0.75],
]
],
'UK_Pound' => [
'locale' => 'en_GB',
'currency_code' => 'GBP',
'currency_symbol' => '£',
'number_format' => '#,##0.00',
'date_format' => 'dd/mm/yyyy',
'values' => [
['Name', 'Amount', 'Date', 'Percentage'],
['John Smith', 1234.56, '2023-12-25', 0.15],
['Mary Jones', -567.89, '2023-12-26', 0.25],
['David Brown', 9999.99, '2023-12-27', 0.75],
]
]
];
$currentRow = 1;
foreach ($testCases as $testName => $config) {
nlog("Creating {$testName} section:");
nlog(" Locale: " . $config['locale']);
nlog(" Currency: " . $config['currency_code'] . " (" . $config['currency_symbol'] . ")");
// Add section header
$worksheet->setCellValue("A{$currentRow}", $testName);
$worksheet->getStyle("A{$currentRow}")->getFont()->setBold(true);
$currentRow++;
// Add data with localization
foreach ($config['values'] as $rowIndex => $rowData) {
$row = $currentRow + $rowIndex;
// Set raw values
$worksheet->setCellValue("A{$row}", $rowData[0]); // Name (string)
$worksheet->setCellValue("B{$row}", $rowData[1]); // Amount (numeric)
$worksheet->setCellValue("C{$row}", $rowData[2]); // Date (string, will be converted)
$worksheet->setCellValue("D{$row}", $rowData[3]); // Percentage (numeric)
// Apply currency formatting to Amount column
$currencyFormat = $this->getCurrencyFormat($config['currency_code'], $config['locale']);
$worksheet->getStyle("B{$row}")->getNumberFormat()->setFormatCode($currencyFormat);
// Apply date formatting to Date column
$dateFormat = $this->getDateFormat($config['locale']);
$worksheet->getStyle("C{$row}")->getNumberFormat()->setFormatCode($dateFormat);
// Convert date string to Excel date
if ($rowIndex > 0) { // Skip header row
$dateValue = \PhpOffice\PhpSpreadsheet\Shared\Date::stringToExcel($rowData[2]);
$worksheet->setCellValue("C{$row}", $dateValue);
}
// Apply percentage formatting to Percentage column
$worksheet->getStyle("D{$row}")->getNumberFormat()->setFormatCode('0.00%');
nlog(" Row {$row}:");
nlog(" Raw Amount: " . $rowData[1]);
nlog(" Currency Format: " . $currencyFormat);
nlog(" Date Format: " . $dateFormat);
}
$currentRow += count($config['values']) + 2; // Add spacing between sections
}
// Test reading back the formatted values
$this->test_read_formatted_values($worksheet);
// Save the file
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$outputFile = storage_path('app/test_localized.xlsx');
$writer->save($outputFile);
nlog("Excel file created: " . $outputFile);
$this->assertFileExists($outputFile, 'Localized Excel file should be created');
// Clean up
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Test reading back the formatted values
*/
private function test_read_formatted_values($worksheet)
{
nlog("Reading back formatted values:");
// Read a few cells to verify formatting
$testCells = [
'B2' => 'US Dollar amount',
'B6' => 'European Euro amount',
'B10' => 'UK Pound amount',
'C2' => 'US Date',
'C6' => 'European Date',
'C10' => 'UK Date',
'D2' => 'US Percentage',
'D6' => 'European Percentage',
'D10' => 'UK Percentage',
];
foreach ($testCells as $cellAddress => $description) {
$cell = $worksheet->getCell($cellAddress);
$rawValue = $cell->getValue();
$formattedValue = $cell->getFormattedValue();
$numberFormat = $cell->getStyle()->getNumberFormat()->getFormatCode();
nlog(" {$description} ({$cellAddress}):");
nlog(" Raw Value: " . var_export($rawValue, true));
nlog(" Formatted Value: " . var_export($formattedValue, true));
nlog(" Number Format: " . $numberFormat);
}
}
/**
* Get currency format based on locale and currency code
*/
private function getCurrencyFormat($currencyCode, $locale)
{
$formats = [
'USD' => [
'en_US' => '$#,##0.00',
'en_CA' => '$#,##0.00',
'default' => '$#,##0.00'
],
'EUR' => [
'de_DE' => '#,##0.00 €',
'fr_FR' => '#,##0.00 €',
'it_IT' => '#,##0.00 €',
'es_ES' => '#,##0.00 €',
'default' => '#,##0.00 €'
],
'GBP' => [
'en_GB' => '£#,##0.00',
'default' => '£#,##0.00'
],
'JPY' => [
'ja_JP' => '¥#,##0',
'default' => '¥#,##0'
],
'default' => '#,##0.00'
];
return $formats[$currencyCode][$locale] ??
$formats[$currencyCode]['default'] ??
$formats['default'];
}
/**
* Get date format based on locale
*/
private function getDateFormat($locale)
{
$formats = [
'en_US' => 'm/d/yyyy',
'en_GB' => 'dd/mm/yyyy',
'de_DE' => 'dd.mm.yyyy',
'fr_FR' => 'dd/mm/yyyy',
'it_IT' => 'dd/mm/yyyy',
'es_ES' => 'dd/mm/yyyy',
'ja_JP' => 'yyyy/m/d',
'default' => 'yyyy-mm-dd'
];
return $formats[$locale] ?? $formats['default'];
}
/**
* Test different data injection methods for worksheets
*/
public function test_worksheet_data_injection_methods()
{
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
// Test 1: Single cell injection
$this->test_single_cell_injection($worksheet);
// Test 2: Row-by-row injection (like CSV)
$this->test_row_by_row_injection($worksheet);
// Test 3: Array-based injection (your preferred method)
$this->test_array_based_injection($worksheet);
// Test 4: Range-based injection
$this->test_range_based_injection($worksheet);
// Test 5: Bulk data injection
$this->test_bulk_data_injection($worksheet);
// Test 6: From array with headers
$this->test_from_array_with_headers($worksheet);
// Test 7: From array with formatting
$this->test_from_array_with_formatting($worksheet);
// Test 8: From array with mixed data types
$this->test_from_array_with_mixed_types($worksheet);
// Save the test file
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$outputFile = storage_path('app/test_data_injection.xlsx');
$writer->save($outputFile);
$this->assertFileExists($outputFile, 'Data injection test file should be created');
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Test 1: Single cell injection
*/
private function test_single_cell_injection($worksheet)
{
nlog("=== Test 1: Single Cell Injection ===");
// Method 1: Direct cell assignment
$worksheet->setCellValue('A1', 'Single Cell Test');
$worksheet->setCellValue('B1', 123.45);
$worksheet->setCellValue('C1', '2023-12-25');
// Method 2: Using coordinates
$worksheet->setCellValue('A2', 'Column 1, Row 2');
$worksheet->setCellValue('B2', 456.78);
// Method 3: Using cell object
$cell = $worksheet->getCell('A3');
$cell->setValue('Cell Object Test');
nlog("Single cell injection completed");
}
/**
* Test 2: Row-by-row injection (like CSV)
*/
private function test_row_by_row_injection($worksheet)
{
nlog("=== Test 2: Row-by-Row Injection (CSV Style) ===");
$data = [
['Name', 'Age', 'Salary', 'Start Date'],
['John Doe', 30, 50000, '2023-01-15'],
['Jane Smith', 25, 45000, '2023-02-20'],
['Bob Wilson', 35, 60000, '2023-03-10'],
];
$startRow = 5;
foreach ($data as $rowIndex => $rowData) {
$currentRow = $startRow + $rowIndex;
foreach ($rowData as $colIndex => $value) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex + 1);
$worksheet->setCellValue($colLetter . $currentRow, $value);
}
}
nlog("Row-by-row injection completed");
}
/**
* Test 3: Array-based injection (your preferred method)
*/
private function test_array_based_injection($worksheet)
{
nlog("=== Test 3: Array-Based Injection ===");
$data = [
['Product', 'Price', 'Quantity', 'Total'],
['Laptop', 999.99, 2, 1999.98],
['Mouse', 25.50, 5, 127.50],
['Keyboard', 89.99, 3, 269.97],
];
// Method 1: Using fromArray() - EASIEST for your use case
$worksheet->fromArray($data, null, 'A10');
// Method 2: Using fromArray() with custom null value
$dataWithNulls = [
['Name', 'Email', 'Phone'],
['John', 'john@example.com', null],
['Jane', null, '555-1234'],
];
$worksheet->fromArray($dataWithNulls, 'N/A', 'A15');
nlog("Array-based injection completed");
}
/**
* Test 4: Range-based injection
*/
private function test_range_based_injection($worksheet)
{
nlog("=== Test 4: Range-Based Injection ===");
$data = [
['ID', 'Name', 'Department'],
[1, 'Alice', 'Engineering'],
[2, 'Bob', 'Marketing'],
[3, 'Charlie', 'Sales'],
];
// Method 1: Using rangeToArray() in reverse
$range = 'A20:D23';
$worksheet->fromArray($data, null, 'A20');
// Method 2: Using named ranges
$worksheet->getParent()->addNamedRange(
new \PhpOffice\PhpSpreadsheet\NamedRange('EmployeeData', $worksheet, 'A20:D23')
);
nlog("Range-based injection completed");
}
/**
* Test 5: Bulk data injection
*/
private function test_bulk_data_injection($worksheet)
{
nlog("=== Test 5: Bulk Data Injection ===");
// Large dataset
$bulkData = [];
for ($i = 0; $i < 100; $i++) {
$bulkData[] = [
'ID-' . $i,
'User-' . $i,
rand(1000, 9999),
date('Y-m-d', strtotime("+{$i} days"))
];
}
// Add headers
array_unshift($bulkData, ['ID', 'Name', 'Score', 'Date']);
// Bulk insert
$worksheet->fromArray($bulkData, null, 'A25');
nlog("Bulk data injection completed: " . count($bulkData) . " rows");
}
/**
* Test 6: From array with headers
*/
private function test_from_array_with_headers($worksheet)
{
nlog("=== Test 6: From Array with Headers ===");
$headers = ['Product', 'Category', 'Price', 'Stock'];
$data = [
['Laptop', 'Electronics', 999.99, 50],
['Desk', 'Furniture', 299.99, 25],
['Book', 'Education', 19.99, 100],
];
// Method 1: Separate headers and data
$worksheet->fromArray([$headers], null, 'A130');
$worksheet->fromArray($data, null, 'A131');
// Method 2: Combined with headers
$fullData = array_merge([$headers], $data);
$worksheet->fromArray($fullData, null, 'A140');
nlog("Array with headers injection completed");
}
/**
* Test 7: From array with formatting
*/
private function test_from_array_with_formatting($worksheet)
{
nlog("=== Test 7: From Array with Formatting ===");
$data = [
['Invoice', 'Amount', 'Date', 'Status'],
['INV-001', 1234.56, '2023-12-25', 'Paid'],
['INV-002', 567.89, '2023-12-26', 'Pending'],
['INV-003', 999.99, '2023-12-27', 'Overdue'],
];
// Insert data
$worksheet->fromArray($data, null, 'A150');
// Apply formatting after insertion
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
// Format headers
$worksheet->getStyle('A150:' . $highestColumn . '150')->getFont()->setBold(true);
// Format currency column
$worksheet->getStyle('B151:B' . $highestRow)->getNumberFormat()->setFormatCode('$#,##0.00');
// Format date column
$worksheet->getStyle('C151:C' . $highestRow)->getNumberFormat()->setFormatCode('m/d/yyyy');
// Color code status
$statusColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
for ($row = 151; $row <= $highestRow; $row++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($statusColumn);
$statusCell = $worksheet->getCell($colLetter . $row);
$status = $statusCell->getValue();
switch ($status) {
case 'Paid':
$statusCell->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$statusCell->getStyle()->getFill()->getStartColor()->setRGB('90EE90');
break;
case 'Pending':
$statusCell->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$statusCell->getStyle()->getFill()->getStartColor()->setRGB('FFD700');
break;
case 'Overdue':
$statusCell->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$statusCell->getStyle()->getFill()->getStartColor()->setRGB('FFB6C1');
break;
}
}
nlog("Array with formatting injection completed");
}
/**
* Test 8: From array with mixed data types
*/
private function test_from_array_with_mixed_types($worksheet)
{
nlog("=== Test 8: From Array with Mixed Data Types ===");
$mixedData = [
['Type', 'String', 'Number', 'Date', 'Boolean', 'Formula'],
['Text', 'Hello World', 42, '2023-12-25', true, '=B2*2'],
['Number', '123', 3.14159, '2023-12-26', false, '=SUM(B3:C3)'],
['Date', '2023-12-27', 100, '2023-12-27', true, '=TODAY()'],
];
// Insert mixed data
$worksheet->fromArray($mixedData, null, 'A170');
// Apply type-specific formatting
$highestRow = $worksheet->getHighestRow();
// Format number column
$worksheet->getStyle('C171:C' . $highestRow)->getNumberFormat()->setFormatCode('#,##0.00');
// Format date column
$worksheet->getStyle('D171:D' . $highestRow)->getNumberFormat()->setFormatCode('yyyy-mm-dd');
// Format boolean column
$worksheet->getStyle('E171:E' . $highestRow)->getNumberFormat()->setFormatCode('"Yes";"No"');
nlog("Mixed data types injection completed");
}
/**
* Test CSV-style vs Array-style performance comparison
*/
public function test_csv_vs_array_performance()
{
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$testData = [];
for ($i = 0; $i < 1000; $i++) {
$testData[] = [
'ID-' . $i,
'Name-' . $i,
rand(1000, 9999),
date('Y-m-d', strtotime("+{$i} days")),
rand(100, 999) / 100
];
}
// Test CSV-style (row by row)
$startTime = microtime(true);
$startRow = 1;
foreach ($testData as $rowIndex => $rowData) {
$currentRow = $startRow + $rowIndex;
foreach ($rowData as $colIndex => $value) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex + 1);
$worksheet->setCellValue($colLetter . $currentRow, $value);
}
}
$csvTime = microtime(true) - $startTime;
// Test Array-style
$startTime = microtime(true);
$worksheet->fromArray($testData, null, 'A1001');
$arrayTime = microtime(true) - $startTime;
nlog("Performance Comparison:");
nlog(" CSV-style (row by row): " . round($csvTime * 1000, 2) . "ms");
nlog(" Array-style (fromArray): " . round($arrayTime * 1000, 2) . "ms");
nlog(" Speed improvement: " . round(($csvTime / $arrayTime), 1) . "x faster");
$this->assertLessThan($csvTime, $arrayTime, 'Array method should be faster than CSV method');
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Test column-based formatting
*/
public function test_column_based_formatting()
{
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
// Sample data
$data = [
['Invoice', 'Amount', 'Date', 'Status', 'Percentage'],
['INV-001', 1234.56, '2023-12-25', 'Paid', 0.15],
['INV-002', 567.89, '2023-12-26', 'Pending', 0.25],
['INV-003', 999.99, '2023-12-27', 'Overdue', 0.75],
['INV-004', 2345.67, '2023-12-28', 'Paid', 0.50],
];
// Insert data
$worksheet->fromArray($data, null, 'A1');
nlog("=== Column-Based Formatting Examples ===");
// Method 1: Format entire columns using column letters
$this->test_column_formatting_methods($worksheet);
// Method 2: Format columns with dynamic detection
$this->test_dynamic_column_formatting($worksheet);
// Method 3: Format columns based on header names
$this->test_header_based_column_formatting($worksheet);
// Method 4: Format columns with conditional formatting
$this->test_conditional_column_formatting($worksheet);
// Save the test file
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$outputFile = storage_path('app/test_column_formatting.xlsx');
$writer->save($outputFile);
$this->assertFileExists($outputFile, 'Column formatting test file should be created');
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Test different column formatting methods
*/
private function test_column_formatting_methods($worksheet)
{
nlog("--- Method 1: Direct Column Formatting ---");
// Format entire columns using column letters
$worksheet->getStyle('A:A')->getFont()->setBold(true); // Invoice column
$worksheet->getStyle('B:B')->getNumberFormat()->setFormatCode('$#,##0.00'); // Amount column
$worksheet->getStyle('C:C')->getNumberFormat()->setFormatCode('m/d/yyyy'); // Date column
$worksheet->getStyle('D:D')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER); // Status column
$worksheet->getStyle('E:E')->getNumberFormat()->setFormatCode('0.00%'); // Percentage column
nlog(" Column A (Invoice): Bold font");
nlog(" Column B (Amount): Currency format");
nlog(" Column C (Date): Date format");
nlog(" Column D (Status): Center aligned");
nlog(" Column E (Percentage): Percentage format");
}
/**
* Test dynamic column formatting
*/
private function test_dynamic_column_formatting($worksheet)
{
nlog("--- Method 2: Dynamic Column Formatting ---");
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
// Format each column based on its position
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$range = $colLetter . '1:' . $colLetter . $highestRow;
switch ($colIndex) {
case 1: // Invoice column
$worksheet->getStyle($range)->getFont()->setBold(true);
nlog(" Column {$colLetter}: Bold font");
break;
case 2: // Amount column
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode('$#,##0.00');
nlog(" Column {$colLetter}: Currency format");
break;
case 3: // Date column
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode('m/d/yyyy');
nlog(" Column {$colLetter}: Date format");
break;
case 4: // Status column
$worksheet->getStyle($range)->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
nlog(" Column {$colLetter}: Center aligned");
break;
case 5: // Percentage column
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode('0.00%');
nlog(" Column {$colLetter}: Percentage format");
break;
}
}
}
/**
* Test header-based column formatting
*/
private function test_header_based_column_formatting($worksheet)
{
nlog("--- Method 3: Header-Based Column Formatting ---");
// Get headers from first row
$headers = [];
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$headers[$colIndex] = $worksheet->getCell($colLetter . '1')->getValue();
}
// Format columns based on header names
$highestRow = $worksheet->getHighestRow();
foreach ($headers as $colIndex => $headerName) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$range = $colLetter . '2:' . $colLetter . $highestRow; // Skip header row
switch (strtolower($headerName)) {
case 'invoice':
$worksheet->getStyle($range)->getFont()->setBold(true);
nlog(" Column '{$headerName}': Bold font");
break;
case 'amount':
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode('$#,##0.00');
nlog(" Column '{$headerName}': Currency format");
break;
case 'date':
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode('m/d/yyyy');
nlog(" Column '{$headerName}': Date format");
break;
case 'status':
$worksheet->getStyle($range)->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
nlog(" Column '{$headerName}': Center aligned");
break;
case 'percentage':
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode('0.00%');
nlog(" Column '{$headerName}': Percentage format");
break;
}
}
}
/**
* Test conditional column formatting
*/
private function test_conditional_column_formatting($worksheet)
{
nlog("--- Method 4: Conditional Column Formatting ---");
$highestRow = $worksheet->getHighestRow();
// Format Status column with conditional colors
$statusRange = 'D2:D' . $highestRow;
$conditionalStyles = $worksheet->getStyle($statusRange)->getConditionalStyles();
// Add conditional formatting for Status column
$conditionalStyles[] = new \PhpOffice\PhpSpreadsheet\Style\Conditional();
$conditionalStyles[0]->setConditionType(\PhpOffice\PhpSpreadsheet\Style\Conditional::CONDITION_CELLIS);
$conditionalStyles[0]->setOperatorType(\PhpOffice\PhpSpreadsheet\Style\Conditional::OPERATOR_EQUAL);
$conditionalStyles[0]->addCondition('"Paid"');
$conditionalStyles[0]->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$conditionalStyles[0]->getStyle()->getFill()->getStartColor()->setRGB('90EE90');
$conditionalStyles[] = new \PhpOffice\PhpSpreadsheet\Style\Conditional();
$conditionalStyles[1]->setConditionType(\PhpOffice\PhpSpreadsheet\Style\Conditional::CONDITION_CELLIS);
$conditionalStyles[1]->setOperatorType(\PhpOffice\PhpSpreadsheet\Style\Conditional::OPERATOR_EQUAL);
$conditionalStyles[1]->addCondition('"Pending"');
$conditionalStyles[1]->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$conditionalStyles[1]->getStyle()->getFill()->getStartColor()->setRGB('FFD700');
$conditionalStyles[] = new \PhpOffice\PhpSpreadsheet\Style\Conditional();
$conditionalStyles[2]->setConditionType(\PhpOffice\PhpSpreadsheet\Style\Conditional::CONDITION_CELLIS);
$conditionalStyles[2]->setOperatorType(\PhpOffice\PhpSpreadsheet\Style\Conditional::OPERATOR_EQUAL);
$conditionalStyles[2]->addCondition('"Overdue"');
$conditionalStyles[2]->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$conditionalStyles[2]->getStyle()->getFill()->getStartColor()->setRGB('FFB6C1');
$worksheet->getStyle($statusRange)->setConditionalStyles($conditionalStyles);
nlog(" Status column: Conditional formatting applied");
}
/**
* Test practical column formatting helper
*/
public function test_practical_column_formatting()
{
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
// Your data
$data = [
['Invoice', 'Amount', 'Date', 'Status', 'Percentage'],
['INV-001', 1234.56, '2023-12-25', 'Paid', 0.15],
['INV-002', 567.89, '2023-12-26', 'Pending', 0.25],
['INV-003', 999.99, '2023-12-27', 'Overdue', 0.75],
];
// Insert data
$worksheet->fromArray($data, null, 'A1');
// Apply column formatting using helper method
$this->applyColumnFormatting($worksheet);
// Save
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$outputFile = storage_path('app/test_practical_column_formatting.xlsx');
$writer->save($outputFile);
$this->assertFileExists($outputFile, 'Practical column formatting file should be created');
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Helper method for practical column formatting
*/
private function applyColumnFormatting($worksheet)
{
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
// Define column formats
$columnFormats = [
1 => ['type' => 'text', 'format' => 'bold'],
2 => ['type' => 'currency', 'format' => '$#,##0.00'],
3 => ['type' => 'date', 'format' => 'm/d/yyyy'],
4 => ['type' => 'text', 'format' => 'center'],
5 => ['type' => 'percentage', 'format' => '0.00%'],
];
// Apply formatting to each column
for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
$range = $colLetter . '2:' . $colLetter . $highestRow; // Skip header
if (isset($columnFormats[$colIndex])) {
$format = $columnFormats[$colIndex];
switch ($format['type']) {
case 'currency':
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode($format['format']);
break;
case 'date':
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode($format['format']);
break;
case 'percentage':
$worksheet->getStyle($range)->getNumberFormat()->setFormatCode($format['format']);
break;
case 'text':
if ($format['format'] === 'bold') {
$worksheet->getStyle($range)->getFont()->setBold(true);
} elseif ($format['format'] === 'center') {
$worksheet->getStyle($range)->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
}
break;
}
}
}
nlog("Applied column formatting to " . $highestColumnIndex . " columns");
}
/**
* Test worksheet naming methods
*/
public function test_worksheet_naming_methods()
{
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
nlog("=== Worksheet Naming Methods ===");
// Method 1: Name the active sheet
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle('Tax Report 2023');
nlog("Method 1: Active sheet renamed to: " . $worksheet->getTitle());
// Method 2: Create new worksheet with name
$newWorksheet = $spreadsheet->createSheet();
$newWorksheet->setTitle('Invoice Summary');
nlog("Method 2: New sheet created with name: " . $newWorksheet->getTitle());
// Method 3: Set active sheet by name
$spreadsheet->setActiveSheetIndexByName('Invoice Summary');
$activeSheet = $spreadsheet->getActiveSheet();
nlog("Method 3: Active sheet is now: " . $activeSheet->getTitle());
// Method 4: Get worksheet by name
$taxWorksheet = $spreadsheet->getSheetByName('Tax Report 2023');
nlog("Method 4: Retrieved sheet by name: " . $taxWorksheet->getTitle());
// Method 5: List all worksheet names
$sheetNames = [];
foreach ($spreadsheet->getAllSheets() as $sheet) {
$sheetNames[] = $sheet->getTitle();
}
nlog("Method 5: All sheet names: " . implode(', ', $sheetNames));
// Method 6: Check if worksheet exists by name
$exists = $spreadsheet->getSheetByName('Tax Report 2023') !== null;
nlog("Method 6: 'Tax Report 2023' exists: " . ($exists ? 'Yes' : 'No'));
// Method 7: Remove worksheet by name
$spreadsheet->removeSheetByIndex(1); // Remove the second sheet
nlog("Method 7: Removed second sheet");
// Method 8: Rename worksheet
$worksheet->setTitle('Updated Tax Report 2023');
nlog("Method 8: Renamed to: " . $worksheet->getTitle());
// Method 9: Get worksheet index by name
$index = $spreadsheet->getIndex($spreadsheet->getSheetByName('Updated Tax Report 2023'));
nlog("Method 9: Sheet index: " . $index);
// Method 10: Validate worksheet name
$validName = $this->validateWorksheetName('Tax Report 2023');
nlog("Method 10: Valid worksheet name: " . ($validName ? 'Yes' : 'No'));
// Test specific expectations
$this->assertEquals('Updated Tax Report 2023', $worksheet->getTitle(), 'Worksheet should be renamed');
$this->assertEquals(1, $spreadsheet->getSheetCount(), 'Should have only one sheet after removal');
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Test practical worksheet naming scenarios
*/
public function test_practical_worksheet_naming()
{
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
nlog("=== Practical Worksheet Naming Scenarios ===");
// Scenario 1: Tax Report with multiple sheets
$taxWorksheet = $spreadsheet->getActiveSheet();
$taxWorksheet->setTitle('Tax Summary');
// Add data to tax sheet
$taxWorksheet->fromArray([
['Tax Period', 'Amount', 'Status'],
['Q1 2023', 5000.00, 'Paid'],
['Q2 2023', 7500.00, 'Pending'],
], null, 'A1');
// Scenario 2: Create Invoice Details sheet
$invoiceWorksheet = $spreadsheet->createSheet();
$invoiceWorksheet->setTitle('Invoice Details');
// Add data to invoice sheet
$invoiceWorksheet->fromArray([
['Invoice #', 'Client', 'Amount', 'Date'],
['INV-001', 'Client A', 1500.00, '2023-01-15'],
['INV-002', 'Client B', 2300.00, '2023-01-20'],
], null, 'A1');
// Scenario 3: Create Summary sheet
$summaryWorksheet = $spreadsheet->createSheet();
$summaryWorksheet->setTitle('Summary');
// Add summary data
$summaryWorksheet->fromArray([
['Report Type', 'Total Amount', 'Record Count'],
['Tax Summary', 12500.00, 2],
['Invoice Details', 3800.00, 2],
], null, 'A1');
// Scenario 4: Set Summary as active sheet
$spreadsheet->setActiveSheetIndexByName('Summary');
$activeSheet = $spreadsheet->getActiveSheet();
nlog("Active sheet is now: " . $activeSheet->getTitle());
// Scenario 5: List all sheets
$allSheets = [];
foreach ($spreadsheet->getAllSheets() as $sheet) {
$allSheets[] = $sheet->getTitle();
}
nlog("All sheets: " . implode(', ', $allSheets));
// Test expectations
$this->assertEquals(3, $spreadsheet->getSheetCount(), 'Should have 3 sheets');
$this->assertEquals('Summary', $spreadsheet->getActiveSheet()->getTitle(), 'Summary should be active');
// Save the test file
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$outputFile = storage_path('app/test_worksheet_naming.xlsx');
$writer->save($outputFile);
$this->assertFileExists($outputFile, 'Worksheet naming test file should be created');
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
/**
* Validate worksheet name (Excel has restrictions)
*/
private function validateWorksheetName($name)
{
// Excel worksheet name restrictions
$restrictedChars = ['\\', '/', '*', '?', ':', '[', ']'];
$maxLength = 31;
// Check for restricted characters
foreach ($restrictedChars as $char) {
if (strpos($name, $char) !== false) {
return false;
}
}
// Check length
if (strlen($name) > $maxLength) {
return false;
}
// Check if name is empty
if (empty(trim($name))) {
return false;
}
return true;
}
}