# easy-invoice/2.4.3/includes/Services/DocumentAttachments.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.3. 257 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.3/code/includes/Services/DocumentAttachments.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.3/raw/includes/Services/DocumentAttachments.php
- Modified: 2026-09-15T12:31:20+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.4.3/code/includes/Services/DocumentAttachments.php#L10-L20`.

```php
<?php
/**
 * Files attached to an invoice or quote.
 *
 * @package Easy_Invoice
 * @subpackage Services
 */

namespace EasyInvoice\Services;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * A timesheet, a signed contract, a delivery note, the photos of the job:
 * clients ask for these alongside the invoice, and every SaaS product lets
 * them travel with it. Here they are media-library files chosen in the
 * builder, listed on the public page and the PDF, and sent with the email.
 */
class DocumentAttachments {

    const META = '_easy_invoice_attachments';

    /**
     * Wire it up.
     *
     * @return void
     */
    public static function init(): void {
        add_filter( 'easy_invoice_invoice_fields', [ __CLASS__, 'invoiceField' ] );
        add_filter( 'easy_invoice_quote_form_fields', [ __CLASS__, 'quoteField' ] );
        add_action( 'admin_enqueue_scripts', [ __CLASS__, 'media' ] );
        // QuoteRepository sets only the keys it knows; invoices go through
        // the form processor, which saves every registered field.
        add_action( 'easy_invoice_quote_set_data_after', [ __CLASS__, 'saveQuote' ], 10, 2 );

        // Inside the design card (part of the PDF capture); the page footer
        // hook remains for a theme override that predates the in-card hook.
        add_action( 'easy_invoice_document_body_after', [ __CLASS__, 'renderOnPage' ], 10, 2 );
        add_action( 'easy_invoice_document_footer', [ __CLASS__, 'renderOnPage' ], 10, 2 );
        add_action( 'easy_invoice_pdf_after_notes', [ __CLASS__, 'renderOnPdf' ], 20, 2 );
        add_filter( 'easy_invoice_email_attachments', [ __CLASS__, 'emailAttachments' ], 10, 3 );
    }

    /**
     * Attachment ids for a document.
     *
     * @param int $document_id Post id.
     * @return int[]
     */
    /**
     * Meta key for a document: invoices use the plain field name, quotes are
     * persisted by the Quote model under a _quote_ prefix.
     *
     * @param int $document_id Post id.
     * @return string
     */
    public static function metaKey( int $document_id ): string {
        return \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE === get_post_type( $document_id ) ? '_easy_invoice_quote_attachments' : self::META;
    }

    public static function ids( int $document_id ): array {
        $raw = get_post_meta( $document_id, self::metaKey( $document_id ), true );
        if ( is_array( $raw ) ) {
            $ids = $raw;
        } else {
            $ids = explode( ',', (string) $raw );
        }
        $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
        return array_values( array_filter( $ids, static function ( $id ) {
            return 'attachment' === get_post_type( $id );
        } ) );
    }

    /**
     * Attachment ids with names, sizes and URLs.
     *
     * @param int $document_id Post id.
     * @return array<int,array{id:int,name:string,size:string,url:string,path:string}>
     */
    public static function files( int $document_id ): array {
        $out = [];
        foreach ( self::ids( $document_id ) as $id ) {
            $path = get_attached_file( $id );
            $out[] = [
                'id'   => $id,
                'name' => basename( (string) $path ) ?: get_the_title( $id ),
                'size' => $path && file_exists( $path ) ? size_format( (int) filesize( $path ) ) : '',
                'url'  => (string) wp_get_attachment_url( $id ),
                'path' => (string) $path,
            ];
        }
        return $out;
    }

    /* ---- Builder ---------------------------------------------------- */

    private static function field( string $tab ): array {
        return [
            'tab_id'      => $tab,
            'name'        => 'attachments',
            'type'        => 'attachments',
            'label'       => __( 'Attachments', 'easy-invoice' ),
            'grid_cols'   => 'sm:col-span-6',
            'section'     => 'notes',
            'order'       => 9,
            'description' => __( 'Files the client can download from the document page and that are sent with the email — a timesheet, a signed contract, delivery photos.', 'easy-invoice' ),
        ];
    }

    public static function invoiceField( $fields ) {
        $fields   = is_array( $fields ) ? $fields : [];
        $fields[] = self::field( 'invoice-tab' );
        return $fields;
    }

    public static function quoteField( $fields ) {
        $fields   = is_array( $fields ) ? $fields : [];
        $fields[] = self::field( 'quote-tab' );
        return $fields;
    }

    /**
     * Persist the field on a quote.
     *
     * @param object $quote Quote model.
     * @param array  $data  Submitted data.
     * @return void
     */
    public static function saveQuote( $quote, $data ): void {
        if ( is_array( $data ) && array_key_exists( 'attachments', $data ) && is_callable( [ $quote, 'setMetaData' ] ) ) {
            $ids = is_array( $data['attachments'] ) ? $data['attachments'] : explode( ',', (string) $data['attachments'] );
            $quote->setMetaData( '_easy_invoice_attachments', implode( ',', array_filter( array_map( 'intval', $ids ) ) ) );
        }
    }

    /**
     * The media library on the builders.
     *
     * @return void
     */
    public static function media(): void {
        $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if ( in_array( $page, [ 'easy-invoice-builder', 'easy-invoice-new', 'easy-invoice-quote-builder' ], true ) ) {
            wp_enqueue_media();
        }
    }

    /**
     * Field markup: chosen files, an Add button, a hidden CSV of ids.
     *
     * @param array $field    Field config.
     * @param mixed $document Model or null.
     * @return string
     */
    public static function renderField( array $field, $document = null ): string {
        $id    = $document && is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
        $files = $id ? self::files( $id ) : [];
        $ids   = implode( ',', array_map( static function ( $f ) { return $f['id']; }, $files ) );

        $html  = '<div class="' . esc_attr( $field['grid_cols'] ?? 'sm:col-span-6' ) . '" id="ei-attachments-field">';
        $html .= '<label class="block text-sm font-medium text-gray-700">' . esc_html( $field['label'] ) . '</label>';
        $html .= '<input type="hidden" name="attachments" id="ei-attachments-ids" value="' . esc_attr( $ids ) . '">';
        $html .= '<ul id="ei-attachments-list" class="mt-2 space-y-1 text-sm">';
        foreach ( $files as $f ) {
            $html .= '<li class="flex items-center justify-between rounded border border-gray-200 px-3 py-1.5" data-id="' . (int) $f['id'] . '">'
                . '<span class="truncate"><a href="' . esc_url( $f['url'] ) . '" target="_blank" rel="noopener" class="text-indigo-600">' . esc_html( $f['name'] ) . '</a> <span class="text-gray-400 text-xs">' . esc_html( $f['size'] ) . '</span></span>'
                . '<button type="button" class="ei-attachment-remove text-xs text-red-600 ml-3">' . esc_html__( 'Remove', 'easy-invoice' ) . '</button></li>';
        }
        $html .= '</ul>';
        $html .= '<button type="button" id="ei-attachments-add" class="mt-2 inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-gray-300 bg-white text-gray-700 hover:bg-gray-50">' . esc_html__( 'Add files', 'easy-invoice' ) . '</button>';
        if ( ! empty( $field['description'] ) ) {
            $html .= '<p class="mt-1 text-xs text-gray-500">' . esc_html( $field['description'] ) . '</p>';
        }
        $html .= '</div>';
        $html .= '<script>document.addEventListener("DOMContentLoaded",function(){var w=document.getElementById("ei-attachments-field");if(!w){return;}var ids=document.getElementById("ei-attachments-ids"),list=document.getElementById("ei-attachments-list");function sync(){ids.value=[].map.call(list.querySelectorAll("li"),function(li){return li.getAttribute("data-id");}).join(",");}'
            . 'list.addEventListener("click",function(e){if(e.target.classList.contains("ei-attachment-remove")){e.target.closest("li").remove();sync();}});'
            . 'document.getElementById("ei-attachments-add").addEventListener("click",function(){if(!window.wp||!wp.media){return;}var f=wp.media({title:' . wp_json_encode( __( 'Attach files', 'easy-invoice' ) ) . ',multiple:true,button:{text:' . wp_json_encode( __( 'Attach', 'easy-invoice' ) ) . '}});f.on("select",function(){f.state().get("selection").each(function(a){a=a.toJSON();if(list.querySelector("li[data-id=\\""+a.id+"\\"]")){return;}var li=document.createElement("li");li.className="flex items-center justify-between rounded border border-gray-200 px-3 py-1.5";li.setAttribute("data-id",a.id);li.innerHTML="<span class=\\"truncate\\"><a class=\\"text-indigo-600\\" target=\\"_blank\\" rel=\\"noopener\\"></a> <span class=\\"text-gray-400 text-xs\\"></span></span><button type=\\"button\\" class=\\"ei-attachment-remove text-xs text-red-600 ml-3\\">' . esc_js( __( 'Remove', 'easy-invoice' ) ) . '</button>";li.querySelector("a").href=a.url;li.querySelector("a").textContent=a.filename||a.title;li.querySelector("span span").textContent=a.filesizeHumanReadable||"";list.appendChild(li);});sync();});f.open();});});</script>';
        return $html;
    }

    /* ---- Output ----------------------------------------------------- */

    /** Public document page: a list under the document. */
    /** @var array<int,bool> Documents whose list is already printed in this request. */
    private static $rendered = [];

    public static function renderOnPage( $document, $type ): void {
        $id    = is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
        if ( ! $id || isset( self::$rendered[ $id ] ) ) {
            return;
        }
        $files = self::files( $id );
        if ( empty( $files ) ) {
            return;
        }
        self::$rendered[ $id ] = true;
        echo '<div class="ei-attachments" style="margin:24px 0 0;padding:14px 0 0;border-top:1px solid #e5e7eb;font-size:13px;">';
        echo '<div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:6px;">' . esc_html__( 'Attachments', 'easy-invoice' ) . '</div><ul style="margin:0;padding:0;list-style:none;">';
        foreach ( $files as $f ) {
            echo '<li style="padding:4px 0;"><a href="' . esc_url( $f['url'] ) . '" target="_blank" rel="noopener" style="color:#4f46e5;">' . esc_html( $f['name'] ) . '</a> <span style="color:#9ca3af;font-size:12px;">' . esc_html( $f['size'] ) . '</span></li>';
        }
        echo '</ul></div>';
    }

    /** PDF: names and links. */
    public static function renderOnPdf( $document, $type ): void {
        $id    = is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
        // A design-layout PDF already printed the list inside the card.
        if ( ! $id || isset( self::$rendered[ $id ] ) ) {
            return;
        }
        $files = self::files( $id );
        if ( empty( $files ) ) {
            return;
        }
        self::$rendered[ $id ] = true;
        echo '<div style="margin-top:14px;font-size:10px;color:#374151;"><div style="text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-size:9px;margin-bottom:3px;">' . esc_html__( 'Attachments', 'easy-invoice' ) . '</div>';
        foreach ( $files as $f ) {
            echo '<div><a href="' . esc_url( $f['url'] ) . '" style="color:#4f46e5;text-decoration:none;">' . esc_html( $f['name'] ) . '</a> <span style="color:#9ca3af;">' . esc_html( $f['size'] ) . '</span></div>';
        }
        echo '</div>';
    }

    /**
     * Send the files with the document's email.
     *
     * @param array  $attachments Paths so far.
     * @param object $document    Model.
     * @param string $type        'invoice' or 'quote'.
     * @return array
     */
    public static function emailAttachments( $attachments, $document, $type ): array {
        $attachments = is_array( $attachments ) ? $attachments : [];
        $id          = is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
        if ( ! $id ) {
            return $attachments;
        }
        /**
         * Filter whether attached files are sent with the email (default yes).
         *
         * @param bool $send        Default true.
         * @param int  $document_id Post id.
         */
        if ( ! apply_filters( 'easy_invoice_email_document_attachments', true, $id ) ) {
            return $attachments;
        }
        foreach ( self::files( $id ) as $f ) {
            if ( '' !== $f['path'] && file_exists( $f['path'] ) ) {
                $attachments[] = $f['path'];
            }
        }
        return array_values( array_unique( $attachments ) );
    }
}

```
