Skip to content

Hooks and filters

The extension points Club Player Manager and its companions mean for you to use. Everything else in the codebase is internal and moves without notice.

Adding to the Overview screen

Club Management → Overview is the plugin's home screen and the place another plugin adds its own content without either plugin knowing about the other. Three actions mark the insertion points, top to bottom.

Hook Fires
cpm_overview_before_content Above the headline cards
cpm_overview_after_stats Between the headline cards and the squad list
cpm_overview_after_content At the foot of the page

Each is passed the club figures the page has already gathered, so a listener never has to query for them again:

Key Type What it is
is_club_admin bool Whether the viewer is a club admin rather than a team secretary
squads object[] Each carries a ->player_count
total_players int
waiting_count int\|null null for a team secretary, who cannot see the waiting list
add_action(
    'cpm_overview_after_content',
    function ( array $summary ) {
        echo '<h2 class="cpm-section-title">Kit orders</h2>';
        // …your own panel.
    }
);

Anything echoed on these hooks goes straight into the page wrapper, so escape it and follow the admin UI conventions — the point of the hooks is that added content looks like it belongs.

cpm_overview_stats

To add a headline card alongside Players, Squads and Waiting list, filter the cards instead. Each is value (int), label (string) and an optional url that turns the card into a link.

add_filter(
    'cpm_overview_stats',
    function ( array $stats, array $summary ) {
        $stats[] = array(
            'value' => 12,
            'label' => __( 'Coaches', 'my-plugin' ),
            'url'   => admin_url( 'admin.php?page=my-coaches' ),
        );
        return $stats;
    },
    10,
    2
);

Respect $summary['is_club_admin'] if your figure is something a team secretary should not see.

Adding to the action list

Club Management → Actions is the club's list of outstanding jobs. A plugin contributes whole rows to it by registering a provider on cpm_action_list_providers.

add_filter(
    'cpm_action_list_providers',
    function ( array $providers ): array {
        $providers['my_kit_orders'] = array(
            'label'    => __( 'Kit orders', 'my-plugin' ),
            'callback' => 'my_plugin_kit_actions',
            // 'capability' => 'cpm_access',   // optional; this is the default
        );
        return $providers;
    }
);

function my_plugin_kit_actions( array $context ): array {
    $actions = array();
    foreach ( CPM_Platform::confirmed_players( $context['season_id'] ) as $player ) {
        if ( my_plugin_has_kit( $player->id ) ) {
            continue;
        }
        $actions[] = array(
            'id'        => 'kit:' . $context['season_id'] . ':' . $player->id,
            'title'     => sprintf( 'Order a shirt for %s', $player->full_name ),
            'detail'    => 'Confirmed for ' . $context['season_name'],
            'url'       => admin_url( 'admin.php?page=my-kit&player=' . $player->id ),
            'squad_ids' => $player->squad_ids,
        );
    }
    return $actions;
}

The callback is handed the context the page already worked out, and answers with what needs doing right now:

Key Type What it is
is_club_admin bool Whether the viewer is a club admin rather than a team secretary
season_id int The active season, or 0 when the club has none — answer with nothing
season_name string For the detail line

Three rules decide whether a provider behaves:

  • Actions are derived, never stored. Your callback runs on every page load, so an action leaves the list by the job being done — there is nothing to retire and no way for the list to ask for something twice.
  • id must be stable and unique within your provider. It is what a dismissal ("set aside") is remembered by, so compose it from what the action is about, never from a row id that could change. Club Player Manager namespaces it with your provider key.
  • squad_ids decides who sees the row. An action naming squads reaches those squads' team secretaries as well as club admins; one naming none is club-wide and only club admins see it. Scoping is applied for you — your callback is never told whose squads are whose.

A provider that throws costs its own section and nothing else.

Reacting to registrations and payments

Three actions fire at the moments other code usually cares about. All of them run inside the request that caused them, so keep listeners quick — or schedule your own job and return.

cpm_player_season_confirmed

