@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/?post_type=lh-portfolio&#038;p=147166>
  a sioc:Post ;
  dc:title "Field Class Architecture" ;
  dcterms:identifier 147166 ;
  dc:modified "2026-09-11T16:41:48Z"^^xsd:dateTime ;
  dc:created "2026-09-09T01:46:02Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-profile-page/field-class-architecture/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded "<ul class=\"lh_portfolio-meta\"><li><strong>Type:</strong> Doc-section</li><li><strong>Part of:</strong> <a href=\"https://lhero.org/portfolio/lh-profile-page/\">LH Profile Page</a></li></ul><h1 class=\"wp-block-heading\">Field Class Architecture</h1><p class=\"wp-block-paragraph\"><code>lh-profile-page.php</code> began as a single &quot;god class&quot; (<code>LH_profile_page_plugin</code>) holding every field&#039;s rendering, validation, and storage logic. Starting with the phone field (v2.03) and continuing through v2.11, each field was extracted into its own dedicated class, following a consistent pattern:</p><h2 class=\"wp-block-heading\">The extraction pattern</h2><p class=\"wp-block-paragraph\">For each field:</p><ol class=\"wp-block-list\"><li><strong>A new <code>includes/lh-profile-{field}-field-class.php</code> file</strong> holds the real, self-contained rendering/validation/storage methods as a static class (<code>LH_Profile_{Field}_Field</code>).</li><li><strong>A back-compat proxy shim</strong> stays on <code>LH_profile_page_plugin</code> for every extracted public method, delegating to the new class. This exists because the original 2.03 phone extraction assumed no external plugin called these methods directly &#8211; that assumption was wrong (<code>ppt-lh-membership-extender</code> called <code>render_phone_input()</code> directly) and fatalled the site the moment 2.03 shipped. Every subsequent extraction leaves a shim regardless of whether an external caller is known, on the same &quot;don&#039;t assume, leave the shim&quot; principle.</li><li><strong><code>register_meta()</code> registration</strong>, where the field has a real validation rule worth enforcing (see Database Schema doc for which fields this applies to).</li></ol><h2 class=\"wp-block-heading\">Shim-call diagnostic logging</h2><p class=\"wp-block-paragraph\">Since the gender/birthdate/address/name extractions (v2.09+), every shim proxy calls a shared <code>maybe_log_external_shim_call()</code> helper as its first line. This inspects the immediate caller&#039;s file via <code>debug_backtrace()</code>: calls originating from within <code>lh-profile-page.php</code> itself (expected, pre-extraction internal usage) are silently ignored; calls from any other file are logged at <code>warning</code> level via the plugin&#039;s <code>write_log()</code> convention, naming the exact method, file, and line.</p><p class=\"wp-block-paragraph\">This exists to turn &quot;we assume nothing external calls this&quot; into &quot;we know nothing external calls this&quot; &#8211; the only way to eventually make shim removal an evidence-based decision rather than a guess. Search LH Debug Log Viewer for &quot;Deprecated shim&quot; to find any external callers the logging has caught. One confirmed find this way: <code>ppt-lh-membership-extender.php</code> was calling <code>render_phone_input()</code>, <code>render_gender_input()</code>, and <code>handle_phone_update()</code> directly &#8211; all three were repointed to call <code>LH_Profile_Phone_Field</code>/<code>LH_Profile_Gender_Field</code> directly (site plugin, updated to v1.01).</p><h2 class=\"wp-block-heading\">Extracted field classes</h2><figure class=\"wp-block-table blocks-engine-table-d7b7ff4692e2-3\"><table><thead><tr><th>Field</th><th>Class</th><th>File</th></tr></thead><tbody><tr><td>Phone (+ secondary)</td><td><code>LH_Profile_Phone_Field</code></td><td><code>lh-profile-phone-field-class.php</code></td></tr><tr><td>Gender</td><td><code>LH_Profile_Gender_Field</code></td><td><code>lh-profile-gender-field-class.php</code></td></tr><tr><td>Birthdate</td><td><code>LH_Profile_Birthdate_Field</code></td><td><code>lh-profile-birthdate-field-class.php</code></td></tr><tr><td>Street address</td><td><code>LH_Profile_Street_Address_Field</code></td><td><code>lh-profile-street-address-field-class.php</code></td></tr><tr><td>Town</td><td><code>LH_Profile_Town_Field</code></td><td><code>lh-profile-town-field-class.php</code></td></tr><tr><td>Postcode</td><td><code>LH_Profile_Postcode_Field</code></td><td><code>lh-profile-postcode-field-class.php</code></td></tr><tr><td>State/province</td><td><code>LH_Profile_State_Province_Field</code></td><td><code>lh-profile-state-province-field-class.php</code></td></tr><tr><td>Country</td><td><code>LH_Profile_Country_Field</code></td><td><code>lh-profile-country-field-class.php</code></td></tr><tr><td>First name</td><td><code>LH_Profile_First_Name_Field</code></td><td><code>lh-profile-first-name-field-class.php</code></td></tr><tr><td>Last name</td><td><code>LH_Profile_Last_Name_Field</code></td><td><code>lh-profile-last-name-field-class.php</code></td></tr><tr><td>Display name</td><td><code>LH_Profile_Display_Name_Field</code></td><td><code>lh-profile-display-name-field-class.php</code></td></tr></tbody></table></figure><p class=\"wp-block-paragraph\">Address fields are five separate classes (not one shared class) deliberately &#8211; matching the one-class-per-field precedent, and because postcode specifically has real per-country format validation worth adding later (a self-contained future change if it stays in its own file).</p><h2 class=\"wp-block-heading\">What was NOT extracted</h2><p class=\"wp-block-paragraph\">The multi-field orchestrator methods &#8211; <code>post_handler()</code>, <code>maybe_do_user_update()</code>, <code>new_user_update()</code>, <code>save_backend_profile_profile_fields()</code>, <code>maybe_add_details_when_csv_is_uploaded()</code> &#8211; remain in the god class. Each reimplements &quot;for each field: check nonce, check if already set, write if empty&quot; for a different trigger context (frontend POST, backend admin save, CRM enquiry, group invite, CSV import). They&#039;re close to duplicates of each other and are the single biggest remaining opportunity to shrink the god class, but consolidating them is a real behaviour-preserving refactor across many call sites, not a quick extraction &#8211; flagged as future work, not attempted.</p><p class=\"wp-block-paragraph\">Emails (<code>lh_profile-emails</code>) was also deliberately left unextracted &#8211; it&#039;s slated for removal, its functionality being superseded by <code>lh-user-identity</code>.</p><p class=\"wp-block-paragraph\">Name field label methods (<code>render_firstname_label()</code>, <code>render_lastname_label()</code>, <code>render_display_name_label()</code>) were extracted then removed entirely (v2.12) once confirmed to have no caller anywhere, live or dead, beyond commented-out code.</p>"^^rdf:XMLLiteral ;
  sioc:content "Type: Doc-sectionPart of: LH Profile PageField Class Architecturelh-profile-page.php began as a single &quot;god class&quot; (LH_profile_page_plugin) holding every field&#039;s rendering, validation, and storage logic. Starting with the phone field (v2.03) and continuing through v2.11, each field was extracted into its own dedicated class, following a consistent pattern:The extraction patternFor each field:A new includes/lh-profile-{field}-field-class.php file holds the real, self-contained rendering/validation/storage methods as a static class (LH_Profile_{Field}_Field).A back-compat proxy shim stays on LH_profile_page_plugin for every extracted public method, delegating to the new class. This exists because the original 2.03 phone extraction assumed no external plugin called these methods directly &#8211; that assumption was wrong (ppt-lh-membership-extender called render_phone_input() directly) and fatalled the site the moment 2.03 shipped. Every subsequent extraction leaves a shim regardless of whether an external caller is known, on the same &quot;don&#039;t assume, leave the shim&quot; principle.register_meta() registration, where the field has a real validation rule worth enforcing (see Database Schema doc for which fields this applies to).Shim-call diagnostic loggingSince the gender/birthdate/address/name extractions (v2.09+), every shim proxy calls a shared maybe_log_external_shim_call() helper as its first line. This inspects the immediate caller&#039;s file via debug_backtrace(): calls originating from within lh-profile-page.php itself (expected, pre-extraction internal usage) are silently ignored; calls from any other file are logged at warning level via the plugin&#039;s write_log() convention, naming the exact method, file, and line.This exists to turn &quot;we assume nothing external calls this&quot; into &quot;we know nothing external calls this&quot; &#8211; the only way to eventually make shim removal an evidence-based decision rather than a guess. Search LH Debug Log Viewer for &quot;Deprecated shim&quot; to find any external callers the logging has caught. One confirmed find this way: ppt-lh-membership-extender.php was calling render_phone_input(), render_gender_input(), and handle_phone_update() directly &#8211; all three were repointed to call LH_Profile_Phone_Field/LH_Profile_Gender_Field directly (site plugin, updated to v1.01).Extracted field classesFieldClassFilePhone (+ secondary)LH_Profile_Phone_Fieldlh-profile-phone-field-class.phpGenderLH_Profile_Gender_Fieldlh-profile-gender-field-class.phpBirthdateLH_Profile_Birthdate_Fieldlh-profile-birthdate-field-class.phpStreet addressLH_Profile_Street_Address_Fieldlh-profile-street-address-field-class.phpTownLH_Profile_Town_Fieldlh-profile-town-field-class.phpPostcodeLH_Profile_Postcode_Fieldlh-profile-postcode-field-class.phpState/provinceLH_Profile_State_Province_Fieldlh-profile-state-province-field-class.phpCountryLH_Profile_Country_Fieldlh-profile-country-field-class.phpFirst nameLH_Profile_First_Name_Fieldlh-profile-first-name-field-class.phpLast nameLH_Profile_Last_Name_Fieldlh-profile-last-name-field-class.phpDisplay nameLH_Profile_Display_Name_Fieldlh-profile-display-name-field-class.phpAddress fields are five separate classes (not one shared class) deliberately &#8211; matching the one-class-per-field precedent, and because postcode specifically has real per-country format validation worth adding later (a self-contained future change if it stays in its own file).What was NOT extractedThe multi-field orchestrator methods &#8211; post_handler(), maybe_do_user_update(), new_user_update(), save_backend_profile_profile_fields(), maybe_add_details_when_csv_is_uploaded() &#8211; remain in the god class. Each reimplements &quot;for each field: check nonce, check if already set, write if empty&quot; for a different trigger context (frontend POST, backend admin save, CRM enquiry, group invite, CSV import). They&#039;re close to duplicates of each other and are the single biggest remaining opportunity to shrink the god class, but consolidating them is a real behaviour-preserving refactor across many call sites, not a quick extraction &#8211; flagged as future work, not attempted.Emails (lh_profile-emails) was also deliberately left unextracted &#8211; it&#039;s slated for removal, its functionality being superseded by lh-user-identity.Name field label methods (render_firstname_label(), render_lastname_label(), render_display_name_label()) were extracted then removed entirely (v2.12) once confirmed to have no caller anywhere, live or dead, beyond commented-out code." ;
  sioc:topic <https://lhero.org/lh_portfolio-type/doc-section/>, <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/lh_portfolio-type/doc-section/> rdfs:seeAlso <https://lhero.org/lh_portfolio-type/doc-section/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
