Feature Implementation Plan: Unified API Filtering System¶
📋 Todo Checklist¶
- [ ] Create a generic
FilterServiceto 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
FilterServiceand 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
ifstatements 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
FilterServicewill 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
FilterServicewill 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.yamlfile 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¶
-
Create
FilterServiceand 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'sfrompart. - 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
andWhereclause to theQueryBuilder.
- Determine the entity class from the
- Files to create:
-
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 theRequestobject to the repository. - Repositories: Inject the
FilterService. The variousfindByFilters...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()); }
- Controllers: Remove all manual filter extraction logic (e.g.,
- Files to modify: All API controllers that list resources (e.g.,
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.yamlconfiguration file. - All API endpoints behave exactly as they did before the refactoring.
- The codebase is more maintainable and scalable.