Controller framework client application developer guide

11 Sep 2026 - dirkvm


Controller Framework 1.0.31 — Client Application Developer Guide
Controller Framework 1.0.31

Client Application Developer Guide

A practical guide to building a PHP 8.3 application on top of the Controller Framework.

Audience: developers who build the application-specific commands, models, views, configuration and business logic on top of the framework.

1. Framework overview

The Controller Framework is an MVC-oriented PHP framework based on the Command, Registry, Strategy, Template Method and related design patterns. The framework supplies the application infrastructure; the client application supplies the domain model, application commands and views.

Browser HTTP request Controller Init + routing Command execution Rendering Command Business/application logic Domain model Member / DomainObject View / Renderer HTML / AJAX / download PDO MySQL / MariaDB Database application data

The normal Release 31 setup uses the Application Controller variant. Requests are mapped through controls.xml to a Command. The Command executes and places response data in the Request. A RenderComponent then turns that result into HTML, a redirect, JSON, a download, an agenda response or another response type.

2. Requirements and installation

2.1 Install with Composer

{
  "require": {
    "samoscon/controller-framework": "^1.0"
  }
}

Then run:

composer install

For a client application, the framework is normally installed below vendor/. The example application shipped with the framework can be used as a starting point.

2.2 Initial database

The example package contains example/DatabaseSetup.sql. It defines the standard member table and the tables required by the framework's remember-me and mail-queue facilities.

Important: the SQL dump is a starting point. Review database credentials, permissions, indexes, foreign keys and application-specific fields before using it in production.

3. Recommended application structure

The framework expects application-specific code to live outside the framework source tree. A typical application based on the supplied example looks like this:

project-root/
├── config/
│   └── app_options.ini
├── MVCFramework/
│   ├── controls.xml
│   ├── commands/
│   │   ├── DefaultCommand.php
│   │   └── admin/
│   ├── model/
│   │   ├── Member.php
│   │   ├── MemberMapper.php
│   │   └── Member_RGLR.php
│   └── views/
│       ├── defaultView.php
│       ├── errorView.php
│       ├── login/
│       └── admin/
├── assets/
├── vendor/
└── index.php

The exact directory names are configurable through app_options.ini, but the example follows this arrangement.

4. Application bootstrap

The application entry point can be very small. The essential operation is to load Composer's autoloader and run the framework Controller.

<?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;
    }
});

controllerframework\controllers\Controller::run();

The supplied example additionally catches Throwable during application initialization and displays a controlled message. In production, do not expose detailed exception information to users.

5. Configuration

The main configuration file is config/app_options.ini. Release 31 requires both a [config] section and a [globals] section.

5.1 Framework configuration

[config]
environment=development
templatepath=/MVCFramework/views
controlsfile=/MVCFramework/controls.xml
loggingpath=/assets/logging/
SettingPurpose
environmentControls development versus production exception presentation. Use production on a public production site.
templatepathBase path for application PHP views.
controlsfilePath to the XML command/render configuration.
loggingpathPath used by AuditTrace for its logfile.

5.2 Global application values

[globals]
APP=BM
_LOGO=assets/logo.jpg
_APPDIR=https://example.org/
_HOMEPAGE=https://example.org/
_ASSETDIR=https://example.org/assets/
_RAND=12345
_MINLEVELTOLOGIN='A'
_CONTROLSFILE='/MVCFramework/controls.xml'

_DBUSER=...
_DBPASSWORD="..."
_DBNAME=...
_DBHOST=localhost

_MAILHOST="mail.example.org"
_MAILHOSTPORT=587
_MAILUSERNAME="website@example.org"
_MAILPASSWORD="..."
_MAILTO=website@example.org
_MAILTONAME=Example Organisation
_MAILFROM=info@example.org
_MAILFROMNAME=Example Organisation
_MAILREPLYTO=team@example.org

Values in [globals] are defined as PHP constants by InitController, so application code can use constants such as APP, _DBNAME and _MAILFROM.

Protect the INI file. It contains database and SMTP credentials. It must not be downloadable through the web server.

5.3 Production settings

Set environment=production in production. The development example may enable display_errors; this must be disabled or removed in production.

6. Paths and controls.xml

In the Application Controller configuration, controls.xml is the central routing and rendering description. Each <command> connects a request path with a Command class and one or more renderers.

