如何设计支持XLSX及含XLSX的ZIP文件的FileProcessor?
Inheritance here would violate the Single Responsibility Principle (SRP)—your existing FileProcessor is responsible for mapping spreadsheet rows to your desired structure, while handling ZIP files involves a separate, distinct concern: extracting content from an archive. Instead, you should use composition or extend your existing FileReader interface to handle ZIP archives, keeping each component focused on one job.
Let’s Break Down the Better Approach
First, let’s fix a tight coupling in your current design: right now FileProcessor directly uses XlsxFileReader. Refactor it to depend on the FileReader interface instead—this makes it flexible to work with any reader implementation (including our upcoming ZIP reader).
Step 1: Refactor FileProcessor to Depend on FileReader Interface
class FileProcessor { private FileReader $reader; private DataMapper $mapper; // Inject FileReader instead of hardcoding XlsxFileReader public function __construct(FileReader $reader, DataMapper $mapper) { $this->reader = $reader; $this->mapper = $mapper; } public function processFile(string $filePath, string $sheetName): array { $rows = $this->reader->readSheet($filePath, $sheetName); return $this->mapper->mapRows($rows); } }
Step 2: Create a ZipFileReader Implementing FileReader
This class will handle extracting the XLSX from the ZIP, then delegate to XlsxFileReader to read the sheet. It keeps the ZIP extraction logic contained within a dedicated reader, which aligns perfectly with SRP.
class ZipFileReader implements FileReader { private XlsxFileReader $xlsxReader; public function __construct(XlsxFileReader $xlsxReader) { $this->xlsxReader = $xlsxReader; } public function readSheet(string $zipFilePath, string $sheetName): array { // Open the ZIP archive $zip = new ZipArchive(); if ($zip->open($zipFilePath) !== true) { throw new Exception("Failed to open ZIP file: $zipFilePath"); } // Locate the first XLSX file in the ZIP (or let users specify the internal path if needed) $xlsxEntryPath = null; for ($i = 0; $i < $zip->numFiles; $i++) { $entryName = $zip->getNameIndex($i); if (strtolower(pathinfo($entryName, PATHINFO_EXTENSION)) === 'xlsx') { $xlsxEntryPath = $entryName; break; } } if (!$xlsxEntryPath) { throw new Exception("No XLSX file found in ZIP archive"); } // Extract XLSX to a temporary file (PHPSpreadsheet works better with physical files) $tempFilePath = tempnam(sys_get_temp_dir(), 'zip_xlsx_'); file_put_contents($tempFilePath, $zip->getFromName($xlsxEntryPath)); $zip->close(); try { // Delegate to XlsxFileReader to handle the actual sheet reading return $this->xlsxReader->readSheet($tempFilePath, $sheetName); } finally { // Clean up temporary file to avoid clutter unlink($tempFilePath); } } }
Step 3: Use the New Components Together
Now you can process both raw XLSX files and ZIP-contained XLSX files using the same FileProcessor:
// Process a regular XLSX file $xlsxReader = new XlsxFileReader(); $processor = new FileProcessor($xlsxReader, new DataMapper()); $rawXlsxResult = $processor->processFile('data.xlsx', 'Sheet1'); // Process an XLSX inside a ZIP file $zipReader = new ZipFileReader($xlsxReader); $zipProcessor = new FileProcessor($zipReader, new DataMapper()); $zipXlsxResult = $zipProcessor->processFile('data.zip', 'Sheet1');
What If the ZIP Contains Multiple XLSX Files?
If you need to handle multiple XLSX entries in one ZIP, create a dedicated ZipBatchProcessor that extracts all XLSX files and uses your existing FileProcessor on each:
class ZipBatchProcessor { private FileProcessor $processor; public function __construct(FileProcessor $processor) { $this->processor = $processor; } public function processZip(string $zipFilePath, string $sheetName): array { $zip = new ZipArchive(); $zip->open($zipFilePath); $results = []; for ($i = 0; $i < $zip->numFiles; $i++) { $entryName = $zip->getNameIndex($i); if (strtolower(pathinfo($entryName, PATHINFO_EXTENSION)) !== 'xlsx') { continue; } $tempFilePath = tempnam(sys_get_temp_dir(), 'zip_xlsx_'); file_put_contents($tempFilePath, $zip->getFromName($entryName)); try { $results[$entryName] = $this->processor->processFile($tempFilePath, $sheetName); } finally { unlink($tempFilePath); } } $zip->close(); return $results; } }
Why Not Inheritance?
Inheriting FileProcessor for ZIP handling would force your ZIP processor to take on two unrelated responsibilities: extracting archives and mapping rows. This makes the code harder to maintain—if you need to update ZIP extraction logic (like supporting password-protected archives), you’re touching the same class that handles data mapping. Composition keeps concerns separated, making each class easier to test, debug, and modify independently.
内容的提问来源于stack exchange,提问作者Jideobi Benedine Ofomah

