Three bugs found deploying a fresh clone to seedproject.com, each fatal to the documented "spin up a new site" flow: - .gitignore: the unanchored `public/` pattern (meant for the root build output) also ignored api/public/ (the framework's front controller, controllers, models, views) and app/public/ (theme static assets: fonts, avatar placeholder). Neither was ever committed, so every fresh clone 500'd on all /api routes and 404'd on theme assets. Anchor the pattern to /public/ and commit both directories. - api/install/dump.sql: stray `CREATE DATABASE ochenta80_db123` (SQLyog export artifact) aborted `php console app:install` for any non-privileged DB user. The schema must import into whatever database the installer connects to. - PluginManager::boot() queries sp_plugins on every request, but no shipped schema creates it — even a successful install 500'd on every endpoint. Add migration 002_create_sp_plugins.sql matching the columns PluginManager reads/writes. Also empty api/system/errors.json, which shipped with stale error logs from an unrelated project. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C3JqxTe7TKaR7xufcMr7Ds
4.6 KiB
4.6 KiB
Controllers — public/controllers/
UI page controllers. Each file maps to a URL segment via auto-routing.
File Naming
PascalCase.php→ e.g.ClientReports.php- URL
/client-reportsauto-routes toClientReports::index()
Base Class
All UI controllers extend AppController (not Controller directly):
core/Controller.php ← framework core, never modify
↑
app/Controllers/AppController.php ← app-wide concerns (injects $UserProfile)
↑
public/controllers/YourController.php
Standard Controller Structure
<?php
class Clients extends AppController
{
public function __construct() {
parent::__construct();
Auth::handleLogin(); // redirect to /login if not authenticated
$this->view->Footer = $this->view->PartialView('admin_footer');
$this->view->SideBar = $this->view->PartialView('admin_sidebar');
$this->view->Header = $this->view->PartialView('admin_header');
$this->view->ToolBar = $this->view->PartialView('admin_toolbar');
}
function index() {
$clients = Db::select("SELECT * FROM sp_clients WHERE active = 1");
$this->view->Clients = $clients;
$this->view->render(__CLASS__ . '/' . __FUNCTION__);
}
function show($id) {
$client = Db::getRow("SELECT * FROM sp_clients WHERE id = :id", [':id' => $id]);
if (!$client) { http_response_code(404); die('Not found'); }
$this->view->Client = $client;
$this->view->render(__CLASS__ . '/' . __FUNCTION__);
}
}
Render Conventions
// Standard admin page (default wrapper)
$this->view->render(__CLASS__ . '/' . __FUNCTION__);
// Login / minimal wrapper
$this->view->render(__CLASS__ . '/' . __FUNCTION__, 'login');
// Welcome screen (config flag)
if (WELCOME) { $this->view->render(__CLASS__ . '/welcome', '', false); }
Injecting Page-Specific Assets
function index() {
// CSS
$this->Styles[] = '/public/assets/plugins/datatables/datatables.bundle.css';
$this->view->Styles = $this->Styles;
// JS
$this->JavaScript[] = '/public/assets/plugins/datatables/datatables.bundle.js';
$this->JavaScript[] = '/public/assets/js/page-script.js';
$this->view->JavaScript = $this->JavaScript;
$this->view->render(__CLASS__ . '/' . __FUNCTION__);
}
Authentication
Auth::handleLogin(); // redirects to /login if not authenticated
Auth::can('users.create') // returns bool
Auth::requirePermission('users.delete'); // halts with 403 JSON if denied
Passing Data to Views
$this->view->Users = Db::select("SELECT * FROM sp_users");
$this->view->PageTitle = 'User List';
// accessed in view as $Users, $PageTitle
CRUD Pattern
function store() {
try {
if (!Validator::required($_POST['name'])) {
throw new Exception('Name is required');
}
$id = Db::insert('sp_clients', [
'name' => $_POST['name'],
'created_at' => date('Y-m-d H:i:s')
]);
redirect('/clients/show/' . $id);
} catch (Exception $e) {
if (DEBUG) { echo $e->getMessage(); }
else {
$this->view->Error = 'Could not save. Please try again.';
$this->view->FormData = $_POST;
$this->view->render(__CLASS__ . '/create');
}
}
}
function update($id) {
Db::update('sp_clients',
['name' => $_POST['name'], 'updated_at' => date('Y-m-d H:i:s')],
'id = :id',
[':id' => $id]
);
redirect('/clients/show/' . $id);
}
function delete($id) {
Db::delete('sp_clients', 'id = :id', [':id' => $id]);
redirect('/clients');
}
Error Handling
function create() {
try {
Db::beginTransaction();
// ... operations ...
Db::commit();
redirect('/clients');
} catch (Exception $e) {
Db::rollback();
if (DEBUG) {
echo "<pre>Error: {$e->getMessage()}\nFile: {$e->getFile()}:{$e->getLine()}</pre>";
} else {
error_log("Client creation failed: " . $e->getMessage());
$this->view->Error = 'An error occurred. Please try again.';
$this->view->render(__CLASS__ . '/create');
}
}
}
Helper / Utility Code Rule
Reusable, stateless functions → public static method in app/Helpers/, never as a private controller method.
Call via HelperClass::methodName(...).