<command path="/admin"
         class="\commands\admin\AdminHomeCommand">

    <view name="/admin/adminhome" />

    <status value="CMD_OK">
        <forward path="/thenameofanotherpath" />
    </status>

    <status value="CMD_ERROR">
        <view name="/errorView" />
    </status>

</command>

6.1 Request lifecycle

HTTP Request path + input Request HttpRequest Command execute() Request state responses + status RenderComponent view / forward / data Command status selects the renderer; the renderer produces the final HTTP response.

The framework validates XML status names against the constants defined by Command. An unknown status causes initialization to fail.

7. Commands

A client Command is the main unit of application logic. It extends controllerframework\controllers\Command. The framework calls execute(); client code implements doExecute() and getLevelOfLoginRequired().

namespace commands\admin;

class AdminHomeCommand
    extends \controllerframework\controllers\Command
{
    public function doExecute(
        \controllerframework\registry\Request $request
    ): int {
        $request->set('title', 'Administration');
        return self::CMD_OK;
    }

    protected function getLevelOfLoginRequired(): void {
        $this->setLoginLevel(
            new \controllerframework\sessions\AdminLogin()
        );
    }
}

7.1 Command status constants

ConstantMeaning
CMD_DEFAULTDefault renderer selection.
CMD_OKNormal successful result.
CMD_ERRORApplication error result.
CMD_INSUFFICIENT_DATAInsufficient input/data.
CMD_ADMINAdministrator-specific result/forward.
CMD_CHANGE_PASSWORDPassword must be changed.
CMD_CONTINUEContinue processing according to application configuration.

7.2 Putting response data in the Request

$this->addResponses($request, [
    'title' => 'Member administration',
    'message' => 'Welcome'
]);

return self::CMD_OK;

The view can subsequently read these values through $request->get('title') and $request->get('message').

7.3 Command decorators

CommandDecorator lets an application command wrap another Command. The decorator can add application-specific behavior before/after delegating to another command. The supplied example uses this pattern to adapt framework functionality supplied by another application framework.

8. Request object

The framework abstracts HTTP and CLI requests behind controllerframework\registry\Request. For web requests, HttpRequest is used; for CLI execution, CliRequest is selected.

MethodPurpose
getPath()Returns the current application path.
setPath($path)Sets the request path.
get($key)Reads a value from request state.
set($key, $value)Stores a value in request state.
addFeedback($msg)Adds a feedback message.
getFeedback()Returns feedback messages.
getFeedbackString()Returns feedback as a string.
setCmdStatus($status)Stores the command result status.
getCmdStatus()Returns the command result status.
forward($path)Performs the framework's HTTP/CLI forward operation.
Important distinction: request state is not automatically a trust boundary. Values that originate from GET, POST, cookies or other client-controlled input must be validated before being used for SQL, headers, redirects, file operations or security decisions. A value being stored in the framework's Request object does not make it trusted.

9. Rendering

The framework separates command execution from response rendering through the RenderComponent interface.

9.1 ViewRenderComponent

A view renderer includes a PHP view file. The configured templatepath is combined with the view name from controls.xml.

<view name="/admin/adminhome" />

With the example configuration, this corresponds to:

MVCFramework/views/admin/adminhome.php

9.2 ForwardRenderComponent

For a redirect/forward:

<status value="CMD_OK">
    <forward path="/activity" />
</status>

If query parameters are required, put a named array into the request as forwardqueryparams. Release 31 builds the query string with http_build_query().

$request->set('forwardqueryparams', [
    'id' => $member->getId(),
    'mode' => 'edit'
]);

10. Data-request renderers

The framework includes generic renderers for non-HTML responses.

RendererRequest valuesOutput
AjaxRenderComponentresults as an arrayJSON
DownloadRenderComponentfilename, columnNames, resultsCSV download
AgendaRenderComponentfilename, resultsiCalendar response
MollieRenderComponentresultsHTTP Location redirect

10.1 AJAX example

<command path="/api/memberlist"
         class="\commands\MemberListCommand">
    <datarequest type="Ajax" />
</command>

The command should put an array into results and return the configured status.

10.2 CSV download example

$request->set('filename', 'members.csv');
$request->set('columnNames', 'id;name;email');
$request->set('results', [
    [1, 'Alice', 'alice@example.org'],
    [2, 'Bob', 'bob@example.org']
]);

