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.
idmust 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_idsdecides 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¶
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¶
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¶
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¶
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:
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 |
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.