🎯 Need Help?

Get Premium Support

Priority assistance from the developer

⬆️ Upgrade

Unlock all features

More from ANWP

Sports Leagues Premium

Multi-sport league management with advanced statistics

Learn more →

AnWP SL Theme Kit

Starter themes & block patterns for Sports Leagues

Learn more →

v0.18.0 Data Model Changes


Football Leagues 0.18.0 moves three entities from WordPress postmeta into dedicated custom tables: clubs, standings, and competitions. This page is for developers who maintain theme template overrides or custom code that reads plugin data directly.

Who should read this

You only need this page if you have theme template overrides in yourtheme/anwp-football-leagues/ or custom PHP that calls get_post_meta() with plugin keys like _anwpfl_city, _anwpfl_table_main, or _anwpfl_multistage. Standard installs without code customizations do not need any changes. Upgrading a site with no custom code? Follow the step-by-step upgrade guide instead.

🎯 What changed

  • Clubs moved to wp_anwpfl_clubs. The CMB2 metabox was replaced with a Vue editor.
  • Standings moved to wp_anwpfl_standings. 32 postmeta keys were consolidated. meta_query lookups became indexed WHERE queries.
  • Competitions moved to wp_anwpfl_competitions. 30 postmeta keys migrated. The CMB2 metabox was split into smaller sidebar metaboxes.

Upgrade timeline

The old postmeta keys still exist after the upgrade. Code that calls get_post_meta() against the old keys keeps working, so you don’t have to rewrite overrides the day 0.18.0 ships. But the postmeta is no longer kept in sync with new writes going through the custom tables, and the keys will be removed when you run the cleanup action:

Football Leagues > Toolbox > Cleanup — separate batch actions for clubs, competitions, and standings. After cleanup runs, the old postmeta is gone and any remaining get_post_meta() calls against the old keys return empty.

Recommended upgrade path

Audit your overrides against the tables below before running the Toolbox cleanup. Keep the postmeta around as a safety net until you’ve verified your theme still renders correctly with the new data access patterns.

How to check what needs updating on your site

Open Settings & Tools > Template Status (FL Premium 0.18.0+). The page scans every FL template override in your active theme and any third-party plugins that hook into anwpfl_template_paths, compares @version docblock tags against the plugin defaults, and flags the exact lines using postmeta patterns that will break after Toolbox cleanup. Start there — it tells you which specific files need the changes described below, instead of reading the tables line-by-line.

Stable public API (no changes needed)

These helpers kept the same signatures and return values. If your override only uses these, you have nothing to change:

  • Clubs: get_club_logo_by_id(), get_club_link_by_id(), get_club_abbr_by_id(), get_club_title_by_id(), is_national_team()
  • Standings: get_standings(), tmpl_get_competition_standings() (same signature, now backed by an indexed query)
  • Competitions: get_competition_title(), get_competition_data_full() (primary accessor — returns formatted data with groups and rounds decoded)

Clubs

Replace get_post_meta() calls with the custom-table accessors. Use get_club_list_row() for common scalar fields (title, logo, main color), and get_row() when you need the full row including JSON-blob columns like club_details, club_social, or club_trophies.

Old patternNew pattern
get_post_meta( $id, '_anwpfl_city', true )anwp_fl()->club->get_row( (int) $id )['city'] ?? ''
$club->{$prefix . 'city'}$club_row['city'] ?? '' (from get_row())
get_all_clubs_data()[ $id ]anwp_fl()->club->get_club_list_row( (int) $id )
get_post_meta( $id, '_anwpfl_main_color', true )anwp_fl()->club->get_club_list_row( (int) $id )['main_color'] ?? ''
get_post_meta( $id, '_anwpfl_address', true )json_decode( anwp_fl()->club->get_row( (int) $id )['club_details'] ?? '', true )['address'] ?? ''
get_post_meta( $id, '_fl_pro_trophies', true )json_decode( anwp_fl()->club->get_row( (int) $id )['club_trophies'] ?? '', true )

get_all_clubs_data() still works but emits a deprecation notice. Replace it with warm_clubs() + get_club_list_row() as shown in the loop pattern below.

Standings

