• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Implementing automated compliance reporting for custom real estate agent listings ledgers using custom PhpSpreadsheet components

Implementing automated compliance reporting for custom real estate agent listings ledgers using custom PhpSpreadsheet components

Automating Real Estate Ledger Compliance with PhpSpreadsheet

For real estate agencies operating with custom listing ledgers, maintaining auditable and compliant records is paramount. This often involves generating detailed reports that adhere to specific regulatory or internal audit requirements. Manually compiling these reports from disparate data sources is not only time-consuming but also prone to human error, jeopardizing compliance. This post details a robust, automated approach using PHP and the PhpSpreadsheet library to generate custom compliance reports directly from your listing data.

Data Model and Compliance Requirements

Our hypothetical scenario involves a real estate agency tracking property listings. A typical ledger might include fields such as: Listing ID, Property Address, Agent Name, Date Listed, Date Sold, Sale Price, Commission Rate, Commission Earned, Client Name, Client Contact, and Compliance Status (e.g., ‘Pending Review’, ‘Approved’, ‘Rejected’).

For compliance reporting, we need to generate a report that:

  • Summarizes all listings within a specified date range.
  • Highlights listings that are ‘Pending Review’ or ‘Rejected’, flagging them for immediate attention.
  • Calculates total commission earned for approved listings.
  • Includes agent-specific performance metrics (total sales, total commission).
  • Ensures data integrity by cross-referencing key fields.

Leveraging PhpSpreadsheet for Custom Reporting

PhpSpreadsheet is a powerful PHP library for reading and writing spreadsheet files. It supports various formats, including XLSX, ODS, CSV, and HTML. For compliance reporting, XLSX is ideal due to its rich formatting capabilities and widespread compatibility.

We’ll create a custom reporting component that fetches data from our database, processes it, and then uses PhpSpreadsheet to construct a well-formatted, informative XLSX report.

Setting Up the Reporting Class

First, ensure you have PhpSpreadsheet installed via Composer:

composer require phpoffice/phpspreadsheet

Now, let’s define a `ComplianceReportGenerator` class. This class will encapsulate the logic for data retrieval and report generation.

Database Interaction (Conceptual)

For this example, we’ll assume a PDO-based database connection. The actual data retrieval logic will depend on your specific database schema and ORM, if any. The key is to fetch the necessary data efficiently.

The `ComplianceReportGenerator` Class

This class will handle the core reporting logic. We’ll inject a database connection (or a data access object) and use PhpSpreadsheet’s `Spreadsheet` and `Writer\Xlsx` classes.

`src/Reporting/ComplianceReportGenerator.php`

<?php

namespace App\Reporting;

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PDO;
use DateTime;

class ComplianceReportGenerator
{
    private PDO $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    /**
     * Generates the compliance report for a given date range.
     *
     * @param DateTime $startDate
     * @param DateTime $endDate
     * @return string The path to the generated XLSX file.
     * @throws \Exception
     */
    public function generateReport(DateTime $startDate, DateTime $endDate): string
    {
        $spreadsheet = new Spreadsheet();
        $sheet = $spreadsheet->getActiveSheet();

        // --- 1. Fetch Data ---
        $listings = $this->fetchListings($startDate, $endDate);

        // --- 2. Prepare Report Structure and Styles ---
        $this->applyStyles($sheet);
        $this->addHeaders($sheet);

        // --- 3. Populate Data Rows ---
        $this->populateData($sheet, $listings);

        // --- 4. Add Summary and Totals ---
        $this->addSummary($sheet, $listings);

        // --- 5. Save the Report ---
        $filename = $this->generateFilename($startDate, $endDate);
        $writer = new Xlsx($spreadsheet);
        $writer->save($filename);

        return $filename;
    }

