Originally published on php-to-js.learnio.dev — part of the PHP to JavaScript course. View this lesson on the original site.
Chapter project: a reusable formatter pipeline
Build a small pipeline that takes raw line items and returns display-ready rows plus a total. You will combine guard-style checks, pure formatters, map / filter / reduce, and error-first parsing from this chapter.
What you are building
| Piece | Responsibility |
|---|---|
parseMoney(value) |
Error-first: return { ok, amount } or { ok: false, error } |
formatCurrency(amount) |
Pure: £19.99 style string |
formatLine(row) |
Pure: label + formatted money |
buildReceipt(lines) |
Orchestrator: filter, map, reduce |
PHP habit (single service method)
<?php
function formatCurrency(float $amount): string {
return '£' . number_format($amount, 2);
}
function buildReceipt(array $lines): array {
$rows = [];
$total = 0.0;
foreach ($lines as $line) {
if (($line['status'] ?? '') !== 'billable') {
continue;
}
$amount = (float) ($line['amount'] ?? 0);
$rows[] = [
'display' => ($line['label'] ?? 'Item') . ': ' . formatCurrency($amount),
];
$total += $amount;
}
return ['rows' => $rows, 'total' => formatCurrency($total)];
}
JavaScript equivalent (pipeline)
function parseMoney(value) {
if (value === '' || value == null) return { ok: false, error: 'Amount required' };
const n = Number(value);
if (Number.isNaN(n)) return { ok: false, error: 'Amount must be a number' };
if (n < 0) return { ok: false, error: 'Amount cannot be negative' };
return { ok: true, amount: n };
}
function formatCurrency(amount) {
return `£${amount.toFixed(2)}`;
}
function formatLine(line) {
return `${line.label}: ${formatCurrency(line.amount)}`;
}
function buildReceipt(lines) {
const billable = lines.filter((l) => l.status === 'billable');
const rows = billable.map((line) => ({
display: formatLine(line),
}));
const total = billable.reduce((sum, line) => sum + line.amount, 0);
return { rows, total: formatCurrency(total) };
}
Done when
You can run the sandbox and explain which functions are pure, where errors are handled first, and how you would add one new formatter without rewriting the whole pipeline.
Try it
Use the amber Try it panel below. Click Run.
const raw = [
{ label: 'Plan', amount: '19.99', status: 'billable' },
{ label: 'Trial', amount: '0', status: 'billable' },
{ label: 'Note', amount: '5', status: 'draft' },
{ label: 'Bad', amount: 'oops', status: 'billable' },
];
const lines = raw.map((row) => {
const parsed = parseMoney(row.amount);
if (!parsed.ok) return { ...row, amount: 0, parseError: parsed.error };
return { ...row, amount: parsed.amount, parseError: null };
});
const receipt = buildReceipt(lines.filter((l) => !l.parseError));
console.log(receipt);
lines.filter((l) => l.parseError).forEach((l) => console.log('skip', l.label, l.parseError));
Expect two display rows, a formatted total, and a logged skip for the invalid amount.
From PHP to JavaScript — Control Flow, Functions, and Scope