Membersactivities framework basic example
12 Sep 2026 -
layout: post author: dirkvm ————–
<!DOCTYPE html>
The example directory included in MembersActivities Framework 1.1.0
is a complete starting-point client application. It demonstrates how the framework
is intended to be integrated rather than merely showing isolated API calls.
The example contains:
The complete example application is available as a ZIP archive, including the folder structure and all underlying example files: Download the Example Folder Structure and Files (ZIP).
The database setup is provided separately and is not included in the ZIP archive: Download DatabaseSetup.sql .
The exact contents may evolve as the example application grows, but the important framework-specific structure is:
example/
│
├── index.php
├── composer.json
├── DatabaseSetup.sql
├── .htaccess
│
├── config/
│ ├── app_options.ini
│ └── [Your google wallet keyfile].json
│
├── MVCFramework/
│ ├── controls.xml
│ │
│ ├── commands/
│ │ ├── DefaultCommand.php
│ │ ├── admin/
│ │ ├── ajax/
│ │ ├── cron/
│ │ ├── downloads/
│ │ ├── mollie/
│ │ └── user/
│ │
│ ├── model/
│ │ ├── Activity.php
│ │ ├── ActivityMapper.php
│ │ ├── Activity_RGLR.php
│ │ ├── Activity_STMP.php
│ │ ├── Costitem.php
│ │ ├── CostitemMapper.php
│ │ ├── Member.php
│ │ ├── MemberMapper.php
│ │ ├── Member_RGLR.php
│ │ ├── Member_PRTN.php
│ │ ├── Payment.php
│ │ ├── PaymentMapper.php
│ │ ├── Payment_RGLR.php
│ │ ├── Payment_YRLY.php
│ │ ├── Subscription.php
│ │ ├── SubscriptionMapper.php
│ │ ├── Subscription_RGLR.php
│ │ ├── Ticket.php
│ │ ├── TicketMapper.php
│ │ ├── Ticket_RGLR.php
│ │ ├── TicketScan.php
│ │ ├── TicketScanMapper.php
│ │ ├── TicketScan_RGLR.php
│ │ └── SubscriptionValidation*.php
│ │
│ └── views/
│ ├── defaultView.php
│ ├── errorView.php
│ ├── admin/
│ ├── login/
│ └── user/
│
└── assets/
├── logging/
├── qrcodes/
└── ...
Before running the example, install:
The example requires MembersActivities Framework 1.1.0 or later. The framework in turn uses Controller Framework 1.0.31.
example directory to a working application directory.
MVCFramework/.
A suitable starting point is to keep the example structure intact until the application is understood, then progressively replace the example's client-specific code.
The example's composer.json should require MembersActivities Framework
1.1.0 or a compatible later release.
{
"require": {
"setasign/fpdf": "^1.8",
"blueimp/jquery-file-upload": "9.22.*",
"tinymce/tinymce": "^8.0",
"chillerlan/php-qrcode": "*",
"samoscon/membersactivities-framework": "^1.1.0",
"google/auth": "^1.53",
"guzzlehttp/guzzle": "^7.10",
"google/apiclient": "^2.19",
"google/apiclient-services": "~0.350"
}
}
Run:
composer install
The result is a vendor/ directory containing the framework and
third-party dependencies.
The example provides DatabaseSetup.sql. Import it into a new
MySQL/MariaDB database.
CREATE DATABASE your_database
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
Then import the supplied SQL script using your preferred database administration tool.
The 1.1.0 database model includes the persistent ticketing model in addition to the existing activity, member, subscription and payment data.
After importing DatabaseSetup.sql, the database contains the required
tables, but the application still needs at least one member account that can
log in as an administrator.
This is especially important for the example because the default configuration
uses _MINLEVELTOLOGIN='A', which means that the login environment
requires an administrator account.
php -r "echo password_hash('Demo123', PASSWORD_DEFAULT), PHP_EOL;"
Copy the generated hash and use it in the password field of the
first member.
member table.
INSERT INTO member
(classification, name, lastname, email, role, password, ownpwd, active)
VALUES
(
'RGLR',
'Demo',
'Administrator',
'admin@example.com',
'A',
'[PASTE THE PASSWORD HASH HERE]',
1,
1
);
| Field | Value | Purpose |
|---|---|---|
classification |
RGLR |
Uses the regular member type implementation supplied by the example. |
role |
A |
Identifies the member as an administrator for the administrator login strategy. |
password |
PHP password_hash() result |
Stores the password as a secure password hash rather than plain text. |
ownpwd |
1 |
Indicates that the member has their own password. |
active |
1 |
Activates the member account. |
Demo123 is only an example password. Generate your own password
hash and use a strong, unique password for any real installation.
Never store the plain-text password in the database.
Edit config/app_options.ini.
_APPDIR=https://[(sub)domeinnaam]/
_HOMEPAGE=https://[your client homepage]
_ASSETDIR=https://[(sub)domeinnaam]/assets/
_SALTRAND=[long random secret]
_RAND=[random integer]
_DBUSER=[database username]
_DBPASSWORD="[database password]"
_DBNAME=[database name]
_DBHOST=localhost
APP=BM_test
_LOGO=assets/[Your logo].jpg
Replace these with the identity of your own application.
_MINLEVELTOLOGIN='A'
The example uses A to indicate an administrator-only login
environment. The alternative U allows administrators and members
to log in as users.
_WTALLOWED='Y'
This controls whether wire transfer is available as an alternative to online payment.
_MOLLIECONFIG="test_[your mollie key]"
Use a test key during development and a live key only in a properly secured production configuration.
_WALLETORIGIN=...
_WALLETCREDENTIALS=config/[your keyfile].json
_WALLETISSUERID=...
Google Wallet is optional. If it is not used, the corresponding configuration can remain disabled or empty according to the application's conventions.
_SALTRAND is used as secret material for AccessToken generation.
It must be a long, unpredictable secret and must not be exposed publicly.
The example's entry point is deliberately small:
<?php
include __DIR__ . '/vendor/autoload.php';
spl_autoload_register(function ($class_name) {
if (preg_match('/\\\\/', $class_name)) {
$class_name = str_replace('\\', DIRECTORY_SEPARATOR, $class_name);
}
$file = __DIR__ . DIRECTORY_SEPARATOR .
'MVCFramework' . DIRECTORY_SEPARATOR .
$class_name . '.php';
if (file_exists($file)) {
require_once $file;
}
});
try {
controllerframework\controllers\Controller::run();
} catch (\Throwable $e) {
http_response_code(500);
echo '<h1>Application initialization failed</h1>';
echo '<p>' .
htmlspecialchars(
$e->getMessage(),
ENT_QUOTES,
'UTF-8'
) .
'</p>';
exit;
}
The sequence is:
The example automatically maps namespaced client classes below
MVCFramework/.
For example:
\model\Activity_RGLR
maps to:
MVCFramework/model/Activity_RGLR.php
and:
\commands\user\PublicActivityCommand
maps to:
MVCFramework/commands/user/PublicActivityCommand.php
This makes it possible for the client application to provide concrete classes expected by the abstract framework classes.
MVCFramework/controls.xml connects URL paths to commands and views.
The root route is:
<command path="/" class="\commands\DefaultCommand">
<view name="/defaultView" />
...
</command>
The activity route is:
<command path="/activity"
class="\commands\user\PublicActivityCommand">
<view name="/user/activity" />
...
</command>
The payment flow contains:
The example's DefaultCommand is a client CommandDecorator around
the framework's default command.
class DefaultCommand
extends \controllerframework\controllers\CommandDecorator
{
public function doExecuteDecorator(
\controllerframework\registry\Request $request
): ?int {
$this->addResponses($request, [
'title' => 'Inloggen'
]);
return null;
}
public function initCommand(): void {
$this->setCommand(
new \membersactivities\commands\DefaultCommand()
);
}
protected function getLevelOfLoginRequired(): void {
$this->setLoginLevel(
new \controllerframework\sessions\NoLoginRequired()
);
}
}
The client changes only what it needs: here, the page title.
The example supplies concrete client classes for the abstract MembersActivities models.
namespace model;
class Activity
extends \membersactivities\model\activities\Activity
{
// client-specific code
}
namespace model;
class Costitem
extends \membersactivities\model\activities\Costitem
{
// client-specific code
}
namespace model;
class Member
extends \membersactivities\model\members\Member
{
// client-specific code
}
Member supports the same parent-child Composite principle as Activity. A Member can therefore have child Members through the member hierarchy.
namespace model;
class Subscription
extends \membersactivities\model\subscriptions\Subscription
{
// client-specific code
}
namespace model;
class Payment
extends \membersactivities\model\subscriptions\Payment
{
// client-specific code
}
namespace model;
class Ticket
extends \membersactivities\model\subscriptions\Ticket
{
// client-specific code
}
namespace model;
class TicketScan
extends \membersactivities\model\subscriptions\TicketScan
{
// client-specific code
}
Most of these classes are intentionally almost empty. Their existence allows the framework's abstract model architecture to be specialized by the client.
The example demonstrates classification-specific type implementations.
class Activity_RGLR
extends \membersactivities\model\activities\ActivityTypeImplementation
{
}
This represents a normal activity without additional rules.
class Activity_STMP
extends \membersactivities\model\activities\ActivityTypeImplementation
{
public function seatmap(): bool {
return true;
}
}
This type tells the activity command/view that a seat map should be used.
class Costitem_RGLR
extends \membersactivities\model\activities\CostitemTypeImplementation
{
}
The example contains:
Member_RGLR
Member_PRTN
These extend MemberTypeImplementation and demonstrate how
client-specific member rules can be added.
For example, Member_RGLR implements a yearly participation fee,
while Member_PRTN contains partner-specific logic.
Ticket_RGLR
TicketScan_RGLR
These provide the classification-specific implementation for tickets and
ticket scans. The normal classification is RGLR.
The example contains three strategies:
SubscriptionValidationPublic
SubscriptionValidationUser
SubscriptionValidationAdmin
The user strategy checks:
if($subscribableitem->activity->subscriptionPeriodOver()) {
return $this->errorcode(
100,
'Inschrijving of annuleren is jammer genoeg niet langer mogelijk.'
);
}
if(!$member->active) {
return $this->errorcode(
200,
'Je lidmaatschap is gedeactiveerd.'
);
}
if($member->subscriptionuntil <
$subscribableitem->activity->date) {
return $this->errorcode(
201,
'Gelieve eerst je lidgeld voor volgend jaar te betalen'
);
}
return $this->errorcode(0);
The admin strategy extends the user strategy and deliberately applies different rules.
The example's PublicActivityCommand illustrates the recommended
extension pattern.
class PublicActivityCommand
extends \controllerframework\controllers\CommandDecorator
{
public function doExecuteDecorator(
\controllerframework\registry\Request $request
): ?int {
$id = filter_var(
$request->get('id'),
FILTER_VALIDATE_INT
);
if(!$id) {
$request->set('errorcode', 'wrongID');
$request->addFeedback("Wrong ID");
return self::CMD_ERROR;
}
$activity = \model\Activity::find($id);
$seatmap =
$activity->activitytypeimplementation->seatmap();
$this->addResponses($request, [
'seatmap' => $seatmap
]);
return null;
}
public function initCommand(): void {
$this->setCommand(
new \membersactivities\commands\user\PublicActivityCommand
);
}
}
The decorator performs client-specific work and then delegates the generic activity operation to the MembersActivities command.
The route is:
/activity?id=123
The client decorator validates the ID, retrieves the activity and determines whether the activity's type requires a seat map.
The generic framework command then prepares the activity, member and cost-item information for the view.
The view renders activity information, cost items and, when enabled, the seat-map interface.
A seat map is a user-interface mechanism for selecting seats. It is not the authoritative source for determining whether a seat is still available.
The application must perform the final availability check on the server immediately before the subscription and ticket data is persisted.
For applications with concurrent reservations, the check and persistence should be handled as one consistent transaction or through equivalent database-level protection.
The example uses a separate CreatePaymentCommand decorator.
It currently delegates without adding extra logic.
public function doExecuteDecorator(
\controllerframework\registry\Request $request
): ?int {
return null;
}
The wrapped framework command performs the generic payment creation.
This is a useful template: client code can remain empty until application-specific behaviour is actually required.
The example's PaymentToMollieCommand extends the framework
Mollie command.
class PaymentToMollieCommand
extends \membersactivities\commands\mollie\PaymentToMollieCommand
{
public function doExecuteDecorator(
\controllerframework\registry\Request $request
): ?int {
$id = filter_var(
$request->get('id'),
FILTER_VALIDATE_INT
);
$request->set(
'paymentConfirmation',
'paymentConfirmation'
);
$request->set(
'orderDescription',
APP . " orderid=" . $id
);
return null;
}
}
This demonstrates an important separation:
The order description is therefore client-specific and travels through the
Request. The payment amount must remain server-side.
The example's Payment_RGLR implements
statusReceived().
When the payment status becomes paid, the client-specific
implementation can:
statusReceived(), rather than treating payment status
as an unrestricted generic property update.
MembersActivities 1.1.0 introduces a persistent Ticket domain model. A subscription can therefore result in individual ticket records rather than relying only on a subscription quantity.
A Ticket contains information such as:
| Property | Purpose |
|---|---|
id | Unique ticket identifier. |
description | Domain description. |
classification | Ticket type classification, normally RGLR. |
subscription_id | Subscription from which the ticket originated. |
token | Unique ticket token used for ticket identification. |
status | Current ticket status. |
used | Indicates whether the ticket has already been used. |
used_at | Timestamp of ticket usage. |
created_at | Creation timestamp. |
Tickets should normally be created when the payment has reached the appropriate successful state. The exact business rule remains client-specific.
This provides a stable domain object for ticket PDFs, QR codes, scanning, Google Wallet and later ticket-related operations.
The example supports ticket scanning through the persistent
Ticket and TicketScan models.
| Property | Purpose |
|---|---|
id | Unique scan record. |
ticket_id | Ticket that was scanned. |
scanner | Scanner or scanner identity. |
scanned_at | Timestamp of the scan. |
result | Result of the scan. |
Typical result values are:
valid
already_used
cancelled
invalid
wrong_activity
The scan operation must be protected against two scanners accepting the same ticket simultaneously.
A conceptual database operation is:
UPDATE ticket
SET
used = 1,
used_at = CURRENT_TIMESTAMP
WHERE
id = :ticket_id
AND status = 'valid'
AND used = 0;
The application should verify that exactly one row was changed before considering the ticket accepted.
Google Wallet integration in 1.1.0 is based on persistent Ticket records.
The application should therefore not derive the number of Wallet tickets
solely from the historical subscription.quantity field.
Each Wallet pass can therefore correspond to an actual Ticket domain object with its own identifier and token.
The Wallet integration remains optional and can be disabled for applications that do not use it.
MembersActivities 1.1.0 is designed to be used together with the security features of Controller Framework 1.0.31.
State-changing requests should use POST and CSRF protection. Client commands can obtain and validate the CSRF token through the Controller Framework command support.
Typical operations include:
AccessToken is intended for protected URLs and flows where a normal authenticated session is not the appropriate mechanism.
Examples include:
AccessToken::generate(
'mollie-order',
(string) $paymentId
);
AccessToken::generate(
'ticket_scan',
(string) $activityId
);
The purpose and identifier are part of the token generation process.
The secret salt is configured through _SALTRAND.
The amount submitted to Mollie must be derived from the server-side Payment object. It must not be trusted from a client request.
Commands exposing administrative data or performing administrative changes
should use the AdminLogin strategy.
Client request data must not be allowed to select arbitrary PHP classes. Use controlled type implementations and CommandDecorators instead.
The example contains administrative commands for:
A typical client decorator explicitly requires administrator login:
protected function getLevelOfLoginRequired(): void {
$this->setLoginLevel(
new \controllerframework\sessions\AdminLogin()
);
}
This should be the normal pattern for commands exposing administrative data or state-changing administrative operations.
For a local PHP development environment, the application's public entry point
should be index.php. Depending on the Controller Framework's
routing requirements and your web-server setup, configure the document root
and rewrite rules according to the supplied .htaccess.
Open the configured application homepage:
https://your-domain.example/
The root command forwards to the configured login/activity/admin flow depending on the returned command status.
https://your-domain.example/activity?id=1
Verify that the activity, cost items and appropriate seat-map behaviour are displayed.
For a seat-map activity, select one or more seats and complete the reservation. Then test the same seat with a second browser or user session. The second reservation must be rejected when the seat has already been reserved.
Use the test Mollie configuration. Verify the complete sequence:
Generate or use a ticket containing its QR/token information and scan it with the scanner application.
Verify at least these cases:
Log in as an administrator and verify activity, cost item, member, payment, ticket and export management as applicable to the example.
The recommended approach is incremental.
Member, Activity, Costitem,
Subscription, Payment, Ticket and
TicketScan to your domain.
Activity_RGLR,
Ticket_RGLR or your own application-specific types.
When upgrading an existing application from MembersActivities 1.0.31 to 1.1.0, the application should be upgraded deliberately rather than treating the framework package update as the only required change.
"samoscon/membersactivities-framework": "^1.1.0"
Run:
composer update samoscon/membersactivities-framework
Apply the database changes required by the 1.1.0 schema, including the persistent ticket model and ticket scan model.
Where the existing application used subscription quantity to represent multiple tickets, determine how those existing subscriptions should be represented as individual Ticket records.
Applications using Google Wallet should adapt the Wallet creation logic to use actual persistent Ticket records.
Applications using seat maps should ensure that final seat availability is checked server-side before the reservation is persisted.
Review custom payment implementations and make sure that successful payment
processing uses the dedicated status mechanism such as
statusReceived().
Applications introducing ticket scanning should create the required
TicketScan records and protect scanner access with the appropriate
ticket_scan AccessToken flow.
| Area | Check |
|---|---|
| Composer | samoscon/membersactivities-framework is set to ^1.1.0. |
| Controller Framework | Controller Framework 1.0.31 is installed and compatible. |
| Configuration | Application, database and integration settings are replaced with client values. |
| Secrets | _SALTRAND, database passwords and API credentials are protected. |
| Database | Reference schema is imported only for a new application or migrated deliberately. |
| Models | Required client model classes exist, including Ticket and TicketScan where used. |
| Composite | Activity and Member parent-child relationships are handled correctly where required. |
| Type implementations | Every classification used by the database has a corresponding client type class. |
| Validation | Public/user/admin subscription rules are explicitly defined. |
| Routing | All required commands and views are registered in controls.xml. |
| Security | POST/CSRF, login levels and AccessToken flows are tested. |
| Payments | Payment amounts come from server-side Payment objects. |
| Mollie | Test webhook and payment confirmation flow works before live deployment. |
| Tickets | Successful payments create the required persistent Ticket records. |
| Ticket scanning | Ticket validation and atomic ticket usage are tested. |
| Seat map | Server-side seat conflict detection is tested. |
| Google Wallet | Wallet passes are generated from persistent Ticket records when enabled. |
| SMTP settings and mail queue processing are tested. | |
| Cron | Absolute paths are used. |
| Production | HTTPS, protected configuration and production credentials are configured. |