Fast Manual Entry

Personal Catalogue: PHP & MySQL

Chapter 8 · Fast Manual Entry

Chapter 1 named the real constraint this whole project starts under: a genuine deadline, with barcode scanning deliberately deferred so items get added by hand for now. Every form built since Chapter 3 works correctly, but none of them are actually fast to use for the real first task — sitting down and typing in a whole existing shelf of books, CDs, and DVDs in one sitting. This chapter builds two features aimed specifically at that first, real bulk-entry session.

The Real Bottleneck: One Round Trip Per Item

Chapter 3's add_item.php redirects to add_item.php?added=1 after a successful insert — which reloads a blank form, but only after showing a confirmation state first. For entering forty items back to back, even that small extra step adds up. The fix isn't more code, it's a smaller change: redirect straight back to a genuinely fresh form, with the cursor already sitting in the Title field, ready to type the next item immediately.

Quick-Add: Redirect Straight Back, Autofocus the Title

Two small, real changes to add_item.php from Chapter 3. First, the redirect target drops the confirmation step and returns straight to the blank form, remembering the last-used item type so a run of ten books in a row doesn't require reselecting "book" from the dropdown every single time:

// After the successful INSERT, instead of Chapter 3's own // header('Location: /add_item.php?added=1'): header("Location: /add_item.php?added=1&last_type={$itemTypeId}"); exit;

And in the GET branch, that remembered type pre-selects the dropdown on the fresh form:

$lastType = (int) ($_GET['last_type'] ?? 0); ?> ... <select name="item_type_id" id="item-type-select" required> <option value="">-- choose --</option> <?php foreach ($itemTypes as $type): ?> <option value="<?= $type['id'] ?>" data-name="<?= htmlspecialchars($type['name']) ?>" <?= (int) $type['id'] === $lastType ? 'selected' : '' ?>> <?= htmlspecialchars($type['name']) ?> </option> <?php endforeach; ?> </select>

Second, the Title field gets a plain autofocus attribute — a real, standard HTML attribute, not a JavaScript trick — so the cursor is already in place the instant the page finishes loading:

<input type="text" name="title" autofocus required>

Together, adding ten items of the same type back to back becomes: type the title, tab to creator, tab to format, hit Enter, and the next blank form is already focused and ready — with the correct item type already pre-selected — with zero mouse clicks needed in between.

Bulk Paste Import: One Type, Many Titles at Once

Quick-add is faster per item, but it's still one HTTP request per item. For the real initial population of the catalogue — copying a list of book titles straight out of a spreadsheet, an email, or a notes app — a genuinely bulk path is worth having: paste many titles at once, pick one shared item type, and insert them all in a single operation.

bulk_add.php
<?php require_once __DIR__ . '/functions.php'; $results = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $itemTypeId = (int) ($_POST['item_type_id'] ?? 0); $rawLines = explode("\n", $_POST['titles'] ?? ''); $titles = array_filter(array_map('trim', $rawLines)); $inserted = []; $skipped = []; if ($itemTypeId > 0 && count($titles) > 0) { $checkStmt = $pdo->prepare('SELECT id FROM items WHERE title = :title AND item_type_id = :type_id'); $insertStmt = $pdo->prepare('INSERT INTO items (item_type_id, title) VALUES (:type_id, :title)'); $pdo->beginTransaction(); try { foreach ($titles as $title) { $checkStmt->execute(['title' => $title, 'type_id' => $itemTypeId]); if ($checkStmt->fetch()) { $skipped[] = $title; continue; } $insertStmt->execute(['type_id' => $itemTypeId, 'title' => $title]); $inserted[] = $title; } $pdo->commit(); } catch (PDOException $e) { $pdo->rollBack(); throw $e; } $results = ['inserted' => $inserted, 'skipped' => $skipped]; } } $itemTypes = get_item_types($pdo); ?> <h1>Bulk Add</h1> <?php if ($results): ?> <p>Added <?= count($results['inserted']) ?> item(s). <?php if ($results['skipped']): ?> Skipped <?= count($results['skipped']) ?> likely duplicate(s): <?= htmlspecialchars(implode(', ', $results['skipped'])) ?> <?php endif; ?> </p> <?php endif; ?> <form method="post"> <label>Type: <select name="item_type_id" required> <option value="">-- choose --</option> <?php foreach ($itemTypes as $type): ?> <option value="<?= $type['id'] ?>"><?= htmlspecialchars($type['name']) ?></option> <?php endforeach; ?> </select> </label><br> <label>Titles, one per line:<br> <textarea name="titles" rows="15" cols="50" autofocus></textarea> </label><br> <button type="submit">Add All</button> </form>
Why the Whole Loop Runs Inside One Transaction
Wrapping every insert in beginTransaction()/commit() means all of the real inserts either succeed together or none of them do — if something genuinely goes wrong partway through pasting in forty titles (a lost database connection, a constraint violation), rollBack() undoes everything that had already run in that batch, rather than leaving the catalogue in a half-imported state with no clean way to tell which titles made it in. It's also genuinely faster: without a transaction, MySQL commits each INSERT to disk individually; wrapped in one transaction, every insert in the batch is committed together in one disk write at the end.
Duplicate Skipping Is a Convenience, Not Real Deduplication
The exact-title-and-type match used to decide what's "already there" is deliberately simple — it won't catch a title typed with different capitalization or an extra space that survived trim()'s own whitespace-only cleanup, and it says nothing about whether two different editions of the same book should really count as duplicates. It's a genuine help during a fast bulk-paste session specifically because obvious re-pastes of the same list are the realistic risk it protects against, not a substitute for actually reviewing the catalogue afterward.

What Bulk Add Deliberately Leaves Out

bulk_add.php only ever sets item_type_id and title — no creator, no format detail, no tags. That's intentional: the whole point is getting a long list of titles into the database fast, with the understanding that any item needing richer detail can be opened afterward through edit_item.php (Chapter 3) at a calmer moment. Trying to also capture every field during a rapid bulk paste would undermine the entire reason this page exists.

Hands-On Exercises

Exercise 1

Add the autofocus attribute and the last_type redirect/pre-selection logic to your own add_item.php. Add three books in a row without touching the mouse (only Tab, typing, and Enter) and confirm the type dropdown stays on "book" for all three without being reselected.

📄 View solution
Exercise 2

Build bulk_add.php against your real database and paste in five book titles, one per line, including one title that's an exact duplicate of a book already in your catalogue from an earlier chapter. Confirm the results message correctly reports 4 added and 1 skipped, and that the database contains exactly one row for the duplicate title, not two.

📄 View solution
Exercise 3

Explain, in your own words, what would go wrong for the user experience if bulk_add.php's own loop ran each INSERT without a surrounding transaction, and a genuine database error occurred after 15 of 40 pasted titles had already been inserted.

📄 View solution

Chapter 8 Quick Reference

  • autofocus — a plain HTML attribute putting the cursor in the Title field the instant a fresh form loads, no JavaScript needed
  • last_type — remembered across the redirect so a run of same-type items doesn't need the dropdown reselected each time
  • bulk_add.php — one type, many titles pasted at once, each checked against an exact duplicate before insert
  • beginTransaction()/commit()/rollBack() — wraps the whole batch so it succeeds or fails as one unit, and commits faster than one insert at a time
  • Deliberately title-only — bulk add skips creator/format/tags on purpose; richer detail is added later via edit_item.php
  • Next chapter: Deployment