Standings used to spread across 32 postmeta keys per standing. They now live in a single row with JSON columns for the tables themselves and a consolidated columns_config JSON for display settings. If you loaded a standing by running a meta_query against _anwpfl_competition and _anwpfl_competition_group, use tmpl_get_competition_standings() instead — it takes the same arguments and runs an indexed query.

Old patternNew pattern
get_post_meta( $id, '_anwpfl_table_main', true )anwp_fl()->standing->get_standing_rows( $id ) (decodes JSON to objects)
get_post_meta( $id, '_anwpfl_table_main_home', true )json_decode( anwp_fl()->standing->get_row( $id )['table_main_home'] ?? '', true )
get_post_meta( $id, '_anwpfl_table_main_away', true )json_decode( anwp_fl()->standing->get_row( $id )['table_main_away'] ?? '', true )
get_post_meta( $id, '_anwpfl_table_notes', true )anwp_fl()->standing->get_row( $id )['table_notes'] ?? ''
get_post_meta( $id, '_anwpfl_table_colors', true )json_decode( anwp_fl()->standing->get_row( $id )['table_colors'] ?? '', true )
get_post_meta( $id, '_anwpfl_points_win', true )anwp_fl()->standing->get_row( $id )['points_win'] ?? 3
get_post_meta( $id, '_anwpfl_columns_order', true )json_decode( anwp_fl()->standing->get_row( $id )['columns_config'] ?? '', true )['order'] ?? []
get_post_meta( $id, '_anwpfl_arrows_dynamic_ranking_data', true )json_decode( anwp_fl()->standing->get_row( $id )['arrows_data'] ?? '', true )
meta_query with _anwpfl_competition + _anwpfl_competition_groupanwp_fl()->competition->tmpl_get_competition_standings( $competition_id, $group_id )

Tip — JSON decode shape

When the plugin decodes table_main internally (via get_standing_rows()), it decodes to objects, not associative arrays, so theme overrides that use $row->club_id keep working. If you call json_decode() yourself, pass true only if your code expects arrays.

Competitions

Competitions now have a two-tier accessor pattern. get_competition_list_row() returns the 23 most-used scalar fields (title, type, logo, stage info, bracket flags) — that’s what you want in a loop. get_row() returns the full row including the large JSON/TEXT columns (stage_groups, stage_rounds, tmpl_layout, custom_content_below, competition_roles). get_competition_data_full() wraps get_row(), applies logo inheritance for secondary stages, and decodes groups and rounds — use it when you’d previously read multiple postmeta keys together.

Fields available from get_competition_list_row()

Old patternNew pattern
get_post_meta( $id, '_anwpfl_type', true )anwp_fl()->competition->get_competition_list_row( (int) $id )['type'] ?? ''
get_post_meta( $id, '_anwpfl_multistage', true )...['multistage'] ?? ''
get_post_meta( $id, '_anwpfl_multistage_main', true )...['multistage_main'] ?? 0
get_post_meta( $id, '_anwpfl_stage_title', true )...['stage_title'] ?? ''
get_post_meta( $id, '_anwpfl_stage_order', true )...['stage_order'] ?? 0
get_post_meta( $id, '_anwpfl_competition_status', true )! empty( ...['is_friendly'] ) (column renamed and retyped to tinyint(1): 1 = friendly, 0 = official; old value 'friendly' maps to 1)
get_post_meta( $id, '_anwpfl_competition_order', true )...['competition_order'] ?? 0
get_post_meta( $id, '_anwpfl_logo', true )...['logo'] ?? ''
get_post_meta( $id, '_anwpfl_logo_big', true )...['logo_big'] ?? ''
get_post_meta( $id, '_anwpfl_matchweek_current', true )...['matchweek_current'] ?? 0
get_post_meta( $id, '_anwpfl_lazy', true )...['is_lazy'] ?? 0 (column renamed)
get_post_meta( $id, '_anwpfl_bracket', true )...['bracket'] ?? ''
get_post_meta( $id, '_anwpfl_bracket_layout_active', true )...['bracket_layout_active'] ?? ''
get_post_meta( $id, '_anwpfl_matchweeks_as_slides', true )...['matchweeks_as_slides'] ?? ''

Shorthand ... above means anwp_fl()->competition->get_competition_list_row( (int) $id ).

Fields that require get_row()

The fields below are large TEXT or JSON columns and are not in the light list. Asking for them via get_competition_list_row() returns null.

