# ivyforms/0.3/backend/src/Repository/Entry/EntryRepository.php

The Innovative Form Builder – IvyForms, version 0.3. 460 lines.

- Page: https://pluginprobe.com/plugins/ivyforms/0.3/code/backend/src/Repository/Entry/EntryRepository.php
- Raw: https://pluginprobe.com/plugins/ivyforms/0.3/raw/backend/src/Repository/Entry/EntryRepository.php
- Modified: 2025-08-20T10:20:12+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/ivyforms/0.3/code/backend/src/Repository/Entry/EntryRepository.php#L10-L20`.

```php
<?php

/**
 * @copyright © TMS-Plugins. All rights reserved.
 * @licence   See LICENCE.md for license details.
 */

namespace IvyForms\Repository\Entry;

// phpcs:disable PSR1.Files.SideEffects
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

use IvyForms\Common\Exceptions\InvalidArgumentException;
use IvyForms\Common\Exceptions\QueryExecutionException;
use IvyForms\Entity\Entry\Entry;
use IvyForms\Factory\Entry\EntryFactory;
use IvyForms\Repository\AbstractRepository;
use IvyForms\Services\InstallActions\DB\Entry\EntriesTable;
use IvyForms\Services\InstallActions\DB\EntryField\EntryFieldsTable as EntryFieldsTable;
use IvyForms\Services\InstallActions\DB\Form\FormsTable;
use IvyForms\Services\Translations\BackendStrings;

class EntryRepository extends AbstractRepository implements EntryRepositoryInterface
{
    public const FACTORY = EntryFactory::class;

    /**
     * Add entry to the database
     *
     * @param Entry $entity
     *
     * @return int
     *
     * @throws QueryExecutionException
     */
    public function add($entity): int
    {
        $data = $entity->getData();

        $result = $this->wpdb->insert(
            $this->table,
            [
                'formId'        => $data['formId'],
                'userId'        => $data['userId'],
                'dateCreated'   => current_time('mysql'),
                'dateEdited'    => current_time('mysql'),
                'status'        => $data['status'],
                'ipAddress'     => $data['ipAddress'],
                'userAgent'     => $data['userAgent'],
                'sourceURL'     => $data['sourceURL'],
                'starred'       => $data['starred'] ? 1 : 0,
            ],
            ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d']
        );

        if ($result === false) {
            throw new QueryExecutionException(
                BackendStrings::getExceptionStrings()['unable_to_add_data'] . __CLASS__
            );
        }

        return $this->wpdb->insert_id;
    }

    /**
     * Update entry in the database
     *
     * @param int  $id
     * @param Entry $entity
     *
     * @return bool
     *
     * @throws QueryExecutionException
     */
    public function update(int $id, $entity): bool
    {
        $result = $this->wpdb->update(
            $this->table,
            [
                'dateEdited'  => current_time('mysql'),
            ],
            ['id' => $id],
            ['%s'],
            ['%d']
        );

        if ($result === false) {
            throw new QueryExecutionException(
                BackendStrings::getExceptionStrings()['unable_update_data'] . __CLASS__
            );
        }

        return (bool)$result;
    }

    /**
     * Update the starred status of an entry.
     *
     * @param int $entryId
     * @param bool $value
     *
     * @return void
     * @throws QueryExecutionException
     */
    public function updateEntryStarred(int $entryId, bool $value): void
    {
        $result = $this->wpdb->update(
            $this->table,
            [
                'starred' => $value ? 1 : 0,
                'dateEdited' => current_time('mysql'),
            ],
            ['id' => $entryId],
            ['%d', '%s'],
            ['%d']
        );

        if ($result === false) {
            throw new QueryExecutionException(
                BackendStrings::getExceptionStrings()['unable_update_starred'] . __CLASS__
            );
        }
    }

    /**
     * Update the status of an entry.
     *
     * @param int $entryId
     * @param string $status
     *
     * @return void
     * @throws QueryExecutionException
     */
    public function updateEntryStatus(int $entryId, string $status): void
    {
        $result = $this->wpdb->update(
            $this->table,
            [
                'status' => $status,
                'dateEdited' => current_time('mysql'),
            ],
            ['id' => $entryId],
            ['%s', '%s'],
            ['%d']
        );

        if ($result === false) {
            throw new QueryExecutionException(
                BackendStrings::getExceptionStrings()['unable_update_status'] . __CLASS__
            );
        }
    }

    /**
     * Search for entries with extended logic (field value, form name) and return paginated results with meta.
     *
     * @param array<string, mixed>|null $params
     * @return array<mixed>
     * @throws InvalidArgumentException|QueryExecutionException
     */
    public function search(?array $params): array
    {
        $queryParams = [];
        $whereClauses = $this->buildWhereClauses($params, $queryParams);

        // Always join entry fields and forms for search/filter/sort
        $entriesTable = EntriesTable::getTableName();
        $entryFieldsTable = EntryFieldsTable::getTableName();
        $formsTable = FormsTable::getTableName();
        $join = " LEFT JOIN {$entryFieldsTable} eft ON e.id = eft.entryId ";
        $join .= " LEFT JOIN {$formsTable} f ON e.formId = f.id ";

        // Sorting
        $sortableColumns = $this->getSortableColumns();
        $sortBy = $params['orderBy'] ?? 'id';
        $order = strtolower($params['order'] ?? 'asc') === 'asc' ? 'ASC' : 'DESC';
        if (!in_array($sortBy, $sortableColumns, true)) {
            $sortBy = 'id';
        }

        // Pagination
        $page = max(($params['page'] ?? 1), 1);
        $perPage = max(($params['perPage'] ?? 10), 1);
        $offset = ($page - 1) * $perPage;
        $queryParams[] = $perPage;
        $queryParams[] = $offset;

        // Main query
        $sql = "SELECT DISTINCT e.* FROM {$entriesTable} e $join 
                    WHERE {$whereClauses} 
                    ORDER BY e.{$sortBy} {$order} LIMIT %d OFFSET %d";
        $results = $this->wpdb->get_results($this->wpdb->prepare($sql, $queryParams), ARRAY_A);
        if ($results === false) {
            throw new QueryExecutionException(
                BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
            );
        }

        // Total count for pagination meta (exclude LIMIT/OFFSET)
        $countParams = $queryParams;
        array_pop($countParams); // offset
        array_pop($countParams); // perPage
        $countSql = "SELECT COUNT(DISTINCT e.id) FROM {$entriesTable} e $join WHERE {$whereClauses}";
        $total = (int)$this->wpdb->get_var($this->wpdb->prepare($countSql, $countParams));

        return [
            'data' => $results,
            'meta' => [
                'page' => $page,
                'perPage' => $perPage,
                'total' => $total,
            ],
        ];
    }

    /**
     * Override addSearchClauses to support searching fieldValue and form name.
     * Extracted search conditions to a helper for complexity reduction.
     *
     * @param array<string, mixed>|null $params
     * @param array<string> $whereClauses
     * @param array<int|string, mixed> $queryParams
     *
     * @return void
     */
    protected function addSearchClauses(?array $params, array &$whereClauses, array &$queryParams): void
    {
        if (!empty($params['search'])) {
            $searchEscaped = '%' . $this->wpdb->esc_like($params['search']) . '%';
            $searchConditions = $this->getEntrySearchConditions($params);
            $whereClauses[] = '(' . implode(' OR ', $searchConditions) . ')';
            // Add the same number of params as conditions, no unused variable
            $queryParams = array_merge($queryParams, array_fill(0, count($searchConditions), $searchEscaped));
        }
    }

    /**
     * Helper to return search conditions for entries (for complexity reduction)
     *
     * @param array<string, mixed>|null $params
     * @return array<string>
     */
    protected function getEntrySearchConditions(?array $params = null): array
    {
        $conditions = [
            'e.id LIKE %s',
            'e.formId LIKE %s',
            'f.name LIKE %s',
        ];
        // Only include fieldValue if searchFieldValue is true (default true for BC)
        if (!isset($params['searchFieldValue']) || $params['searchFieldValue']) {
            $conditions[] = 'eft.fieldValue LIKE %s';
        }
        return $conditions;
    }

    /**
     * Override addFilterClauses to prefix columns with the correct alias (e.g., e.)
     * so that dynamic filtering works with JOINs and aliases.
     */
    protected function addFilterClauses(?array $params, array &$whereClauses, array &$queryParams): void
    {
        if (!empty($params['filters'])) {
            $filterableColumns = $this->getFilterableColumns();
            foreach ($params['filters'] as $key => $value) {
                if (in_array($key, $filterableColumns, true) && $value !== null && $value !== '') {
                    // Prefix with e. for the entries table
                    $whereClauses[] = "e.{$key} = %s";
                    $queryParams[] = sanitize_text_field($value);
                }
            }
        }
    }

    /**
     * Get the columns that can be searched in the database (including virtual columns for search).
     *
     * @return array<string>
     */
    protected function getSearchableColumns(): array
    {
        return [
            'id',
            'formId',
            'fieldValue',   // virtual, from EntryFields table
            'name',         // virtual, from Forms table
        ];
    }

    /**
     * Get filterable columns
     *
     * @return array<string>
     */
    protected function getFilterableColumns(): array
    {
        return ['starred', 'status', 'formId'];
    }

    /**
     * Get sortable columns
     *
     * @return array<string>
     */
    protected function getSortableColumns(): array
    {
        return ['id', 'dateCreated'];
    }

    /**
     * Get the date column
     *
     * @return string
     */
    protected function getDateColumn(): string
    {
        return 'e.dateCreated';
    }

    /**
     * @param int $entryId
     *
     * @return bool
     */
    public function exists(int $entryId): bool
    {
        $query = $this->wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table} WHERE id = %d",
            $entryId
        );

        $count = (int) $this->wpdb->get_var($query);

        return $count > 0;
    }

    /**
     * Get the count of entries for a specific form.
     *
     * @param int $formId
     * @return int
     */
    public function getCountByFormId(int $formId): int
    {
        $query = $this->wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table} WHERE formId = %d",
            $formId
        );

        $count = (int) $this->wpdb->get_var($query);

        return $count;
    }

    /**
     * Get the count of entries for the filter dropdown.
     *
     * @param array<string, mixed>|null $params
     * @return array<string, int>
     */
    public function getFilterCount(?array $params = null): array
    {
        $where = '';
        $queryParams = [];
        if (!empty($params['filters']['formId'])) {
            $where = 'WHERE formId = %d';
            $queryParams[] = (int)$params['filters']['formId'];
        }
        $sql = "SELECT 
            SUM(status = 'read') as readTrueCount,
            SUM(status = 'unread') as readFalseCount,
            SUM(starred = 1) as starredTrueCount,
            SUM(starred = 0) as starredFalseCount
        FROM {$this->table} $where";
        $result = $this->wpdb->get_row(
            $queryParams ? $this->wpdb->prepare($sql, ...$queryParams) : $sql,
            ARRAY_A
        );
        // Convert all values to integers and provide defaults if query returns null
        return array_map('intval', $result ?: [
            'readTrueCount' => 0,
            'readFalseCount' => 0,
            'starredTrueCount' => 0,
            'starredFalseCount' => 0,
        ]);
    }

    /**
     * Get entry fields for the given entries.
     *
     * @param array<int, array<string, mixed>> $entries
     * @return array<mixed>
     *
     * @throws QueryExecutionException|InvalidArgumentException
     */
    public function getEntryFields(array $entries): array
    {
        if (empty($entries)) {
            return [];
        }

        // Extract entry IDs from the entries array
        $entryIds = array_column($entries, 'id');

        // Prepare placeholders for the query
        $placeholders = implode(',', array_fill(0, count($entryIds), '%d'));
        $entryFieldsTable = EntryFieldsTable::getTableName();

        // Query to fetch fields for the given entry IDs
        $query = "SELECT * FROM {$entryFieldsTable} WHERE entryId IN ($placeholders)";

        // Execute the query with the entry IDs as parameters
        $results = $this->wpdb->get_results(
            $this->wpdb->prepare($query, $entryIds),
            ARRAY_A
        );

        if ($results === false) {
            throw new QueryExecutionException(
                BackendStrings::getExceptionStrings()['unable_get_entry_fields'] . __CLASS__
            );
        }

        return $results;
    }

    /**
     * Get the count of entries for multiple form IDs.
     *
     * @param int[] $formIds
     * @return array<int, int> formId => count
     * @throws InvalidArgumentException
     */
    public function getEntryCountByFormIds(array $formIds): array
    {
        if (empty($formIds)) {
            return [];
        }
        $formIds = array_map('intval', $formIds);
        $placeholders = implode(',', array_fill(0, count($formIds), '%d'));
        $tableName = EntriesTable::getTableName();
        $query = "SELECT formId, COUNT(*) as count FROM {$tableName} WHERE formId IN ($placeholders) GROUP BY formId";
        $results = $this->wpdb->get_results($this->wpdb->prepare($query, $formIds), ARRAY_A);
        $counts = [];
        foreach ($results as $row) {
            $counts[(int)$row['formId']] = (int)$row['count'];
        }

        // Ensure all requested IDs are present in the result (default to 0)
        foreach ($formIds as $id) {
            if (!isset($counts[$id])) {
                $counts[$id] = 0;
            }
        }
        return $counts;
    }
}

```
