# CODEBASE.md
> **AI INSTRUCTIONS:** 
> 1. Copy the entirety of this file and paste it into your first prompt with Claude, ChatGPT, or any LLM. 
> 2. Once pasted, use this prompt to start building: 
> 
> *"I am using FrankPHP. Based on this CODEBASE.md, help me scaffold a new Controller for [Your Feature] that handles [X] and uses the [Y] middleware. Ensure it follows the hydration and response patterns defined in the core."*

> Framework version: **1.0**
> Last updated: <!-- update this when framework files change -->
> 
> **How to use this file:** Paste it at the start of any Claude conversation before describing your task.
> Add your application-specific section at the bottom as you build. Keep it accurate — Claude trusts this document.

---

## 1. What This Framework Is

A lightweight, multi-tenant PHP MVC framework. No Laravel, no Symfony, no Composer dependencies. Everything is hand-rolled and explicit. The design philosophy is: readable over clever, explicit over magic.

**Key characteristics:**
- PSR-4 autoloading, namespace root `App\`
- Multi-tenancy is a first-class citizen — every authenticated route is tenant-scoped
- Middleware pipeline handles auth, tenant resolution, and JSON parsing before the controller is touched
- Models handle DB access only — business logic lives in Services
- Views use output buffering into a layout file — no templating engine

---

## 2. Directory Structure

```
/
├── bootstrap.php           # Autoloader, DB init, all route definitions — returns $router
├── Config/
│   └── config.php          # DB credentials, timezone, app options — returns array
├── Core/
│   ├── BaseController.php  # view() helper — resolves and renders view files
│   ├── BaseModel.php       # fill(), save(), delete(), find(), all(), where(), hydration
│   ├── Database.php        # PDO singleton — connect() and getPdo()
│   ├── MiddlewareInterface.php  # handle(Request, callable $next)
│   ├── Request.php         # Wraps $_GET, $_POST, $_SERVER, rawBody, bodyParams, user, tenant
│   ├── Response.php        # Static: json(), view(), redirect()
│   └── Router.php          # add(), dispatch(), middleware pipeline builder
├── Controllers/
│   ├── AuthController.php          # Login, logout, forgot/reset password
│   ├── TenantSettingsController.php # Tenant settings read/write
│   ├── UserController.php          # /me route
│   └── Api/V1/                     # API controllers (apiAuth middleware)
├── Middleware/
│   ├── AuthMiddleware.php          # Session check, loads $request->user
│   ├── TenantMiddleware.php        # Resolves tenant_id param, loads $request->tenant
│   ├── JsonBodyParserMiddleware.php # Parses raw JSON body into $request->bodyParams
│   └── ApiAuthMiddleware.php       # <!-- document when implemented -->
├── Models/
│   ├── User.php            # Extends BaseModel — findById, findByEmail, saveSettings, password reset methods
│   └── Tenant.php          # Standalone (does not extend BaseModel) — findById, findAll, saveSettings
├── Services/               # Business logic — return ServiceResult objects
│   └── PasswordResetService.php  # requestPasswordReset(), validateToken(), resetPassword()
├── Views/
│   ├── layouts/
│   │   ├── app-main.php    # Authenticated app shell — header, sidebar nav, page-content-area
│   │   └── authViews.php   # Unauthenticated shell — Bootstrap only, no nav
│   ├── auth/               # login, forgot-password, forgot-password-sent, reset-password, reset-password-error
│   ├── settings/           # tenant-settings view
│   └── partials/           # app-header.php, app-nav.php
├── sql/
│   └── schema.sql          # Auto-executed on first boot if users table missing
└── assets/
    ├── css/
    │   ├── app-core.css        # Design tokens, global components (cards, buttons, forms, tabs, tasks)
    │   ├── app-header.css      # Topbar, logo, dropdown, header buttons
    │   ├── app-pages-settings.css  # Settings vnav, panels, toggle rows, alerts
    │   └── authViews.css       # login-card sizing
    └── js/
```

---

## 3. Request Lifecycle

```
index.php (entry point)
  └── sets APP_BASE_DIR constant
  └── requires bootstrap.php → gets $router
  └── $router->dispatch(new Request())
        └── matches route pattern
        └── builds middleware pipeline (outermost first, innermost last)
        └── each middleware calls $next($request) to continue
        └── controller method receives (Request $request, array $params)
        └── returns Response::view() / Response::json() / Response::redirect()
