@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-doc&#038;p=147290>
  a sioc:Post ;
  dc:title "Architectural notes" ;
  dcterms:identifier 147290 ;
  dc:modified "2026-09-13T13:58:10Z"^^xsd:dateTime ;
  dc:created "2026-09-13T01:21:07Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-buddypress-group-taxonomies/architectural-notes/> ;
  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-buddypress-group-taxonomies/">LH BuddyPress Group Taxonomies</a></li></ul>
<h2 class="wp-block-heading">File structure (as of 1.1.4)</h2>



<p class="wp-block-paragraph">The former two god classes (<code>LH_BP_Group_Taxonomies_Admin</code>, <code>LH_BP_Group_Taxonomies_Frontend</code>) were split into eight focused, single-concern files. This wasn&#8217;t a mechanical file-per-method split — each cluster was kept together specifically where its methods share request-scoped instance state, and split apart everywhere else:</p>



<ul class="wp-block-list">
<li><code>class-admin-bulk-tags.php</code> (<code>LH_BP_Group_Taxonomies_Admin_Bulk_Tags</code>) — the edit-tags.php-emulation cluster (submenu registration, fake post-type registration, the whole add/edit/delete-tag action dispatcher). Kept together: these all read/write <code>$admin_screens</code>/<code>$admin_pages</code> instance state.</li>
<li><code>class-admin-list-tables.php</code> (<code>LH_BP_Group_Taxonomies_Admin_List_Tables</code>) — Groups admin list table integration: taxonomy filter dropdowns, columns, term edit/delete link targets. No shared state with anything else.</li>
<li><code>class-admin-term-columns.php</code> (<code>LH_BP_Group_Taxonomies_Admin_Term_Columns</code>) — term list table column customization (Posts count → Groups count). No shared state.</li>
<li><code>class-admin-group-metabox.php</code> (<code>LH_BP_Group_Taxonomies_Admin_Group_Metabox</code>) — the single-group admin edit screen&#8217;s tag metabox. Delegates rendering/saving to <code>LH_BP_Group_Taxonomies_Tag_Editor</code>.</li>
<li><code>class-frontend.php</code> (<code>LH_BP_Group_Taxonomies_Frontend</code>, slimmed) — directory SQL filtering (<code>parse_select()</code>/<code>parse_total()</code>/<code>parse_groups_query()</code>), BP theme-compat handling, and single-tag display. Kept together deliberately: one hook resolves &#8220;which tag is this directory view for&#8221; into <code>$this-&gt;term</code>/<code>$this-&gt;taxonomy</code>/<code>$this-&gt;tax_query</code>, and several later, unrelated-looking hook callbacks (theme-compat, count filters, tag-info display) depend on that having already run. Splitting this further would need a shared state object between classes rather than a mechanical extraction, so it stayed as one class.</li>
<li><code>class-frontend-tag-editor.php</code> (<code>LH_BP_Group_Taxonomies_Tag_Editor</code>) — renders and saves the tag-selection checkboxes on group creation/edit. All-static, no shared state — called from both the front-end flow and <code>Admin_Group_Metabox</code>.</li>
<li><code>class-frontend-group-lifecycle.php</code> (<code>LH_BP_Group_Taxonomies_Group_Lifecycle</code>) — visibility-change term-count fixup, cleanup on group deletion, single-group template hierarchy entry. No shared state with anything else.</li>
<li><code>class-taxonomy.php</code> (<code>LH_BP_Group_Taxonomies_Taxonomy</code>) — unchanged, the data layer described below.</li>
</ul>



<h2 class="wp-block-heading">Groups directory filtering</h2>



