Skip to content

Feature Implementation Plan: Unified API Filtering System

📋 Todo Checklist

  • [ ] Create a generic FilterService to dynamically apply filters to Doctrine queries.
  • [ ] Create a configuration file to define all allowed API filters.
  • [ ] Refactor all existing API list endpoints and their repositories to use the new FilterService.
  • [ ] Write unit tests for the FilterService and integration tests for the refactored endpoints.
  • [ ] Final Review and Testing

🔍 Analysis & Investigation

Codebase Structure

  • This plan will introduce a new service, src/Service/Api/FilterService.php, and a new configuration file, config/packages/api_filters.yaml.
  • It will refactor and simplify all API-facing repositories (CoffeeBeanRepository, RegionRepository, VarietyRepository, etc.) and their corresponding controllers.

Current Architecture & Problem

  • Problem: The current architecture has filtering logic scattered across multiple repository methods. Each time a new filter is needed, developers must manually add if statements and query builder conditions, leading to repetitive code, potential inconsistencies, and high maintenance overhead.
  • Solution: This plan refactors the architecture to a centralized, configuration-driven model. A single FilterService will read a list of allowed filters from a config file and apply them dynamically. This adheres to the Don't Repeat Yourself (DRY) principle and makes the system much more scalable.

Dependencies & Integration Points

  • Doctrine: The FilterService will interact directly with the Doctrine Query Builder, making it a core part of the data access layer.
  • Symfony DI: The service will be configured and injected into the repositories that need it.

Considerations & Challenges

  • Refactoring Effort: The main challenge is the one-time effort to refactor all existing filter logic. This must be done carefully to ensure no functionality is lost and that all endpoints behave as they did before.
  • Configuration Design: The structure of the api_filters.yaml file needs to be well-designed to handle different filter types (boolean, string, UUID arrays, etc.) in a generic way.

📝 Implementation Plan

Prerequisites

  • No new external dependencies are required.

Step-by-Step Implementation

  1. Create FilterService and Configuration

    • Files to create: src/Service/Api/FilterService.php, config/packages/api_filters.yaml
    • api_filters.yaml: Define all allowed filters for each entity type. This configuration will be the single source of truth for what can be filtered.
      parameters:
          api_filters:
              App\Entity\CoffeeBean:
                  isSingleOrigin: { type: 'boolean', field: 'originType' }
                  available: { type: 'boolean', field: 'available' }
                  roastLevelIds: { type: 'uuid_array', field: 'roastLevels' }
              App\Entity\Region:
                  countryIds: { type: 'uuid_array', field: 'country' }
              App\Entity\Variety:
                  speciesIds: { type: 'uuid_array', field: 'species' }
      
    • FilterService.php: Create the service. It will have one primary public method, apply(QueryBuilder $qb, Request $request). This method will:
      • Determine the entity class from the QueryBuilder's from part.
      • Look up the allowed filters for that entity in the configuration.
      • Iterate over the request's query parameters.
      • If a query parameter matches an allowed filter, apply the corresponding andWhere clause to the QueryBuilder.
  2. Refactor Controllers and Repositories

    • Files to modify: All API controllers that list resources (e.g., CoffeeBeanController, LocationController) and their corresponding repositories.
    • Changes needed:
      • Controllers: Remove all manual filter extraction logic (e.g., if ($request->query->has('isSingleOrigin'))). The controller's role is simplified to just passing the Request object to the repository.
      • Repositories: Inject the FilterService. The various findByFilters... methods will be dramatically simplified. For example, a repository method that handles filtering and pagination will look like this:
        // Before
        public function findByFiltersWithPagination(array $filters = [], int $limit = 20, int $offset = 0): Paginator
        {
            $qb = $this->createQueryBuilder('r');
            // ... many lines of if statements for each filter ...
            $qb->setMaxResults($limit)->setFirstResult($offset);
            return new Paginator($qb->getQuery());
        }
        
        // After
        public function findByRequest(Request $request, int $limit = 20, int $offset = 0): Paginator
        {
            $qb = $this->createQueryBuilder('r');
            $this->filterService->apply($qb, $request); // One line to handle all filters
            $qb->setMaxResults($limit)->setFirstResult($offset);
            return new Paginator($qb->getQuery());
        }
        

Testing Strategy

  • Unit Tests: Write extensive unit tests for the FilterService. Test each filter type (boolean, array, etc.) to ensure it builds the correct query condition.
  • Integration Tests: For each refactored endpoint, write integration tests that pass various combinations of filters and assert that the API returns the correctly filtered data. This ensures the refactoring did not cause any regressions.

🎯 Success Criteria

  • All API filtering logic is centralized in the FilterService.
  • Repository methods are significantly simplified and no longer contain repetitive filter logic.
  • Adding a new filter is as simple as adding an entry to the api_filters.yaml configuration file.
  • All API endpoints behave exactly as they did before the refactoring.
  • The codebase is more maintainable and scalable.