Annotations Reference

Reference for Canvas framework annotations. Annotations configure routing, aspect-oriented programming, and controller configuration.

Routing Annotations

@Route

Purpose: Defines HTTP routes in controller methods.

/**
 * @Route("/users/{id}", methods={"GET", "PUT", "DELETE"}, name="user.edit")
 */
public function edit(int $id) { ... }

Parameters:

Parameter Type Required Description
path string Yes URL pattern with optional {placeholders}
methods array No HTTP methods (default: ["GET"])
name string No Named route identifier for URL generation

@RoutePrefix

Purpose: Automatically prefixes all routes in a controller class with a common path segment. Useful for API versioning or grouping related endpoints.

Use When: Multiple routes in a controller share a common path prefix (e.g., "/api", "/v1", "/admin").

/**
 * @RoutePrefix("/api/v1")
 */
class ProductController extends BaseController {

    /**
     * @Route("/products")
     * Final route: /api/v1/products
     */
    public function index() { ... }

    /**
     * @Route("/products/{id}")
     * Final route: /api/v1/products/{id}
     */
    public function show(int $id) { ... }
}

Parameters:

Parameter Type Required Description
value string Yes Path prefix to prepend to all routes in the controller (leading/trailing slashes are automatically normalized)

Aspect-Oriented Programming

@InterceptWith

Purpose: Applies cross-cutting concerns (caching, auth, logging) to controller methods or entire classes. Multiple aspects can be stacked.

/**
 * @InterceptWith(CacheAspect::class, ttl=300, tags={"reports", "admin"})
 * @InterceptWith(RequireAuthAspect::class)
 */
public function reports() { ... }

Parameters:

Parameter Type Required Description
class string Yes Fully qualified aspect class name (e.g., CacheAspect::class)
...parameters mixed No Additional named parameters passed to aspect constructor

Common Aspect-Specific Parameters:

Parameter Type Used By Description
ttl int CacheAspect Cache time-to-live in seconds
tags array CacheAspect Cache invalidation tags for grouped clearing
limit int RateLimitAspect Maximum requests allowed in time window
window int RateLimitAspect Time window in seconds for rate limiting
permission string RequirePermissionAspect Required authorization permission string

Cache Configuration

@CacheContext

Purpose: Configures cache behavior for methods that inject CacheInterface. Allows per-method customization of cache driver, namespace, and connection parameters without modifying aspect configuration.

Use When: Different methods need different cache backends (e.g., Redis for sessions, file cache for temporary data) or isolated cache namespaces.

class ProductService {

    /**
     * Uses Redis cache with 'products' namespace
     * @CacheContext(driver="redis", namespace="products")
     */
    public function getProducts(CacheInterface $cache): array {
        return $cache->remember('all_products', 3600, function() {
            return $this->repository->findAll();
        });
    }

    /**
     * Uses file cache with custom namespace
     * @CacheContext(driver="file", namespace="temp_reports")
     */
    public function generateReport(CacheInterface $cache): string {
        return $cache->get('report_123');
    }

    /**
     * Uses Redis with custom connection config
     * @CacheContext(driver="redis", namespace="sessions", host="session-redis.local", port=6380)
     */
    public function getSession(CacheInterface $cache, string $sessionId): ?array {
        return $cache->get("session_{$sessionId}");
    }
}

Parameters:

Parameter Type Required Description
driver string No Cache driver name (e.g., "file", "redis", "memcached"). Must match a driver configured in config/cache.php. Falls back to default driver if not specified.
namespace string No Cache namespace for key isolation (default: "default"). Useful for separating cache concerns (e.g., "sessions", "products", "temp").
...custom mixed No Driver-specific configuration parameters (e.g., host, port, database for Redis; servers for Memcached). These override config/cache.php settings for this method.

Configuration Priority:

Cache configuration is resolved in the following order (later sources override earlier ones):

  1. Default driver configuration from config/cache.php
  2. @CacheContext annotation parameters
  3. Runtime parameters passed to Container::get(CacheInterface::class, $params)

Complete Controller Example

/**
 * API v1 Product Controller
 * All routes automatically prefixed with /api/v1
 *
 * @RoutePrefix("/api/v1")
 * @InterceptWith(RequireAuthAspect::class)
 * @InterceptWith(AuditLogAspect::class)
 */
class ProductController extends BaseController {

    /**
     * List all products (cached for 5 minutes)
     * Final route: GET /api/v1/products
     *
     * @Route("/products", methods={"GET"}, name="products.index")
     * @InterceptWith(CacheAspect::class, ttl=300, namespace="products")
     */
    public function index() {
        return $this->productRepository->findAll();
    }

    /**
     * View single product (cached for 10 minutes)
     * Final route: GET /api/v1/products/{id}
     *
     * @Route("/products/{id}", methods={"GET"}, name="products.show")
     * @InterceptWith(CacheAspect::class, ttl=600, namespace="products")
     */
    public function show(int $id) {
        return $this->productRepository->find($id);
    }

    /**
     * Create new product (requires permission, wrapped in transaction)
     * Final route: POST /api/v1/products
     *
     * @Route("/products", methods={"POST"}, name="products.create")
     * @InterceptWith(RequirePermissionAspect::class, permission="products.create")
     * @InterceptWith(TransactionAspect::class)
     */
    public function create() {
        $product = $this->productRepository->create($this->request->all());
        return $this->created($product);
    }

    /**
     * Update product (requires permission, wrapped in transaction, rate limited)
     * Final route: PUT /api/v1/products/{id}
     *
     * @Route("/products/{id}", methods={"PUT"}, name="products.update")
     * @InterceptWith(RequirePermissionAspect::class, permission="products.edit")
     * @InterceptWith(TransactionAspect::class)
     * @InterceptWith(RateLimitAspect::class, limit=60, window=3600)
     */
    public function update(int $id) {
        $product = $this->productRepository->update($id, $this->request->all());
        return $this->success($product);
    }

    /**
     * Delete product (requires permission, wrapped in transaction)
     * Final route: DELETE /api/v1/products/{id}
     *
     * @Route("/products/{id}", methods={"DELETE"}, name="products.delete")
     * @InterceptWith(RequirePermissionAspect::class, permission="products.delete")
     * @InterceptWith(TransactionAspect::class)
     */
    public function delete(int $id) {
        $this->productRepository->delete($id);
        return $this->noContent();
    }
}