Architecture notes

Structure

lh-crm.php holds the main class, LH_Crm_plugin: identity methods, form meta accessors, shared helpers, the Contact Form block, frontend script and admin notices. Each area of behaviour is a separate class in includes/ that extends LH_Crm_plugin and is its own singleton: contact form post type, enquiry post type, form submission, notifications, bbPress adjustments and MCP abilities. None of the plugin’s own classes is wrapped in a class_exists() guard: on this platform a guard like that can report true before the class is declared and silently skip the class’s setup. libraries/ holds four self-contained drop-in classes (form response post type, IP address taxonomy, user agent meta, script registration); these do keep a guard, because other plugins may ship the same files, and whichever copy loads first is the one the network uses. LH CRM’s copies log through LH CRM’s own write_log(). Consolidating every bundled copy is tracked as task 147743. View templates live in partials/.

Load order

The libraries and component classes are loaded by load_components(), called from the main class constructor, which runs as soon as the plugin file is read. Only the main class’s own hooks (block, frontend script, admin notices) are registered later, on bp_loaded.

Do not move component loading to bp_loaded or any later hook. The enquiry post type registers its five statuses on init at priority 1000. The wp-statuses library bundled in LH Agora also runs wp_statuses_register on init at priority 1000, and converts every status registered before it into its own status object. Callbacks at the same priority run in the order they were added, so LH CRM’s callback must be added before wp-statuses adds its own. If it is added later, the enquiry statuses stay plain objects and wp-statuses throws a fatal error (a call to is_builtin()) during REST server setup, which breaks the REST API, and with it MCP, across the whole network. This happened in 1.5.6. The same rule applies to any LH plugin that registers post statuses at init priority 1000.

BuddyPress dependency

LH CRM depends on BuddyPress. The main class’s hooks are registered on bp_loaded, so without BuddyPress the block and frontend script never load. The component classes are loaded when the plugin file is read (see Load order) and call BuddyPress functions directly (groups, xProfile, notifications), so the plugin should not be activated on a network without BuddyPress.

How a submission is handled

  1. On wp, a POST containing the form’s submitted flag and form ID is picked up and its nonce checked.
  2. The enquirer is resolved. A logged-in visitor is always the enquirer: any posted email is ignored, and a posted first or last name is only saved where the account has none. A logged-out visitor is matched or created from the submitted email, through lh_user_provisioning_ensure_user() when LH User Provisioning is active, otherwise a built-in fallback.
  3. lh_crm_http_post_after_user fires. Plugins whose profile fields save themselves (for example LH User Experience Taxonomy) save here.
  4. Submitted xProfile fields are read from the request in save_data(), after the nonce check, and passed to handle_bp_edit(). It only fills fields that are empty for the enquirer: a stored value is never changed from the enquiry form, whoever submits it. Values are passed to BuddyPress as submitted, so the field type validates them and BuddyPress (or the plugin that owns the field) sanitises them on save.
  5. The form’s configured group action (add to group, email invite or silent invite) is applied to the enquirer.
  6. If the form was on a singular page, an enquiry is created (or the existing one returned when the reference matches), the submitter’s IP address and user agent are recorded against it by the bundled libraries, and lh_crm_enquiry_created fires for any other plugin listening.
  7. If the form has a Notification template, the auto-reply is emailed to the enquirer.
  8. The page content is then replaced with the form’s Response template.

Because the nonce is printed into the page for the frontend script, a page cached for longer than the nonce lifetime will make submissions fail. Pages carrying a form should not be cached for long.

The logged-in form

For a logged-in user the form shows their email, their name and every filled profile field as greyed, disabled inputs, and only offers inputs for missing details. Disabled inputs are never submitted, and the server side enforces the same rule regardless of what is posted.

Some profile fields are xProfile field types from other plugins that keep their data outside BuddyPress’s own profile data table (Gender and Phone in LH Profile Page are user meta; User Experience in LH User Experience Taxonomy is a user taxonomy term), so bp_field_has_data() reports them empty. The form renders each field with the suppress_if_populated property those field types honour, and treats a field as filled if BuddyPress has data for it, if its displayed value is not empty, or if the field type printed nothing. The greyed value comes from bp_get_the_profile_field_value() (which LH Profile Page filters), then lh_crm_frontend_field_display_value, and otherwise reads On your profile. Field types listed in lh_crm_frontend_always_editable_field_types skip all this and stay editable.

Design decisions

Known issues

History

Related plugins