Legacy Bridge
Canvas integrates into existing PHP applications. Legacy URLs continue working while you add Canvas services and controllers incrementally.
Enable Legacy Support
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();
Route Fallthrough
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
How Canvas Finds Legacy Files
Canvas uses file resolvers to map URLs to filesystem paths.
Default File Resolver
Canvas provides a DefaultFileResolver that handles common URL-to-file mapping patterns. This resolver is automatically registered when legacy support is enabled.
How It Works
The DefaultFileResolver uses a priority-based pattern matching system:
- URL Normalization: Removes leading/trailing slashes from the path
- Direct .php Requests: URLs ending in
.phpmap directly to filesystem paths - Pattern Matching: For non-.php URLs, tries multiple file patterns in order
Resolution Patterns
For 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.
Examples
// 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
Custom File Resolvers
When the DefaultFileResolver doesn't match your application's structure, create a custom resolver. Common scenarios include:
- Non-standard file organization: Files stored in unconventional locations
- Dynamic routing patterns: WordPress-style permalinks, date-based URLs, or custom URL schemes
- Multiple file extensions: Using .inc.php, .html, or other extensions
- Virtual paths: URLs that don't correspond directly to filesystem structure
Creating a Custom Resolver
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
}
}
Custom Resolver Examples
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;
}
Resolver Chain
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:
- Handle special cases first with custom resolvers
- Fall back to standard patterns with DefaultFileResolver
- Combine multiple resolution strategies
Use Canvas Services in Legacy Files
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";
Legacy Preprocessing
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
]);
Preprocessing Transformations
Canvas transforms legacy code before execution:
- Header Function Conversion:
header()calls convert to Canvas header management - HTTP Response Code Conversion:
http_response_code()transforms to Canvas response code management - Exit/Die Conversion:
die()andexit()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
Recursive Preprocessing
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
How It Works
- Canvas discovers all include/require statements in the main file
- Each discovered file is recursively preprocessed
- Include paths are rewritten to point to preprocessed versions
- Preprocessed files are cached for performance
Limitations
- Dynamic Includes: Includes with variable paths cannot be detected:
include $dynamicPath; - Conditional Includes: Includes inside conditionals are still preprocessed, which may cause issues if the condition would normally prevent execution
- Performance: Deep include hierarchies may slow down initial preprocessing (cached after first run)
Disabling Preprocessing
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:
- Legacy code doesn't use
header(),die(), orexit() - Debugging preprocessing issues
- Specific requirements for direct header/exit handling
- Legacy files already compatible with Canvas request flow
Clearing the Preprocessing Cache
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:
- After modifying legacy PHP files
- After modifying included files
- When debugging preprocessing issues
- Before deploying to production (optional - cache regenerates automatically)
Custom Error Pages
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.
Creating Custom Error Pages
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>
Available Error Pages
- 404.php - Not found errors (when no resolver finds a matching file)
- 500.php - Server errors (when legacy file execution fails)
If custom error pages don't exist, Canvas uses its default error pages.
Migration Path
Adopt Canvas incrementally without breaking existing functionality:
Phase 1: Enable Legacy Support
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]);
Phase 2: Create New Features with Canvas
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, []));
}
}
Phase 3: Gradually Migrate Legacy Routes
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.
Phase 4: Remove Legacy Files
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
]);