seedproject-web/api/public/controllers/controllers.md
Carlos Arias f41fa652ac fix: repair the fresh-clone flow — track api/public + app/public, fix installer schema
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
2026-07-05 00:15:55 +00:00

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-reports auto-routes to ClientReports::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(...).