Old patternNew pattern
get_post_meta( $id, '_anwpfl_groups', true )json_decode( anwp_fl()->competition->get_row( (int) $id )['stage_groups'] ?? '' ) (objects)
get_post_meta( $id, '_anwpfl_rounds', true )json_decode( anwp_fl()->competition->get_row( (int) $id )['stage_rounds'] ?? '' ) (objects)
get_post_meta( $id, '_anwpfl_tmpl_layout', true )anwp_fl()->competition->get_row( (int) $id )['tmpl_layout'] ?? ''
get_post_meta( $id, '_anwpfl_custom_content_below', true )anwp_fl()->competition->get_row( (int) $id )['custom_content_below'] ?? ''
get_post_meta( $id, '_anwpfl_bracket_options', true )anwp_fl()->competition->get_row( (int) $id )['bracket_options'] ?? ''
get_post_meta( $id, '_anwpfl_role_competition_supervisor', true )anwp_fl()->competition->get_row( (int) $id )['competition_roles'] ?? '' (JSON)
get_post_meta( $id, '_anwpfl_format_robin', true )anwp_fl()->competition->get_row( (int) $id )['format_robin'] ?? ''
get_post_meta( $id, '_anwpfl_format_knockout', true )anwp_fl()->competition->get_row( (int) $id )['format_knockout'] ?? ''

Pitfall — list_row vs row

If the plugin warmed the light cache before your template ran (common for loops over matches or standings), calling get_competition_list_row() for a non-light field returns null silently — no warning. Use get_row() for anything in the table above, or call get_competition_data_full() when you need the full formatted record.

Warm-up pattern for loops

When rendering a list (matches, standings, player stats), collect the entity IDs first, warm the cache in one query, then call the per-ID accessors inside the loop. Every read after the warm hits the per-request cache with zero extra queries.

Clubs

$club_ids = wp_list_pluck( $games, 'home_club' );
anwp_fl()->club->warm_clubs( $club_ids );

foreach ( $games as $game ) {
    $club = anwp_fl()->club->get_club_list_row( (int) $game->home_club );
    // $club['title'], $club['logo'], $club['main_color'], etc.
}

Competitions (light)

Use warm_competitions() when the loop only needs scalar fields (title, logo, stage info).

$competition_ids = wp_list_pluck( $games, 'competition_id' );
anwp_fl()->competition->warm_competitions( $competition_ids );

foreach ( $games as $game ) {
    $comp = anwp_fl()->competition->get_competition_list_row( (int) $game->competition_id );
    // $comp['title'], $comp['logo'], $comp['multistage'], etc.
}

Competitions (full)

Use warm_competitions_full() when you need stage_groups, stage_rounds, or any of the full-row-only fields.

anwp_fl()->competition->warm_competitions_full( $competition_ids );

foreach ( $competition_ids as $competition_id ) {
    $data = anwp_fl()->competition->get_competition_data_full( (int) $competition_id );
    // $data['title'], $data['groups'], $data['rounds'], + all formatted fields
}

Historical roster fallback (0.18.0)

The active-roster migration moved squad data from per-season postmeta into wp_anwpfl_clubs.squad (active) and wp_anwpfl_clubs.squad_seasons (historical archive). The migration backfilled squad_seasons from any pre-existing per-season postmeta, but most clubs only ever had postmeta for one season. Old seasons without an archive entry used to silently render the current squad, or return an empty list from the public REST API.

0.18.0 adds a stats-derived fallback between the archive lookup and the active-roster fallback. When squad_seasons['s:N'] is empty for the requested season, the plugin derives the roster from match participation data (wp_anwpfl_lineups starters + named bench, augmented with wp_anwpfl_players for stats-only entries) and uses that instead.

You don’t need to call this directly — it’s wired into the same accessors theme overrides already use:

  • tmpl_prepare_club_squad( $club_id, true, $season_id ) — used by every shortcode-squad*.php template and by match-lineups.php / match-missing.php / match-formation.php / match-player-stats.php.
  • get_club_season_players( [ 'club_id' => X, 'id' => N ] ) — used by Match Setup admin, the live-match editor, and the suspension-risk path.
  • GET /wp-json/anwpfl/v1/clubs/X/squad?season_id=N — the public REST endpoint.

