Files
citrus-cms/resources/views/guide/excel-row-handler-guide.md
T
2026-04-28 21:15:09 +03:00

14 KiB

ExcelRowHandler Service Guide

Overview

The ExcelRowHandler service is a modern, object-oriented replacement for the addRowInTable helper function. It provides a clean and maintainable way to add formatted rows to Excel sheets in the Register Creator system.

Location

  • Service Class: App\Services\RegisterCreator\ExcelRowHandler
  • Integration: App\Services\RegisterCreator\DocumentProcessors\AbstractDocumentProcessor

Features

  • Duplicate Detection: Automatically prevents duplicate report entries
  • File Management: Handles PDF and XLSX file copying with smart overwrite logic
  • Page Counting: Automatically counts PDF pages using pdftk
  • Excel Formatting: Preserves styles, formulas, merged cells, and row heights
  • Comprehensive Logging: Provides detailed logging with emoji indicators
  • Statistics Tracking: Tracks file types and successful operations
  • Type Safety: Full PHP type hints for better IDE support and error prevention

Architecture

Service Class: ExcelRowHandler

The main service class handles all row addition operations:

use App\Services\RegisterCreator\ExcelRowHandler;

$handler = new ExcelRowHandler();

$newRow = $handler->addRow(
    $search,              // Array of file paths to search
    $selectDocument,      // Document information array
    $fullFolder,          // Target folder path
    $lineNumber,          // Line identifier
    $documentDate,        // Document date
    $rowNo,              // Row sequence number
    $sheet,              // PhpSpreadsheet Worksheet object
    $currentRow,         // Current row position
    $override            // Whether to overwrite existing files
);

Integration with AbstractDocumentProcessor

The service is integrated into AbstractDocumentProcessor for easy use in all document processors:

protected function addRowToExcel(
    array $search,
    string $lineNumber,
    string $documentDate,
    int $rowNo
): int

Usage in Document Processors

Basic Usage Example

Here's how to use the service in a document processor:

class MyDocumentProcessor extends AbstractDocumentProcessor
{
    public function process(
        array $weldLogData,
        array $document,
        Worksheet $sheet,
        int &$currentRow,
        array $settings
    ): int {
        // Initialize processor (this creates the ExcelRowHandler instance)
        $this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
        
        // Get line identifier
        $lineIdentifier = $weldLogData[$this->registerColumnBased];
        
        // Search for files
        $search = $this->searchFiles("{$document['path']}/*{$lineIdentifier}*.pdf");
        
        // Get document date
        $placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
        $documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
        
        // Add row to Excel using the service
        $newRow = $this->addRowToExcel(
            $search,
            $lineIdentifier,
            $documentDate,
            1  // Row number in sequence
        );
        
        if ($newRow > $currentRow) {
            $currentRow = $newRow;
        }
        
        return $currentRow;
    }
}

Real Example: DrawingsProcessor

class DrawingsProcessor extends AbstractDocumentProcessor
{
    public function process(
        array $weldLogData,
        array $document,
        Worksheet $sheet,
        int &$currentRow,
        array $settings
    ): int {
        $this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
        
        $this->log("Processing Drawings");
        
        $lineIdentifier = $weldLogData[$this->registerColumnBased];
        $normalized = $this->normalizeSearchTerm($lineIdentifier);
        $search = $this->searchFiles("{$document['path']}/*{$normalized}*.pdf");
        
        $placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
        $documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
        
        // Use the new ExcelRowHandler service method
        $newRow = $this->addRowToExcel(
            $search,
            $lineIdentifier,
            $documentDate,
            1
        );
        
        if ($newRow > $currentRow) {
            $currentRow = $newRow;
        }
        
        return $currentRow;
    }
}

Method Parameters

addRow() Method

Parameter Type Description
$search array Array of file paths to search for
$selectDocument array Document information containing title, path, type, order, etc.
$fullFolder string Target folder path (e.g., "storage/documents/project/line/")
$lineNumber string Line number or identifier
$documentDate string Document date
$rowNo int Row sequence number
$sheet Worksheet PhpSpreadsheet Worksheet object (passed by reference)
$currentRow int Current row position in Excel
$override bool Whether to overwrite existing files (default: false)

Returns: int - Next row position

addRowToExcel() Helper Method

Parameter Type Description
$search array Array of file paths to search for
$lineNumber string Line number or identifier
$documentDate string Document date
$rowNo int Row sequence number

Returns: int - Next row position

Document Information Structure

The $selectDocument array should contain:

[
    'title1' => 'Main title',
    'title2' => 'NDT Report Number',
    'title3' => 'PDF Document Title',
    'title4' => 'NDT Register Title',
    'path' => 'storage/documents/...',
    'type' => 'drawing|template|...',
    'order' => 0,  // Display order
    'file_name' => 'Custom file name (optional)',
    'incoming_control_description' => 'Description (optional)'
]

Excel Template Placeholders

The service replaces the following placeholders in the Excel template:

Placeholder Description
{rowNo} Row sequence number
{type_order_no} Document type order number (e.g., 1 for QA, 2 for WDB)
{sub_number} Sub-number within document type (1, 2, 3...)
{full_number} Combined number in format "type_order_no.sub_number" (e.g., "1.1", "1.2", "3.1")
{rowTitle} Document title (processed)
{documentReportNumber} Line number/identifier
{documentDate} Formatted document date
{constructor} Contractor name
{pageCount} Number of pages in PDF
{pageIndex} Page range (e.g., "1-3")
{title1} Document title 1
{ndt_report_no} NDT report number (title2)
{pdf_document_title} PDF document title (title3)
{ndt_register_title} NDT register title (title4)

Log Format

The service generates structured logs with emoji indicators:

📋 1. Document Title - LINE001 | 1 file(s)
   -- filename.pdf
   📁 File: copied
   
📋 2. Another Document - LINE002 | 2 file(s)
   -- document1.pdf
   -- document2.pdf
   📁 File: updated
   📊 XLSX: copied

❌ 3. Missing Document - LINE003 | NO FILES FOUND
   Search paths:
   -- expected_filename.pdf

🔄 DUPLICATE: Report Title - LINE001

Log Indicators

  • 📋 = Document processed
  • = Success
  • = Error or not found
  • 🔄 = Duplicate (skipped)
  • 📁 = PDF file operation
  • 📊 = XLSX file operation
  • = Warning or missing optional file

File Copy Status

  • copied = New file copied
  • updated = File content changed, updated
  • overwritten = File forcefully overwritten (override=true)
  • same content, skipped = File exists with same content, no action needed

File Management

PDF Files

All PDF files are automatically:

  1. Validated for existence
  2. Page counted using pdftk
  3. Copied to target location
  4. Named according to order and title

XLSX Files (Templates)

For documents with type="template":

  1. Searches for corresponding .xlsx file
  2. Copies alongside PDF if found
  3. Logs result (found/not found)

Smart Copy Logic

The service uses MD5 hash comparison to avoid unnecessary file operations:

  • If file exists and content is identical → skip
  • If file exists and content differs → update
  • If override flag is true → always overwrite

Statistics

The service tracks statistics that can be accessed statically:

// Get file type statistics
$stats = ExcelRowHandler::getFileTypeStats();
// Returns: ['pdf' => 150, 'xlsx' => 25, ...]

// Get successful operations count
$count = ExcelRowHandler::getSuccessfulOperations();
// Returns: 150

// Reset statistics
ExcelRowHandler::resetStatistics();

Duplicate Prevention

The service automatically prevents duplicate entries based on:

  • Document title (title2)
  • Line number

If a duplicate is detected:

  1. Row is skipped
  2. Duplicate log is written
  3. Original row position is returned (no increment)

Error Handling

The service handles errors gracefully:

try {
    $newRow = $this->addRowToExcel($search, $lineNumber, $documentDate, 1);
} catch (\Throwable $th) {
    // Error is logged automatically
    // Exception is re-thrown for upstream handling
}

Error Logging

All errors are logged to:

  1. Laravel log (with context)
  2. Project-specific log.txt file
  3. Console output

Cache Dependencies

The service relies on cached values:

Cache Key Description
rc_contractor Contractor name
rc_firstPage First page number
rc_lastPage Last page number
rc_template_row Template row number

These are managed by RegisterCreatorService.

Migration from addRowInTable Helper

Old Way (Helper Function)

$newRow = addRowInTable(
    $search,
    $document,
    $this->getFullFolder(),
    $lineIdentifier,
    $documentDate,
    1,
    $this->sheet,
    $currentRow,
    $this->settings['override'] ?? true
);

New Way (Service Method)

$newRow = $this->addRowToExcel(
    $search,
    $lineIdentifier,
    $documentDate,
    1
);

Benefits of Migration

  1. Cleaner Code: Less parameters, more intuitive
  2. Better Testability: Service can be mocked and tested
  3. Type Safety: Full type hints
  4. Object-Oriented: Follows SOLID principles
  5. Easier Maintenance: All logic in one place
  6. Better IDE Support: Autocomplete and hints

Best Practices

1. Always Initialize

$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);

This creates the ExcelRowHandler instance and sets up necessary properties.

2. Update Current Row

$newRow = $this->addRowToExcel(...);

if ($newRow > $currentRow) {
    $currentRow = $newRow;
}

Always check if row was actually added before updating position.

3. Handle Search Results

$search = $this->searchFiles("{$document['path']}/*{$normalized}*.pdf");

if (empty($search)) {
    $this->log("No files found for pattern", "warning");
    return $currentRow;
}

Check if files exist before calling addRowToExcel.

4. Use Normalized Search Terms

$normalized = $this->normalizeSearchTerm($lineIdentifier);
$search = $this->searchFiles("{$document['path']}/*{$normalized}*.pdf");

Normalize search terms for better file matching.

5. Log Processing Steps

$this->log("Processing Drawings");
$newRow = $this->addRowToExcel(...);
$this->log("Drawings processed successfully");

Use the log() method for consistent logging.

Troubleshooting

Files Not Found

  1. Check file paths in search array
  2. Verify storage permissions
  3. Check if files exist in expected location
  4. Review log.txt for exact search paths

Duplicate Detection Issues

  1. Check title2 field consistency
  2. Verify line number format
  3. Reset statistics if testing: ExcelRowHandler::resetStatistics()

Page Count Issues

  1. Ensure pdftk is installed: which pdftk
  2. Check LANG environment variable
  3. Verify PDF file is not corrupted

Excel Formatting Issues

  1. Verify template row is set correctly
  2. Check placeholder names match exactly
  3. Ensure template has proper merged cells

Performance Considerations

  1. File Operations: Uses Laravel Storage for efficiency
  2. MD5 Comparison: Only calculates when necessary
  3. Static Properties: Shared across instances for statistics
  4. Lazy Loading: PDFs only opened when needed

Testing

Example test structure:

use Tests\TestCase;
use App\Services\RegisterCreator\ExcelRowHandler;

class ExcelRowHandlerTest extends TestCase
{
    public function test_adds_row_successfully()
    {
        $handler = new ExcelRowHandler();
        
        // Setup test data
        $search = ['storage/documents/test.pdf'];
        $document = ['title2' => 'Test', 'order' => 0, ...];
        
        // Execute
        $newRow = $handler->addRow(
            $search,
            $document,
            'storage/documents/test/',
            'LINE001',
            '2024-01-01',
            1,
            $this->mockSheet,
            10,
            false
        );
        
        // Assert
        $this->assertEquals(11, $newRow);
    }
}

Version History

  • v1.0 (2024-10): Initial release
    • Migrated from addRowInTable helper function
    • Added full type hints
    • Improved error handling
    • Added statistics tracking