return self::CMD_OK;
Header safety: values used for Location and Content-Disposition must be trusted and validated by the application. Never pass raw user-controlled values directly into these headers. Download filenames must not contain CR, LF or other HTTP header control characters.

11. Login and sessions

Release 31 centralizes login/session management in LoginManager. The LoginManager is lazily obtained from the Registry and is responsible for starting the PHP session when needed.

11.1 Login levels

ClassBehavior
NoLoginRequiredAlways validates successfully. Use for public commands.
UserLoginRequires an active member and allows a normal user session.
AdminLoginRequires an active member with role A.
protected function getLevelOfLoginRequired(): void {
    $this->setLoginLevel(
        new \controllerframework\sessions\UserLogin()
    );
}

11.2 Normal login

The normal application login flow typically validates the username and password through LoginManager, then calls:

$user = $loginManager->login($memberid, true);

The second argument controls whether remember-me is enabled.

11.3 Remember-me

Release 31 implements persistent login with a selector/validator token stored in the database. Only a hash of the validator is stored server-side. The remember-me lifetime is 30 days and tokens are rotated after successful reuse.

When remember-me authentication is active, $_SESSION['rememberMe'] is set to true. The session does not expire because of the normal inactivity timer; the persistent authentication is bounded by the remember-token mechanism.

11.4 Inactivity timeout

After successful authentication the framework regenerates the PHP session ID. Logout clears the session, removes the session cookie and removes remember-me tokens/cookie.

11.5 Passwords

Release 31 uses PHP's password_hash() and password_verify(). Legacy SHA-256 password hashes can be accepted during migration and are automatically replaced by a modern password hash after a successful legacy login.

12. CSRF protection

Use the framework's Csrf helper for state-changing forms. The token is generated with random_bytes(32), stored in the PHP session and compared with hash_equals().

12.1 In a Command

Command provides protected helpers:

$token = $this->getCsrfToken();

if (!$this->validateCsrfToken($request)) {
    $request->set('errorcode', 'InvalidCsrfToken');
    return self::CMD_ERROR;
}

12.2 In the HTML form

<form method="post">
    <input type="hidden"
           name="csrf_token"
           value="<?= htmlspecialchars(
               $this->getCsrfToken(),
               ENT_QUOTES,
               'UTF-8'
           ) ?>">

    <button type="submit">Save</button>
</form>

In a normal client view, obtain the token through the application command/view design you use. The framework's token parameter name is csrf_token.

Protect state changes. Use CSRF validation for POST operations that change passwords, records, settings, payments or other security-sensitive application state. Do not rely on the session cookie alone.

13. Access tokens

Controller Framework 1.0.31 introduces the AccessToken class in the controllerframework\security namespace.

AccessToken provides an additional token-based security mechanism for protecting specific URLs or application operations. It is particularly useful when an application needs to protect an internal URL against unauthorized direct access, in addition to the normal authentication and authorization mechanisms.

13.1 Generating an access token

An access token is generated from a purpose and an identifier:

use controllerframework\security\AccessToken;

$accessToken = AccessToken::generate(
    'mollie-order',
    (string) $orderid
);

The purpose identifies what the token is intended for. The identifier binds the token to a specific application object or operation.

The token generation mechanism uses a server-side secret salt and an HMAC-SHA-256 calculation. The resulting token is deterministic for the same purpose and identifier.

13.2 Validating an access token

The receiving Command must validate the token before performing the protected operation:

use controllerframework\security\AccessToken;

if (!AccessToken::validate(
    'mollie-order',
    (string) $orderid,
    $accessToken
)) {
    throw new \RuntimeException(
        'Invalid access token.'
    );
}

The validation operation uses a timing-safe comparison through hash_equals().

13.3 Example: protecting an internal payment URL

A typical application flow can generate an access token when creating an order:

$accessToken = AccessToken::generate(
    'mollie-order',
    (string) $orderid
);

$request->set('forwardqueryparams', [
    'id' => $orderid,
    'amount' => $amount,
    'access_token' => $accessToken
]);

return self::CMD_OK;

The receiving Command validates the token before accessing the order or creating the Mollie payment:

$orderid = $request->get('id');
$accessToken = $request->get('access_token');

if (!AccessToken::validate(
    'mollie-order',
    (string) $orderid,
    $accessToken
)) {
    throw new \RuntimeException(
        'Invalid access token.'
    );
}