<p class="wp-block-paragraph">Implemented via <code>parse_select()</code> / <code>parse_total()</code> in <code>LH_BP_Group_Taxonomies_Frontend</code>, hooked on BuddyPress&#8217;s own <code>bp_groups_get_paged_groups_sql</code> / <code>bp_groups_get_total_groups_sql</code> filters. <code>parse_groups_query()</code> (hooked on <code>bp_before_has_groups_parse_args</code>) collects every matching taxonomy present — either a directory URL segment or a <code>$_GET</code> param — into a WP_Tax_Query-shaped multi-clause array (AND relation across taxonomies). <code>parse_select()</code>/<code>parse_total()</code> then build a <code>WP_Tax_Query</code> from that array and inject its JOIN/WHERE SQL directly into BuddyPress&#8217;s group query (<code>WP_Tax_Query::get_sql( 'g', 'id' )</code>) — the same technique BuddyPress&#8217;s own native <code>group_type</code> filtering uses internally. Because <code>$wpdb</code>&#8216;s term-table properties resolve to whichever site is current, this join correctly scopes to the current site&#8217;s own term tables even on a network-activated install where BuddyPress&#8217;s <code>bp_groups</code> table is centralised on the main site.</p>



<p class="wp-block-paragraph">The first taxonomy/term matched is also kept on <code>$this-&gt;taxonomy</code> / <code>$this-&gt;term</code> for single-tag display contexts that only show one tag name — when multiple taxonomies are active at once, only the first is named in that display copy, though all are applied to the actual filtering.</p>



<p class="wp-block-paragraph">An earlier PHP-side implementation (<code>restrict_by_taxonomy()</code>, using <code>get_objects_in_term()</code> + <code>array_intersect()</code> against <code>$_GET</code>) was removed in 1.0.1 in favour of the SQL-join approach above. Verified live on Princes Park Touch (site ID 2) that it correctly resolves to the site&#8217;s own term tables (e.g. <code>lhero_2_term_relationships</code>), not shared/network tables.</p>



<h2 class="wp-block-heading">Reusable tax_query API</h2>



<p class="wp-block-paragraph"><code>LH_BP_Group_Taxonomies_Taxonomy::get_group_ids_by_tax_query( array $tax_query )</code> returns matching group IDs for an arbitrary WP_Tax_Query-shaped argument — a single flat clause, or multiple clauses combined with a top-level <code>relation</code> key, exactly as WordPress core&#8217;s native <code>tax_query</code> argument works elsewhere (e.g. <code>WP_Query</code>). It runs standalone against BuddyPress&#8217;s own groups table via <code>$wpdb</code>, so it&#8217;s callable from any PHP code, not just the groups-directory-page context <code>parse_select()</code>/<code>parse_total()</code> are tied to.</p>



<pre class="wp-block-code"><code>// Multiple clauses, AND
$group_ids = LH_BP_Group_Taxonomies_Taxonomy::get_group_ids_by_tax_query( array(
    'relation' =&gt; 'AND',
    array( 'taxonomy' =&gt; 'lh_group_tags', 'terms' =&gt; 12, 'field' =&gt; 'term_id' ),
    array( 'taxonomy' =&gt; 'lh_comps_team_season', 'terms' =&gt; 'season-1', 'field' =&gt; 'slug' ),
) );</code></pre>



<p class="wp-block-paragraph">As of 1.1.0, <code>lh-competitions</code>&#8216;s <code>return_common_group_ids_by_term_id()</code> and <code>return_common_group_ids_by_term_slug()</code> use this method internally instead of independently calling <code>get_objects_in_term()</code>.</p>



<p class="wp-block-paragraph"><strong>Forward-compatibility note:</strong> this array shape deliberately mirrors a generic <code>tax_query</code> argument BuddyPress core is adding to <code>BP_Groups_Group::get()</code> (landed on trunk June 2026, not yet in a stable release — confirmed absent from the BuddyPress 14.4.0 build installed on this network as of the 1.1.0 changelog entry). Once this network upgrades to a BuddyPress release that includes it, callers can switch to <code>groups_get_groups( array( 'tax_query' =&gt; $tax_query ) )</code> directly, and this method (along with <code>parse_select()</code>/<code>parse_total()</code>) can likely be retired in favour of BuddyPress&#8217;s native support.</p>



