01 · Topworktops

From a custom PHP framework to Symfony 8, without a rewrite

Sector
Kitchen worktop supplier, UK
System
Internal back-office application
Work
Framework migration, PHP upgrade, tests, backups

The system

An internal back-office application for a UK kitchen worktop supplier. Staff use it every day to run the whole order lifecycle: quotes and invoices, clients, products and warehouse stock, delivery scheduling with vans and live maps, customer SMS, PDF delivery and collection notes, and sales statistics. Customers receive signed links to their invoices from it. If it stops, deliveries stop.

The history

The first version was written more than twelve years ago on a custom in-house PHP framework. In 2015 I moved it to Symfony 3. That version then ran in production for ten years and over a thousand commits. That is the part people forget about modernization: the goal is a system that needs nothing dramatic for the next decade.

By 2026 Symfony 3.4 was long out of support, PHP had moved to 8.4, the front end was still Bower and Bootstrap 3, and PDFs depended on a wkhtmltopdf binary that no longer existed on any server.

The approach

Read before I change. I mapped the application first: 111 routes, around 25 entities, raw SQL that keeps denormalized invoice totals in sync, hardcoded VAT-inclusive math, invoice numbering that must not be "improved", and customer links signed with a checksum. Those went on a do-not-touch list before the first dependency was bumped.

Then the framework layer was replaced in small, validated phases, with business behaviour kept byte for byte:

  • Symfony 3.4 to Symfony 8.1, PHP 8.4, Doctrine ORM 3 on PostgreSQL. Annotations became attributes, controllers got constructor injection, parameters.yml became .env. Table and column names never changed.
  • FOSUserBundle to native Symfony Security. The existing user table and bcrypt hashes were kept, so nobody has to reset a password. Two idempotent migrations convert the legacy serialized roles to JSON and drop the dead columns.
  • Bower and hand-vendored assets to AssetMapper and importmap. No Node build. Bootstrap 3 to 5. jQuery stays, because the legacy plugins that depend on it still earn their place.
  • Unmaintained packages replaced: wkhtmltopdf to dompdf, Swiftmailer to Mailer, Guzzle to HttpClient, dev-master bundles to current releases.

What was added

An application nobody tests is an application nobody dares to upgrade, so this time tests came with the migration. A smoke suite renders login and every main page against a test database. A dedicated test checks every importmap bundle and CSS reference, so a missing asset fails the test run instead of a customer's browser. New integrations are built with tests from day one, against mocked HTTP: live van positions from the vehicle tracker drawn on the delivery map, and nightly database backups compressed and shipped to Cloudflare R2 with automatic retention and a size check on every upload.

AI-assisted tooling was part of the work: exploring the code, generating tests, documenting behaviour. The next step, now in progress, is bringing AI tools into the application itself.

Results

AreaBeforeAfter
FrameworkSymfony 3.4 (EOL)Symfony 8.1
PHP7.38.4
AuthFOSUserBundleNative Security, same users, same passwords
AssetsBower, hand-vendoredAssetMapper + importmap, no Node
PDFswkhtmltopdf binarydompdf
Automated tests086
Backupsmanualnightly, off-site, verified, pruned

Business rules unchanged. Same invoices, same totals, same VAT, same customer links. A documented codebase with an architecture guide, the migration plan and a written caution list that the next developer, human or AI, can pick up without guessing.

02 · Pizza Djikov

A pizza ordering site rebuilt on Symfony 7.4, with the kitchen wired in

Sector
Restaurant, two locations, Sofia, Bulgaria
System
Online ordering site and admin
Work
Rebuild on Symfony, Vue cart, till integration

The system

The online ordering site for a pizza restaurant with two locations in Sofia, Bulgaria. Customers browse the menu, build a pizza with options and paid add-ons, and order for delivery or collection, as guests or from an account with order history and one-click reorder. Staff manage products, neighbourhoods, delivery rules, pages and orders in a separate admin area. A pizza place lives on dinner-time traffic. If ordering breaks on a Friday night, there is no second chance that week.

The history

The first site was written about ten years ago on a custom in-house "framework". It worked, and then it stopped being changeable. Every improvement the business asked for was either impossible or too risky to attempt. The design had aged with it.

On top of that came a hard requirement: the restaurants had moved to a new version of their till system, Mistral, with a cloud API called Zamboo. Orders from the website needed to land in it directly instead of being handled by hand.

The approach

My default is incremental modernization, and I argue against rewrites on this site. This project is the exception that shows how I decide. A custom framework nobody maintains, with no upgrade path, a redesign already on the table and a new integration that touched the core of the ordering flow: there was nothing worth preserving except the business rules.

So I preserved the business rules. Read before I change applies to a running site as much as to code. I went through the live system and wrote down how pricing actually behaved: a fixed discount for collection, a different discount per neighbourhood for delivery, a delivery fee and minimum order per neighbourhood, packaging charged per item, and items that were already on offer excluded from any further discount. Those rules went into the new system unchanged. Everything around them was built fresh.

What was built

  • Symfony 7.4, PHP 8.4, Doctrine ORM 3 on PostgreSQL. Attribute routing, constructor injection, Symfony Security with separate firewalls for customers and staff, Symfony Forms for the whole admin.
  • A redesigned, responsive front end in Bulgarian: menu by category, product pages with options and add-ons, allergens, lunch menu, restaurant pages with maps, editable content pages and a site-wide announcement banner staff can switch on from the admin.
  • The cart rewritten in Vue 3. The old cart was the part nobody could touch, because every rule interacted with every other rule. The new one is a single Vue component, built with Vite into one file and mounted inside the Twig page. It talks to small JSON endpoints, keeps the server-side session cart as the source of truth, and recalculates totals live as the customer switches between collection and delivery or changes neighbourhood. The server recomputes everything again at checkout. The browser is for display, never for trust.
  • Dual currency. Bulgaria adopted the euro in January 2026. Every price shows in euro and leva at the fixed rate, in the cart, on the product pages and in the admin.
  • Mistral via Zamboo. Each product carries its Mistral id, and an admin page lists the till's own catalogue so the two can be matched without guessing. When an order comes in, the admin order screen picks it up without a reload. One click translates it into the till's format: every product line by Mistral id, the chosen options as text, add-ons, packaging lines, the delivery fee, and the discount as a negative line, with the address, phone and comment attached. It is posted to the restaurant's own endpoint, and the order is marked completed and assigned to that restaurant. The kitchen gets the ticket. Nobody retypes anything.

Results

AreaBeforeAfter
FrameworkCustom in-house PHP, unmaintainedSymfony 7.4, PHP 8.4, Doctrine 3, PostgreSQL
DesignTen years oldRedesigned, responsive
CartCould not be changed safelyVue 3 component, pricing rules in one place, verified server-side
Orders to the kitchenHandled by hand / Semi automatedSent to Mistral with one click from the admin
CurrencyLevaEuro and leva, fixed rate
Delivery rulesFixed in codeDiscount, fee and minimum order per neighbourhood, editable in the admin

The business rules that made the old site valuable are still there. Everything that made it untouchable is gone. Since launch the site has taken a steady stream of improvements, including the euro changeover and a new discount model, each one a small change instead of a project.

Have a system like these?

Describe it in a few lines and I will tell you where I would start.

Tell me about your system