Skip to main content

Introduction

The Filters system provides a powerful, pipeline-based approach to filtering Eloquent queries in the Gestión de Ventas Laravel ERP. It implements a modular architecture where individual filter classes can be composed together to create complex filtering logic.

Architecture

The filter system is built on three core components:

FilterInterface

Contract that all filters must implement

QueryFilter

Base class that orchestrates filter application

Individual Filters

Specific filter implementations for each criteria

FilterInterface

The base contract that all filters implement:
app/Filters/Contracts/FilterInterface.php
<?php

namespace App\Filters\Contracts;

use Illuminate\Database\Eloquent\Builder;

interface FilterInterface
{
    public function apply(Builder $query): Builder;
}
Every filter must implement the apply() method, which receives an Eloquent query builder and returns the modified builder.

QueryFilter Base Class

The abstract base class that coordinates filter application:
app/Filters/Base/QueryFilter.php
<?php

namespace App\Filters\Base;

use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
use App\Filters\Contracts\FilterInterface;

abstract class QueryFilter implements FilterInterface
{
    protected Request $request;
    protected Builder $query;

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    public function apply(Builder $query): Builder
    {
        $this->query = $query;

        foreach ($this->filters() as $key => $filterClass) {
            if ($this->request->filled($key)) {
                (new $filterClass($this->request))->apply($this->query);
            }
        }

        return $this->query;
    }

    /**
     * Map request keys to Filter classes
     */
    abstract protected function filters(): array;
}
Key Features:
  • Iterates through defined filters
  • Only applies filters when the request parameter is present
  • Maintains the query builder reference throughout the pipeline
  • Child classes define the filter mapping via filters() method

How It Works

1

Define Filter Classes

Create a main filter class extending QueryFilter that maps request parameters to individual filter implementations.
class SaleFilters extends QueryFilter
{
    protected function filters(): array
    {
        return [
            'search'       => SaleSearchFilter::class,
            'client_id'    => SaleClientFilter::class,
            'from_date'    => SaleDateFilter::class,
            'status'       => SaleStatusFilter::class,
        ];
    }
}
2

Create Individual Filters

Each filter implements FilterInterface and contains specific filtering logic.
class SaleStatusFilter implements FilterInterface
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        $value = $this->request->input('status');
        return $value ? $query->where('status', $value) : $query;
    }
}
3

Apply in Controllers

Instantiate the filter class with the request and apply it to your query.
public function index(Request $request)
{
    $sales = (new SaleFilters($request))
        ->apply(Sale::query()->with(['client', 'warehouse']))
        ->latest()
        ->paginate(15);

    return view('sales.index', compact('sales'));
}

Filter Types

The system includes several common filter patterns:

Search Filters

Perform LIKE queries across multiple fields:
app/Filters/Sales/SalesFilters/SaleSearchFilter.php
class SaleSearchFilter implements FilterInterface
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        $value = $this->request->input('search');
        if (!$value) return $query;

        return $query->where(function($q) use ($value) {
            $q->where('number', 'like', "%{$value}%")
              ->orWhere('notes', 'like', "%{$value}%")
              ->orWhereHas('client', function($subQ) use ($value) {
                  $subQ->where('name', 'like', "%{$value}%");
              });
        });
    }
}

Date Range Filters

Filter records between two dates:
app/Filters/Sales/SalesFilters/SaleDateFilter.php
class SaleDateFilter implements FilterInterface
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        $from = $this->request->input('from_date');
        $to = $this->request->input('to_date');

        return $query
            ->when($from, function($q) use ($from) {
                return $q->whereDate('sale_date', '>=', Carbon::parse($from));
            })
            ->when($to, function($q) use ($to) {
                return $q->whereDate('sale_date', '<=', Carbon::parse($to));
            });
    }
}

Amount Range Filters

Filter by minimum and maximum amounts:
app/Filters/Sales/SalesFilters/SaleAmountRangeFilter.php
class SaleAmountRangeFilter implements FilterInterface
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        $min = $this->request->input('min_amount');
        $max = $this->request->input('max_amount');

        return $query
            ->when($min, fn($q) => $q->where('total_amount', '>=', $min))
            ->when($max, fn($q) => $q->where('total_amount', '<=', $max));
    }
}

Exact Match Filters

Filter by exact field values:
app/Filters/Sales/SalesFilters/SaleStatusFilter.php
class SaleStatusFilter implements FilterInterface
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        $value = $this->request->input('status');
        return $value ? $query->where('status', $value) : $query;
    }
}

Boolean Logic Filters