```

**Middleware execution order for a standard tenant route `[$tm, $auth]`:**
1. `TenantMiddleware` — resolves `{tenant_id}` from route params, loads tenant row, sets `$request->tenant`
2. `AuthMiddleware` — checks `$_SESSION['user_id']`, loads user row, validates user belongs to tenant, sets `$request->user`
3. Controller method executes

**If any middleware fails:** it calls `Response::redirect()` or `http_response_code()` and returns without calling `$next` — the pipeline stops.

---

## 4. Routing

All routes are defined in `bootstrap.php`. The router is returned and dispatched from the entry point.

```php
$router->add(METHOD, PATTERN, HANDLER, [MIDDLEWARE]);
```

**Handler formats:**
```php
'ControllerName@method'                    // shorthand — resolves to App\Controllers\ControllerName
'App\\Controllers\\Full\\Path@method'      // fully qualified
```

**Route parameter syntax:** `{param_name}` — captured into `$request->routeParams['param_name']`

**Middleware shorthands** (defined as variables in bootstrap.php):
```php
$tm      = 'TenantMiddleware';
$auth    = 'AuthMiddleware';
$apiAuth = 'ApiAuthMiddleware';
$json    = 'JsonBodyParserMiddleware';
```

**Standard middleware combinations:**
- Public page: `[]`
- Public POST (login): `[$json]`
- Authenticated tenant page: `[$tm, $auth]`
- Authenticated tenant POST: `[$tm, $auth, $json]`
- API endpoint: `[$tm, $apiAuth, $json]`

**Important:** `$json` populates `$request->bodyParams`. Without it, POST body is only available via `$_POST`.

---

## 5. Controllers

Extend `BaseController`. Receive `(Request $request, array $params)`.

```php
namespace App\Controllers;

use App\Core\BaseController;
use App\Core\Request;
use App\Core\Response;

class ExampleController extends BaseController
{
    public function index(Request $request, array $params): mixed
    {
        $tenant = $request->tenant; // set by TenantMiddleware
        $user   = $request->user;   // set by AuthMiddleware

        return $this->view('section/view-name', [
            'title'  => 'Page Title',
            'tenant' => $tenant,
            'user'   => $user,
        ]);
    }
}
```

**`$this->view($name, $params)`** resolves to `Views/$name.php`, extracts `$params` into scope, and requires the file. The view file is responsible for buffering its own content and requiring a layout.

**Reading request data:**
```php
$request->bodyParams           // parsed JSON or $_POST (requires $json middleware)
$request->post                 // raw $_POST
$request->get                  // $_GET
$request->routeParams          // URL segment params e.g. ['tenant_id' => '1']
$request->input('key', $default) // checks bodyParams → post → get in order
$request->header('Name')       // request header
$request->tenant               // tenant array (set by TenantMiddleware)
$request->user                 // user array (set by AuthMiddleware)
```

---

## 6. Views and Layouts

Views use PHP output buffering. The pattern is consistent across all views:

```php
<?php
$title = 'Page Title';
ob_start();
?>
<!-- HTML content here — $tenant, $user, and any passed params are in scope -->
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/app-main.php'; // or authViews.php
?>
```

**Layouts:**
- `app-main.php` — full authenticated shell. Renders `$content` inside `.page-content-area`. Includes header and nav partials. Expects `$title`, `$tenant`, `$user` in scope.
- `authViews.php` — minimal unauthenticated shell. Renders `$content` only. Used for login, password reset pages.

**Variables available in views** (via `extract()` in `Response::view()`): everything passed in the params array to `$this->view()`.

---

## 7. Models

### BaseModel

All application models that map to a single DB table should extend `BaseModel`.

```php
namespace App\Models;
use App\Core\BaseModel;

class Thing extends BaseModel
{
    protected string $table = 'things'; // optional — defaults to lowercase classname + 's'

