22 Jul 2026
Building a regulatory compliance engine into an e-commerce site
One of the client sites I built is a home fragrance store selling candles, reed diffusers and wax melts. Sounded like a simple brief. Then I found out every fragrance oil sold in the UK needs a legally correct hazard label under CLP regulation (Classification, Labelling and Packaging), and working that label out means combining hazard data from every ingredient in the recipe. Not really what I expected to be building into a Laravel app.
The regulation itself is more interesting than I expected once you're actually in it. Each ingredient's Safety Data Sheet lists hazard codes with cutoff percentages, and CLP's "additive method" has you sum each ingredient's weighted contribution per hazard code across the whole recipe, then check the total against a threshold. Skin sensitisation kicks in at a much lower percentage than flammability, and some hazards take precedence over others (skin sensitisation subsumes plain irritation, "Danger" beats "Warning" as a signal word). Get any of that wrong and you've mislabelled a physical product going out to a real customer.
The part I enjoyed solving most wasn't the regulation logic, it was getting the ingredient data in
without someone typing it all in by hand. Suppliers hand over Safety Data Sheets as PDFs. The
hazard statement section reads fine with normal PDF text extraction and a bit of regex. The
ingredient composition table doesn't. Table structure just doesn't survive PDF-to-text extraction
reliably enough to trust, so that one section shells out to a small Python script using
pdfplumber, which is properly good at pulling structured tables out of PDFs, with a regex
fallback if Python isn't available on the box. Not the tidiest setup if you like everything in one
language, but it's the right tool for the job, and it means someone can upload a supplier's PDF and
get correct, audited hazard data out the other end instead of transcribing it themselves.
Roughly, the additive method looks like this:
function additiveScore(array $ingredients, string $hazardCode): float
{
return array_sum(array_map(
fn ($ingredient) => $ingredient->percentage * $ingredient->hazardWeight($hazardCode),
$ingredients,
));
}
Sum the weighted contributions, compare against the cutoff for that hazard code, done. The real version handles precedence between hazards and builds a human-readable audit trail so the label can be explained if anyone ever asks why a product carries a particular warning, but the core of it really is this simple.