# easy-invoice/2.3.8/includes/Services/InvoiceNumberService.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.3.8. 244 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.3.8/code/includes/Services/InvoiceNumberService.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.3.8/raw/includes/Services/InvoiceNumberService.php
- Modified: 2026-06-26T12:33:58+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.3.8/code/includes/Services/InvoiceNumberService.php#L10-L20`.

```php
<?php
/**
 * Invoice Number Service
 *
 * @package Easy_Invoice
 * @subpackage Services
 */

namespace EasyInvoice\Services;

/**
 * InvoiceNumberService Class
 * 
 * Handles automatic generation of invoice numbers based on settings.
 */
class InvoiceNumberService {

    /**
     * MySQL named lock used to serialise concurrent invoice-number
     * generation. Distinct from the quote-number lock so the two flows
     * never block each other.
     */
    private const NUMBER_LOCK_NAME = 'easy_invoice_invoice_number_gen';

    /**
     * Generate the next invoice number
     *
     * @return string The generated invoice number
     */
    public function generateNextNumber(): string {
        // Get settings
        $prefix = get_option('easy_invoice_invoice_prefix', 'INV-');

        // Serialise the read-check-write triplet against concurrent
        // generation. Without this, two simultaneous create-invoice
        // requests can both read the same counter value, both pass
        // findNextUniqueNumber() (because neither has written to
        // wp_postmeta yet), and both emit the same invoice number.
        // The lock auto-releases on MySQL connection close, so we
        // can't leak it on a fatal PHP error.
        $lock_acquired = $this->acquireNumberLock();
        try {
            // Get the next number to use
            $next_number = get_option('easy_invoice_next_invoice_number', 1);

            // Find the next unique number
            $unique_number = $this->findNextUniqueNumber($next_number, $prefix);

            // Update the counter to the number we actually used + 1 for next time
            update_option('easy_invoice_next_invoice_number', $unique_number + 1);

            return $prefix . str_pad($unique_number, 6, '0', STR_PAD_LEFT);
        } finally {
            if ($lock_acquired) {
                $this->releaseNumberLock();
            }
        }
    }
    
    /**
     * Get the next invoice number without incrementing
     *
     * @return string The next invoice number
     */
    public function getNextNumber(): string {
        $prefix = get_option('easy_invoice_invoice_prefix', 'INV-');
        $next_number = get_option('easy_invoice_next_invoice_number', 1);
        
        // Find what the next unique number would be
        $unique_number = $this->findNextUniqueNumber($next_number, $prefix);
        
        return $prefix . str_pad($unique_number, 6, '0', STR_PAD_LEFT);
    }
    
    /**
     * Reset the invoice number counter
     *
     * @param int $new_starting_number The new starting number
     * @return void
     */
    public function resetCounter(int $new_starting_number = 1): void {
        update_option('easy_invoice_next_invoice_number', $new_starting_number);
    }
    
    /**
     * Check if an invoice number already exists
     *
     * @param string $invoice_number The invoice number to check
     * @return bool True if the number exists, false otherwise
     */
    public function numberExists(string $invoice_number): bool {
        global $wpdb;
        
        $result = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->postmeta} 
            WHERE meta_key = '_easy_invoice_number' 
            AND meta_value = %s",
            $invoice_number
        ));
        
        return intval($result) > 0;
    }
    
    /**
     * Generate a unique invoice number (handles duplicates)
     *
     * @return string A unique invoice number
     */
    public function generateUniqueNumber(): string {
        // Get settings
        $prefix = get_option('easy_invoice_invoice_prefix', 'INV-');

        // Same concurrency guard as generateNextNumber() — see comment
        // there for the rationale. These two methods are duplicate
        // public entry points kept for backward-compat; both need the
        // lock so neither call site is a race window.
        $lock_acquired = $this->acquireNumberLock();
        try {
            // Get the next number to use from the current settings
            $next_number = get_option('easy_invoice_next_invoice_number', 1);

            // Find the next unique number
            $unique_number = $this->findNextUniqueNumber($next_number, $prefix);

            // Update the counter to the number we actually used + 1 for next time
            update_option('easy_invoice_next_invoice_number', $unique_number + 1);

            $final_number = $prefix . str_pad($unique_number, 6, '0', STR_PAD_LEFT);

            return $final_number;
        } finally {
            if ($lock_acquired) {
                $this->releaseNumberLock();
            }
        }
    }

    /**
     * Acquire a MySQL named lock for the read-check-write triplet.
     * Returns true if the lock was acquired (and must be released by
     * the caller), false on timeout or backend failure (caller falls
     * through to the unsynchronised path — the secondary
     * numberExists() check in findNextUniqueNumber() still defends
     * against the worst case).
     *
     * Timeout is 3s — if the database is so contended that even this
     * fails, blocking the user's create-invoice request longer is
     * worse than the residual race risk.
     */
    private function acquireNumberLock(): bool {
        global $wpdb;
        $result = $wpdb->get_var($wpdb->prepare(
            'SELECT GET_LOCK(%s, %d)',
            self::NUMBER_LOCK_NAME,
            3
        ));
        return (int) $result === 1;
    }

    /**
     * Release the MySQL named lock. Safe to call multiple times — if
     * the lock isn't held by this connection, RELEASE_LOCK returns
     * NULL and the call is a no-op.
     */
    private function releaseNumberLock(): void {
        global $wpdb;
        $wpdb->query($wpdb->prepare(
            'SELECT RELEASE_LOCK(%s)',
            self::NUMBER_LOCK_NAME
        ));
    }
    
    /**
     * Find the next unique number starting from the given number
     *
     * @param int $start_number The number to start checking from
     * @param string $prefix The prefix to use for checking
     * @return int The next unique number
     */
    private function findNextUniqueNumber(int $start_number, string $prefix): int {
        $current_number = $start_number;
        $max_attempts = 1000; // Prevent infinite loops
        $attempts = 0;
        
        while ($attempts < $max_attempts) {
            $invoice_number = $prefix . str_pad($current_number, 6, '0', STR_PAD_LEFT);
            
            if (!$this->numberExists($invoice_number)) {
                return $current_number;
            }
            
            $current_number++;
            $attempts++;
        }
        
        // If we can't find a unique number, add timestamp to ensure uniqueness
        return $current_number + time();
    }
    
    /**
     * Get the highest invoice number from existing invoices
     *
     * @return int The highest invoice number found
     */
    public function getHighestInvoiceNumber(): int {
        global $wpdb;
        
        $prefix = get_option('easy_invoice_invoice_prefix', 'INV-');
        
        // Get all invoice numbers from the database
        $results = $wpdb->get_results($wpdb->prepare(
            "SELECT meta_value FROM {$wpdb->postmeta} 
            WHERE meta_key = '_easy_invoice_number' 
            AND meta_value LIKE %s
            ORDER BY meta_value DESC
            LIMIT 1",
            $prefix . '%'
        ));
        
        if (empty($results)) {
            return 0;
        }
        
        $highest_number = $results[0]->meta_value;
        
        // Extract the number part (remove prefix and padding)
        $number_part = str_replace($prefix, '', $highest_number);
        $number_part = ltrim($number_part, '0');
        
        return intval($number_part);
    }
    
    /**
     * Format an invoice number with custom formatting
     *
     * @param int $number The number to format
     * @param string $prefix The prefix to use
     * @param int $padding The number of digits to pad to
     * @return string The formatted invoice number
     */
    public function formatNumber(int $number, string $prefix = 'INV-', int $padding = 6): string {
        return $prefix . str_pad($number, $padding, '0', STR_PAD_LEFT);
    }
} 
```