do_action( 'cpm_player_season_confirmed', int $season_id, int $player_id );

A player has been newly confirmed for a season, whether by the parent approving through the form or by an admin or team secretary setting the status directly. Fires once per player, on the transition only — not on every save of an already-confirmed row.

cpm_season_registration_confirmed

do_action( 'cpm_season_registration_confirmed', int $season_id, int $player_id, string $status );

The same moment, with the confirmed status that was set. This is the hook the plugin's own confirmation automations listen on; use it when you need to know which confirmed status applies, and cpm_player_season_confirmed when you only need the fact.

cpm_registration_payment_confirmed

do_action( 'cpm_registration_payment_confirmed', int $player_product_id, int $player_id );

A Registration product's fee has cleared for a player — the moment the club's registration-paid alert goes out. $player_product_id identifies the specific player-and-product row, not the product.

It deliberately does not wait for the Direct Debit: a one-off fee never sets a mandate up at all, and an instalment plan's mandate goes active later. Products of other types — kit, subs — are purchases rather than registrations and never raise it.

Email tuning

Three filters, for sites where the defaults are wrong. All of them are code-only; none has a settings screen, because a club that needs to change them has a reason a checkbox would not capture.

Filter Default What it changes
cpm_email_queue_batch_size 10 Messages drained per background pass. Raise it on a host with generous timeouts, lower it on a slow one. Floored at 1.
cpm_email_queue_retention_days 3 How long finished queue rows are kept before the daily prune removes them
cpm_email_log_retention_days 90 How long the Email Log keeps a sent message. Raise it if the club needs a longer record of what it sent whom.
// A host that times out at 30 seconds is happier with smaller batches.
add_filter( 'cpm_email_queue_batch_size', fn() => 5 );

// Keep two years of email history.
add_filter( 'cpm_email_log_retention_days', fn() => 730 );

Raising log retention grows a table that is never otherwise trimmed. That is the intended trade, but it is a trade.

English FA

efa_allowed_source_hosts

apply_filters( 'efa_allowed_source_hosts', string[] $hosts );

The hosts English FA will fetch a fixtures or results feed from. The URL field on a team causes your server to make a request, so the host is checked against this allowlist first. Defaults to:

array( 'fulltime.thefa.com', 'thefa.com', 'fulltime-league.thefa.com' )

Widen it if your league mirrors Full-Time somewhere else:

add_filter(
    'efa_allowed_source_hosts',
    function ( array $hosts ) {
        $hosts[] = 'fulltime.mycountyfa.com';
        return $hosts;
    }
);

Host names are lower-cased and empty entries dropped, so case does not matter. Everything you add here is somewhere your server can be made to send a request — add only hosts you trust.

Scheduled jobs

Not hooks to extend, but the events worth knowing about when diagnosing a quiet site.

Event Schedule What it does
cpm_process_email_queue one-off, rescheduled while work remains Drains the outgoing email queue
cpm_prune_email_queue daily Removes finished queue rows past their retention
cpm_prune_email_log daily Removes log entries past their retention
efa_sync_matches the interval set in English FA's settings Fetches fixtures and results
wp cron event list | grep -E 'cpm|efa'
wp cron event run cpm_process_email_queue

A site with DISABLE_WP_CRON set and no system cron will queue email and never send it — see Troubleshooting.

Capabilities

Two, and they are what to check rather than manage_options if you are adding a screen that should follow the plugin's access rules:

Capability Who has it
cpm_access Any club admin or team secretary — "can reach the plugin at all"
cpm_manage_all Club admins only

A team secretary's view is scoped to their own squads in the data layer, not by hiding menu items, so a screen you add on cpm_access must do its own scoping. The platform layer provides the helpers for that.

Building a companion plugin

If you are going further than a hook — your own admin screens inside the Club Management menu, reading the club's data, sharing its look — read the platform layer. It covers what is public, how to depend on it without breaking during a deploy, and the version contract between a companion plugin and the core.