Custom Membership Term plugins
You can add project-specific membership term logic by implementing a new plugin that implements MembershipTermInterface and is discovered by the Membership Term manager.
Steps
1. Create a plugin class
- Place the class in your module under
src/Plugin/MembershipTerm/(e.g.src/Plugin/MembershipTerm/CustomTerm.php). - Extend
Drupal\crm_membership\Plugin\MembershipTermBase. - Implement
ContainerFactoryPluginInterfaceand acreate()factory method for dependency injection (the base class requiresEntityTypeManager,Time,DurationService, and a logger). - Add the
#[MembershipTerm]PHP attribute so the plugin is discovered.
Attribute parameters (from Drupal\crm_membership\Attribute\MembershipTerm):
| Parameter | Type | Description |
|---|---|---|
id |
string | Plugin ID (machine name). Required. |
label |
TranslatableMarkup | null | Human-readable label shown in the Membership Type form. |
allowOverrideStartDate |
bool | Whether the start date can be overridden when activating/renewing. Default false. |
allowOverrideEndDate |
bool | Whether the end date can be overridden. Default false. |
durationConfigKey |
string | null | Config key for duration (e.g. for duration_field). Optional. |
timeGapKey |
string | null | Config key for time gap between renewals. Optional. |
2. Example (minimal)
<?php
declare(strict_types=1);
namespace Drupal\my_module\Plugin\MembershipTerm;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\crm\Entity\Contact;
use Drupal\crm_membership\Attribute\MembershipTerm;
use Drupal\crm_membership\Plugin\MembershipTermBase;
#[MembershipTerm(
id: "my_custom_term",
label: new TranslatableMarkup("My custom term"),
allowOverrideStartDate: TRUE,
allowOverrideEndDate: TRUE,
)]
final class CustomTerm extends MembershipTermBase {
public function isActiveFor(Contact $contact): bool {
// Your logic, or delegate to parent for period + grace handling.
return parent::isActiveFor($contact);
}
public function isExpiredFor(Contact $contact): bool {
return FALSE;
}
public function activate(?array $contacts = NULL): void {
// Create initial period(s) using $this->addMembershipPeriod().
}
public function renew(?array $contacts = NULL): void {
// Add new period(s) via addMembershipPeriod().
}
public function expire(): void {
parent::expire();
}
public function cancel(): void {
// Programmatic only — no UI calls this.
parent::cancel();
}
public function allowRenewal(): bool {
return TRUE;
}
}
The base class implements setMembership/getMembership, getSettings, setStartDate/setEndDate, addMembershipPeriod(), and DI via create(). Override lifecycle methods and optionally buildConfigurationForm, validateConfigurationForm, and saveConfiguration.
3. Reference implementations
Built-in Fixed duration — full production example with duration, rollover, grace period, and configuration form:
- Class:
Drupal\crm_membership\Plugin\MembershipTerm\FixedDuration - File:
src/Plugin/MembershipTerm/FixedDuration.php
Test Counter plugin — minimal plugin with custom DI (State service) used in kernel tests:
- Class:
Drupal\crm_membership_test\Plugin\MembershipTerm\Counter - File:
tests/crm_membership_test/src/Plugin/MembershipTerm/Counter.php
Enable the crm_membership_test module in test environments to use the Counter plugin.
4. Configuration form (optional)
Override these methods to expose settings on the Membership Type form:
buildConfigurationForm(array $current_config, FormStateInterface $form_state): arrayvalidateConfigurationForm(array $form, FormStateInterface $form_state): voidsaveConfiguration(array $values): array
Submitted values are stored in the type’s membership_term_config and passed to the plugin as configuration. See Configuration reference.
5. Alter hook
Other modules can alter plugin definitions via hook_crm_membership_term_info() (see Alter and extend).
6. Use your plugin
- Clear caches so the new plugin is discovered.
- Edit a Membership Type (or create one) at Administration → Structure → CRM → Membership Types.
- Select your plugin in the “Membership term” dropdown and save. Any configuration form you added will appear there (AJAX-refreshed when the selection changes).
- New memberships of that type will use your plugin for activation, renewal, expiration, and status checks.
cancel() is programmatic only
There is no admin form or route that calls cancel(). Use it from custom code when you need to end a membership outside the normal expire flow.