Full-featured PHP MVC with annotation-based routing, aspect-oriented programming, and ObjectQuel ORM. Production-ready for greenfield projects. Uniquely equipped for legacy modernization with deterministic fallthrough — Canvas routes first, existing files run unchanged.
Canvas routes first, falls through to legacy files. One system, two execution paths, zero rewrites required.
Canvas checks routes first, then falls through to existing files with deterministic mapping (/users → users.php). Preserves header(), exit(), direct output — migration blockers solved.
Extract authentication, logging, validation into reusable aspects. Before/Around/After execution with full inheritance - business logic stays clean.
QUEL-inspired queries: range of p is Post, traverse relationships with via, native regex support. Replace raw SQL gradually.
Routes live on methods: @Route("/posts/{id:int}"). Built-in parameter validation, HTTP method constraints, no separate config files.
Built-in CSRF protection, security headers, input hardening. Stop manually adding security checks to every endpoint.
Visual debugging with database queries, request analysis, custom panels. Legacy mysqli/PDO calls auto-instrumented — observability without code changes.
Two execution paths in one system — Canvas routes and legacy files run side-by-side with service bridging
Step 1: Legacy file keeps working
// legacy/products.php - still works unchanged
session_start();
if (!isset($_SESSION['user']) ||
$_SESSION['role'] !== 'admin') {
header('Location: /login');
exit;
}
$conn = mysqli_connect(/*...*/);
$id = (int)$_GET['id'];
$result = mysqli_query($conn,
"SELECT * FROM products WHERE id = $id");
$product = mysqli_fetch_assoc($result);
log_admin_action($_SESSION['user'],
'view_product', $id);
include 'templates/product.php';
Step 2: Bridge to Canvas services (no DI required)
// legacy/products.php - modernize piece by piece
session_start();
if (!isset($_SESSION['user']) ||
$_SESSION['role'] !== 'admin') {
header('Location: /login');
exit;
}
// Start using ObjectQuel in legacy file
$em = canvas('EntityManager');
$product = $em->find(
Product::class,
(int)$_GET['id']
);
log_admin_action($_SESSION['user'],
'view_product', $product->id);
include 'templates/product.php';
Step 3: Move to controller (one URL at a time, when ready)
/**
* @InterceptWith(RequireAuthAspect::class)
* @InterceptWith(RequireAdminAspect::class)
* @InterceptWith(AuditLogAspect::class)
*/
class ProductController extends BaseController {
/**
* @Route("/products/{id:int}")
*/
public function show(int $id) {
$product = $this->em()->find(Product::class, $id);
return $this->render('product.tpl', $product);
}
}
// Canvas handles this route now - legacy file no longer called
After 25 years modernizing PHP codebases, I kept hitting the same wall: frameworks that force you to rewrite everything or stay stuck in legacy hell.
Most frameworks claiming "legacy support" just mean you can require old files somewhere. Canvas defines a complete migration and coexistence model —
deterministic routing, side-effect compatibility, service bridging, and automatic instrumentation. No compatibility hacks. No forced rewrites. Just steady improvement.
— Floris, Quellabs
Canvas doesn't force you to abandon your current tools and setup
Canvas ships with Smarty support, but you can use Twig, Blade, or plain PHP templates.
No schema changes required. ObjectQuel maps to your existing tables as-is.
Same servers, same deployment pipeline. Just add Canvas via Composer.
Install Canvas via Composer and start building immediately
# Create a new Canvas project with skeleton
composer create-project quellabs/canvas-skeleton my-app
# Add Canvas to existing codebase
composer require quellabs/canvas
Start modernizing your legacy PHP codebase today.