Tags
Personal Catalogue: PHP & MySQL
Chapter 5 · Tags
Chapter 2 built tags and item_tags, the many-to-many pairing
that lets a book carry several tags at once. This chapter finally wires that schema up to
the real add/edit forms — creating tags on the fly, attaching and detaching them from an
item, and running the actual "find every book tagged Python" query the whole feature
exists for.
Two Real Operations, One Shared Helper Function
Both adding a new item and editing an existing one need to end up with the same result:
a correct, current set of rows in item_tags for that item's own id. Rather
than writing that logic twice, one shared sync_item_tags() function handles
both cases — insert new pairings, and (for edit specifically) remove pairings that were
unchecked:
DELETE every existing
item_tags row for this item, then INSERT the whole new set fresh.
That works, but it's needless churn — for an item whose tags didn't actually change, it
still runs a real delete and a real insert for every tag, every single save. The diff
approach (array_diff in both directions) only touches the rows that
genuinely changed, which matters more as a habit than as a performance concern at this
scale — it's the same principle behind not rewriting a whole file when only one line
changed.
Adding the Tag Checkboxes to the Form
The catalogue's own real spec only ever asks for tags on books — so the tag section is
shown only when item_type_id corresponds to book, reusing the
exact same dropdown-driven show/hide technique Chapter 4 already built for the label
switching:
Then, added to Chapter 4's own updateLabels() function (or a second listener
on the same change event):
<div> (via display: none) still submits its own
checked checkboxes if the form is somehow submitted while it's hidden — CSS visibility and
form submission are unrelated. This is why the actual enforcement of "only books get
tags" happens where it matters, in PHP, described next — the hidden section is purely a
UI convenience, matching the same cosmetic-vs-validation distinction Chapter 4 already
drew for the type-specific labels.
Handling Tags on Insert
Added to add_item.php's own POST branch, right after the item itself is
inserted (so a real item_id exists to attach tags to), and deliberately
gated on the item actually being a book:
array_filter() with no callback drops any empty strings from the exploded
comma-separated list — this matters for the common real case of a trailing comma
("Fantasy, Short Stories,"), which would otherwise try to create a tag with
an empty name.
Handling Tags on Edit
edit_item.php needs the item's own current tags loaded for the GET branch
(to pre-check the right boxes), and the same insert-or-sync logic on POST:
And in the checkbox markup itself, each box is pre-checked if its id is in
$itemTagIds:
else branch (sync_item_tags($pdo, $id, [])), an
item that started as a book, picked up two tags, and was then re-edited to become a DVD
would keep those two tags attached forever — invisible in the UI (since the tags section
is hidden for non-books) but still sitting in item_tags. Passing an empty
array explicitly clears them, matching the honest rule "only books have tags."
The Query the Whole Feature Exists For
With attachment working, filtering the catalogue by tag is a single query — this is a preview of what Chapter 6's own search page uses directly:
Every real book tagged "Python" — including ones tagged Python and something
else, since item_tags is a genuine many-to-many table — comes back in one
query, exactly the search Chapter 1's own real deadline framing said mattered most.
Hands-On Exercises
Add the tag checkboxes, the "New tag(s)" text input, and the show/hide JavaScript to your own add_item.php. Add a new book with two brand-new tags typed into the comma-separated field (tags that don't exist yet), then confirm both were created in tags and correctly attached in item_tags.
Add the pre-checked checkboxes and the sync logic (including the else branch) to edit_item.php. Edit an existing book to remove one of its two tags and add a third, then confirm item_tags reflects exactly the new set — the removed tag's row gone, the third tag's row added, and the untouched tag's row unchanged.
Run the tag-filter query from this chapter's own closing section against your real database for a tag you've actually used, and write out the full result. Then explain, in one or two sentences, why a book tagged with two different tags only appears once in a plain SELECT DISTINCT items.* FROM ... version of this query but could appear twice without the DISTINCT.
Chapter 5 Quick Reference
find_or_create_tag()— looks up a tag by name, creating it only if it doesn't already existsync_item_tags()— diffs the desired tag set against what's currently attached, only touching rows that actually changed- Tags section shown only for books — a UI convenience via
display: none, never a substitute for the real server-sideitem_type_id === 1check - The
elsebranch on edit — clears tags explicitly if an item's type changes away from book, so stale rows can't linger unseen - The filter query — a single three-table join, no
UNION, the direct payoff of Chapter 2's own shared-items-table decision - Next chapter: Search — real lookup across the whole catalogue, not just by tag