Skip to main content

Overview

ControllerResolver is responsible for discovering controller classes in your application, managing their lifecycle, and resolving their dependencies. It automatically scans your src/controller directory, caches controller classes, and instantiates them with proper dependency injection. This component is used internally by Framefox’s routing system to resolve controller references and create controller instances when handling requests.

How It Works

The controller resolver follows this workflow:
  1. Discovery: Scans src/controller directory for Python files containing controller classes
  2. Registration: Maps controller names to their class definitions
  3. Caching: Stores controller classes for quick lookup
  4. Resolution: Resolves controller instances with dependency injection when needed

Controller Discovery

Naming Conventions

The resolver automatically discovers controllers based on these conventions:
  • File names: Any .py file in src/controller/ (excluding __init__.py)
  • Class names: Classes ending with Controller (e.g., UserController, ArticleController)
  • Controller names: Generated by removing Controller suffix and converting to lowercase
    • UserControlleruser
    • ArticleControllerarticle
    • AdminDashboardControlleradmindashboard

Directory Structure

The resolver recursively scans subdirectories:
src/
  controller/
    user_controller.py          # Discovered as 'user'
    article_controller.py       # Discovered as 'article'
    admin/
      dashboard_controller.py   # Discovered as 'dashboard'
      settings_controller.py    # Discovered as 'settings'

Constructor

__init__()

Initializes the controller resolver and automatically discovers all controllers.
def __init__(self)
What it does:
  • Initializes the service container for dependency injection
  • Sets up logging for controller operations
  • Creates an empty controller cache
  • Discovers and registers all controllers in src/controller
Example:
from framefox.core.controller.controller_resolver import ControllerResolver

# The resolver is typically instantiated by the framework
resolver = ControllerResolver()
# At this point, all controllers have been discovered

Methods

resolve_controller()

Resolves a controller by name and returns an instantiated controller object with all dependencies injected.
def resolve_controller(self, controller_name: str) -> Any
controller_name
str
required
The name of the controller to resolve (without the “Controller” suffix, lowercase)
return
Any
An instantiated controller object with all dependencies resolved
Raises:
  • ControllerNotFoundError: If no controller with the given name exists
  • ControllerInstantiationError: If the controller cannot be instantiated
  • ControllerDependencyError: If a required dependency cannot be resolved
Example:
# Resolve a controller by name
user_controller = resolver.resolve_controller('user')

# Call a method on the resolved controller
response = user_controller.index()
How it works:
  1. Checks if controller name exists in the name-to-class mapping
  2. Checks the controller cache for previously loaded classes
  3. If not cached, loads the controller class from the file system
  4. Resolves all constructor dependencies
  5. Instantiates and returns the controller

_discover_controller_paths() (Internal)

Scans the src/controller directory and builds a mapping of controller names to file paths.
def _discover_controller_paths(self) -> Dict[str, Path]
return
Dict[str, Path]
Dictionary mapping controller names to their file paths
Raises:
  • DuplicateControllerError: If multiple controllers with the same generated name are found
  • ControllerModuleError: If a controller module cannot be imported
What it does:
  1. Recursively scans src/controller for .py files
  2. Imports each module and inspects for controller classes
  3. Generates controller names by removing “Controller” suffix
  4. Detects and prevents duplicate controller names
  5. Builds mappings for quick lookup

_load_controller_class() (Internal)

Loads a controller class from the file system by name.
def _load_controller_class(self, controller_name: str) -> Optional[Type]
controller_name
str
required
The name of the controller to load
return
Optional[Type]
The controller class if found, None otherwise

_load_from_path() (Internal)

Loads a controller class from a specific file path.
def _load_from_path(self, file_path: Path) -> Optional[Type]
file_path
Path
required
The path to the Python file containing the controller
return
Optional[Type]
The controller class if found, None otherwise
Raises:
  • ControllerModuleError: If the module cannot be imported

_create_controller_instance() (Internal)

Creates an instance of a controller class with dependency injection.
def _create_controller_instance(self, controller_class: Type) -> Any
controller_class
Type
required
The controller class to instantiate
return
Any
An instantiated controller object
Raises:
  • ControllerInstantiationError: If instantiation fails

_resolve_controller_dependencies() (Internal)

Resolves constructor dependencies for a controller class using the service container.
def _resolve_controller_dependencies(self, controller_class: Type) -> list
controller_class
Type
required
The controller class to resolve dependencies for
return
list
List of resolved dependency instances in the order they appear in the constructor
Raises:
  • ControllerDependencyError: If a required dependency cannot be resolved
How it works:
  1. Inspects the controller’s __init__ signature
  2. For each parameter (except self):
    • If type-annotated, retrieves the dependency from the service container
    • If has a default value, uses the default if retrieval fails
    • Raises an error if required dependency cannot be resolved
  3. Returns list of resolved dependencies

Dependency Injection

The resolver automatically injects dependencies into controller constructors based on type annotations.

Basic Example

from framefox.core.controller.abstract_controller import AbstractController
from src.service.user_service import UserService
from src.repository.user_repository import UserRepository

class UserController(AbstractController):
    def __init__(self, user_service: UserService, user_repo: UserRepository):
        super().__init__()
        self.user_service = user_service
        self.user_repo = user_repo
    
    def list(self):
        users = self.user_service.get_all_users()
        return self.render('user/list.html', {'users': users})