<h2 class="wp-block-heading">MCP abilities load order</h2>



<p class="wp-block-paragraph">The abilities file loads on <code>plugins_loaded</code> (priority 20), independent of the <code>bp_loaded</code>/<code>bp_init</code>-based bootstrap used for the BuddyPress-facing functionality (<code>LH_BP_Group_Taxonomies_Taxonomy</code> starts on <code>bp_init</code>, priority 11). Guarded on <code>function_exists( 'wp_register_ability' )</code> as defense-in-depth, though the plugin&#8217;s declared minimum (WordPress 6.9+, see Requirements below) already guarantees the Abilities API is present.</p>



<h2 class="wp-block-heading">Requirements</h2>



<ul class="wp-block-list">
<li>WordPress 6.9+ (declared via the plugin&#8217;s <code>Requires at least</code> header — the bundled Abilities API is what actually needs 6.9, not the BuddyPress-facing functionality itself, which would run fine on 6.0)</li>
<li>PHP 8.1+</li>
<li>BuddyPress 12.0+ with the Groups component active</li>
</ul>
"""^^rdf:XMLLiteral ;
  sioc:content """Type: Doc-sectionPart of: LH BuddyPress Group Taxonomies
File structure (as of 1.1.4)



The former two god classes (LH_BP_Group_Taxonomies_Admin, LH_BP_Group_Taxonomies_Frontend) were split into eight focused, single-concern files. This wasn&#8217;t a mechanical file-per-method split — each cluster was kept together specifically where its methods share request-scoped instance state, and split apart everywhere else:




class-admin-bulk-tags.php (LH_BP_Group_Taxonomies_Admin_Bulk_Tags) — the edit-tags.php-emulation cluster (submenu registration, fake post-type registration, the whole add/edit/delete-tag action dispatcher). Kept together: these all read/write $admin_screens/$admin_pages instance state.
class-admin-list-tables.php (LH_BP_Group_Taxonomies_Admin_List_Tables) — Groups admin list table integration: taxonomy filter dropdowns, columns, term edit/delete link targets. No shared state with anything else.
class-admin-term-columns.php (LH_BP_Group_Taxonomies_Admin_Term_Columns) — term list table column customization (Posts count → Groups count). No shared state.
class-admin-group-metabox.php (LH_BP_Group_Taxonomies_Admin_Group_Metabox) — the single-group admin edit screen&#8217;s tag metabox. Delegates rendering/saving to LH_BP_Group_Taxonomies_Tag_Editor.
class-frontend.php (LH_BP_Group_Taxonomies_Frontend, slimmed) — directory SQL filtering (parse_select()/parse_total()/parse_groups_query()), BP theme-compat handling, and single-tag display. Kept together deliberately: one hook resolves &#8220;which tag is this directory view for&#8221; into $this-&gt;term/$this-&gt;taxonomy/$this-&gt;tax_query, and several later, unrelated-looking hook callbacks (theme-compat, count filters, tag-info display) depend on that having already run. Splitting this further would need a shared state object between classes rather than a mechanical extraction, so it stayed as one class.
class-frontend-tag-editor.php (LH_BP_Group_Taxonomies_Tag_Editor) — renders and saves the tag-selection checkboxes on group creation/edit. All-static, no shared state — called from both the front-end flow and Admin_Group_Metabox.
class-frontend-group-lifecycle.php (LH_BP_Group_Taxonomies_Group_Lifecycle) — visibility-change term-count fixup, cleanup on group deletion, single-group template hierarchy entry. No shared state with anything else.
class-taxonomy.php (LH_BP_Group_Taxonomies_Taxonomy) — unchanged, the data layer described below.




Groups directory filtering



Implemented via parse_select() / parse_total() in LH_BP_Group_Taxonomies_Frontend, hooked on BuddyPress&#8217;s own bp_groups_get_paged_groups_sql / bp_groups_get_total_groups_sql filters. parse_groups_query() (hooked on bp_before_has_groups_parse_args) collects every matching taxonomy present — either a directory URL segment or a $_GET param — into a WP_Tax_Query-shaped multi-clause array (AND relation across taxonomies). parse_select()/parse_total() then build a WP_Tax_Query from that array and inject its JOIN/WHERE SQL directly into BuddyPress&#8217;s group query (WP_Tax_Query::get_sql( 'g', 'id' )) — the same technique BuddyPress&#8217;s own native group_type filtering uses internally. Because $wpdb&#8216;s term-table properties resolve to whichever site is current, this join correctly scopes to the current site&#8217;s own term tables even on a network-activated install where BuddyPress&#8217;s bp_groups table is centralised on the main site.



The first taxonomy/term matched is also kept on $this-&gt;taxonomy / $this-&gt;term for single-tag display contexts that only show one tag name — when multiple taxonomies are active at once, only the first is named in that display copy, though all are applied to the actual filtering.



An earlier PHP-side implementation (restrict_by_taxonomy(), using get_objects_in_term() + array_intersect() against $_GET) was removed in 1.0.1 in favour of the SQL-join approach above. Verified live on Princes Park Touch (site ID 2) that it correctly resolves to the site&#8217;s own term tables (e.g. lhero_2_term_relationships), not shared/network tables.



Reusable tax_query API



LH_BP_Group_Taxonomies_Taxonomy::get_group_ids_by_tax_query( array $tax_query ) returns matching group IDs for an arbitrary WP_Tax_Query-shaped argument — a single flat clause, or multiple clauses combined with a top-level relation key, exactly as WordPress core&#8217;s native tax_query argument works elsewhere (e.g. WP_Query). It runs standalone against BuddyPress&#8217;s own groups table via $wpdb, so it&#8217;s callable from any PHP code, not just the groups-directory-page context parse_select()/parse_total() are tied to.



// Multiple clauses, AND
$group_ids = LH_BP_Group_Taxonomies_Taxonomy::get_group_ids_by_tax_query( array(
    'relation' =&gt; 'AND',
    array( 'taxonomy' =&gt; 'lh_group_tags', 'terms' =&gt; 12, 'field' =&gt; 'term_id' ),
    array( 'taxonomy' =&gt; 'lh_comps_team_season', 'terms' =&gt; 'season-1', 'field' =&gt; 'slug' ),
) );



As of 1.1.0, lh-competitions&#8216;s return_common_group_ids_by_term_id() and return_common_group_ids_by_term_slug() use this method internally instead of independently calling get_objects_in_term().



Forward-compatibility note: this array shape deliberately mirrors a generic tax_query argument BuddyPress core is adding to BP_Groups_Group::get() (landed on trunk June 2026, not yet in a stable release — confirmed absent from the BuddyPress 14.4.0 build installed on this network as of the 1.1.0 changelog entry). Once this network upgrades to a BuddyPress release that includes it, callers can switch to groups_get_groups( array( 'tax_query' =&gt; $tax_query ) ) directly, and this method (along with parse_select()/parse_total()) can likely be retired in favour of BuddyPress&#8217;s native support.



MCP abilities load order



The abilities file loads on plugins_loaded (priority 20), independent of the bp_loaded/bp_init-based bootstrap used for the BuddyPress-facing functionality (LH_BP_Group_Taxonomies_Taxonomy starts on bp_init, priority 11). Guarded on function_exists( 'wp_register_ability' ) as defense-in-depth, though the plugin&#8217;s declared minimum (WordPress 6.9+, see Requirements below) already guarantees the Abilities API is present.



Requirements




WordPress 6.9+ (declared via the plugin&#8217;s Requires at least header — the bundled Abilities API is what actually needs 6.9, not the BuddyPress-facing functionality itself, which would run fine on 6.0)
PHP 8.1+
BuddyPress 12.0+ with the Groups component active

""" ;
  sioc:topic <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/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