// Only after validation:
// - load the order
// - verify the order data
// - create the payment
// - continue with the application flow
Validate before processing. An access token must be validated before the protected database operation or other sensitive operation is performed. Do not load or modify protected application data first and validate the token afterwards.

13.4 AccessToken is not authentication

An AccessToken is an additional security mechanism. It does not replace normal user authentication, authorization, CSRF protection or HTTPS.

The purpose of an access token is to demonstrate that the caller possesses a token that corresponds to the expected purpose and identifier. The application must still enforce all other security requirements that apply to the operation.

13.5 Token secret

The security of AccessToken depends on the confidentiality of the server-side salt used by the framework. The salt must therefore never be exposed to clients or committed to publicly accessible configuration or source code.

13.6 No server-side token storage

AccessToken does not require a database table or other server-side token storage. The token is calculated from the purpose, identifier and server-side secret.

Because the token is deterministic, generating the same token again for the same purpose and identifier produces the same value. The AccessToken class itself does not provide token expiration, one-time use or token revocation.

Important: if an application requires expiry, one-time use or revocation, these requirements must be implemented separately by the application. An access token should therefore not be interpreted as a short-lived session token or as a replacement for an authenticated session.

14. Database and domain objects

The framework uses PDO and provides a DomainObject/Mapper abstraction. The application's model classes normally extend framework domain classes, while application-specific Mapper classes provide the table and object mapping.

14.1 DomainObject basics

class Activity extends \controllerframework\db\DomainObject
{
    public static function getInstance(array $row): DomainObject
    {
        $activity = new self($row['id']);
        $activity->initProperties($row);
        return $activity;
    }
}

Domain objects expose:

Activity::find($id);
Activity::findAll();
Activity::insert([...]);

$activity->update([...]);
$activity->delete();

$activity->getId();

14.2 Mapper

A specialized Mapper supplies the database table name and object construction logic.

final class ActivityMapper
    extends \controllerframework\db\Mapper
{
    protected function tablename(): string {
        return 'activity';
    }

    protected function doCreateObject(
        string $classname,
        array $row
    ): \controllerframework\db\DomainObject {
        return $classname::getInstance($row);
    }
}

14.3 Allowed fields

Release 31 validates update fields against an allow-list. A specialized Mapper should extend the allowed fields supplied by its parent.

protected function getAllowedFields(): array
{
    return array_merge(
        parent::getAllowedFields(),
        [
            'name',
            'email',
            'active'
        ]
    );
}

14.4 findAll() and free SQL

findAll(string $selectclause = '') intentionally permits an application-defined SQL fragment. This is a flexible API, but it is not a parameterized value.

$members = Member::findAll( "WHERE active = 1 ORDER BY name" );
Security rule: never build this clause directly from $_GET, $_POST, cookies or other untrusted input. PDO::prepare() cannot make a SQL fragment safe when the fragment itself is concatenated into the SQL statement. Validate/whitelist application-controlled choices before constructing the clause.

15. Member model

The framework provides a generic controllerframework\members\Member abstraction and MemberMapper. A client application normally subclasses these classes.

namespace model; class Member extends \controllerframework\members\Member { public function initiatePassword(string $pwd = ''): string { return '...'; } }
namespace model; final class MemberMapper extends \controllerframework\members\MemberMapper { }

15.1 Member type implementations

Membership-specific behavior can be implemented through subclasses such as Member_RGLR. The classification stored in the member row determines the concrete type implementation.

class Member_RGLR extends \controllerframework\members\MemberTypeImplementation { public function getYearlyParticipationFee( \model\Member $member ): int { return 350; } }

16. Mail

The framework uses Symfony Mailer. SMTP settings are supplied through the global configuration constants.

16.1 Immediate mail

\controllerframework\mail\Mailer::sendMail( 'Subject', '<p>Hello</p>', 'one@example.org, two@example.org', 'recipient@example.org' );

The framework constructs an HTML message, adds the configured sender/reply-to information and sends through the configured SMTP transport.

16.2 Mail queue

MailQueue::add() stores a mail in the mail_queue table. MailerQueue can then be used by an application-side scheduled process to send queued messages.

$id = \controllerframework\mail\MailQueue::add( 'Newsletter', '<p>Hello members</p>', 'website@example.org', 'alice@example.org, bob@example.org' );

The supplied SQL schema includes queue status, attempts and timestamps so the client application can implement a scheduled queue processor. MailQueue::add() only stores the message; MailerQueue::sendMail() is the low-level helper used by such a processor to send one queued message.