    /**
     * Fetches listing data from the database.
     *
     * @param DateTime $startDate
     * @param DateTime $endDate
     * @return array
     */
    private function fetchListings(DateTime $startDate, DateTime $endDate): array
    {
        $sql = "
            SELECT
                listing_id,
                property_address,
                agent_name,
                date_listed,
                date_sold,
                sale_price,
                commission_rate,
                commission_earned,
                client_name,
                compliance_status
            FROM
                listings
            WHERE
                date_listed BETWEEN :start_date AND :end_date
            ORDER BY
                date_listed ASC
        ";

        $stmt = $this->db->prepare($sql);
        $stmt->bindValue(':start_date', $startDate->format('Y-m-d'));
        $stmt->bindValue(':end_date', $endDate->format('Y-m-d'));
        $stmt->execute();

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Applies basic styling to the worksheet.
     *
     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet
     */
    private function applyStyles($sheet): void
    {
        // Set default font
        $sheet->getDefaultStyle()->getFont()->setName('Arial')->setSize(10);

        // Column widths (adjust as needed)
        $sheet->getColumnDimension('A')->setWidth(12); // Listing ID
        $sheet->getColumnDimension('B')->setWidth(30); // Address
        $sheet->getColumnDimension('C')->setWidth(20); // Agent
        $sheet->getColumnDimension('D')->setWidth(12); // Date Listed
        $sheet->getColumnDimension('E')->setWidth(12); // Date Sold
        $sheet->getColumnDimension('F')->setWidth(15); // Sale Price
        $sheet->getColumnDimension('G')->setWidth(12); // Commission Rate
        $sheet->getColumnDimension('H')->setWidth(15); // Commission Earned
        $sheet->getColumnDimension('I')->setWidth(25); // Client
        $sheet->getColumnDimension('J')->setWidth(15); // Compliance Status

        // Header style
        $headerStyle = [
            'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
            'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '4F81BD']],
            'borders' => ['bottom' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => '000000']]],
            'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
        ];
        $sheet->getStyle('A1:J1')->applyFromArray($headerStyle);

        // Currency format
        $currencyFormat = '#,##0.00';
        $sheet->getStyle('F2:F1000')->getNumberFormat()->setFormatCode($currencyFormat); // Adjust range as needed
        $sheet->getStyle('H2:H1000')->getNumberFormat()->setFormatCode($currencyFormat);

        // Percentage format
        $sheet->getStyle('G2:G1000')->getNumberFormat()->setFormatCode('0.00%');

