Loom73 MVC¶
Loom73 uses a small, explicit MVC architecture.
It does not hide the request flow behind a route registry, service container or automatic controller wiring.
Application bootstrap¶
All public HTTP requests enter through:
The bootstrap:
- defines application paths;
- loads Composer dependencies and Loom73 configuration;
- loads
config/.env; - starts the named PHP session;
- configures the timezone and HTML content type;
- registers the Loom73 autoloader;
- loads module configuration;
- dispatches the request.
The web server document root must point to:
Application code, configuration, storage and vendor files must remain outside the public web root.
Routing¶
The rewritten request path is read from the url query parameter and split into fragments.
For example:
dispatches to:
and:
dispatches to:
Hyphenated controller fragments become PascalCase class names:
Hyphenated method fragments become underscore-separated method names:
The default controller and method come from:
The base URL therefore normally resolves to:
Only the public methods declared in the controllers can be dispatched.Missing routes and views¶
If the controller or method does not exist, Loom73 returns:
and renders the public error view:
If a dispatched controller method has no corresponding view, Template returns:
and renders:
Internal path information is shown only when:
Production error pages remain generic.
Controllers¶
Application controllers live in:
and use:
Controllers extend:
A controller coordinates the request. It may:
inspect request data
apply authentication or ability guards
call models and services
set template variables
record meaningful actions
redirect after a mutation
disable normal rendering for JSON or binary responses
A controller should make the application flow easy to follow.
Dependencies¶
Controllers should instantiate the models and services they actually use.
use Loom73\Beam\User;
use Loom73\Woodframe\Ctrl;
use Loom73\Yarn\Asset;
class UserCtrl extends Ctrl
{
protected User $User;
protected Asset $Asset;
public function __construct(
string $model,
string $controller,
string $method
) {
parent::__construct($model, $controller, $method);
$this->User = new User();
$this->Asset = new Asset();
}
}
Ctrl retains an optional _model property for compatibility and generic helpers. New controller code should prefer explicitly named dependencies.
Controller property names¶
Models, services and reusable components use PascalCase properties:
Records, query results and other runtime data use camelCase:
This is a Loom73 convention rather than a general PHP requirement.
GET and POST¶
Loom73 follows a predictable request convention:
A GET action may retrieve data and render a page or form.
A POST action should normally:
- verify the request method and CSRF token;
- read and validate input;
- perform the mutation;
- add a flash message;
- redirect.
Example:
public function save(): void
{
if (!$this->isPost(notEmpty: true)):
return;
endif;
$result = $this->User->create([
// Validated values.
]);
$this->evaluateResponse($result);
}
isPost() validates CSRF by default:
isGet() checks GET requests:
httpCheck() remains available for compatibility. New code should prefer isPost() and isGet().
Reading POST values¶
post() returns a trimmed value without applying a filter:
posted() preserves Loom73's historical default filtering:
$username = $this->posted('username');
$email = $this->posted(
'email',
FILTER_SANITIZE_EMAIL
);
$password = $this->posted(
'password',
false
);
Input normalization and output escaping solve different problems.
Input:
normalize and validate according to the field
Output:
escape according to the destination context
Views must still escape values for HTML, attributes, URLs, JSON or other output contexts as appropriate.
CSRF¶
Loom73 stores the CSRF token in the application session.
Views can print a hidden field with:
POST actions normally validate it through isPost().
After a completed mutation or authentication attempt, the controller may destroy the current token so a new one is generated for the next form.
Authentication and guards¶
The base controller initializes the authentication context.
Available state includes:
This does not protect every route automatically.
Controllers apply guards explicitly:
$this->requireAuth();
$this->requireAdmin();
$this->requireEditor();
$this->requireAbility('manage_users');
This keeps route access visible in the action that requires it.
Models¶
Application data-access classes live in:
They extend:
A model defines at least its table and normally its primary key:
namespace Loom73\Beam;
class User extends Model
{
protected string $table = 'auth_user';
protected string $pkey = 'idauth_user';
protected bool $dates = true;
protected bool $softDeletes = true;
}
Use the common Model methods for ordinary persistence. Write explicit SQL in the concrete model when it expresses the domain more clearly.
public function apiByUsername(string $username): QueryResult
{
$sql = '
SELECT username, role
FROM auth_user
WHERE username = :username
LIMIT 1
';
return $this->query($sql, [
'username' => $username,
]);
}
SQL belongs in models or dedicated data-access services, not in views.
QueryResult handling¶
Model operations return QueryResult.
$result = $this->User->getById($id);
if ($result->fails()):
// The database operation failed.
endif;
if ($result->isEmpty()):
// The operation succeeded but returned no record.
endif;
$user = $result->first();
evaluateResponse() provides a common mutation flow for ordinary CRUD actions. Controllers may handle a result directly when the use case requires different behavior.
Views and templates¶
Views live in:
For example:
Controllers assign variables with:
The template extracts those variables before composing the response.
A standard HTML response may include:
application/views/head.php
controller-specific or global header
controller-specific submenu
the requested view
controller-specific or global footer
application/views/foot.php
A view may:
render semantic markup
read assigned variables
escape output
include partials and presentational components
perform small display-only decisions
A view should not:
Automatic rendering¶
Normal controller actions do not call render() directly.
Ctrl renders the configured template from its destructor when:
This keeps ordinary page actions concise.
For responses that must bypass the HTML template, call:
This is used for responses such as:
Redirect helpers disable rendering automatically and terminate execution.
Flash messages and redirects¶
Mutation actions can redirect with a typed message:
$this->redirectWithSuccess('/user/profile', [
'message' => MSG_PROFILE_UPDATE_SUCCESS,
'data' => null,
'error' => null,
]);
Available helpers include:
Flash messages survive the redirect and are consumed when rendered.
Diagnostic error details should only be exposed while development debugging is enabled.
Ledger¶
Controllers can record meaningful actions through:
$this->recordAction(
action: 'user.update',
ownerType: 'user',
ownerId: (string) $userId,
summary: 'User profile updated'
);
Anonymous actions use:
Ledger failures are logged but should not reverse a successful application operation.
Alternative control syntax¶
Loom73 templates prefer PHP's alternative control syntax where it improves the readability of mixed PHP and HTML:
<?php if ($is_authenticated): ?>
<a href="/user/profile">Profile</a>
<?php else: ?>
<a href="/user/login">Login</a>
<?php endif; ?>
Ordinary braces remain appropriate inside classes and for PHP-only logic.
Request summary¶
Request
↓
public/index.php bootstrap
↓
route fragments
↓
controller and method
↓
guards and input handling
↓
model or service
↓
QueryResult or domain result
↓
view, redirect, JSON or binary response
The MVC rule is: