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.

Canvas Request Resolution Flow

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:

  1. URL Normalization: Removes leading/trailing slashes from the path
  2. Direct .php Requests: URLs ending in .php map directly to filesystem paths
  3. 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() 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

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
  1. Canvas discovers all include/require statements in the main file
  2. Each discovered file is recursively preprocessed
  3. Include paths are rewritten to point to preprocessed versions
  4. 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(), or exit()
  • 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
]);