        // Date format
        $sheet->getStyle('D2:D1000')->getNumberFormat()->setFormatCode('yyyy-mm-dd');
        $sheet->getStyle('E2:E1000')->getNumberFormat()->setFormatCode('yyyy-mm-dd');
    }

    /**
     * Adds the report headers.
     *
     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet
     */
    private function addHeaders($sheet): void
    {
        $headers = [
            'Listing ID', 'Property Address', 'Agent Name', 'Date Listed', 'Date Sold',
            'Sale Price', 'Commission Rate', 'Commission Earned', 'Client Name', 'Compliance Status'
        ];
        $sheet->fromArray([$headers], null, 'A1');
    }

    /**
     * Populates the data rows in the spreadsheet.
     *
     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet
     * @param array $listings
     */
    private function populateData($sheet, array $listings): void
    {
        $rowNum = 2; // Start from the second row after headers
        $complianceIssues = [];
        $totalCommissionEarned = 0;
        $approvedListingsCount = 0;

        foreach ($listings as $listing) {
            $rowData = [
                $listing['listing_id'],
                $listing['property_address'],
                $listing['agent_name'],
                $listing['date_listed'] ? new DateTime($listing['date_listed']) : '',
                $listing['date_sold'] ? new DateTime($listing['date_sold']) : '',
                $listing['sale_price'] ?? 0,
                $listing['commission_rate'] ?? 0,
                $listing['commission_earned'] ?? 0,
                $listing['client_name'],
                $listing['compliance_status'],
            ];
            $sheet->fromArray([$rowData], null, 'A' . $rowNum);

            // --- Compliance Checks ---
            if (in_array($listing['compliance_status'], ['Pending Review', 'Rejected'])) {
                $complianceIssues[] = $listing;
                // Highlight row for compliance issues
                $sheet->getStyle('A' . $rowNum . ':J' . $rowNum)->applyFromArray([
                    'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFC7CE']], // Light red fill
                    'font' => ['color' => ['rgb' => '9C0006']], // Dark red font
                ]);
            }

            // --- Summary Calculations ---
            if ($listing['compliance_status'] === 'Approved') {
                $totalCommissionEarned += (float)($listing['commission_earned'] ?? 0);
                $approvedListingsCount++;
            }

            $rowNum++;
        }

        // Store compliance issues for summary section
        $sheet->getProperties()->setCustomProperty('complianceIssues', $complianceIssues);
        $sheet->getProperties()->setCustomProperty('totalCommissionEarned', $totalCommissionEarned);
        $sheet->getProperties()->setCustomProperty('approvedListingsCount', $approvedListingsCount);
    }

    /**
     * Adds a summary section to the report.
     *
     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet
     * @param array $listings
     */
    private function addSummary($sheet, array $listings): void
    {
        $currentRow = $sheet->getHighestRow() + 3; // Start summary below data

        // Summary Title
        $sheet->setCellValue('A' . $currentRow, 'Compliance Summary');
        $sheet->getStyle('A' . $currentRow)->getFont()->setBold(true)->setSize(14);
        $currentRow++;

        // Retrieve calculated values from custom properties
        $totalCommissionEarned = $sheet->getProperties()->getCustomProperty('totalCommissionEarned') ?? 0;
        $approvedListingsCount = $sheet->getProperties()->getCustomProperty('approvedListingsCount') ?? 0;
        $complianceIssues = $sheet->getProperties()->getCustomProperty('complianceIssues') ?? [];

        // Summary Rows
        $sheet->setCellValue('A' . $currentRow, 'Total Approved Listings:');
        $sheet->setCellValue('B' . $currentRow, $approvedListingsCount);
        $sheet->getStyle('A' . $currentRow)->getFont()->setBold(true);
        $currentRow++;

        $sheet->setCellValue('A' . $currentRow, 'Total Commission Earned (Approved):');
        $sheet->setCellValue('B' . $currentRow, $totalCommissionEarned);
        $sheet->getStyle('A' . $currentRow)->getFont()->setBold(true);
        $sheet->getStyle('B' . $currentRow)->getNumberFormat()->setFormatCode('#,##0.00'); // Apply currency format
        $currentRow++;

        // Compliance Issues Section
        if (!empty($complianceIssues)) {
            $currentRow++; // Add a blank row for spacing
            $sheet->setCellValue('A' . $currentRow, 'Listings Requiring Attention:');
            $sheet->getStyle('A' . $currentRow)->getFont()->setBold(true)->setSize(12);
            $currentRow++;

            // Headers for compliance issues table
            $complianceHeaders = ['Listing ID', 'Property Address', 'Agent', 'Status'];
            $sheet->fromArray([$complianceHeaders], null, 'A' . $currentRow);
            $sheet->getStyle('A' . $currentRow . ':D' . $currentRow)->getFont()->setBold(true);
            $currentRow++;

            // Populate compliance issues data
            foreach ($complianceIssues as $issue) {
                $sheet->setCellValue('A' . $currentRow, $issue['listing_id']);
                $sheet->setCellValue('B' . $currentRow, $issue['property_address']);
                $sheet->setCellValue('C' . $currentRow, $issue['agent_name']);
                $sheet->setCellValue('D' . $currentRow, $issue['compliance_status']);
                // Apply same highlighting as in data rows
                $sheet->getStyle('A' . $currentRow . ':D' . $currentRow)->applyFromArray([
                    'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFC7CE']],
                    'font' => ['color' => ['rgb' => '9C0006']],
                ]);
                $currentRow++;
            }
        }
    }

    /**
     * Generates a unique filename for the report.
     *
     * @param DateTime $startDate
     * @param DateTime $endDate
     * @return string
     */
    private function generateFilename(DateTime $startDate, DateTime $endDate): string
    {
        $dir = __DIR__ . '/../../reports/'; // Relative path to reports directory
        if (!is_dir($dir)) {
            mkdir($dir, 0775, true);
        }
        $filename = sprintf(
            'compliance_report_%s_to_%s_%s.xlsx',
            $startDate->format('Ymd'),
            $endDate->format('Ymd'),
            (new DateTime())->format('His')
        );
        return realpath($dir) . DIRECTORY_SEPARATOR . $filename;
    }
}

Integrating the Report Generator

This generator can be invoked from a command-line script, a scheduled task (cron job), or a web interface. Below is an example of how to use it within a PHP script.

Example Usage Script

