Canvas integrates into existing PHP applications. Legacy URLs continue working while you add Canvas services and controllers incrementally.
Configure Canvas to fall back to legacy files:
// public/index.php
$kernel = new Kernel([
'legacy_enabled' => true,
'legacy_path' => dirname(__FILE__) . "/../legacy"
]);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
Canvas checks its routes first, then falls back to legacy files:
URL: /admin/users
1. Check Canvas routes → Not found
2. Load legacy/admin/users.php → Executes
3. Canvas services available via canvas() function
Canvas uses file resolvers to map URLs to filesystem paths.
Canvas provides a DefaultFileResolver that handles common URL-to-file mapping patterns. This resolver is automatically registered when legacy support is enabled.
The DefaultFileResolver uses a priority-based pattern matching system:
.php map directly to filesystem pathsFor a request to /admin/users, the resolver checks these paths in order:
1. Direct file: legacy/admin/users.php
2. Index file: legacy/admin/users/index.php
The first matching file that exists and is readable is returned. If no file matches, Canvas returns a 404 error.
// Direct file exists
URL: /admin/dashboard
Resolves to: legacy/admin/dashboard.php
// Directory with index file
URL: /admin/users
Resolves to: legacy/admin/users/index.php
// Direct .php request
URL: /admin/script.php
Resolves to: legacy/admin/script.php
// Root path
URL: /
Resolves to: legacy/index.php
When the DefaultFileResolver doesn't match your application's structure, create a custom resolver. Common scenarios include:
Implement the FileResolverInterface which requires a single method:
// src/Legacy/CustomFileResolver.php
namespace App\Legacy;
use Quellabs\Canvas\Legacy\FileResolverInterface;
use Symfony\Component\HttpFoundation\Request;
class CustomFileResolver implements FileResolverInterface {
private string $legacyPath;
public function __construct(string $legacyPath) {
$this->legacyPath = $legacyPath;
}
public function resolve(string $path, Request $request): ?string {
// Return absolute file path if found, null otherwise
}
}
WordPress-Style Permalinks:
public function resolve(string $path, Request $request): ?string {
// Match /blog/post-slug pattern
if (str_starts_with($path, '/blog/')) {
$slug = substr($path, 6);
return $this->legacyPath . "/wp-content/posts/{$slug}.php";
}
return null;
}
Date-Based URLs:
public function resolve(string $path, Request $request): ?string {
// Match /2024/03/article-title pattern
if (preg_match('#^/(\d{4})/(\d{2})/(.+)$#', $path, $matches)) {
$year = $matches[1];
$month = $matches[2];
$slug = $matches[3];
return $this->legacyPath . "/articles/{$year}/{$month}/{$slug}.php";
}
return null;
}
Canvas uses a resolver chain - resolvers are tried in the order they were registered. The first resolver to return a non-null value wins.
// Multiple resolvers are tried in order
$handler->addResolver(new WordPressResolver($legacyPath));
$handler->addResolver(new CustomAdminResolver($legacyPath));
$handler->addResolver(new DefaultFileResolver($legacyPath)); // Fallback
This allows you to:
Access Canvas capabilities in existing PHP files:
// legacy/admin/dashboard.php
use Quellabs\Canvas\Legacy\LegacyBridge;
// Method 1: Global canvas() function
$users = canvas('EntityManager')->findBy(User::class, ['active' => true]);
$totalUsers = count(canvas('EntityManager')->findBy(User::class, []));
// Method 2: LegacyBridge directly
$em = LegacyBridge::get('EntityManager');
$users = $em->findBy(User::class, ['active' => true]);
echo "Found " . count($users) . " active users out of " . $totalUsers . " total users";
Canvas by default preprocesses legacy files to integrate them cleanly with the framework.
// Default configuration
$kernel = new Kernel([
'debug_mode' => true,
'legacy_enabled' => true,
'legacy_path' => dirname(__FILE__) . "/../legacy",
'legacy_preprocessing' => true // Default
]);
Canvas transforms legacy code before execution:
header() calls convert to Canvas header managementhttp_response_code() transforms to Canvas response code managementdie() and exit() convert to Canvas exceptions// Original legacy code
header('Content-Type: application/json');
http_response_code(201);
echo json_encode(['data' => $data]);
die();
// Canvas converts this to maintain request/response flow
Preprocessing recursively processes included files. Canvas uses a RecursiveLegacyPreprocessor that discovers and preprocesses all include/require dependencies.
Canvas preprocesses the entire file dependency tree:
// legacy/main.php - Preprocessed
header('Content-Type: text/html'); // Converted by Canvas
die('Stopping here'); // Converted to Canvas exception
include 'includes/helper.php'; // Also preprocessed recursively
// legacy/includes/helper.php - Also preprocessed
header('Location: /redirect'); // Converted by Canvas
die('Stopping execution'); // Converted to Canvas exception
include $dynamicPath;Disable preprocessing when not needed:
$kernel = new Kernel([
'debug_mode' => true,
'legacy_enabled' => true,
'legacy_path' => dirname(__FILE__) . "/../legacy",
'legacy_preprocessing' => false
]);
Disable preprocessing when:
header(), die(), or exit()During development, you may need to clear the preprocessing cache after modifying legacy files:
// Clear preprocessing cache
$kernel->getLegacyHandler()->clearCache();
// Or via command line (if you have a CLI command set up)
php sculpt legacy:clear-cache
When to clear cache:
Canvas allows you to customize error pages for legacy applications. When a legacy file is not found, Canvas looks for custom error pages in your legacy directory.
Place error pages in your legacy directory:
// legacy/404.php
<!DOCTYPE html>
<html>
<head>
<title>Page Not Found</title>
</head>
<body>
<div class="error-box">
<h1>404 - Page Not Found</h1>
<p>Sorry, the page you're looking for doesn't exist.</p>
<p><a href="/">Return to Homepage</a></p>
</div>
</body>
</html>
If custom error pages don't exist, Canvas uses its default error pages.
Adopt Canvas incrementally without breaking existing functionality:
Add Canvas to your application while keeping all legacy code running:
// public/index.php
$kernel = new Kernel([
'legacy_enabled' => true,
'legacy_path' => dirname(__FILE__) . "/../legacy"
]);
// All existing URLs continue working
// Start using Canvas services in legacy files
$users = canvas('EntityManager')->findBy(User::class, ['active' => true]);
Build new features using Canvas controllers and routes:
// src/Controllers/ApiController.php
class ApiController extends BaseController {
/**
* @Route("/api/users", methods={"GET"})
*/
public function users() {
return $this->json($this->em()->findBy(User::class, []));
}
/**
* @Route("/api/products", methods={"GET"})
*/
public function products() {
return $this->json($this->em()->findBy(Product::class, []));
}
}
Convert legacy files to Canvas controllers one route at a time:
// Before: legacy/admin/users.php
// Now: src/Controllers/AdminController.php
/**
* @Route("/admin/users", methods={"GET"})
*/
public function users() {
$users = $this->em()->findBy(User::class, ['active' => true]);
return $this->render('admin/users.tpl', compact('users'));
}
Since Canvas routes are checked first, your new controller takes precedence over the legacy file. The legacy file remains as a fallback until you're ready to remove it.
Once all routes are migrated and tested, remove legacy files and disable legacy support:
$kernel = new Kernel([
'legacy_enabled' => false // All routes now handled by Canvas
]);