PHP code example of quellabs / canvas

1. Go to this page and download the library: Download quellabs/canvas library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

quellabs / canvas example snippets



use Quellabs\Canvas\Kernel;
use Symfony\Component\HttpFoundation\Request;

ernel->handle($request);
$response->send();


namespace App\Controllers;

use Quellabs\Canvas\Annotations\Route;
use Quellabs\Canvas\Controllers\BaseController;

class BlogController extends BaseController {
    
    /**
     * @Route("/")
     */
    public function index() {
        return $this->render('home.tpl');
    }
    
    /**
     * @Route("/posts")
     */
    public function list() {
        $posts = $this->em->findBy(Post::class, ['published' => true]);
        return $this->render('posts.tpl', $posts);
    }

    /**
     * @Route("/posts/{id:int}")
     */
    public function show(int $id) {
        $post = $this->em->find(Post::class, $id);
        return $this->render('post.tpl', $post);
    }
}

// Find by primary key - fastest lookup method
$user = $this->em->find(User::class, $id);

// Simple filtering using findBy - perfect for basic criteria
$activeUsers = $this->em->findBy(User::class, ['active' => true]);
$recentPosts = $this->em->findBy(Post::class, ['published' => true]);

// Basic ObjectQuel query
$results = $this->em->executeQuery("
    range of p is App\\Entity\\Post
    retrieve (p) where p.published = true
    sort by p.publishedAt desc
");

// Queries with relationships and parameters
$techPosts = $this->em->executeQuery("
    range of p is App\\Entity\\Post
    range of u is App\\Entity\\User via p.authorId
    retrieve (p, u.name) where p.title = /^Tech/i
    and p.published = :published
    sort by p.publishedAt desc
", [
    'published' => true
]);

class ProductController extends BaseController {
    
    /**
     * @Route("/products/{id:int}")
     */
    public function show(int $id) {
        // Only matches numeric IDs
        // /products/123 ✓  /products/abc ✗
    }
    
    /**
     * @Route("/users/{username:alpha}")
     */
    public function profile(string $username) {
        // Only matches alphabetic characters
        // /users/johndoe ✓  /users/john123 ✗
    }
    
    /**
     * @Route("/posts/{slug:slug}")
     */
    public function post(string $slug) {
        // Matches URL-friendly slugs
        // /posts/hello-world ✓  /posts/hello_world ✗
    }
}

class FileController extends BaseController {
    
    /**
     * @Route("/files/{path:**}")
     */
    public function serve(string $path) {
        // Matches any path depth with wildcards
        // /files/css/style.css → path = "css/style.css"
        // /files/images/icons/user.png → path = "images/icons/user.png"
        return $this->serveFile($path);
    }
}

/**
 * @RoutePrefix("/api/v1")
 */
class ApiController extends BaseController {
    
    /**
     * @Route("/users")  // Actual route: /api/v1/users
     */
    public function users() {
        return $this->json($this->em->findBy(User::class, []));
    }
    
    /**
     * @Route("/users/{id:int}")  // Actual route: /api/v1/users/{id}
     */
    public function user(int $id) {
        $user = $this->em->find(User::class, $id);
        return $this->json($user);
    }
}

class UserController extends BaseController {
    
    /**
     * @Route("/users", methods={"GET"})
     */
    public function index() {
        // Only responds to GET requests
    }
    
    /**
     * @Route("/users", methods={"POST"})
     */
    public function create() {
        // Only responds to POST requests
    }
    
    /**
     * @Route("/users/{id:int}", methods={"PUT", "PATCH"})
     */
    public function update(int $id) {
        // Responds to both PUT and PATCH
    }
}


namespace App\Controllers;

use Quellabs\Canvas\Annotations\Route;
use Quellabs\Canvas\Annotations\InterceptWith;
use Quellabs\Canvas\Canvas\Validation\ValidateAspect;
use App\Validation\UserValidation;

class UserController extends BaseController {
    