When resolve_controller('user') is called:
  1. The resolver inspects UserController.__init__
  2. Finds type annotations: UserService and UserRepository
  3. Retrieves these services from the service container
  4. Instantiates: UserController(user_service_instance, user_repo_instance)

Optional Dependencies

You can specify optional dependencies with default values:
class ArticleController(AbstractController):
    def __init__(self, 
                 article_service: ArticleService,
                 cache_service: CacheService = None):
        super().__init__()
        self.article_service = article_service
        self.cache_service = cache_service
    
    def show(self, article_id: int):
        # Use cache if available
        if self.cache_service:
            cached = self.cache_service.get(f'article:{article_id}')
            if cached:
                return cached
        
        article = self.article_service.get_by_id(article_id)
        return self.render('article/show.html', {'article': article})

Caching

The resolver uses two levels of caching for performance:
  1. Name-to-Class Mapping: Built during initial discovery
  2. Controller Cache: Stores loaded controller classes
Both caches persist for the lifetime of the resolver instance.
# First call: loads and caches the controller class
controller1 = resolver.resolve_controller('user')

# Second call: retrieves from cache (much faster)
controller2 = resolver.resolve_controller('user')

# Each call creates a NEW instance, but class loading is cached
assert controller1 is not controller2  # Different instances
assert type(controller1) is type(controller2)  # Same class

Error Handling

The resolver provides detailed error messages for common issues:

ControllerNotFoundError

Thrown when a controller cannot be found:
try:
    resolver.resolve_controller('nonexistent')
except ControllerNotFoundError as e:
    # Error includes searched paths for debugging
    print(f"Controller not found: {e}")

DuplicateControllerError

Thrown during discovery if multiple controllers have the same name:
# src/controller/user_controller.py
class UserController(AbstractController):
    pass

# src/controller/admin/user_controller.py
class UserController(AbstractController):  # Duplicate!
    pass

# Raises: DuplicateControllerError('user', [...])
Solution: Rename one of the controllers or use unique class names.

ControllerModuleError

Thrown if a controller module has import errors:
# src/controller/broken_controller.py
from nonexistent_module import Something  # Import error!

class BrokenController(AbstractController):
    pass

# Raises: ControllerModuleError with import details

ControllerDependencyError

Thrown when a required dependency cannot be resolved:
class ProductController(AbstractController):
    def __init__(self, product_service: ProductService):
        super().__init__()
        self.product_service = product_service

# If ProductService is not registered in the service container:
# Raises: ControllerDependencyError

ControllerInstantiationError

Thrown if controller instantiation fails for any reason:
class FailingController(AbstractController):
    def __init__(self):
        super().__init__()
        raise Exception("Something went wrong!")

# Raises: ControllerInstantiationError with details

Best Practices

1. Follow Naming Conventions

Always end controller class names with Controller:
# ✅ Good
class UserController(AbstractController):
    pass

# ❌ Bad
class User(AbstractController):
    pass

2. Use Type Annotations for Dependencies

Always type-annotate constructor parameters:
# ✅ Good
def __init__(self, user_service: UserService):
    self.user_service = user_service

# ❌ Bad (dependency won't be resolved)
def __init__(self, user_service):
    self.user_service = user_service

3. Avoid Name Collisions

Ensure controller names are unique across your application:
# ❌ Bad - Both generate 'user' as the controller name
# src/controller/user_controller.py
class UserController(AbstractController):
    pass

# src/controller/admin/user_controller.py
class UserController(AbstractController):
    pass

# ✅ Good - Use distinct names
# src/controller/user_controller.py
class UserController(AbstractController):
    pass

# src/controller/admin/user_controller.py
class AdminUserController(AbstractController):
    pass

4. Call super().init()

Always call the parent constructor when overriding __init__:
# ✅ Good
class UserController(AbstractController):
    def __init__(self, user_service: UserService):
        super().__init__()  # Important!
        self.user_service = user_service

# ❌ Bad
class UserController(AbstractController):
    def __init__(self, user_service: UserService):
        self.user_service = user_service  # Missing super().__init__()

5. Register Dependencies in Service Container

Ensure all injected services are registered:
# In your service configuration
from framefox.core.di.service_container import ServiceContainer
from src.service.user_service import UserService

container = ServiceContainer()
container.register('UserService', UserService)

Complete Example

Here’s a complete example showing controller resolution with dependency injection:
# src/service/article_service.py
class ArticleService:
    def get_all(self):
        return Article.query.all()
    
    def get_by_id(self, article_id: int):
        return Article.query.get(article_id)

# src/controller/article_controller.py
from framefox.core.controller.abstract_controller import AbstractController
from src.service.article_service import ArticleService

class ArticleController(AbstractController):
    def __init__(self, article_service: ArticleService):
        super().__init__()
        self.article_service = article_service
    
    def list(self):
        articles = self.article_service.get_all()
        return self.render('article/list.html', {'articles': articles})
    
    def show(self, article_id: int):
        article = self.article_service.get_by_id(article_id)
        return self.render('article/show.html', {'article': article})

# The framework automatically resolves and instantiates:
resolver = ControllerResolver()
article_controller = resolver.resolve_controller('article')
# ArticleService is automatically injected!

See Also

Build docs developers (and LLMs) love