Skip to content

Architecture overview

The CRM Membership module is built on a plugin-based architecture for membership lifecycle management. The main building blocks are entities, the Membership Term plugin system, the MembershipService, the IS_MEMBER event, and background processing via cron and a queue worker.

High-level diagram

flowchart TB
  subgraph entities [Entities]
    Membership[Membership]
    MembershipType[MembershipType]
    MembershipPeriod[MembershipPeriod]
  end

  subgraph plugins [Plugin system]
    TermManager[MembershipTermManager]
    TermPlugin[MembershipTerm plugins]
  end

  subgraph api [Services and events]
    MembershipService[MembershipService]
    Events[IS_MEMBER event]
  end

  subgraph background [Background processing]
    Cron[hook_cron]
    Queue[MembershipExpiration queue]
  end

  MembershipType --> Membership
  Membership --> MembershipPeriod
  Membership --> TermManager
  TermManager --> TermPlugin
  MembershipService --> Membership
  MembershipService --> Events
  Cron --> Queue
  Queue --> Membership

Components

Component Description See
Entities Membership (content), MembershipType (config bundle), MembershipPeriod (content). Define the data model and CRUD. Entities
Plugin system Membership Term plugins determine how membership periods are calculated (fixed, rolling, lifetime) and how activation, renewal, and expiration behave. Plugin system
MembershipService Central API: check if a contact is a member of a target, and load memberships for a contact or target. Uses term plugins and dispatches the IS_MEMBER event. MembershipService
Events MembershipEvents::IS_MEMBER allows other modules to override or extend “is this contact a member of this target?” logic. Events
Cron and queue Cron finds active memberships without periods, or non-expired memberships with only expired periods, and enqueues them; the queue worker applies the term plugin’s expire logic. Cron and queue

Data flow (membership check)

  1. Code calls MembershipService::isMember(Contact $contact, Contact $target).
  2. The service queries memberships with status = active linking the contact to the target and, for each, delegates to the membership’s term plugin isActiveFor() (including grace periods).
  3. If no direct membership is found, the service also checks indirect memberships for the target (e.g. household memberships) and again uses the term plugin to see if the contact is active in any current period.
  4. If still not a member, the service dispatches the IS_MEMBER event; subscribers can call markAsMember() to mark the contact as a member.
  5. The method returns whether the contact is considered a member by any of the above.

Status field vs plugin activity

Memberships store a status field (active, future, expired). MembershipService queries status = active before calling term plugins. Plugin isActiveFor() can still return false for an “active” membership if periods have ended (including grace). See Status and periods.

Next steps

  • Entities — Membership, MembershipType, MembershipPeriod fields and links.
  • Status and periods — How status and current_periods are maintained.
  • Plugin system — Built-in term plugins and how to implement custom ones.
  • Services & API — Using MembershipService and events in code.