    /**
     * @Route("/users/create", methods={"GET", "POST"})
     * @InterceptWith(ValidateAspect::class, validator=UserValidation::class)
     */
    public function create(Request $request) {
        if ($request->isMethod('POST')) {
            // Check validation results set by ValidateAspect
            if ($request->attributes->get('validation_passed', false)) {
                // Process valid form data
                $user = new User();
                $user->setName($request->request->get('name'));
                $user->setEmail($request->request->get('email'));
                $user->setPassword(password_hash($request->request->get('password'), PASSWORD_DEFAULT));
                
                $this->em->persist($user);
                $this->em->flush();
                
                return $this->redirect('/users');
            }
            
            // Validation failed - render form with errors
            return $this->render('users/create.tpl', [
                'errors' => $request->attributes->get('validation_errors', []),
                'old'    => $request->request->all()
            ]);
        }
        
        // Show empty form for GET requests
        return $this->render('users/create.tpl');
    }
}


namespace App\Validation;

use Quellabs\Canvas\Validation\Contracts\SanitizationInterface;
use Quellabs\Canvas\Validation\Rules\NotBlank;
use Quellabs\Canvas\Validation\Rules\Email;
use Quellabs\Canvas\Validation\Rules\Length;
use Quellabs\Canvas\Validation\Rules\ValueIn;

class UserValidation implements SanitizationInterface {
    
    public function getRules(): array {
        return [
            'name' => [
                new NotBlank('Name is             new ValueIn(['admin', 'user', 'moderator'], 'Please select a valid role')
            ]
        ];
    }
}

/**
 * @Route("/api/users", methods={"POST"})
 * @InterceptWith(ValidateAspect::class, validator=UserValidation::class, auto_respond=true)
 */
public function createUser(Request $request) {
    // For API requests, validation failures automatically return JSON:
    // {
    //   "message": "Validation failed", 
    //   "errors": {
    //     "email": ["Please enter a valid email address"],
    //     "password": ["Password must be at least 8 characters"]
    //   }
    // }
    
    // This code only runs if validation passes
    $user = $this->createUserFromRequest($request);
    return $this->json(['success' => true, 'user_id' => $user->getId()]);
}


namespace App\Validation\Rules;

use Quellabs\Canvas\Validation\Contracts\SanitizationRuleInterface;

class StrongPassword implements SanitizationRuleInterface {
    
    public function validate($value, array $options = []): bool {
        if (empty($value)) {
            return false;
        }
        
        // Must contain uppercase, lowercase, number, and special character
        return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/', $value);
    }
    
    public function getMessage(): string {
        return 'Password must contain uppercase, lowercase, number, and special character';
    }
}

'password' => [
    new NotBlank(),
    new Length(8),
    new StrongPassword()
]


namespace App\Controllers;

use Quellabs\Canvas\Annotations\Route;
use Quellabs\Canvas\Annotations\InterceptWith;
use Quellabs\Canvas\Sanitization\SanitizeAspect;
use App\Sanitization\UserSanitization;

class UserController extends BaseController {
    
    /**
     * @Route("/users/create", methods={"GET", "POST"})
     * @InterceptWith(SanitizeAspect::class, sanitizer=UserSanitization::class)
     */
    public function create(Request $request) {
        if ($request->isMethod('POST')) {
            // Request data is automatically sanitized before reaching this point
            $name = $request->request->get('name');     // Already trimmed and cleaned
            $email = $request->request->get('email');   // Already normalized
            $bio = $request->request->get('bio');       // Already stripped of dangerous HTML
            
            // Process the clean data
            $user = new User();
            $user->setName($name);
            $user->setEmail($email);
            $user->setBio($bio);
            
            $this->em->persist($user);
            $this->em->flush();
            
            return $this->redirect('/users');
        }
        
        return $this->render('users/create.tpl');
    }
}


namespace App\Sanitization;

use Quellabs\Canvas\Sanitization\Contracts\SanitizationInterface;
use Quellabs\Canvas\Sanitization\Rules\Trim;
use Quellabs\Canvas\Sanitization\Rules\EmailSafe;
use Quellabs\Canvas\Sanitization\Rules\StripTags;
use Quellabs\Canvas\Sanitization\Rules\HtmlEscape;

class UserSanitization implements SanitizationInterface {
    
    public function getRules(): array {
        return [
            'name' => [
                new Trim(),                    // Remove leading/trailing whitespace
                new StripTags()                // Remove HTML tags
            ],
            'email' => [
                new Trim(),
                new EmailSafe()                // Remove non-email characters
            ],
            'website' => [
                new Trim(),
                new UrlSafe()                  // Remove unsafe URL characters
            ]
        ];
    }
}

class ContactController extends BaseController {
    
    /**
     * @Route("/contact", methods={"GET", "POST"})
     * @InterceptWith(ValidateAspect::class, validator=ContactValidation::class)
     * @InterceptWith(SanitizeAspect::class, sanitizer=ContactSanitization::class)
     */
    public function contact(Request $request) {
        if ($request->isMethod('POST')) {
            // Data flow: Raw Input → Sanitization → Validation → Controller
            // Only clean, valid data reaches your business logic
            
            if ($request->attributes->get('validation_passed', false)) {
                $this->processContactForm($request);
                return $this->redirect('/contact/success');
            }
            
            return $this->render('contact.tpl', [
                'errors' => $request->attributes->get('validation_errors', []),
                'old'    => $request->request->all()  // Already sanitized data
            ]);
        }
        
        return $this->render('contact.tpl');
    }
}

class ProductSanitization implements SanitizationInterface {
    
    public function getRules(): array {
        return [
            'title' => [
                new Trim(),                   // 1. Remove whitespace
                new StripTags(),              // 2. Remove HTML
            ],
            'description' => [
                new Trim(),
                new StripTags(['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'])
            ],
            'slug' => [
                new Trim(),
                new PathSafe()                // Make safe for URLs/paths
            ]
        ];
    }
}


namespace App\Sanitization\Rules;

use Quellabs\Canvas\Sanitization\Contracts\SanitizationRuleInterface;

class CleanPhoneNumber implements SanitizationRuleInterface {
    
    public function sanitize(mixed $value): mixed {
        if (!is_string($value)) {
            return $value;
        }
        
        // Remove all non-numeric characters except + for international prefix
        // Keeps: digits and leading + sign
        // Removes: spaces, hyphens, parentheses, dots, etc.
        $cleaned = preg_replace('/[^0-9+]/', '', $value);
        
        // Ensure + only appears at the beginning
        if (str_contains($cleaned, '+')) {
            $cleaned = '+' . str_replace('+', '', $cleaned);
        }
        
        return $cleaned;
    }
}

// If sanitization class doesn't exist
InvalidArgumentException: Sanitization class 'App\Invalid\Class' does not exist

// If class doesn't implement SanitizationInterface  
InvalidArgumentException: Sanitization class 'App\MyClass' must implement SanitizationInterface

// If sanitization class can't be instantiated
RuntimeException: Failed to instantiate sanitization class 'App\MyClass': Constructor 


use Quellabs\Canvas\Security\CsrfProtectionAspect;

class ContactController extends BaseController {
    
    /**
     * @Route("/contact", methods={"GET", "POST"})
     * @InterceptWith(CsrfProtectionAspect::class)
     */
    public function contact(Request $request) {
        if ($request->isMethod('POST')) {
            // CSRF token automatically validated before this method runs
            $this->processContactForm($request);
            return $this->redirect('/contact/success');
        }
        
        // Token available in template via request attributes
        return $this->render('contact.tpl', [
            'csrf_token' => $request->attributes->get('csrf_token'),
            'csrf_token_name' => $request->attributes->get('csrf_token_name')
        ]);
    }
}

/**
 * @InterceptWith(CsrfProtectionAspect::class, 
 *     tokenName="_token", 
 *     headerName="X-Custom-CSRF-Token",
 *     intention="contact_form",
 *     exemptMethods={"GET", "HEAD"},
 *     maxTokens=20
 * )
 */
public function sensitiveAction() {
    // Custom CSRF configuration
}


use Quellabs\Canvas\Security\SecurityHeadersAspect;

/**
 * @InterceptWith(SecurityHeadersAspect::class)
 */
class SecureController extends BaseController {
    