Filter based on conditional logic:
app/Filters/Client/ClientHasDebtFilter.php
class ClientHasDebtFilter implements FilterInterface
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        $value = $this->request->input('has_debt');
        
        if ($value === 'yes') {
            return $query->where('balance', '>', 0);
        }
        
        if ($value === 'no') {
            return $query->where('balance', '<=', 0);
        }
        
        return $query;
    }
}

Real-World Examples

Sales Filtering System

app/Filters/Sales/SalesFilters/SaleFilters.php
class SaleFilters extends QueryFilter
{
    protected function filters(): array
    {
        return [
            'search'       => SaleSearchFilter::class,
            'client_id'    => SaleClientFilter::class,
            'warehouse_id' => SaleWarehouseFilter::class,
            'payment_type' => SalePaymentTypeFilter::class,
            'tipo_pago_id' => SaleTipoPagoFilter::class,
            'status'       => SaleStatusFilter::class,
            'from_date'    => SaleDateFilter::class,
            'min_amount'   => SaleAmountRangeFilter::class,
        ];
    }
}

Payment Filtering System

app/Filters/Accounting/PaymentsFilters/PaymentFilters.php
class PaymentFilters extends QueryFilter
{
    protected function filters(): array
    {
        return [
            'search'       => PaymentSearchFilter::class,
            'client_id'    => PaymentClientFilter::class,
            'tipo_pago_id' => PaymentMethodFilter::class,
            'status'       => PaymentStatusFilter::class,
            'from_date'    => PaymentDateFilter::class,
            'min_amount'   => PaymentAmountRangeFilter::class,
        ];
    }
}

Client Filtering System

app/Filters/Client/ClientFilters.php
class ClientFilters extends QueryFilter
{
    protected function filters(): array
    {
        return [
            'search'         => ClientSearchFilter::class,
            'estado_cliente' => ClientBusinessStatusFilter::class,
            'state'          => ClientStateFilter::class,
            'type'           => ClientTypeFilter::class,
            'tax_type'       => ClientTaxIdentifierFilter::class,
            'from_date'      => ClientDateFilter::class,
            'has_debt'       => ClientHasDebtFilter::class,
            'over_limit'     => ClientOverLimitFilter::class,
        ];
    }
}

Controller Integration

Here’s a complete example from the Payment controller:
app/Http/Controllers/Accounting/PaymentController.php
public function index(Request $request)
{
    $visibleColumns = $request->input('columns', PaymentTable::defaultDesktop());
    $perPage = $request->input('per_page', 15);

    $payments = (new PaymentFilters($request))
        ->apply(Payment::query()->with(['client', 'receivable', 'tipoPago', 'creator']))
        ->latest()
        ->paginate($perPage)
        ->withQueryString();

    $catalogs = $this->catalogService->getForFilters();

    if ($request->ajax()) {
        return view('accounting.payments.partials.table', [
            'items'          => $payments,
            'visibleColumns' => $visibleColumns,
            'allColumns'     => PaymentTable::allColumns(),
        ])->render();
    }

    return view('accounting.payments.index', array_merge(
        [
            'items'          => $payments,
            'visibleColumns' => $visibleColumns,
            'allColumns'     => PaymentTable::allColumns(),
        ],
        $catalogs
    ));
}

Export with Filters

Filters can be reused for data exports:
app/Http/Controllers/Accounting/PaymentController.php
public function export(Request $request)
{
    try {
        // Apply the same filters as the main table
        $query = (new PaymentFilters($request))
            ->apply(Payment::query());

        $fileName = 'reporte-pagos-' . now()->format('d-m-Y-H-i') . '.xlsx';

        return Excel::download(new PaymentsExport($query), $fileName);
    } catch (\Exception $e) {
        return back()->with('error', "Error al generar el reporte: " . $e->getMessage());
    }
}

Benefits

Modularity

Each filter is a separate, testable class with a single responsibility

Reusability

Filters can be shared across different modules (e.g., date filters)

Maintainability

Easy to add, modify, or remove filters without affecting others

Type Safety

Clear contracts ensure consistent behavior across all filters

Available Filter Systems

The ERP includes pre-built filter systems for:
  • Sales: Sales orders, invoices, NCF logs
  • Accounting: Payments, receivables, journal entries, document types
  • Clients: Client management with business status, debt, and credit limits
  • Inventory: Stock levels, movements, warehouses
  • Products: Products with categories, units, and active status
  • Point of Sale: POS terminals with status and client filters
  • Equipment: Equipment tracking by type and location

Next Steps

Creating Filters

Learn how to create custom filter implementations

Query Builder

Understand Eloquent query building patterns

Build docs developers (and LLMs) love