`scripts/generate_compliance_report.php`

<?php

require __DIR__ . '/../vendor/autoload.php';

use App\Reporting\ComplianceReportGenerator;
use Dotenv\Dotenv;

// Load environment variables (for database credentials)
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();

// --- Database Connection ---
$dbHost = $_ENV['DB_HOST'];
$dbName = $_ENV['DB_NAME'];
$dbUser = $_ENV['DB_USER'];
$dbPass = $_ENV['DB_PASS'];
$dbCharset = 'utf8mb4';

$dsn = "mysql:host=$dbHost;dbname=$dbName;charset=$dbCharset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $dbUser, $dbPass, $options);
} catch (\PDOException $e) {
    throw new \PDOException($e->getMessage(), (int)$e->getCode());
}

// --- Report Generation ---
$startDate = new DateTime('2023-01-01');
$endDate = new DateTime('2023-12-31');

$reportGenerator = new ComplianceReportGenerator($pdo);

try {
    $reportPath = $reportGenerator->generateReport($startDate, $endDate);
    echo "Compliance report generated successfully: " . realpath($reportPath) . "\n";
} catch (\Exception $e) {
    echo "Error generating report: " . $e->getMessage() . "\n";
    // Log the error for further investigation
    error_log("Compliance Report Generation Failed: " . $e->getMessage() . "\n" . $e->getTraceAsString());
}

?>

To use this script, you’ll need a .env file in your project root with your database credentials:

DB_HOST=localhost
DB_NAME=your_database_name
DB_USER=your_db_user
DB_PASS=your_db_password

Advanced Considerations and Enhancements

Error Handling and Logging

The provided example includes basic try-catch blocks. In a production environment, robust logging is crucial. Use a dedicated logging library (e.g., Monolog) to record successful report generations, errors, and warnings. This aids in debugging and auditing.

Data Validation and Integrity

Before populating the spreadsheet, implement stricter data validation. For instance, ensure that `commission_earned` is a reasonable calculation based on `sale_price` and `commission_rate`. Flag discrepancies. The current script highlights rows with ‘Pending Review’ or ‘Rejected’ status, but more complex validation rules can be added within the `populateData` method.

Security of Generated Reports

Generated reports often contain sensitive financial and client information. Ensure the directory where reports are saved (`reports/` in the example) is not publicly accessible via the web server. Implement access control mechanisms if users need to download reports through a web interface. Consider encrypting sensitive reports if they are stored long-term.

Performance Optimization

For very large datasets (tens of thousands of listings), consider:

  • Fetching data in batches to avoid memory exhaustion.
  • Using PhpSpreadsheet’s cell caching mechanisms if applicable, though for typical report sizes, direct population is usually fine.
  • Optimizing your SQL queries (e.g., ensuring appropriate indexes on `date_listed`).

Customizable Report Templates

For more complex layouts or branding, you could start with a pre-formatted XLSX template file and then use PhpSpreadsheet to load the template and populate specific cells or ranges. This separates the report design from the data generation logic.

Agent-Specific Reporting

To generate agent-specific reports, you would modify the `fetchListings` query to include an `agent_id` parameter and potentially group results by agent within the `populateData` or a new method. You could then iterate through agents, generating a separate report or a dedicated section for each.

Conclusion

By implementing a custom reporting solution with PhpSpreadsheet, real estate agencies can automate the generation of compliance reports, significantly reducing manual effort and the risk of errors. This approach provides a scalable, maintainable, and auditable system for managing critical compliance documentation, freeing up valuable resources for core business activities.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Step-by-Step Guide: Offloading high-frequency custom subscription logs metadata writes to a Redis KV store
  • How to design a modular Command Query Responsibility Segregation (CQRS) architecture for enterprise-level custom plugins
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in user transaction ledgers
  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to affiliate click tracking logs
  • WordPress Development Recipe: Real-time custom event triggers using WebSockets and Transients API

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (41)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (64)
  • WordPress Plugin Development (70)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Step-by-Step Guide: Offloading high-frequency custom subscription logs metadata writes to a Redis KV store
  • How to design a modular Command Query Responsibility Segregation (CQRS) architecture for enterprise-level custom plugins
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in user transaction ledgers

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala