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.
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
- PHP 8.3 through versions below 9.0, as required by the Composer constraint
^8.3. - Composer.
- MySQL/MariaDB-compatible PDO database connectivity.
- Apache or another web server capable of running the PHP application.
- The Composer dependency
samoscon/controller-framework. - Symfony Mailer is included as a framework dependency for mail functionality.
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.
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/
| Setting | Purpose |
|---|---|
environment | Controls development versus production exception presentation. Use production on a public production site. |
templatepath | Base path for application PHP views. |
controlsfile | Path to the XML command/render configuration. |
loggingpath | Path 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.
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
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
| Constant | Meaning |
|---|---|
CMD_DEFAULT | Default renderer selection. |
CMD_OK | Normal successful result. |
CMD_ERROR | Application error result. |
CMD_INSUFFICIENT_DATA | Insufficient input/data. |
CMD_ADMIN | Administrator-specific result/forward. |
CMD_CHANGE_PASSWORD | Password must be changed. |
CMD_CONTINUE | Continue 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.
| Method | Purpose |
|---|---|
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. |
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.
| Renderer | Request values | Output |
|---|---|---|
AjaxRenderComponent | results as an array | JSON |
DownloadRenderComponent | filename, columnNames, results | CSV download |
AgendaRenderComponent | filename, results | iCalendar response |
MollieRenderComponent | results | HTTP 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;
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
| Class | Behavior |
|---|---|
NoLoginRequired | Always validates successfully. Use for public commands. |
UserLogin | Requires an active member and allows a normal user session. |
AdminLogin | Requires 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
- Normal user sessions: 60 minutes of inactivity.
- Administrator sessions: 15 minutes of inactivity.
- Remember-me sessions: no inactivity timeout; the remember-token mechanism provides the persistent login period.
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.
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
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.
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" );
$_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.
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.
| Environment | Behavior |
|---|---|
development | Displays a more detailed exception message, file and line information. |
production | Returns 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
- Use prepared statements and bound parameters for values.
- Do not concatenate user input into SQL.
- Do not place user input in
findAll()'s free SQL clause. - Keep Mapper table names and allowed update fields application-controlled.
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
- Do not use arbitrary user input as a redirect destination.
- Do not use arbitrary user input as a download filename.
- Prevent CR/LF and other header control characters in filenames.
- Keep payment callback/redirect URLs under application control.
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
- Use a distinct purpose for each protected operation.
- Bind the token to the relevant application identifier.
- Validate the token before performing the protected operation.
- Keep the server-side token secret confidential.
- Transmit access tokens only over HTTPS.
- Do not log access tokens.
- Do not treat an access token as a replacement for authentication or authorization.
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
- The framework starts sessions through
LoginManager. - Session cookies are configured as Secure, HttpOnly and SameSite=Lax.
- Session IDs are regenerated after successful authentication.
- Do not expose or copy session identifiers into application data.
19.7 CSRF
Validate CSRF tokens for state-changing POST operations.
19.8 Production
- Set
environment=production. - Disable
display_errors. - Protect
app_options.ini. - Protect logs and uploaded/private files.
- Use HTTPS so Secure cookies and credentials are protected in transit.
20. Recommended development workflow
- Install the framework with Composer.
- Copy/adapt the example application structure.
- Create and protect
config/app_options.ini. - Configure the database and mail transport.
- Set up the member table and the remember-token table if login persistence is required.
- Define application paths in
controls.xml. - Create a Command for each application use case.
- Select the correct login level in each protected Command.
- Put command results into the Request.
- Select a view or data renderer in
controls.xml. - Use DomainObject/Mapper classes for persistent application entities.
- Add CSRF validation to state-changing forms.
- Use
AccessTokenwhere an additional token-based protection layer is required. - Validate access tokens before executing the protected operation.
- Test normal login, logout, inactivity timeout and remember-me.
- Run Composer validation and security auditing before deployment.
- Set production configuration and verify that no credentials or development diagnostics are exposed.
21. Quick reference
| Task | Typical API/configuration |
|---|---|
| Start framework | Controller::run() |
| Get Registry | Registry::instance() |
| Get request | Registry::instance()->getRequest() |
| Get database | Registry::instance()->getDb() |
| Get LoginManager | Registry::instance()->getLoginManager() |
| Read/write request data | $request->get() / $request->set() |
| Successful command | return self::CMD_OK; |
| Error command | return self::CMD_ERROR; |
| Public command | new NoLoginRequired() |
| User command | new UserLogin() |
| Admin command | new AdminLogin() |
| Generate access token | AccessToken::generate($purpose, $identifier) |
| Validate access token | AccessToken::validate($purpose, $identifier, $token) |
| Load one object | MyObject::find($id) |
| Load collection | MyObject::findAll() |
| Update object | $object->update([...]) |
| Delete object | $object->delete() |
| CSRF token | $this->getCsrfToken() |
| Validate CSRF | $this->validateCsrfToken($request) |
| Immediate mail | Mailer::sendMail() |
| Queue mail | MailQueue::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.