Membersactivities framework client application development guide
16 Sep 2026 - dirkvm
The MembersActivities Framework is an application framework for building web applications that manage members, activities, subscriptions, payments, tickets and related functionality.
It is built on top of the Controller Framework and provides reusable domain functionality while allowing each client application to define its own user interface, business rules, subscription validation, activity types, payment descriptions, authentication configuration, mail configuration and optional integrations.
Version 1.1.0 introduces persistent ticket entities and ticket scanning as first-class framework concepts.
The client application should normally extend and configure the framework rather than modify framework source code.
MembersActivities Framework 1.1.0 introduces several changes compared with version 1.0.31.
Tickets are now represented by individual Ticket domain objects and stored in the ticket database table.
Previously, a subscription's quantity represented the number of tickets. In version 1.1.0, each ticket receives its own persistent identity and token.
This makes it possible to individually identify, validate, cancel and scan tickets.
The new TicketScan model records ticket scan information and supports scan results such as:
valid
already_used
cancelled
invalid
wrong_activity
The framework also provides an atomic ticket-claim operation so that two scanners cannot successfully use the same ticket at the same time.
Version 1.1.0 adds a dedicated ticket_scan AccessToken for an activity.
$token = \controllerframework\security\AccessToken::generate(
'ticket_scan',
(string) $activityId
);
This token can be used by a client ticket scanner to protect ticket-scanning operations for a specific activity.
When an activity uses a seat map, the framework now checks whether requested seats have been reserved between the time the seat map was displayed and the time the subscription is submitted.
This reduces the possibility of two users purchasing the same seat concurrently.
Google Wallet integration now uses persistent ticket records rather than generating ticket numbers from the subscription quantity.
Each Google Wallet object therefore corresponds to an actual ticket entity.
The administrator payment edit command no longer updates the payment status through the generic update operation. Payment status transitions should be handled through the dedicated payment status mechanism, such as statusReceived.
Version 1.1.0 adds:
ticket table;ticket_scan table;expires_at in remember_tokens;subject, body and recipient fields in mail_queue.| Component | Requirement |
|---|---|
| PHP | 8.3 or compatible PHP 8.x |
| Controller Framework | 1.0.31 |
| MembersActivities Framework | 1.1.0 |
| Database | MySQL / MariaDB |
| Database access | PDO |
| Web server | Apache or compatible PHP web server |
| Composer | Required |
The framework uses a MySQL/MariaDB PDO connection. Oracle is not a supported database platform for this release.
A client application installs the framework through Composer:
{
"require": {
"samoscon/membersactivities-framework": "^1.1.0"
}
}
MembersActivities Framework 1.1.0 requires:
samoscon/controller-framework ^1.0.31
Composer installs that dependency automatically.
composer install
Use composer install for deployments based on a committed composer.lock.
application/
│
├── index.php
├── composer.json
├── composer.lock
│
├── config/
│ └── app_options.ini
│
├── MVCFramework/
│ ├── controls.xml
│ ├── commands/
│ │ ├── DefaultCommand.php
│ │ ├── admin/
│ │ ├── user/
│ │ ├── ajax/
│ │ ├── downloads/
│ │ └── mollie/
│ ├── model/
│ └── views/
│
└── assets/
For ticket-enabled applications, client-specific ticket implementations may be added to the model directory:
model/
├── Ticket.php
├── Ticket_RGLR.php
├── TicketMapper.php
├── TicketScan.php
├── TicketScan_RGLR.php
└── TicketScanMapper.php
The exact directory structure may be adapted, but framework classes should remain in the Composer package and client-specific classes should remain in the client application.
Configuration contains application-specific values such as environment, paths, database credentials, mail settings and security secrets.
[config]
environment=development
templatepath=/MVCFramework/views
controlsfile=/MVCFramework/controls.xml
loggingpath=/assets/logging/
[globals]
APP=MyApplication
_APPDIR=https://www.example.com/
_HOMEPAGE=https://www.example.com/
_ASSETDIR=https://www.example.com/assets/
_SALTRAND=...
_RAND=...
_DBUSER=...
_DBPASSWORD="..."
_DBNAME=...
_DBHOST=localhost
_SALTRAND must remain secret. The configuration file must not be publicly downloadable.
_SALTRAND is used by the Controller Framework's AccessToken implementation. It should be a long, cryptographically random secret.
The reference schema contains the following principal tables:
member
activity
costitem
subscription
payment
ticket
ticket_scan
remember_tokens
mail_queue
The ticket table contains the persistent identity and state of each ticket.
| Field | Purpose |
|---|---|
id | Unique ticket identifier. |
description | Ticket description. |
classification | Ticket type, defaulting to RGLR. |
subscription_id | Subscription to which the ticket belongs. |
token | Unique ticket token used for ticket identification. |
status | Ticket state: valid or cancelled. |
used | Indicates whether the ticket has already been claimed. |
used_at | Date and time at which the ticket was claimed. |
created_at | Ticket creation timestamp. |
The ticket_scan table records scans associated with a ticket.
| Field | Purpose |
|---|---|
id | Unique scan identifier. |
description | Scan description. |
classification | Scanner type, defaulting to RGLR. |
ticket_id | Ticket that was scanned. |
scanner | Identifier of the scanner or scanning application. |
scanned_at | Scan timestamp. |
result | Result of the scan. |
remember_tokens.expires_at is now nullable:
expires_at DATETIME DEFAULT NULL
A client application should document how a null expiration value is interpreted by its authentication policy.
The following fields are nullable:
subject
body
recipient
This allows mail queue records to represent processing states where these values are not yet available or are supplied by another part of the mail-processing workflow.
Commands return statuses such as CMD_DEFAULT, CMD_OK, CMD_CONTINUE and CMD_ERROR. Routing and rendering are defined through the application's controller configuration.
A client command normally extends:
\controllerframework\controllers\Command
Example:
namespace commands\user;
class MyCommand extends \controllerframework\controllers\Command
{
public function doExecute(
\controllerframework\registry\Request $request
): int {
// Application logic
return self::CMD_DEFAULT;
}
protected function getLevelOfLoginRequired(): void
{
$this->setLoginLevel(
new \controllerframework\sessions\NoLoginRequired()
);
}
}
A command should obtain and validate request data, perform the required operation, prepare response data and return an appropriate command status.
The CommandDecorator is one of the most important extension mechanisms. It allows client-specific behaviour to be added around a reusable MembersActivities command.
class PublicActivityCommand
extends \controllerframework\controllers\CommandDecorator
{
public function doExecuteDecorator(
\controllerframework\registry\Request $request
): ?int {
$request->set(
'validator',
new \model\SubscriptionValidationPublic()
);
return null;
}
public function initCommand(): void
{
$this->setCommand(
new \membersactivities\commands\user\ActivityCommand()
);
}
protected function getLevelOfLoginRequired(): void
{
$this->setLoginLevel(
new \controllerframework\sessions\NoLoginRequired()
);
}
}
This pattern avoids modifying generic framework commands for every possible client application's business rule.
Subscription validation uses SubscriptionValidationStrategy. A client application provides a concrete strategy containing its own business rules.
class SubscriptionValidationUser
extends \membersactivities\model\subscriptions\
SubscriptionValidationStrategy
{
protected function doCheckSubscription(
\controllerframework\members\Member $member,
\membersactivities\model\activities\Costitem $costitem
): array {
if (!$member->active) {
return $this->errorcode(
200,
'Your membership is inactive.'
);
}
return $this->errorcode(0);
}
}
The framework can then call the strategy before creating a subscription.
CommandDecorator to select the concrete strategy.
The Controller Framework's Request object transports information through the command chain.
$request->get('id');
$request->set('validator', $validator);
$request->addFeedback('Something went wrong.');
$request->set(
'forwardqueryparams',
['id' => $id]
);
The Request object is a transport mechanism, not a trust boundary. Data originating from a browser must still be validated.
State-changing requests should use POST and CSRF protection.
// Generate token
$responses['csrf_token'] = $this->getCsrfToken();
// Validate token
if (!$this->validateCsrfToken($request)) {
$request->set('errorcode', 'InvalidCsrfToken');
return self::CMD_ERROR;
}
Do not implement state-changing operations through GET requests.
CSRF protection requires an active session. Production applications should use secure session cookies with appropriate Secure, HttpOnly and SameSite settings and should use HTTPS.
Commands define the required authentication level. Typical levels are:
new \controllerframework\sessions\NoLoginRequired()
new \controllerframework\sessions\UserLogin()
new \controllerframework\sessions\AdminLogin()
Administrative commands that expose or modify sensitive data should normally require AdminLogin.
Controller Framework 1.0.31 provides \controllerframework\security\AccessToken for protected public operations.
$accessToken =
\controllerframework\security\AccessToken::generate(
'mollie-order',
(string) $orderid
);
\controllerframework\security\AccessToken::validate(
'mollie-order',
(string) $orderid,
$accessToken
);
MembersActivities Framework 1.1.0 introduces a dedicated purpose for ticket scanning:
$ticketScanToken =
\controllerframework\security\AccessToken::generate(
'ticket_scan',
(string) $activityId
);
The generated token is returned by the activity administration command as:
ticket_scantoken
The token identifies the activity for which ticket scanning is authorized.
Payment amounts must come from trusted server-side data. A browser must never be allowed to determine the amount sent to Mollie.
The payment object should be loaded from the database and its server-side amount used when creating the payment.
In version 1.1.0, administrator payment editing should not use the generic update operation to change payment status. Payment status changes should go through the dedicated payment status mechanism.
Mollie integration is optional. Relevant commands include:
PaymentToMollieCommand
OrderToMollieCommand
WebhookFromMollieCommand
PaymentConfirmationCommand
The generic payment command can be decorated by the client application.
The payment amount is obtained from the server-side Payment object. The order description can be supplied by the client-specific CommandDecorator through the Request:
$request->set(
'orderDescription',
'Concert tickets - May 2027'
);
The underlying OrderToMollieCommand reads this value. This allows different order types to have different descriptions without modifying the generic framework command.
The webhook is a machine-to-machine endpoint rather than a normal user interface. It should retrieve the current payment status from Mollie and update the corresponding server-side payment.
The customer redirect is not the authoritative source of payment status.
Payment confirmation should validate the protected order identifier and its AccessToken before displaying information associated with a payment.
Applications using tickets should create or update ticket state based on the confirmed server-side payment state rather than trusting client-side payment information.
Core domain models include:
Activity
ActivityComposite
Costitem
Member
MemberComposite
Subscription
Payment
Ticket
TicketScan
GoogleWalletTicket
The client application should use the model API where possible instead of directly manipulating database records.
The MembersActivities Framework uses the Composite design pattern for two important domain concepts: Activities and Members.
This allows a domain object to represent either an individual object or a parent containing child objects, while exposing a consistent object-oriented model to the client application.
An Activity can have child activities. This is represented by the ActivityComposite concept.
A parent activity can therefore act as a container for one or more child activities without requiring the client application to handle parent and child activities as fundamentally different objects.
This is useful when an application wants to group related activities under a common parent.
For example, an application could model a concert series as a parent activity with individual concerts as child activities:
The exact interpretation of the parent-child relationship is application-specific. The framework provides the object model and persistence mechanism; the client application defines how the hierarchy is presented and used.
The same Composite principle is also used for Member.
A member can have child members. This allows a client application to represent relationships where one member acts as a parent of one or more other members.
This can, for example, be used to model a household or another application-specific member hierarchy.
The child members remain normal Member objects. The parent-child relationship does not require the client application to introduce a separate domain concept for every level of the hierarchy.
The Activity and Member hierarchies follow the same object-oriented principle:
| Concept | Parent | Children |
|---|---|---|
| Activity | Parent Activity | Child Activities |
| Member | Parent Member | Child Members |
Conceptually, both can be represented as:
This is an application of the Composite Object-Oriented Design Pattern: a parent object and its child objects belong to the same domain type and can therefore be treated consistently by application code.
Client applications should use the framework's model relationships rather than reproducing parent-child logic in controllers or views.
For Activities, this means that application code should take the ActivityComposite relationship into account when displaying or processing activities.
For Members, the same principle applies to the Member hierarchy.
The hierarchy can be visualized as:
Because the relationship is recursive, more than one level of nesting can be represented where the application's business rules require it.
Version 1.1.0 introduces Ticket as a first-class domain object.
The framework provides:
\membersactivities\model\subscriptions\Ticket
\membersactivities\model\subscriptions\TicketMapper
\membersactivities\model\subscriptions\TicketTypeImplementation
These classes are framework components. A client application can provide concrete implementations where application-specific ticket behaviour is required.
The database represents the active state using status and used.
| State | Meaning |
|---|---|
status = valid, used = 0 | Ticket can be used. |
status = valid, used = 1 | Ticket has already been used. |
status = cancelled | Ticket has been cancelled and must not be accepted. |
Every ticket has a unique token stored in the token field.
The framework provides:
$ticket = \model\Ticket::findByToken($token);
This allows a scanner to identify a ticket without exposing or relying on internal database IDs.
The framework provides:
$success = $ticket->claim();
The operation is atomic. Internally it only changes the ticket when:
status = 'valid'
AND
used = 0
This is important for ticket scanning because two scanners may attempt to validate the same ticket almost simultaneously.
Only the scanner that successfully performs the atomic claim should treat the ticket as newly accepted.
TicketTypeImplementation provides an extension point for application-specific ticket types.
The framework uses the ticket classification field to select the corresponding implementation.
The default classification is:
RGLR
A client application may therefore provide a corresponding regular ticket implementation where required.
Version 1.1.0 introduces the TicketScan domain model.
The framework provides:
\membersactivities\model\subscriptions\TicketScan
\membersactivities\model\subscriptions\TicketScanMapper
\membersactivities\model\subscriptions\TicketScanTypeImplementation
As with Ticket, these are framework components that can be specialized by the client application.
The database supports the following scan results:
valid
already_used
cancelled
invalid
wrong_activity
The client scanner should distinguish between these results so that the operator can understand why a ticket was accepted or rejected.
The exact user interface is application-specific, but the server should remain authoritative for ticket validation.
A client application can create a TicketScan record containing information such as:
ticket_id
scanner
result
scanned_at
This provides an audit trail of ticket scanning activity.
TicketScanTypeImplementation provides an extension point for different scanner implementations.
The default classification is:
RGLR
A client application may therefore provide a corresponding regular scan implementation where required.
Version 1.1.0 adds server-side conflict detection for seat reservations.
When an activity uses a seat map, the framework checks requested seats against reservations that already exist for the relevant cost item.
This check is important because the seat map shown to the user may become outdated while another user is completing a purchase.
The framework uses mapper classes such as ActivityMapper, CostitemMapper, PaymentMapper, SubscriptionMapper, TicketMapper and TicketScanMapper.
For example:
$payment = \model\Payment::find($id);
$ticket = \model\Ticket::findByToken($token);
TicketMapper provides operations including:
claimTicket(int $ticketId): bool
findByToken(string $token): ?\model\Ticket
claimTicket() uses a database update with conditions on both the ticket status and used flag. This provides atomic protection against simultaneous scans.
Special care is required with Mapper::findAll(), because it accepts a free SQL select clause.
$mapper->findAll($selectClause);
Views are responsible for presentation. Commands should prepare data and pass it to the view.
Command routing is defined in controls.xml. A simplified example:
<command
path="/activity"
class="\commands\user\PublicActivityCommand">
<view name="/user/activity" />
<status value="CMD_ERROR">
<view name="/errorView" />
</status>
</command>
Use the Controller Framework's centralized error handling for unexpected exceptions.
try {
$activity = \model\Activity::find($id);
}
catch (\Throwable $ex) {
\controllerframework\error\ErrorHandler
::handleException($ex);
$request->addFeedback(
'Unable to retrieve the requested activity.'
);
return self::CMD_ERROR;
}
Do not expose stack traces or exception dumps to users. Most importantly, do not catch an exception and then continue as if the failed operation had succeeded.
Take special care with values used in:
Location headers;Content-Disposition;Validate or whitelist such values before placing them in HTTP headers. When constructing query strings, prefer http_build_query() to manual concatenation.
The framework supports asynchronous mail processing through the mail_queue table. Typical statuses include:
pending
sending
sent
failed
A typical workflow is:
Cron jobs should use absolute paths:
php /absolute/path/to/application/index.php cleanupMailQueue
Do not rely on the current working directory in cron jobs.
Google Wallet integration is optional. Applications using it require appropriate Google credentials, an issuer ID and application configuration.
Version 1.1.0 changes the relationship between subscriptions and Google Wallet tickets.
Previously, the framework generated Google Wallet ticket objects based on the subscription quantity.
The framework now obtains the actual ticket records and creates a Wallet object for each persistent ticket.
The ticket ID is used as the ticket number/object identifier.
The framework provides extension points such as:
ActivityTypeImplementation
CostitemTypeImplementation
PaymentTypeImplementation
SubscriptionTypeImplementation
TicketTypeImplementation
TicketScanTypeImplementation
Client-specific business rules should be implemented in these extension points where appropriate rather than accumulating all business logic in controllers.
Add the route to controls.xml.
namespace commands\user;
class MyActivityCommand
extends \controllerframework\controllers\CommandDecorator
{
public function doExecuteDecorator(
\controllerframework\registry\Request $request
): ?int {
// Application-specific preparation
return null;
}
public function initCommand(): void
{
$this->setCommand(
new \membersactivities\commands\user\ActivityCommand()
);
}
protected function getLevelOfLoginRequired(): void
{
$this->setLoginLevel(
new \controllerframework\sessions\UserLogin()
);
}
}
$request->set(
'validator',
new \model\SubscriptionValidationUser()
);
Create the appropriate template.
<status value="CMD_ERROR">
<view name="/errorView" />
</status>
Test authenticated and unauthenticated users, invalid identifiers, invalid POST data, invalid CSRF tokens and expected failure paths.
Version 1.1.0 introduces database and application-level changes. Existing applications should therefore be upgraded in a controlled sequence.
{
"require": {
"samoscon/membersactivities-framework": "^1.1.0"
}
}
The Controller Framework remains at version 1.0.31.
Apply the new definitions from Basic Example guide -> DatabaseSetup.sql for:
ticket;ticket_scan;remember_tokens.expires_at;Existing subscriptions previously represented multiple tickets through the quantity field.
For applications that use tickets, each relevant subscription should now have corresponding ticket records.
The migration process must generate unique ticket tokens and preserve any application-specific information required by the client.
Applications using the ticket functionality should provide concrete implementations of the new framework models and their mappers where required.
Applications using Google Wallet should verify that existing subscriptions have corresponding ticket records before generating new Wallet objects.
Applications introducing ticket scanning should:
ticket_scan token;ticket table createdticket_scan table createdapp_options.ini protected_SALTRAND generated and secretMembersActivities Framework 1.1.0 is intended to be used as a reusable domain framework rather than as a complete application.
Both Activity and Member can participate in recursive parent-child relationships following the Composite design pattern.
The ticket model introduces another important domain relationship: a subscription can contain multiple persistent tickets, and each ticket can have multiple scan records.
The key development principle remains:
Version 1.1.0 extends this principle to tickets and ticket scanning while retaining the existing Composite-based domain model for Activities and Members.
This makes client applications easier to maintain and allows framework upgrades without unnecessarily modifying application code.