Architectural Notes & Gotchas
Composer-managed dependencies
Three third-party libraries, all under vendor/ via composer.json (not manually bundled copies):
giggsey/libphonenumber-for-php-lite– phone number parsing/validation (E.164).tuqqu/gender-detector(v0.5.2) – migrated from a manually-bundledincludes/gender-detector/copy this session. Note the API changed between versions: the old bundled copy used->detect($name)returning a string; the current Composer version uses->getGender($name)returning aGenderenum.guess_first_name_gender()uses the new API – but this method has zero callers anywhere in the codebase currently (confirmed via grep), so it's dead code, not wired to any hook.writecrow/country_code_converter(v1.3.0) – migrated from a manually-bundledincludes/country_code_converter-main/copy this session. Sameconvert()API in both versions, no call-site rewrite needed. Used inregister_core_scripts()to derive the phone widget's default country from the site's timezone.
The old bundled copies (includes/gender-detector/, includes/country_code_converter-main/) are safe to delete – nothing references them anymore.
Birthdate storage format history
lh_profile-birthdate was originally a UNIX timestamp. This was a poor fit: no meaningful time-of-day, an implicit timezone-dependent midnight, negative-timestamp handling needed for anyone born before 1970, and it fought the native <input type="date"> field's own Y-m-d value round-trip for no benefit. It was also written inconsistently – some code paths wrote a timestamp, others wrote a raw date string directly.
v2.13 switched storage to a plain Y-m-d ISO date string, alongside a full one-off data migration:
- Backup taken first (
lh_profile-birthdate_backup_20260908/_readable_backup_20260908meta keys, covering every user). - A cross-field comparison (both
lh_profile-birthdateandlh_profile-birthdate_readablenormalised to a common format before comparing, to avoid false positives from pure formatting differences) found one genuine discrepancy: one user's primary field had been overwritten with a bad value while the_readablecompanion still held the correct original – restored from there. - Four throwaway test accounts (
shawfactor+...@gmail.comaddresses) were found and deleted. - 39 real members had a birthdate that converted to an implausible year (post-2016, given the club has no members under 12). Investigation showed this was caused by a live bug – not root-caused – that writes the write-time "now" into the field: some rows landed within days of the affected user's own registration date (consistent with a signup-time default), while others showed the exact same date despite completely unrelated registration years (consistent with a separate batch/cron process touching existing records on one specific day). Both
lh_profile-birthdateandlh_profile-birthdate_readableheld the identical wrong value for these 39, meaning the true original birthdate was unrecoverable – they were cleared rather than left holding a wrong value dressed up as clean data. - Every remaining row converted from its stored format (timestamp or already-correct string) to
Y-m-d.
The root cause of the "now" bug was never traced – deliberately deferred, since this plugin had many structural changes around this time and some bad data was considered likely regardless.
Minimum-age filter (v2.14)
render_birthdate_input()'s max attribute is filterable via lh_profile_birthdate_minimum_age_years (default 0, i.e. today – unchanged behaviour). Restricting it to "today minus N years" is a client-side-only deterrent against the "now" bug's laziness angle: the native HTML5 date input's constraint validation refuses to submit a too-recent value, while the calendar picker UI itself stays fully browsable – nothing is hidden or disabled beyond blocking selection past the cutoff. Not server-side validation. See task 147165 (queue lh-membership) for wiring this up to a real configured value.
Discovery: lh_profile-emails reveals likely duplicate accounts
While investigating birthdate data, a related discovery: 1,888 users have a non-empty lh_profile-emails value, and the large majority follow a firstname.lastname@theaustraliatimes.com.au <-> real-email cross-reference pattern in both directions – strongly suggesting a large number of duplicate WordPress accounts (one with a real email as primary, one with a fabricated placeholder email as primary, each referencing the other as "secondary"). A saved query ("Users with a secondary email", lh_profile-emails, post 147118) and a written exploration brief exist for this – it's an lh-user-identity-adjacent problem, out of scope for this plugin, not investigated further here.
File size / MCP editing note
lh-profile-page.php (the god class) is ~115KB. Proposing changes to it via lh-mcp-code-changes MCP tooling risks payload-size issues; the working pattern this session was to build/verify the change in a local sandbox copy, then hand the full file back for manual upload via SFTP/hosting panel, rather than propose-code-change/apply-code-change. The extracted field class files (each a few KB) apply cleanly via the normal MCP flow.
Manual-upload ordering matters: bp_init() unconditionally require_onces all nine xProfile extender files, and plugin_init() unconditionally require_onces every field class file – if a file containing a require_once for another file is uploaded before that other file exists, any page load in that window fatals. Always upload newly-created dependency files before the file that requires them.