FetchHandler Refactoring - 2026-01-06
Overview
This document describes the refactoring that eliminated the circular dependency between ConfigurationService and PreviewHandler by extracting remote fetching logic into a dedicated FetchHandler.
Problem Statement
Original Architecture (Circular Dependency)
ConfigurationService
└── PreviewHandler
└── ConfigurationService (CIRCULAR!)
Issue: PreviewHandler needed to call ConfigurationService::fetchRemoteConfiguration() to get remote configuration data, but ConfigurationService already depends on PreviewHandler for preview functionality. This created a circular dependency that required complex lazy-loading workarounds.
Why This Was Bad
- Tight Coupling: PreviewHandler was unnecessarily coupled to the entire ConfigurationService just to fetch remote data
- Fragile Solution: Required lazy-loading via container and nullable properties
- Hard to Test: Circular dependencies make unit testing difficult
- Confusing Responsibility: ConfigurationService had too many responsibilities
Solution: Extract FetchHandler
New Architecture (No Circular Dependencies)
ConfigurationService
├── PreviewHandler
│ └── FetchHandler (✅ No circular dependency!)
└── FetchHandler
ImportHandler
└── FetchHandler (bonus: can also use it!)
Key Insight: PreviewHandler doesn't need the entire ConfigurationService - it only needs the ability to fetch remote configuration data. By extracting this into FetchHandler, we break the circular dependency.
Implementation
1. Created FetchHandler
File: lib/Service/Configuration/FetchHandler.php
Responsibilities:
- Fetch JSON/YAML data from URLs
- Fetch remote configuration data for Configuration entities
- Parse response data (JSON/YAML detection)
- Error handling for remote requests
Dependencies:
Client(Guzzle HTTP client)LoggerInterface(PSR logger)
Key Methods:
public function getJSONfromURL(string $url): array|JSONResponse
public function fetchRemoteConfiguration(Configuration $configuration): array|JSONResponse
private function decode(string $data, string $type): ?array
2. Updated ConfigurationService
Changes:
- Added
getFetchHandler()method for lazy loading FetchHandler - Modified
getJSONfromURL()to delegate to FetchHandler - Modified
fetchRemoteConfiguration()to delegate to FetchHandler
Example:
private function getFetchHandler(): FetchHandler
{
return $this->container->get('OCA\OpenRegister\Service\Configuration\FetchHandler');
}
public function fetchRemoteConfiguration(Configuration $configuration): array|JSONResponse
{
return $this->getFetchHandler()->fetchRemoteConfiguration($configuration);
}
3. Updated PreviewHandler
Before:
class PreviewHandler
{
private ?ConfigurationService $configurationService = null;
private ?ContainerInterface $container = null;
private function getConfigurationService(): ConfigurationService
{
if ($this->configurationService === null) {
$this->configurationService = $this->container->get(ConfigurationService::class);
}
return $this->configurationService;
}
public function previewConfigurationChanges(Configuration $configuration): array|JSONResponse
{
$remoteData = $this->getConfigurationService()->fetchRemoteConfiguration($configuration);
// ...
}
}
After:
class PreviewHandler
{
private readonly FetchHandler $fetchHandler;
public function __construct(
RegisterMapper $registerMapper,
SchemaMapper $schemaMapper,
LoggerInterface $logger,
FetchHandler $fetchHandler // ✅ Direct injection!
) {
$this->fetchHandler = $fetchHandler;
// ...
}
public function previewConfigurationChanges(Configuration $configuration): array|JSONResponse
{
$remoteData = $this->fetchHandler->fetchRemoteConfiguration($configuration);
// ...
}
}
Benefits:
- ✅ No lazy loading needed
- ✅ No nullable properties
- ✅ No circular dependency
- ✅ Clean constructor injection
- ✅ Immutable (
readonly) property
4. Bonus: ImportHandler Can Also Use FetchHandler
Although not required for this refactoring, ImportHandler can now also use FetchHandler instead of duplicating the fetching logic. This creates a single source of truth for all remote fetching operations.
Testing
All services resolve correctly without hanging:
✅ FetchHandler resolved!
✅ PreviewHandler resolved (with FetchHandler)!
✅ ConfigurationService resolved!
✅ Import functionality works!
Benefits of This Refactoring
1. No Circular Dependencies
- Clean dependency graph
- Each service has clear responsibilities
- No lazy-loading hacks needed
2. Better Separation of Concerns
- FetchHandler: Remote data fetching
- PreviewHandler: Comparison logic
- ConfigurationService: Orchestration
3. Easier Testing
- FetchHandler can be mocked easily
- PreviewHandler tests don't need full ConfigurationService
- Unit tests are simpler
4. More Reusable
- FetchHandler can be used by any service that needs remote fetching
- Not coupled to configuration-specific logic
5. Better Performance
- No lazy-loading overhead
- Direct dependency injection
- Immutable properties (readonly)
Code Quality Improvements
Before (Complexity: High)
- Circular dependency
- Lazy loading with nullable properties
- Container passed to constructor
- Complex initialization logic
- Hard to understand flow
After (Complexity: Low)
- Linear dependency graph
- Direct constructor injection
- Readonly properties
- Simple, clear flow
- Easy to understand
Migration Notes
Backward Compatibility
The public API of ConfigurationService and PreviewHandler remains unchanged. All existing code continues to work without modification.
Auto-wiring
FetchHandler uses auto-wiring and doesn't require explicit DI registration in Application.php. Nextcloud's DI container automatically resolves it based on constructor parameters.
Recommendations for Future Development
- Keep handlers focused: Each handler should have a single responsibility
- Extract shared logic: If multiple services need the same functionality, extract it to a dedicated handler
- Avoid circular dependencies: Use composition over inheritance, and extract shared functionality when needed
- Use readonly properties: For dependencies that don't change after construction
- Prefer constructor injection: Over lazy loading when possible
Files Modified
- ✅
lib/Service/Configuration/FetchHandler.php(NEW) - ✅
lib/Service/ConfigurationService.php(UPDATED) - ✅
lib/Service/Configuration/PreviewHandler.php(UPDATED)
Conclusion
This refactoring successfully eliminates the circular dependency between ConfigurationService and PreviewHandler by extracting fetching logic into a dedicated FetchHandler. The result is a cleaner architecture with better separation of concerns, easier testing, and improved maintainability.
Status: ✅ COMPLETED
Date: 2026-01-06
Impact: Breaking the circular dependency, improved architecture
Tests: All passing