    // Declare all columns as typed public properties
    public ?int    $id         = null;
    public int     $tenant_id;
    public string  $name;
    public ?string $created_at = null;
}
```

**Key methods:**

| Method | Description |
|--------|-------------|
| `fill(array $data)` | Hydrates properties from array, casting to declared PHP types |
| `save()` | INSERT ... ON DUPLICATE KEY UPDATE — works for both create and update |
| `delete()` | DELETE by primary key, AND tenant_id if property exists |
| `find(int $id, int $tenantId)` | Returns hydrated object or null — tenant-scoped |
| `all(int $tenantId)` | Returns array of hydrated objects — tenant-scoped |
| `where(int $tenantId, string $col, mixed $val, string $op)` | Returns array of hydrated objects |

**Type casting in `fill()`:**
- `int`, `float`, `bool`, `string` — cast directly
- `float` — strips `$` and `,` (currency cleaning)
- `bool` — uses `filter_var` with `FILTER_VALIDATE_BOOLEAN`
- `DateTimeImmutable` / `DateTime` — constructed from value string
- `array` — parsed from JSON string or comma-separated string
- Nullable properties receiving `""`, `null`, or `"-1"` are set to `null`

**`save()` excludes** `id`, `created_at`, `updated_at`, `tenant_id` from the UPDATE clause (they are included in the INSERT).

### Tenant Model

`Tenant` does **not** extend `BaseModel`. It is a standalone class with its own PDO instance.

Key methods: `findById(int $id)`, `findAll()`, `saveSettings(int $tenantId, array $data)`.

`saveSettings()` has an internal allowlist of writable columns — only those columns can be updated via settings forms.

---

## 8. Services and ServiceResult

Business logic that is more than a single DB call lives in a Service class in `App\Services\`.

Services return a `ServiceResult` object (not shown in framework core — document your implementation here).

**Expected ServiceResult interface based on usage in AuthController:**
```php
$result->success          // bool
$result->message          // string — error or info message
$result->get('key')       // retrieve a named value from the result payload
$result->get('key', $default)
$result->isValidationError()  // bool — distinguishes validation from system errors
```

Controllers should delegate to services and only handle routing the result to the correct view or redirect:

```php
$service = new SomeService();
$result  = $service->doSomething($input);

if ($result->success) {
    Response::redirect('/somewhere');
}

return $this->view('some/view', ['error' => $result->message]);
```

---

## 9. Database

`Database::connect(array $config)` creates the PDO singleton. Called once in `bootstrap.php`.

`Database::getPdo()` returns the singleton — called by `BaseModel::__construct()` and standalone models.

**On first boot:** if the `users` table does not exist, `schema.sql` is executed automatically and seed data is inserted (two tenants, two admin users with password `password`).

**PDO settings:** `ERRMODE_EXCEPTION`, `FETCH_ASSOC`.

All queries use prepared statements. Multi-statement mode is enabled only during schema import, then disabled.

---

## 10. Authentication and Multi-tenancy

**Session:** PHP native sessions. `$_SESSION['user_id']` is the only session value the framework sets.

**AuthMiddleware** on every protected route:
1. Starts session if not started
2. Redirects to `/login?next={current_url}` if `user_id` not in session
3. Loads user via `User::findById()`
4. If route has `{tenant_id}` param, verifies `user.tenant_id === route tenant_id` — returns 403 if mismatch
5. Sets `$request->user` (array)

**TenantMiddleware** resolves before AuthMiddleware in the stack:
1. Reads `{tenant_id}` from route params — redirects to `/logout` if missing
2. Loads tenant via `Tenant::findById()` — returns 404 if not found
3. Sets `$request->tenant` (array)

**Login flow:** `AuthController@login` → verifies email + `password_verify()` → sets `$_SESSION['user_id']` → redirects to `/tenant/{tenant_id}/dashboard`.

**Role checking** is done in controllers, not middleware. Pattern used in `TenantSettingsController`:
```php
if (!in_array($user['role'] ?? '', ['admin', 'owner'], true)) {
    return Response::redirect('/tenant/{id}/somewhere?error=forbidden');
}
```

---

## 11. CSS and Frontend Conventions

**Never write inline styles. Never create single-use CSS classes.**

**Decision order when styling anything:**
1. Standard Bootstrap 5 utility class
2. Existing class in `app-core.css`
3. Existing class in the relevant feature CSS file
4. Add a new generic reusable class to the appropriate CSS file

**CSS file responsibilities:**

| File | Scope |
|------|-------|
| `app-core.css` | Design tokens (`--brand-primary`, `--grey-*` scale), global components: cards, buttons, forms, tabs, task components, modals |
| `app-header.css` | Topbar, logo, dropdown menus, header icon buttons |
| `app-pages-settings.css` | Settings vnav, content panels, toggle rows, scope badges, settings alerts |
| `authViews.css` | `.login-card` sizing only |

**Design tokens (defined in `:root` in `app-core.css`):**
```css
--brand-primary        /* #8B5CF6 — purple */
--brand-primary-dark   /* #5B21B6 */
--brand-primary-light  /* #F5F3FF */
--brand-primary-glass  /* 10% opacity purple */
--color-success        /* #10B981 */
--color-warning        /* #F59E0B */
--color-danger         /* #EF4444 */
--grey-50 through --grey-900  /* 10-step neutral scale */
--sidebar-width        /* 260px */
--sidebar-collapsed    /* 80px */
--topbar-height        /* 56px */
```

**Button classes** (from `app-core.css`): `.btn-brand-primary`, `.btn-outline-secondary`, `.btn-outline-primary`, `.btn-outline-danger`, `.btn-danger`, `.btn-circular`

**Card pattern:**
```html
<div class="card">
    <div class="card-header-modern">
        <h5>Title</h5>
        <button class="btn btn-brand-primary">Action</button>
    </div>
    <div class="card-body p-3">
        <!-- content -->
    </div>