SMTP credentials: keep mail credentials in protected configuration and use the encryption/port required by the SMTP provider. Do not commit real passwords to source control.

17. Audit tracing

The framework provides AuditableItem and AuditableItemTrait. A class can implement the interface and use the trait, then call notifyAuditTrace().

class MyService implements \controllerframework\audit\AuditableItem { use \controllerframework\audit\AuditableItemTrait; public function updateSomething(): void { $this->notifyAuditTrace( __FUNCTION__, ['important application event'] ); // ... } }

AuditTrace writes to logfile.txt under the configured loggingpath. Make sure that directory is writable by PHP and is not unnecessarily exposed as a public download location.

18. Error handling

Controller::run() registers ErrorHandler. The handler distinguishes CLI execution from web execution and uses the configured environment.

EnvironmentBehavior
developmentDisplays a more detailed exception message, file and line information.
productionReturns HTTP 500 and displays a generic error message.

Application code should still handle expected business errors through Command statuses such as CMD_ERROR rather than throwing exceptions for normal user input.

19. Security rules for client developers

19.1 Treat all client input as untrusted

This includes GET parameters, POST fields, cookies, HTTP headers and values returned by external clients.

19.2 SQL

19.3 HTML output

Escape untrusted values when inserting them into HTML. The framework example uses htmlspecialchars(..., ENT_QUOTES, 'UTF-8').

19.4 HTTP headers and redirects

When a protected command is requested without a valid login, the framework temporarily stores the original application path in the originalPath cookie so that the application can return the user to the requested path after authentication. Client applications should not populate or modify this cookie themselves. Redirect targets must remain under application control.

19.5 Access tokens

Remember that the framework's AccessToken implementation does not provide expiration, one-time use or revocation. Applications requiring those properties must implement them separately.

19.6 Sessions

19.7 CSRF

Validate CSRF tokens for state-changing POST operations.

19.8 Production

20. Recommended development workflow

  1. Install the framework with Composer.
  2. Copy/adapt the example application structure.
  3. Create and protect config/app_options.ini.
  4. Configure the database and mail transport.
  5. Set up the member table and the remember-token table if login persistence is required.
  6. Define application paths in controls.xml.
  7. Create a Command for each application use case.
  8. Select the correct login level in each protected Command.
  9. Put command results into the Request.
  10. Select a view or data renderer in controls.xml.
  11. Use DomainObject/Mapper classes for persistent application entities.
  12. Add CSRF validation to state-changing forms.
  13. Use AccessToken where an additional token-based protection layer is required.
  14. Validate access tokens before executing the protected operation.
  15. Test normal login, logout, inactivity timeout and remember-me.
  16. Run Composer validation and security auditing before deployment.
  17. Set production configuration and verify that no credentials or development diagnostics are exposed.

21. Quick reference

TaskTypical API/configuration
Start frameworkController::run()
Get RegistryRegistry::instance()
Get requestRegistry::instance()->getRequest()
Get databaseRegistry::instance()->getDb()
Get LoginManagerRegistry::instance()->getLoginManager()
Read/write request data$request->get() / $request->set()
Successful commandreturn self::CMD_OK;
Error commandreturn self::CMD_ERROR;
Public commandnew NoLoginRequired()
User commandnew UserLogin()
Admin commandnew AdminLogin()
Generate access tokenAccessToken::generate($purpose, $identifier)
Validate access tokenAccessToken::validate($purpose, $identifier, $token)
Load one objectMyObject::find($id)
Load collectionMyObject::findAll()
Update object$object->update([...])
Delete object$object->delete()
CSRF token$this->getCsrfToken()
Validate CSRF$this->validateCsrfToken($request)
Immediate mailMailer::sendMail()
Queue mailMailQueue::add()

22. Final design principle

The Controller Framework deliberately separates framework infrastructure from application responsibility. The framework provides routing, command execution, rendering, sessions, authentication, database mapping, CSRF support, access tokens, mail and audit facilities. The client application remains responsible for its domain rules, authorization details beyond the supplied login levels, validation of business input and the safe use of flexible APIs.

Release 31 baseline: build application code around the framework APIs described here, keep untrusted input out of SQL fragments and HTTP headers, protect state-changing requests with CSRF tokens, use access tokens as an additional protection layer where appropriate, and deploy with production error handling and protected configuration.