    /**
     * @Route("/admin/dashboard")
     */
    public function dashboard() {
        // Response automatically 

/**
 * @InterceptWith(SecurityHeadersAspect::class,
 *     frameOptions="DENY",
 *     noSniff=true,
 *     xssProtection=true,
 *     hstsMaxAge=31536000,
 *     hstsIncludeSubdomains=true,
 *     referrerPolicy="strict-origin-when-cross-origin",
 *     csp="default-src 'self'; script-src 'self' 'unsafe-inline'"
 * )
 */
public function secureApi() {
    // Strict security headers for sensitive operations
}

// Strict policy for admin areas
csp: "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'"

// Moderate policy for regular pages
csp: "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:"

// Development policy (more permissive)
csp: "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: blob:"


use Quellabs\Canvas\Cache\CacheAspect;

class ReportController extends BaseController {
    
    /**
     * @Route("/reports/monthly")
     * @InterceptWith(CacheAspect::class, ttl=3600)
     */
    public function monthlyReport() {
        // Expensive operation cached for 1 hour
        return $this->generateMonthlyReport();
    }
    
    /**
     * @Route("/reports/user/{id:int}")
     * @InterceptWith(CacheAspect::class, namespace="user_reports", ttl=1800)
     */
    public function userReport(int $id) {
        // Each user ID gets separate cache entry
        // Cached for 30 minutes in "user_reports" context
        return $this->generateUserReport($id);
    }
}

/**
 * @InterceptWith(CacheAspect::class, 
 *     namespace="products",
 *     key="product_catalog", 
 *     ttl=7200
 * )
 */
public function productCatalog() {
    // Uses fixed cache key regardless of arguments
    // Useful for methods that always return the same data
}

/**
 * @InterceptWith(CacheAspect::class,
 *     namespace="api_responses",                  // Cache namespace
 *     key=null,                                   // Auto-generate from method
 *     ttl=3600,                                   // 1 hour cache
 *     lockTimeout=10,                             // File lock timeout
 *     gracefulFallback=true                       // Execute method if caching fails
 * )
 */
public function expensiveOperation($param1, $param2) {
    // Custom cache configuration
}

// Method: ProductController::getProduct($id, $roduct_controller.get_product.arg0:123_arg1:true"

// Method: UserController::search($query, $filters)
// Arguments: ["admin", ["active" => true, "role" => "admin"]]
// Generated key: "user_controller.search.arg0:admin_arg1:array_a1b2c3"

// config/inspector.php
return [
    'enabled'  => true,  // Enables the inspector
    // ... other config
];


namespace App\Debug;

use Quellabs\Contracts\Inspector\EventCollectorInterface;
use Quellabs\Contracts\Inspector\InspectorPanelInterface;
use Symfony\Component\HttpFoundation\Request;

class CachePanel implements InspectorPanelInterface {
    
    private EventCollectorInterface $collector;
    private array $cacheEvents = [];
    
    public function __construct(EventCollectorInterface $collector) {
        $this->collector = $collector;
    }
    
    public function getSignalPatterns(): array {
        return ['debug.cache.*']; // Listen for cache-related events
    }
    
    public function processEvents(): void {
        $this->cacheEvents = $this->collector->getEventsBySignals($this->getSignalPatterns());
    }
    
    public function getName(): string {
        return 'cache';
    }
    
    public function getTabLabel(): string {
        return 'Cache (' . count($this->cacheEvents) . ')';
    }
    
    public function getIcon(): string {
        return '💾';
    }
    
    public function getData(Request $request): array {
        return [
            'events' => $this->cacheEvents,
            'hits' => array_filter($this->cacheEvents, fn($e) => $e['type'] === 'hit'),
            'misses' => array_filter($this->cacheEvents, fn($e) => $e['type'] === 'miss')
        ];
    }
    
    public function getStats(): array {
        $hits = count(array_filter($this->cacheEvents, fn($e) => $e['type'] === 'hit'));
        $total = count($this->cacheEvents);
        
        return [
            'cache_hit_ratio' => $total > 0 ? round(($hits / $total) * 100) . '%' : 'N/A'
        ];
    }
    
    public function getJsTemplate(): string {
        return <<<'JS'
return `
    <div class="debug-panel-section">
        <h3>Cache Operations (${data.events.length} total)</h3>
        <div class="canvas-debug-info-grid">
            <div class="canvas-debug-info-item">
                <span class="canvas-debug-label">Hits:</span>
                <span class="canvas-debug-value">${data.hits.length}</span>
            </div>
            <div class="canvas-debug-info-item">
                <span class="canvas-debug-label">Misses:</span>
                <span class="canvas-debug-value">${data.misses.length}</span>
            </div>
        </div>
        
        <div class="canvas-debug-item-list">
            ${data.events.map(event => `
                <div class="canvas-debug-item ${event.type === 'miss' ? 'error' : ''}">
                    <div class="canvas-debug-item-header">
                        <span class="canvas-debug-status-badge ${event.type === 'hit' ? 'success' : 'error'}">
                            ${event.type.toUpperCase()}
                        </span>
                        <span class="canvas-debug-text-mono">${escapeHtml(event.key)}</span>
                    </div>
                </div>
            `).join('')}
        </div>
    </div>
`;
JS;
    }
    
    public function getCss(): string {
        return <<<'CSS'
/* Custom styles for cache panel */
.canvas-debug-item.cache-miss {
    border-left: 4px solid #dc3545;
}

.canvas-debug-item.cache-hit {
    border-left: 4px solid #28a745;
}
CSS;
    }
}

// config/inspector.php
return [
    'enabled' => true,
    'panels' => [
        'cache' => \App\Debug\CachePanel::class,
        'security' => \App\Debug\SecurityPanel::class,
        'mail' => \App\Debug\MailPanel::class,
    ],
];


namespace App\Services;

use Quellabs\SignalHub\HasSignals;

class CacheService {
    use HasSignals;
    
    private Signal $cacheSignal;
     
    public function __construct() {
        $this->cacheSignal = $this->createSignal(['array'], 'debug.cache.get');
    }
    
    public function get(string $key): mixed {
        $startTime = microtime(true);
        
        $value = $this->backend->get($key);
        $executionTime = (microtime(true) - $startTime) * 1000;
        
        // Emit debug event for cache operations
        $this->cacheSignal->emit([
            'key' => $key,
            'type' => $value !== null ? 'hit' : 'miss',
            'execution_time_ms' => round($executionTime, 2)
        ]);
        
        return $value;
    }
}


namespace App\Tasks;

use Quellabs\Contracts\TaskScheduler\AbstractTask;

class DatabaseCleanupTask extends AbstractTask {
    
    public function handle(): void {
        // Your task logic here
        $this->cleanupExpiredSessions();
        $this->archiveOldLogs();
        $this->optimizeTables();
    }
    
    public function getDescription(): string {
        return "Clean up expired sessions and optimize database tables";
    }
    
    public function getSchedule(): string {
        return "0 2 * * *"; // Run daily at 2 AM
    }
    
    public function getName(): string {
        return "database-cleanup";
    }
    
    public function getTimeout(): int {
        return 1800; // 30 minutes timeout
    }
    
    public function enabled(): bool {
        return true; // Task is enabled
    }
    
    // Optional: Handle task failures
    public function onFailure(\Exception $exception): void {
        error_log("Database cleanup failed: " . $exception->getMessage());
        // Send notification, log to monitoring system, etc.
    }
    
    // Optional: Handle task timeouts
    public function onTimeout(\Exception $exception): void {
        error_log("Database cleanup timed out: " . $exception->getMessage());
        // Perform cleanup, send alerts, etc.
    }
    
    private function cleanupExpiredSessions(): void {
        // Implementation details...
    }
    
    private function archiveOldLogs(): void {
        // Implementation details...
    }
    
    private function optimizeTables(): void {
        // Implementation details...
    }
}

"0 */6 * * *"     // Every 6 hours
"30 2 * * *"      // Daily at 2:30 AM
"0 0 * * 0"       // Weekly on Sunday midnight
"0 0 1 * *"       // Monthly on the 1st
"*/15 * * * *"    // Every 15 minutes
"0 9 * * 1-5"     // Weekdays at 9 AM
"0 0 * * 1,3,5"   // Monday, Wednesday, Friday at midnight


namespace App\Services;

use Quellabs\SignalHub\HasSignals;
use Quellabs\SignalHub\Signal;

class UserService {
    use HasSignals;
    
    public Signal $userRegistered;
    
    public function __construct() {
        // Define a signal that passes a User object
        $this->userRegistered = $this->createSignal('userRegistered', [User::class]);
    }
    
    public function register(string $email, string $password): User {
        $user = new User($email, $password);
        $this->saveUser($user);
        
        // Notify other parts of the app
        $this->userRegistered->emit($user);
        
        return $user;
    }
}


namespace App\Services;

class EmailService {
    
    public function __construct(
        UserService $userService,
        private MailerInterface $mailer
    ) {
        // Connect to the userRegistered signal
        $userService->userRegistered->connect($this, 'sendWelcomeEmail');
    }
    
    public function sendWelcomeEmail(User $user): void {
        // Send welcome email when a user registers
        $this->mailer->send($user->getEmail(), 'Welcome!', 'welcome-template');
    }
}


// Create a system-wide signal
$signalHub = new SignalHub();
$loginSignal = $signalHub->createSignal('user.login', [User::class]);

// Connect a handler
$loginSignal->connect(function(User $user) {
    echo "User {$user->name} logged in";
});

// Emit the signal from anywhere
$loginSignal->emit($currentUser);


class UserController extends BaseController {
    
    public function __construct(private UserService $userService) {}
    
    /**
     * @Route("/register", methods={"POST"})
     */
    public function register(Request $request) {
        $email = $request->request->get('email');
        $password = $request->request->get('password');
        
        // This automatically emits the userRegistered signal
        $user = $this->userService->register($email, $password);
        
        return $this->json(['success' => true]);
    }
}


// public/index.php
use Quellabs\Canvas\Kernel;
use Symfony\Component\HttpFoundation\Request;

path' => __DIR__ . '/../'  // Path to your existing files
]);

$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();


// legacy/users.php - existing file, now enhanced with Canvas
use Quellabs\Canvas\Legacy\LegacyBridge;

// Access Canvas services in legacy code
$em = canvas('EntityManager');
$users = $em->findBy(User::class, ['active' => true]);

// Use ObjectQuel for complex queries
$recentUsers = $em->executeQuery("
    range of u is App\\Entity\\User
    retrieve u where u.active = true and u.createdAt > :since
    sort by u.createdAt desc
    limit 10
", ['since' => date('Y-m-d', strtotime('-30 days'))]);

echo "Found " . count($users) . " active users<br>";

foreach ($recentUsers as $user) {
    echo "<h3>{$user->name}</h3>";
    echo "<p>Joined: " . $user->createdAt->format('Y-m-d') . "</p>";
}


// src/Legacy/CustomFileResolver.php
use Quellabs\Canvas\Legacy\FileResolverInterface;

class CustomFileResolver implements FileResolverInterface {
    
    public function resolve(string $path): ?string {
        // Handle WordPress-style routing
        if ($path === '/') {
            return $this->legacyPath . '/index.php';
        }
        
        // Map URLs to custom file structure
        if (str_starts_with($path, '/blog/')) {
            $slug = substr($path, 6);
            return $this->legacyPath . "/wp-content/posts/{$slug}.php";
        }
        
        // Handle custom admin structure
        if (str_starts_with($path, '/admin/')) {
            $adminPath = substr($path, 7);
            return $this->legacyPath . "/backend/modules/{$adminPath}.inc.php";
        }
        
        return null; // Fall back to default behavior
    }
}

// Register with kernel
$kernel->getLegacyHandler()->addResolver(new CustomFileResolver);


// public/index.php
$kernel = new Kernel([
    'legacy_enabled'       => true,
    'legacy_path'          => __DIR__ . '/../legacy',
    'legacy_preprocessing' => true  // Default: enabled
]);


namespace App\Aspects;

use Quellabs\Contracts\AOP\BeforeAspect;
use Quellabs\Contracts\AOP\AroundAspect;
use Quellabs\Contracts\AOP\AfterAspect;
use Symfony\Component\HttpFoundation\Response;

// Before Aspects - Execute before the method, can stop execution
class RequireAuthAspect implements BeforeAspect {
    public function __construct(private AuthService $auth) {}
    
    public function before(MethodContext $context): ?Response {
        if (!$this->auth->isAuthenticated()) {
            return new RedirectResponse('/login');
        }
        
        return null; // Continue execution
    }
}

// Around Aspects - Wrap the entire method execution
class CacheAspect implements AroundAspect {
    public function around(MethodContext $context, callable $proceed): mixed {
        $key = $this->generateCacheKey($context);
        
        if ($cached = $this->cache->get($key)) {
            return $cached;
        }
        
        $result = $proceed(); // Call the original method
        $this->cache->set($key, $result, $this->ttl);
        return $result;
    }
}

// After Aspects - Execute after the method, can modify response
class AuditLogAspect implements AfterAspect {
    public function after(MethodContext $context, Response $response): void {
        $this->logger->info('Method executed', [
            'controller' => get_class($context->getTarget()),
            'method' => $context->getMethodName(),
            'user' => $this->auth->getCurrentUser()?->id
        ]);
    }
}

/**
 * @InterceptWith(RequireAuthAspect::class)
 * @InterceptWith(AuditLogAspect::class)
 */
class UserController extends BaseController {
    // All methods automatically get authentication and audit logging
    
    /**
     * @Route("/users")
     * @InterceptWith(CacheAspect::class, ttl=300)
     */
    public function index() {
        // Gets: RequireAuth + AuditLog (inherited) + Cache (method-level)
        return $this->em->findBy(User::class, ['active' => true]);
    }
}

class BlogController extends BaseController {
    
    /**
     * @Route("/posts")
     * @InterceptWith(CacheAspect::class, ttl=600)
     * @InterceptWith(RateLimitAspect::class, limit=100, window=3600)
     */
    public function list() {
        // Method gets caching and rate limiting
        return $this->em->findBy(Post::class, ['published' => true]);
    }
}

/**
 * @InterceptWith(CacheAspect::class, ttl=3600, tags={"reports", "admin"})
 * @InterceptWith(RateLimitAspect::class, limit=10, window=60)
 */
public function expensiveReport() {
    // Cached for 1 hour with tags, rate limited to 10 requests per minute
}

class CacheAspect implements AroundAspect {
    public function __construct(
        private CacheInterface $cache,
        private int $ttl = 300,
        private array $tags = []
    ) {}
}

/**
 * @InterceptWith(RequireAuthAspect::class)
 * @InterceptWith(AuditLogAspect::class)
 */
abstract class AuthenticatedController extends BaseController {
    // Base authenticated functionality
}

/**
 * @InterceptWith(RequireAdminAspect::class)
 * @InterceptWith(RateLimitAspect::class, limit=100)
 */
abstract class AdminController extends AuthenticatedController {
    // Admin-specific functionality - inherits auth + audit
}

class UserController extends AdminController {
    /**
     * @Route("/admin/users")
     */
    public function manage() {
        // Automatically inherits: RequireAuth + AuditLog + RequireAdmin + RateLimit
        return $this->em->findBy(User::class, []);
    }
}

// Different template engines
$twig = $this->container->for('twig')->get(TemplateEngineInterface::class);
$blade = $this->container->for('blade')->get(TemplateEngineInterface::class);

// Different cache backends
$redis = $this->container->for('redis')->get(CacheInterface::class);
$file = $this->container->for('file')->get(CacheInterface::class);
bash
composer dump-autoload
bash
php ./vendor/bin/sculpt schedule:run