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);
}
}
// 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')
]);
}
}
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
}
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);
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 = []
) {}
}