If you want to invoke the helper from a custom override, the public method is anwp_fl()->club->derive_roster_from_stats( int $club_id, int $season_id ): array. Returns roster shape compatible with the active squad: [ ['id', 'number', 'position', 'status'], ... ] sorted by position priority (g/d/m/f) then jersey number. Cached per-request and in the anwp_match_data cache group (busted automatically on match save and club save).

Limitations of the derived roster

Stats data carries less detail than the per-season postmeta did. Customers and override authors should know what’s recoverable and what isn’t:

  • Players who never appeared in a match are absent. Training-only and cup-only-pre-eliminated players don’t have lineup or stats rows. The derived squad is shorter than the real one was.
  • status is always empty. Loan / trial / “left” flags can’t be recovered from match data. Suspension warnings (FL Premium) treat all derived players as at-risk — false positives, no false negatives.
  • Staff fallback is unchanged. No per-match staff table exists. Old-match staff displays still show the current staff. Backfill the per-season staff archive manually via the squad editor if accuracy matters.
  • Jersey numbers reflect the most recent custom number for the season, not the number worn in any specific match. Mid-season number changes resolve to the latest value. The lineup row’s custom_numbers column still drives per-match accuracy in the lineup data itself.
  • Positions are always current. wp_anwpfl_players has no per-match position column — only wp_anwpfl_player_data.position (the player’s current position) is available. A player who was a defender in 2014 but moved to midfield in 2026 will show m in the 2014 derived roster.

Customers who care about historical accuracy should backfill the archive manually using “Save active as season” in the squad editor (the same workflow that already exists). The fallback is a safety net, not a replacement for archived data.

Developer-facing changes

These changes don’t appear in the main changelog because they only matter to developers extending the plugin. They are listed here for completeness so you don’t have to dig through git history when something behaves differently than before.

New extension hooks

  • anwpfl/standing/row_template — register custom row keys before the standing table is populated. Useful for adding columns that aren’t tied to match stats.
  • anwpfl/standing/accumulate_match_stats — fires per match while the table is being computed. Use it to maintain custom counters keyed off the match data.
  • anwpfl/standing/post_compute_table — fires after the table is fully computed. Use it for derived columns that need totals from other rows (e.g. point differentials).
  • anwpfl/api_import/skip_squad_write and anwpfl/api_import/skip_staff_write (FL Premium) — return true to opt out of squad / staff writes per club during API-Football import. Useful when you maintain a club’s roster manually and don’t want the importer to overwrite it.
  • anwpfl/player/use_lighter_player_obj_list and anwpfl/staff/use_lighter_staff_list — force the lighter shape used on large sites (>20,000 players or staff). Drops photo, birthdate, and flag fields from the picker payload only — saved squads are unaffected.

Behavior changes

  • API import: reset_squad and reset_staff_squad now clear the entire active roster / staff instead of one season’s bucket. The active-roster model has no per-season buckets, so the previous semantics no longer apply. If you triggered these flags from custom integrations expecting per-season scope, the behavior is now wider.
  • Transfer “Register in squad” (FL Premium) — players added through the Transfer UI always go on the active roster. The season_id field in the REST payload is now ignored.
  • Plugin Health checks — Games without Competition now correctly handles games with no competition assigned (previously returned a partial result). The competition filter on the Plugin Health page now works for all competition IDs (previously broke on IDs above a threshold).

Deprecations

  • anwp_fl()->club->get_all_clubs_data() — deprecated since 0.17.3. Emits a runtime deprecation notice. Replacement: warm_clubs() + get_club_list_row().
  • anwp_fl()->competition->get_competition_group_standing() — deprecated in 0.18.0. Emits a runtime deprecation notice. Zero internal callers; kept only for theme override compatibility.
  • anwp_fl_pro()->club->is_national_club() — deprecated in 0.18.0. Emits a runtime deprecation notice. Thin wrapper around the canonical core method. Replacement: anwp_fl()->club->is_national_team().
  • anwp_fl()->competition->get_competitions() — soft-deprecated. Prefer the lighter get_competitions_data(). The runtime notice is deferred until more internal callers migrate.

📚 Related

  • REST API reference — the public REST API added in 0.18.0 reads from the same custom tables.