Yes, we all understand the necessity of developing systems using cutting-edge technologies. But honestly, legacy cases like this are still among us. I will be really glad if you drop a comment letting us know that you understand our struggle and are fighting with something similar!
Picture this: you have a massive system running on something that only IT dinosaurs and a few museum exhibits remember today – yes, I’m talking about good old Classic ASP.
Eight years ago, our company said, "We can't go on like this!" and we committed to a heroic deed: migrating to modern Symfony. However, since our system is massive and capacities are limited, a complete rewrite all at once would have meant a pure disaster (and probably mass resignations).
So, we chose the method of gradual slicing. We develop new features cleanly in Symfony, while keeping the old ASP code alive. As a reward, every programmer was given one day a week dedicated to sacred refactoring.
Sounds like a great plan, right? I agree. Until you hit the reality of two completely different worlds coexisting.
The Problem: How to Make Symfony and ASP Live Under One (Directory) Roof?
The structure of our project was, let's say, a bit exotic:
- The ASP code sits right in the
rootdirectory. There is no dedicatedpublicfolder for it; therootITSELF is its public folder. - Symfony has to be physically separated from ASP but remain part of the same project. So, we tucked the Symfony source code into
/root/src. - Naturally, Symfony has its own
publicdirectory in/root/public. - And to top it off, Symfony bundles reside nicely in
/root/bundles.
Over time, we wanted to make development more efficient. First, we moved to the local Symfony CLI server, later we integrated Webpack Encore, and today – in 2026 – we are successfully migrating to Asset Mapper.
Here is a guide on how we overcame three levels of this bizarre "boss fight".
Level 1: Symfony Server vs. "Where is my root?!"
While we used IIS in production and WAMP for local development, everything ran relatively smoothly. The problems started the moment we discovered the magic of the local Symfony CLI server. It desperately searched for the root folder wherever app_dev.php was located.
However, our entry point was hidden in /root/public/app.php. The Symfony server stubbornly decided that our public folder would be /root/public/app.php. Oops.
How do you force the Symfony server to tolerate this structural anarchy? We looked for a solution for a long time until we arrived at an elegant "hack". As they say in the legacy world: "If it works, don't touch it!"
We created a file /root/app_dev.php directly in the root directory with this little gem:
<?php
$uri = $_SERVER['REQUEST_URI'];
// If the request is routed to our first attempt at migrating to PHP using a custom framework
if (preg_match('~(/rdsfm/)(\?.*)?~', $uri, $matches)) {
header("Location: {$matches[1]}index.php{$matches[2]}");
return;
}
// Otherwise, send it without mercy to the real Symfony
header("Location: /public/app_dev.php");
return;
Done. A simple routing on the level of a plain PHP file beautifully spun up a modern Symfony server within our ancient legacy project.
Level 2: Webpack Encore and Black Magic in ASP
Introducing Webpack Encore for the Symfony part was a walk in the park. Hell broke loose when we needed to wire that same build into the old ASP code.
Logically, Classic ASP doesn't have a native JSON parser. And why would it? Back when it was created, JSON was probably just a wild dream of a few visionaries. For the integration, we needed a script that would help create the entrypoint. AI had to lend a helping hand here, because a regular mortal would have lost their mind writing this code.
We created a file rds_js.asp, where we literally manually parse the JSON, extract the necessary section from it, and generate HTML tags:
<%
' **********************************************************************************************
' * THIS WAS CREATED BY AI. NOBODY KNOWS EXACTLY WHAT IT DOES OR HOW IT WORKS. IT'S A BLACK HOLE...*
' **********************************************************************************************
'
' REAL DESCRIPTION (for mortals):
' This script loads and manually parses the public/build/entrypoints.json file,
' because Classic ASP does not know JSON. It searches for the "old_rds" section, extracts the "js" array,
' and generates the appropriate <script src="..." defer> tag for each found file.
'
dim fs_rds, f_rds, content_rds, entrypointsPath_rds
dim startOldRds, endOldRds, oldRdsSection
dim startJs, endJs, jsSection
dim jsFiles, jsFile, i
entrypointsPath_rds = Server.MapPath("/public/build/entrypoints.json")
set fs_rds = Server.CreateObject("Scripting.FileSystemObject")
' ... the rest of the code follows here, turning this dark magic into reality
%>
Since we didn't want to pull all the heavy dependencies from Symfony into ASP, we created a special entrypoint that contained only the absolute necessities:
webpack.config.js
Encore
.setOutputPath('public/build/')
.setPublicPath('/public/build')
.addEntry('app', './assets/app.js')
.addEntry('old_rds', './assets/old_rds.js') // Our legacy breadcrumb
Level 3: The Year 2026 and the Arrival of Asset Mapper
The year is 2026. We are deployed exclusively on a local network, disconnected from the internet, and a few extremely complex ASP scripts still haunt our system. A complete rewrite of these monsters is still a long-term goal.
In all our other (modern) projects, we're already happily using Asset Mapper. The only place where Node.js still annoys us is this legacy project. "Let's finally get rid of it!" I told myself one day, full of naive optimism.
However, we once again ran into our old friend – the problem with the specific directory structure. On the local development environment, Asset Mapper worked perfectly because assets are not versioned there.
The real problem was the production build. Suddenly, assets were built directly into /root/assets, which was completely wrong for our structure. After long hours of exploring the guts of the Asset Mapper library and futile attempts at decorating services, I finally found an elegant and simple solution.
All it took was to properly instruct the path resolver for production only:
config/packages/prod/asset_mapper.yaml
when@prod:
services:
asset_mapper.public_assets_path_resolver:
class: Symfony\Component\AssetMapper\Path\PublicAssetsPathResolver
arguments:
$publicPrefix: '/assets_build/'
And then clearly tell Composer where to install everything:
composer.json
{
"scripts": {
"auto-scripts": {
"assets:install ./": "symfony-cmd",
"importmap:install": "symfony-cmd",
"asset-map:compile": "symfony-cmd"
}
},
"extra": {
"symfony-web-dir": "./public",
"public-dir": "./public",
"symfony-assets-install": "absolute",
"installer-paths": {}
}
}
But what about our good old ASP code? Since we ditched Node.js and Webpack Encore, our terrifying AI-generated script for parsing JSON (rds_js.asp) was no longer needed!
Instead, we decided to leverage Symfony itself. We created a simple Symfony Console command that runs automatically after composer install (via the post-install scripts in composer.json). This command uses the native ImportMapRenderer to render the required HTML tags for the Asset Mapper and directly saves them into the rds_js.asp file.
Here is the code of this command:
projects/rds/src/Command/GenerateRdsJsCommand.php
<?php
#[AsCommand(
name: 'app:generate-rds-js',
description: 'Generates rds_js.asp using ImportMapRenderer',
)]
class GenerateRdsJsCommand extends Command
{
//...
protected function execute(InputInterface $input, OutputInterface $output): int
{
$html = $this->importMapRenderer->render(['old_rds']);
$html = str_replace(['"/assets/', "'/assets/"], ['"/public/assets/', "'/public/assets/"], $html);
$html = str_replace(['"/assets_build/', "'/assets_build/"], ['"/public/assets_build/', "'/public/assets_build/"], $html);
$targetFile = dirname(__DIR__, 2) . '/rds/rds_js.asp';
if (file_put_contents($targetFile, $html) !== false) {
$output->writeln("<info>Successfully generated $targetFile</info>");
return Command::SUCCESS;
}
$output->writeln("<error>Failed to write to $targetFile</error>");
return Command::FAILURE;
}
}
Now, the old ASP codebase simply includes the generated rds_js.asp file and happily consumes modern assets mapped directly by Symfony. No more manual JSON parsing!
Conclusion
Working with a legacy system while trying to integrate modern technologies is not always a walk in the park. It requires a massive dose of patience, the courage to use non-standard "hacks", and occasionally the help of AI when you need to do something as absurd as parsing JSON in Classic ASP.
But you know what? That feeling when you get rid of another outdated dependency and the whole creaky behemoth moves one step closer to a modern architecture—that is simply priceless.
Do you have similar Frankenstein systems in your company? How do you fight them? Share your experiences in the comments!
Top comments (0)