</div>
```

**Settings page pattern:** `.settings-vnav` + `.settings-panel` + `.settings-group` + `.settings-toggle-row` — see `app-pages-settings.css`.

**External dependencies loaded via CDN:**
- Bootstrap 5.3.2 (CSS + JS bundle)
- Bootstrap Icons 1.10.5
- Syncfusion EJ2 32.1.19 (Bootstrap5 theme + ej2.min.js)

---

## 12. Security Notes

- All DB queries use PDO prepared statements
- `password_hash()` / `password_verify()` for passwords
- `htmlspecialchars()` on all user-supplied output in views
- Tenant isolation enforced at middleware level (route param vs session user) and at model level (tenant_id in all queries)
- `saveSettings()` methods in both `User` and `Tenant` maintain explicit column allowlists — arbitrary columns cannot be written via settings forms
- Multi-statement PDO mode is disabled after schema import

---

## 13. What The Framework Deliberately Does Not Include

- No ORM — queries are hand-written SQL via PDO
- No template engine — plain PHP views
- No dependency injection container
- No event system
- No queue system
- No built-in CSRF protection — <!-- add if/when implemented -->
- No built-in rate limiting — <!-- add if/when implemented -->
- No Composer — zero external dependencies

---

## 14. Application Layer (Complete This Section)

> Everything below this line is specific to **your application**. Keep it updated as you build.
> Claude will use this section alongside the framework section above.

### Application Name
<!-- e.g. Revisio360 — CRM for professional services -->

### Directory Tree Additions
<!-- List any controllers, models, services, and views you have added beyond the framework defaults -->

### Data Model
<!-- One entry per table -->

#### `tenants`
| Column | Type | Notes |
|--------|------|-------|
| id | int PK | |
| name | varchar | |
| slug | varchar | |
| <!-- add your columns --> | | |

#### `users`
| Column | Type | Notes |
|--------|------|-------|
| id | int PK | |
| tenant_id | int FK | |
| email | varchar | |
| name | varchar | |
| password_hash | varchar | |
| role | varchar | admin, owner, user |
| timezone | varchar | nullable — inherits from tenant if null |
| date_format | varchar | nullable — inherits from tenant if null |
| <!-- add your columns --> | | |

#### `password_reset_tokens`
| Column | Type | Notes |
|--------|------|-------|
| id | int PK | |
| user_id | int FK | |
| tenant_id | int FK | |
| token_hash | varchar | |
| expires_at | datetime | |
| used_at | datetime | nullable |
| created_at | datetime | |

<!-- Add your application tables here following the same pattern -->

### Route Inventory
<!-- Keep this in sync with bootstrap.php -->

| Method | Pattern | Controller@method | Middleware |
|--------|---------|-------------------|------------|
| GET | /login | AuthController@showLogin | none |
| POST | /login | AuthController@login | json |
| GET | /logout | AuthController@logout | none |
| GET | /forgot-password | AuthController@showForgotPassword | none |
| POST | /forgot-password | AuthController@sendPasswordReset | none |
| GET | /reset-password | AuthController@showResetPassword | none |
| POST | /reset-password | AuthController@resetPassword | none |
| GET | /tenant/{tenant_id}/dashboard | HomeController@dashboard | tm, auth |
| GET | /tenant/{tenant_id}/tenant-settings | TenantSettingsController@index | tm, auth |
| POST | /tenant/{tenant_id}/tenant-settings/save | TenantSettingsController@save | tm, auth, json |
| GET | /tenant/{tenant_id}/account | AccountSettingsController@index | tm, auth |
| POST | /tenant/{tenant_id}/account/save | AccountSettingsController@save | tm, auth, json |
| <!-- add rows --> | | | |

### Services
<!-- Document each service, its public methods, and what ServiceResult values it returns -->

### Business Rules
<!-- Anything Claude needs to know about your domain logic that isn't obvious from the code -->

### Known Deviations From Framework Conventions
<!-- If you've done something that breaks the patterns above, document it here so Claude doesn't try to "fix" it -->
