Controller Framework 1.0.31
Basic Implementation User Guide
A step-by-step tutorial that starts with an empty PHP application and ends with a login-protected screen displaying:
Hi [name of User], welcome to our login protected environment.
and a Logout button.
A minimal PHP 8.3 web application with Composer, the Controller Framework, a MySQL/MariaDB database, a public login screen, CSRF protection, a
UserLogin-protected welcome screen and the framework's
LogoutCommand.
Contents
- The basic concept
- Prerequisites
- Create the application structure
- Install the framework with Composer
- Create the application bootstrap
- Configure the framework
- Create the database
- Create the minimal Member model
- Create the Commands
- Connect everything in controls.xml
- Create the login and welcome views
- Run and test the application
- Understand what happens internally
- Where to go from here
1. The basic concept
The Application Controller variant of the framework follows a simple request lifecycle:
index.phpController::run()The important principle is that controls.xml connects a URL path to a
Command and to the renderer used for its result. The Command contains the application
logic; the View contains the presentation.
2. Prerequisites
- PHP 8.3 or later within the framework's
^8.3requirement. - Composer.
- MySQL or MariaDB.
- A web server capable of rewriting application paths to
index.php. - HTTPS. Release 30 deliberately configures the session cookie as
Secure.
environment=production and do not expose PHP
errors to visitors.3. Create the application structure
Start with an empty web application directory. Create this structure:
basic-app/
├── composer.json
├── index.php
├── .htaccess
├── DatabaseSetup.sql
├── config/
│ ├── app_options.ini
│ └── .htaccess
├── assets/
│ └── logging/
└── MVCFramework/
├── controls.xml
├── commands/
│ ├── LoginCommand.php
│ └── WelcomeCommand.php
├── model/
│ ├── Member.php
│ ├── MemberMapper.php
│ └── Member_RGLR.php
└── views/
├── loginView.php
├── welcomeView.php
└── includes/
└── head.phpThis keeps framework-independent client code under MVCFramework/, while
Composer installs the framework itself under vendor/.
Note: Complete minimal example
A ready-to-copy version of all files used in this tutorial is supplied as a ZIP archive alongside this guide. It contains the same application structure and code shown below.
4. Install the framework with Composer
Create composer.json:
{
"name": "example/controller-framework-app",
"require": {
"php": "^8.3",
"samoscon/controller-framework": "^1.0.31"
}
}
Run:
composer install
Composer creates vendor/autoload.php. Your application will load this
before starting the Controller Framework.
5. Create the application bootstrap
The application's only real entry point is index.php:
<?php
require __DIR__ . '/vendor/autoload.php';
spl_autoload_register(function (string $className): void {
$file = __DIR__
. '/MVCFramework/'
. str_replace('\\', '/', $className)
. '.php';
if (is_file($file)) {
require_once $file;
}
});
controllerframework\controllers\Controller::run();
There are two autoloading responsibilities:
- Composer loads the framework classes.
- The small client autoloader loads your own classes from
MVCFramework/.
Controller::run() starts the framework. Internally it registers the
ErrorHandler, initializes the application and then handles the current request.
6. Configure the framework
Create config/app_options.ini. The minimal configuration contains a
[config] section and a [globals] section because
InitController requires both.
[config]
environment=development
templatepath=/MVCFramework/views
controlsfile=/MVCFramework/controls.xml
loggingpath=/assets/logging/
[globals]
APP=BASIC
_APPDIR=https://your-domain.example/
_HOMEPAGE=https://your-domain.example/
_MINLEVELTOLOGIN=U
_DBUSER=your_database_user
_DBPASSWORD="your_database_password"
_DBNAME=your_database_name
_DBHOST=localhost
For the complete framework mail/error configuration, add the mail globals shown in the supplied example project.
InitController determines the application root, loads
config/app_options.ini, creates the application configuration, defines the
configured globals, locates controls.xml, compiles it with
RenderCompiler, and stores the resulting command map in the
Registry.
Protect the configuration directory from direct HTTP access with config/.htaccess.
7. Create the database
The framework's authentication uses the client application's member table.
The remember-me implementation also uses remember_tokens.
For this tutorial, create both tables and one test user.
CREATE TABLE member (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
description VARCHAR(190) DEFAULT 'description',
classification VARCHAR(4) NOT NULL DEFAULT 'RGLR',
parent_id INT UNSIGNED DEFAULT 0,
name VARCHAR(45) DEFAULT 'name',
lastname VARCHAR(45) DEFAULT 'lastname',
email VARCHAR(255) DEFAULT NULL,
role VARCHAR(5) NOT NULL DEFAULT 'U',
password VARCHAR(256) DEFAULT NULL,
ownpwd TINYINT(1) DEFAULT 0,
active TINYINT(1) DEFAULT 0,
subscriptionuntil DATE DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_member_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE remember_tokens (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
member_id INT UNSIGNED NOT NULL,
selector CHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at DATETIME NULL DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_remember_selector (selector),
KEY idx_remember_member (member_id),
KEY idx_remember_expires (expires_at),
CONSTRAINT fk_remember_member
FOREIGN KEY (member_id) REFERENCES member(id)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Insert a test member. The password below is a PHP password_hash() result
for the tutorial password Demo123!:
INSERT INTO member
(classification, name, lastname, email, role, password, ownpwd, active)
VALUES
(
'RGLR',
'Demo User',
'Example',
'demo@example.com',
'U',
'$2y$12$CM39.EaqzfUqdivsgnL8bu2Kyz1jMasOI9A8xWMoujgAYk94lvLTu',
1,
1
);
8. Create the minimal Member model
There is one framework-specific requirement that is easy to miss:
User::getInstance() and the member framework expect the concrete client
member class to be \model\Member.
8.1 Member.php
<?php
namespace model;
class Member extends \controllerframework\members\Member
{
public function initiatePassword(string $pwd = ''): string
{
return 'Your temporary password is ' . htmlspecialchars(
$pwd,
ENT_QUOTES,
'UTF-8'
);
}
}
The method is required because the framework's abstract Member declares
initiatePassword().
8.2 MemberMapper.php
<?php
namespace model;
final class MemberMapper extends \controllerframework\members\MemberMapper
{
}
The framework derives the mapper for \model\Member as
\model\MemberMapper.
8.3 Member_RGLR.php
<?php
namespace model;
final class Member_RGLR
extends \controllerframework\members\MemberTypeImplementation
{
public function getYearlyParticipationFee(Member $member): int
{
return 0;
}
}
The classification value RGLR causes the framework to look
for \model\Member_RGLR. For this minimal example, the membership fee is irrelevant,
so the implementation returns zero.
9. Create the Commands
9.1 LoginCommand
The login screen is public, so it uses NoLoginRequired. It performs the
credential check only when the form is submitted.
<?php
namespace commands;
use controllerframework\controllers\Command;
use controllerframework\registry\Request;
use controllerframework\sessions\NoLoginRequired;
final class LoginCommand extends Command
{
public function doExecute(Request $request): int
{
$this->reg->getLoginManager();
$passwordIsValid = true;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$this->validateCsrfToken($request)) {
$request->set('errorcode', 'InvalidCsrfToken');
return self::CMD_ERROR;
}
$username = trim((string) $request->get('username'));
$password = (string) $request->get('password');
$memberId = $this->reg->getLoginManager()
->validateUsername($username);
if ($memberId !== false) {
$passwordIsValid = $this->reg->getLoginManager()
->validatePassword((int) $memberId, $password);
} else {
$passwordIsValid = false;
}
if ($passwordIsValid) {
$keepLoggedIn =
$request->get('rememberMe') === 'Y';
$this->reg->getLoginManager()->login(
(int) $memberId,
$keepLoggedIn
);
return self::CMD_OK;
}
}
$request->set('csrf_token', $this->getCsrfToken());
$request->set('passwordIsValid', $passwordIsValid);
return self::CMD_DEFAULT;
}
protected function getLevelOfLoginRequired(): void
{
$this->setLoginLevel(new NoLoginRequired());
}
}
execute() belongs to the framework and
checks the configured login level. Your application implements only
doExecute() and getLevelOfLoginRequired().
9.2 WelcomeCommand
This is the first genuinely protected application screen.
<?php
namespace commands;
use controllerframework\controllers\Command;
use controllerframework\registry\Request;
use controllerframework\sessions\User;
use controllerframework\sessions\UserLogin;
final class WelcomeCommand extends Command
{
public function doExecute(Request $request): int
{
$user = User::getInstance();
if ($user === null) {
return self::CMD_ERROR;
}
$request->set('name', (string) $user->name);
return self::CMD_OK;
}
protected function getLevelOfLoginRequired(): void
{
$this->setLoginLevel(new UserLogin());
}
}
The important line is:
$this->setLoginLevel(new UserLogin());
From that moment on, the framework itself protects the command. The command does not need to implement its own session checks.
UserLogin requires an active member and uses a 60-minute inactivity period
for a normal login. If remember-me authentication is used, Release 30 deliberately lets
the persistent remember token determine the session duration rather than applying that
inactivity timeout.
9.3 Logout
No client Command is required. Release 30 already provides:
\controllerframework\commands\login\LogoutCommand
It calls LoginManager::logout(), removes the session and remember-me
tokens, and can then forward to the login path.
10. Connect everything in controls.xml
Now map the paths to Commands and renderers:
<?xml version="1.0" encoding="UTF-8"?>
<control>
<command path="/" class="\commands\LoginCommand">
<view name="/loginView" />
<status value="CMD_OK">
<forward path="/welcome" />
</status>
<status value="CMD_ERROR">
<view name="/loginView" />
</status>
</command>
<command path="/welcome" class="\commands\WelcomeCommand">
<view name="/welcomeView" />
<status value="CMD_ERROR">
<view name="/loginView" />
</status>
</command>
<command path="/logout"
class="\controllerframework\commands\login\LogoutCommand">
<status value="CMD_OK">
<forward path="/" />
</status>
</command>
</control>
There are now exactly three application paths:
| Path | Command | Login | Result |
|---|---|---|---|
/ | LoginCommand | None | Login view or forward to /welcome |
/welcome | WelcomeCommand | UserLogin | Welcome view |
/logout | Framework LogoutCommand | None | Logout and forward to / |
11. Create the login and welcome views
11.1 Login view
The login form contains the CSRF token supplied by the Command:
<form method="post" action="/">
<input
type="hidden"
name="csrf_token"
value="<?= htmlspecialchars(
(string) $request->get('csrf_token'),
ENT_QUOTES,
'UTF-8'
) ?>"
>
<label for="username">Email address</label>
<input type="email" name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<label>
<input type="checkbox" name="rememberMe" value="Y">
Remember me
</label>
<button type="submit">Log in</button>
</form>
The complete example also displays a generic error when the credentials are invalid.
11.2 Welcome view
The Command puts the authenticated user's name into the Request:
$request->set('name', (string) $user->name);
The view then displays it safely:
<h1>
Hi <?= htmlspecialchars(
(string) $request->get('name'),
ENT_QUOTES,
'UTF-8'
) ?>,
welcome to our login protected environment.
</h1>
<form method="get" action="/logout">
<button type="submit">Logout</button>
</form>
The result for the demo user is:
12. Run and test the application
- Create the database and execute
DatabaseSetup.sql. - Enter the database credentials in
config/app_options.ini. - Set
_APPDIRto the real HTTPS application URL. - Run
composer install. - Make sure
assets/logging/is writable by the web server. - Open the application root in a browser.
Test 1 — Login page
You should see the login form at /.
Test 2 — Invalid password
Enter the correct email and an incorrect password. The same login page should be displayed with a generic authentication error.
Test 3 — Successful login
Use:
Email: demo@example.com
Password: Demo123!
The framework creates the authenticated session and forwards to /welcome.
Test 4 — Direct access to /welcome
Log out and directly open /welcome. UserLogin rejects the
request and the Command infrastructure records the original path. The login view is
rendered. After a successful login, the framework's forwarding mechanism can return the
user to the original protected path.
Test 5 — Logout
Click Logout. The framework's LogoutCommand destroys the
session and removes remember-me tokens, then forwards to /.
13. Understand what happens internally
13.1 First request: GET /
GET /Controller::run()InitApplicationControllerRenderCompilerLoginCommandloginView.php13.2 POST / with valid credentials
LoginManager::login() regenerates the session ID, stores the member ID and
session state, and optionally creates a 30-day remember-me token.
13.3 GET /welcome
Before WelcomeCommand::doExecute() is allowed to run, the framework invokes
the Command's configured UserLogin strategy. That strategy checks the current
user and membership state. Only after validation succeeds does the framework execute the
Command.
13.4 Rendering
After the Command returns its status, HandleRequestApplicationController
asks the corresponding RenderComponentDescriptor for the renderer. For
CMD_OK, ViewRenderComponent includes welcomeView.php.
14. Where to go from here
Once this minimal application works, the natural next steps are:
- Add more protected Commands using
UserLogin. - Add administrator screens using
AdminLogin. - Move reusable UI elements into view includes.
- Add domain objects and Mappers for your own tables.
- Use
CommandDecoratorwhen integrating reusable application framework Commands. - Add password initiation/change flows using the framework's existing login Commands.
- Use
DatarequestRenderComponent, downloads, AJAX or forwards where required. - Add CSRF validation to every state-changing POST Command.
- Switch to
environment=productionand harden server configuration before deployment.
Command::execute() and LoginRequired hierarchy enforce it.