MD Files
These two files are written for AI assistants (Claude, GPT, etc.) working on projects built with
MagratheaPHP2. View them here, or download a copy to hand to your own assistant.
Instructions for AI Assistants — MagratheaPHP2
This file is a guide for any AI assistant (Claude, GPT, Gemini, etc.) working on this codebase. Read it before doing any task. It explains the project structure, conventions, key design decisions, and how to approach common tasks.
What This Project Is
MagratheaPHP2 is a full PHP framework primarily designed for building RESTful APIs. It also ships with an ORM, a query builder, a file-based cache, a logging system, an email system, CSS/JS compression, and an admin panel.
- Namespace root:
Magrathea2
- Source code:
src/
- Documentation:
documentation/
- Config format: INI files (
config/magrathea.conf)
- PHP version: 8.0+
- Database: MySQL / MariaDB via MySQLi
Source Code Map
src/
├── MagratheaPHP.php ← Main bootstrap/entry point (start here)
├── Config.php ← INI-based config
├── ConfigApp.php ← DB-stored key-value config
├── ConfigFile.php ← Config file I/O
├── Singleton.php ← Base singleton pattern
├── MagratheaHelper.php ← Static utility functions
├── MagratheaModel.php ← ORM base model (abstract)
├── MagratheaModelControl.php ← ORM static query interface (abstract)
├── MagratheaApi.php ← RESTful API router
├── MagratheaApiControl.php ← API controller base
├── MagratheaCache.php ← File-based response cache
├── MagratheaMail.php ← Email (native mail())
├── MagratheaMailSMTP.php ← Email (SMTP via PHPMailer)
├── Authentication.php ← JWT token generation
├── Logger.php ← File-based logging
├── Debugger.php ← Debug mode manager
├── _Functions.php ← Global procedural helpers + autoloader
├── _FunctionsDebug.php ← Debug helpers
├── _EnumTrait.php ← Enum trait
├── DB/
│ ├── Database.php ← MySQLi wrapper (Singleton)
│ ├── Query.php ← Fluent SELECT query builder
│ ├── QueryInsert.php ← INSERT query builder
│ ├── QueryUpdate.php ← UPDATE query builder
│ ├── QueryDelete.php ← DELETE query builder
│ ├── QueryHelper.php ← Query utilities
│ └── DatabaseSimulate.php ← Mock database for tests
├── Exceptions/
│ ├── MagratheaException.php ← Base exception
│ ├── MagratheaApiException.php ← API exceptions (carries HTTP status)
│ ├── MagratheaDBException.php ← DB exceptions
│ ├── MagratheaConfigException.php
│ └── MagratheaModelException.php
├── Compressors/
│ ├── MagratheaCompressor.php ← Abstract base
│ ├── CssCompressor.php ← CSS + SCSS → minified CSS
│ └── JavascriptCompressor.php ← JS → minified JS
├── Errors/
│ └── ErrorManager.php
├── Tests/
│ ├── TestsManager.php
│ ├── TestsHelper.php
│ └── phpUnitBootstrap.php
├── Bootstrap/ ← Setup wizard
└── Admin/ ← Admin panel system
├── Admin.php ← Admin config object
├── AdminManager.php ← Admin runtime (Singleton)
├── AdminFeature.php ← Feature base class
├── AdminMenu.php ← Menu builder
├── AdminForm.php ← Form helpers
├── AdminElements.php ← UI element helpers
├── AdminDatabase.php
├── AdminUrls.php
├── AdminUsers.php
├── ObjectManager.php
├── CodeCreator.php
├── CodeManager.php
├── iAdmin.php
├── Install.php
├── Start.php
└── Features/
├── AppConfig/ ← Manage DB-stored config in admin
├── Cache/ ← Cache management
├── CrudObject/ ← Auto-generated CRUD UI
├── FileEditor/ ← Server-side file editor
├── User/ ← Admin user management
├── UserLogs/ ← Admin action log
└── ApiExplorer/ ← API endpoint browser
Key Concepts
1. Singleton Services
All manager/service classes are singletons. Always access them via ClassName::Instance(), never new ClassName():
Database::Instance()
Config::Instance()
Logger::Instance()
Debugger::Instance()
MagratheaCache::Instance()
AdminManager::Instance()
2. Model + Control Pair
Every database entity has two classes:
- Model (
extends MagratheaModel): instance-level, handles a single row, provides CRUD operations
- Control (
extends MagratheaModelControl): static-only, handles queries returning collections
3. Query Builder Doesn't Execute
Query only builds SQL strings. Execution is done by Database::Instance()->QueryAll($query->SQL()) or through the Control methods.
4. API Structure
MagratheaApi — the router/runner. Register routes with ->Add() or ->Crud(), then call ->Run().
MagratheaApiControl — base for controllers. Override List(), Read(), Create(), Update(), Delete().
- JWT auth is built-in: use
GetAuthorizationToken(), GetTokenInfo(), jwtEncode(), jwtDecode().
5. Configuration
Config files use INI format. Multi-environment: [section:environment] overrides [section] when that environment is active. Environment variable interpolation: password = $=MY_ENV_VAR.
Common Tasks
Adding a new database entity
- Create
models/MyThing.php extending MagratheaModel
- Define
$dbTable, $dbPk, $dbValues
- Create
controls/MyThingControl.php extending MagratheaModelControl
- Set
$modelName, $modelNamespace, $dbTable
Adding a new API endpoint
- Create/extend
api/MyThingApiControl.php extending MagratheaApiControl
- Override methods:
List(), Read(), Create(), Update(), Delete()
- Register in your entry point with
$api->Add(...) or $api->Crud(...)
Adding admin CRUD
- Create a class extending
AdminCrudObject in admin/
- Set
$modelName, $modelNamespace, $controlName, $controlNamespace, $label
- Register via
$admin->AddFeaturesArray([new MyThingAdminFeature()])
Debugging queries
MagratheaPHP::Instance()->Debug();
Debugger::Instance()->Show();
Caching an API response
In the controller method:
public function List(): array {
$this->Cache("my_cache_key");
$data = MyControl::GetAll();
$this->Cache("my_cache_key", json_encode($data));
return $data;
}
Conventions & Rules
| Rule | Reason |
|---|
Never instantiate Singletons with new | Framework guarantees one instance |
Always call Query::Clean() or use PrepareAndExecute() for user input | SQL injection prevention |
Config secrets use $=ENV_VAR in the conf file | Keep secrets out of version control |
Models return ToJson() in API responses (not raw ToArray()) | ToJson() handles relations recursively |
Call MagratheaPHP::LoadVendor() before anything else | Registers the autoloader |
Use ->Prod() in production, ->Dev() in development | Controls error display and debug verbosity |
API controllers throw MagratheaApiException | Automatically converted to structured JSON error |
Documentation
Full documentation lives in documentation/:
| File | Topic |
|---|
documentation/index.md | Master index |
documentation/getting-started.md | Quickstart |
documentation/core/ | MagratheaPHP, Config, Singleton, Helper, Functions |
documentation/database/ | Database, Query Builder, Model, Control |
documentation/api/ | MagratheaApi, ApiControl, Authentication |
documentation/admin/ | Admin panel and features |
documentation/utilities/ | Cache, Mail, Logger, Debugger, Compressors |
documentation/exceptions/ | Exception hierarchy |
documentation/advanced/ | Design patterns, Testing |
External Dependencies
| Package | Used for |
|---|
firebase/php-jwt | JWT token encode/decode |
phpmailer/phpmailer | SMTP email |
scssphp/scssphp | SCSS compilation |
tedivm/jshrink | JS minification |
components/jquery | Admin panel jQuery |
twbs/bootstrap | Admin panel Bootstrap |
Things AI Assistants Should Watch Out For
- Don't assume
new Database() is valid — it's a singleton.
- Query Builder methods return
$this — they don't execute SQL.
MagratheaApiException is caught by the router and turned into a JSON error — throw it freely in controllers.
Config::Get() reads from the [default] section — use GetConfig("section/key") for named sections.
Save() on a model auto-detects insert vs. update — based on whether PK is set.
- The admin panel requires
session_start() — ensure StartSession() is called in bootstrap.
- Route parameters use
{param} syntax in MagratheaApi::Add() — they arrive as array in the controller method.
MagratheaModelControl methods are all static — don't call them on instances.
$dbValues defines the column schema — fields not listed here are ignored on insert/update.
AllowAll() opens CORS to everyone — use Allow([...]) in production for specific origins.
- Author: Paulo Henrique Martins — Platypus Technology
- Official docs: https://www.platypusweb.com.br/magratheaphp2
- License: MIT
---
name: magrathea-php2
description: How to correctly generate code using the MagratheaPHP2 framework. Read before writing any PHP code for a project that uses this framework.
---
# skills.MD — How to Use MagratheaPHP2 in a Project
This file teaches an AI assistant how to correctly generate code using the MagratheaPHP2 framework. It is a practical, task-oriented cookbook. Read it before writing any PHP code for a project that uses this framework.
Reference files:
- `instructions.MD` — project structure and rules
- `documentation/index.md` — full API reference
---
## Table of Contents
1. [Project Bootstrap](#1-project-bootstrap)
2. [Configuration](#2-configuration)
3. [Creating a Model](#3-creating-a-model)
4. [Creating a Control](#4-creating-a-control)
5. [Building Queries](#5-building-queries)
6. [Creating an API Class](#6-creating-an-api-class)
7. [Creating an API Controller](#7-creating-an-api-controller)
8. [JWT Authentication](#8-jwt-authentication)
9. [Caching](#9-caching)
10. [Logging & Debugging](#10-logging--debugging)
11. [Sending Email](#11-sending-email)
12. [Admin Panel — Entry Point](#12-admin-panel--entry-point)
13. [Admin Panel — Admin Class](#13-admin-panel--admin-class)
14. [Admin Panel — CRUD Features](#14-admin-panel--crud-features)
15. [Error Handling](#15-error-handling)
16. [Testing](#16-testing)
17. [Complete Application Skeleton](#17-complete-application-skeleton)
18. [Checklist Before Delivering Code](#18-checklist-before-delivering-code)
---
## 1. Project Bootstrap
**Every entry point starts the same way**, and in real projects this bootstrap lives in one shared file — conventionally `app/_inc.php` — that every entry point (`public/index.php`, `public/admin.php`) requires. Never skip any step.
Real projects organize domain code as **features**, not flat `models/`/`controls`/ folders: one folder per table/domain under `app/features/<Name>/`, added via `->AddFeature(...)` (see §3–4 for what goes inside). Plain `->AddCodeFolder(...)` is still used for non-feature folders like `admin`, `api`, `shared`.
```php
<?php
// app/_inc.php
require __DIR__ . "/../vendor/autoload.php";
error_reporting(E_ALL);
ini_set("display_errors", "1");
try {
Magrathea2\MagratheaPHP::Instance()
->MinVersion("2.1.19") // optional, pins a minimum framework version
->AppPath(realpath(dirname(__FILE__))) // this folder (app/) becomes appRoot
->AddCodeFolder(
"admin",
"api",
"api/Authentication",
"api/Controls",
"shared",
)
->AddFeature( // adds features/<Name> + features/<Name>/Base for each
"Article", "Author",
)
->Dev() // or ->Prod() in production
->Load(); // reads config/magrathea.conf, relative to appRoot's parent
} catch (Exception $ex) {
\Magrathea2\p_r($ex);
}
```
Entry points then just require it:
```php
<?php
// public/index.php
require __DIR__ . "/../app/_inc.php";
// ... build and Run() the API class ...
```
**Rules:**
- `AppPath()` must be called before `Load()`; `config/magrathea.conf` is expected one level **above** `AppPath()` (i.e. sibling of `app/`, not inside it)
- `AddCodeFolder()` paths are relative to `AppPath`
- `AddFeature("Article", ...)` adds `<appRoot>/features/Article` and `<appRoot>/features/Article/Base` to the autoloader — see §3 for what lives in each
- Use `->Dev()` locally, `->Prod()` on servers
- Call `->StartSession()` before any session use (admin panel, etc.)
- `->Connect()` is optional in `_inc.php` — many projects let each request connect lazily; call it explicitly if you need the DB immediately
---
## 2. Configuration
**Config file location:** `<appRoot>/config/magrathea.conf`
Sections are named by **environment**, not by topic. `[general]` holds cross-environment settings, including `use_environment` (which section is active by default — usually overridden by `->Dev()` / `->Prod()`). Each environment section (`[dev]`, `[production]`, …) repeats the full flat set of keys it needs — there is no `[section:env]` override/inheritance syntax.
### Writing a config file
```ini
[general]
use_environment = "default"
time_zone = "America/Sao_Paulo"
[dev]
db_host = "localhost"
db_name = "my_db"
db_user = "root"
db_pass = "secret"
db_port = "3306"
site_path = "/var/www/html"
logs_path = "../logs"
cache_path = "../cache"
timezone = "America/Sao_Paulo"
server_url = "http://localhost:8080"
jwt_key = "a-very-long-random-string-here"
[production]
db_host = "db.prod.server"
db_name = "my_db"
db_user = "app_user"
db_pass = "$=DB_PASSWORD"
db_port = "3306"
site_path = "/var/www/html"
logs_path = "../logs"
cache_path = "../cache"
timezone = "America/Sao_Paulo"
server_url = "https://myapp.example.com"
jwt_key = "$=JWT_SECRET"
```
Keys are quoted strings; indentation with a tab under each section header is the convention seen in real configs (not required by the parser, but keep it for consistency).
### Reading config in code
```php
use Magrathea2\Config;
// Read a single key from the active environment section
$host = Config::Instance()->GetConfig("db_host");
// Read the full active-environment section
$env = Config::Instance()->GetConfigSection();
echo $env["db_host"];
```
**Never hardcode credentials.** Use `$=ENV_VAR_NAME` for secrets.
---
## 3. Creating a Model
A model maps to one database table, and lives in its own **feature** folder (added via `->AddFeature("Article", ...)` in `_inc.php`, see §1). This is the convention observed across real production Magrathea2 projects — prefer it over a flat `models/` folder.
Each feature is **split into a generated `Base` class and a concrete class**:
- `app/features/Article/Base/ArticleBase.php` — holds `$dbTable`, `$dbPk`, `$dbValues`, and FK **relations**. Marked as generated ("changes can be overwritten through the admin") because Magrathea's admin tooling can regenerate it from the DB schema — but hand-writing it in this exact shape is normal when that tooling isn't in play.
- `app/features/Article/Article.php` — the concrete class, `extends Base\ArticleBase`, empty unless you add real business-logic methods.
```php
<?php
// app/features/Article/Base/ArticleBase.php
## FILE GENERATED BY MAGRATHEA.
## This file was automatically generated and changes can be overwritten through the admin
namespace App\Models\Base;
use Magrathea2\iMagratheaModel;
use Magrathea2\MagratheaModel;
class ArticleBase extends MagratheaModel implements iMagratheaModel {
public $id, $title, $body, $author_id, $published, $views;
public $created_at, $updated_at;
protected $autoload = null;
public function __construct($id = 0) {
$this->MagratheaStart();
if (!empty($id)) {
$pk = $this->dbPk;
$this->$pk = $id;
$this->GetById($id);
}
}
public function MagratheaStart() {
$this->dbTable = "articles";
$this->dbPk = "id";
$this->dbValues["id"] = "int";
$this->dbValues["title"] = "string";
$this->dbValues["body"] = "text";
$this->dbValues["author_id"] = "int";
$this->dbValues["published"] = "boolean";
$this->dbValues["views"] = "int";
$this->dbValues["created_at"] = "datetime";
$this->dbValues["updated_at"] = "datetime";
// FK relation example — lazy-loaded getter/setter pair
$this->relations["properties"]["Author"] = null;
$this->relations["methods"]["Author"] = "GetAuthor";
$this->relations["lazyload"]["Author"] = "true";
$this->relations["external"]["author_id"] = "\App\Author\Author";
}
public function GetControl() {
return new \App\Controls\Base\ArticleControlBase();
}
// >>> relations:
public function GetAuthor() {
if ($this->relations["properties"]["Author"] != null) return $this->relations["properties"]["Author"];
$this->relations["properties"]["Author"] = new \App\Author\Author($this->author_id);
return $this->relations["properties"]["Author"];
}
public function SetAuthor($author) {
$this->relations["properties"]["Author"] = $author;
$this->author_id = $author->GetID();
return $this;
}
}
```
```php
<?php
// app/features/Article/Article.php
namespace App\Models;
class Article extends \App\Models\Base\ArticleBase {
public function __construct($id = 0) {
parent::__construct($id);
}
// real business-logic methods go here, e.g.:
public function IsPublished(): bool {
return (bool) $this->published;
}
}
```
**Important:** declare every `$dbValues` field as a public property on the Base class (untyped, comma-declared as above, matching the generator's style). Do NOT use `#[\AllowDynamicProperties]`.
For simple internal tools without the feature/Base convention, a flat single-class model (`$dbTable`/`$dbPk`/`$dbValues` directly in one class extending `MagratheaModel`) also works — but match whatever convention the rest of the project already uses.
### Supported field types
| Type | PHP equivalent |
|------|---------------|
| `int` | integer |
| `boolean` | bool (stored as TINYINT 0/1) |
| `string` | string (VARCHAR etc.) |
| `text` | string (TEXT column) |
| `float` | float (DECIMAL/FLOAT) |
| `datetime` | string in `Y-m-d H:i:s` format |
| `uuid` | string (CHAR(36)), auto-generated UUIDv7 on insert if unset |
`uuid` behaves the same way `created_at`/`updated_at` already do: declare `$this->dbValues["uuid"] = "uuid";` and the framework fills it in on `Insert()` (via `Uuid::V7()`) whenever the property is left empty. An explicitly pre-set value (e.g. from a fixture or migration) is respected and never overwritten. Nothing changes on `Update()` — UUIDs are immutable once assigned. Requires MagratheaPHP2 `2.1.30`+.
### Using a model
```php
// Create and insert
$article = new Article();
$article->title = "Hello World";
$article->body = "My first article.";
$article->published = true;
$article->created_at = now();
$id = $article->Save(); // returns new ID (or ->Insert() explicitly)
// Load by PK
$article = new Article(42);
echo $article->title;
// Update
$article->title = "Updated Title";
$article->Save(); // detects existing PK → UPDATE
// Delete
$article->Delete();
// Serialize for API response — use ToArray() for flat map, ToJson() for full envelope
return $article->ToArray();
```
---
## 4. Creating a Control
Every model gets a companion Control class for static data access — same feature-folder Base split as the model.
```php
<?php
// app/features/Article/Base/ArticleControlBase.php
## FILE GENERATED BY MAGRATHEA.
## This file was automatically generated and changes can be overwritten through the admin
namespace App\Controls\Base;
use Magrathea2\MagratheaModelControl;
class ArticleControlBase extends MagratheaModelControl {
protected static $modelNamespace = "App\\Models\\";
protected static $modelName = "Article";
protected static $dbTable = "articles";
}
```
```php
<?php
// app/features/Article/ArticleControl.php
namespace App\Controls;
use Magrathea2\DB\Query;
class ArticleControl extends \App\Controls\Base\ArticleControlBase {
// real query/business-logic methods go here, e.g.:
public function GetPublished() {
return $this->GetWhere(["published" => 1]);
}
}
```
### Using a control
```php
use App\Controls\ArticleControl;
// All records
$all = ArticleControl::GetAll();
// Filtered
$published = ArticleControl::GetWhere(["published" => 1]);
// First match
$article = ArticleControl::GetRowWhere(["id" => 42]);
// Custom SQL
$recent = ArticleControl::RunQuery(
"SELECT * FROM articles WHERE created_at > '2024-01-01' ORDER BY created_at DESC LIMIT 5"
);
// Count
$total = ArticleControl::QueryOne("SELECT COUNT(*) FROM articles WHERE published = 1");
// Paginated (with total — costs an extra COUNT(*) query)
$total = 0;
$query = \Magrathea2\DB\Query::Select()
->Obj(new \App\Models\Article())
->Where(["published" => 1])
->Order("created_at DESC");
$page = ArticleControl::RunPagination($query, $total, page: 0, limit: 10);
// Paginated for an API response — no COUNT(*) query, has_more computed via limit+1 trick.
// Return the MagratheaPagination directly from a controller action; see §7.
$query = \Magrathea2\DB\Query::Select()
->Obj(new \App\Models\Article())
->Where(["published" => 1])
->Order("created_at DESC");
$pagination = ArticleControl::GetPagination($query, page: 0, limit: 10); // MagratheaPagination
```
---
## 5. Building Queries
Use the Query Builder for anything beyond simple `GetWhere` calls.
```php
use Magrathea2\DB\Query;
use Magrathea2\DB\Database;
// Basic SELECT
$sql = Query::Select()
->Table("articles")
->Where(["published" => 1])
->Order("created_at DESC")
->Limit(10)
->Page(0) // page * limit = OFFSET
->SQL();
// SELECT with model (auto-fills table and fields)
$sql = Query::Select()
->Obj(\App\Models\Article::class)
->Where(["published" => 1])
->SQL();
// JOIN
$sql = Query::Select()
->Obj(\App\Models\Article::class)
->SelectExtra("u.name AS author_name")
->Inner("users u", "u.id = articles.author_id")
->Where(["articles.published" => 1])
->SQL();
// INSERT
$sql = Query::Insert()
->Table("articles")
->Values(["title" => "New Post", "published" => 0, "created_at" => now()])
->SQL();
// UPDATE
$sql = Query::Update()
->Table("articles")
->SetArray(["title" => "New Title", "updated_at" => now()])
->Where("id = 42")
->SQL();
// DELETE
$sql = Query::Delete()
->Table("articles")
->Where("id = 42")
->SQL();
// Execute
$rows = Database::Instance()->QueryAll($sql);
$one = Database::Instance()->QueryRow($sql);
$val = Database::Instance()->QueryOne($sql);
```
**Always use `Query::Clean()` or `PrepareAndExecute()` for user input:**
```php
$safe = Query::Clean($_GET["search"]);
$sql = "SELECT * FROM articles WHERE title LIKE '%$safe%'";
// Better: prepared statement
$result = Database::Instance()->PrepareAndExecute(
"SELECT * FROM articles WHERE title LIKE ?",
["s"],
["%{$_GET['search']}%"]
);
```
---
## 6. Creating an API Class
The preferred pattern is to encapsulate all route definitions in a class that extends `MagratheaApi`. This makes the API reusable — both the HTTP entry point and the Admin ApiExplorer instantiate the same class.
```php
<?php
namespace App;
use Magrathea2\MagratheaApi;
use App\Api\AuthControl;
use App\Api\ArticleApiControl;
class MyAppApi extends MagratheaApi {
public function __construct() {
$this->Initialize();
}
public function Initialize() {
$this->AllowAll();
$this->SetAddress("/api/v1");
$this->HealthCheck(true); // includes "database": "ok"|"fail" in response
$this->SetAuth();
$this->Articles();
}
private function SetAuth() {
$auth = new AuthControl();
$this->BaseAuthorization($auth, "ValidateToken");
$this->Add("POST", "auth/login", $auth, "Login");
$this->Add("GET", "me", $auth, "Me", true);
}
private function Articles() {
$api = new ArticleApiControl();
$this->Add("GET", "articles", $api, "List", true);
$this->Add("GET", "articles/:id", $api, "Read", true);
$this->Add("POST", "articles", $api, "Create", true);
$this->Add("PUT", "articles/:id", $api, "Update", true);
$this->Add("DELETE", "articles/:id", $api, "Delete", true);
}
}
```
**HTTP entry point** (`public/index.php`) — thin wrapper that runs the API class:
```php
<?php
// shared bootstrap (bootstrap.php) sets up MagratheaPHP
$api = require __DIR__ . '/api.php';
$api->Run();
```
**Shared setup file** (`public/api.php`) — returns the configured instance, importable by both entry point and admin:
```php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
// ... MagratheaPHP bootstrap ...
return new \App\MyAppApi();
```
**Key rules:**
- Group routes into private methods by domain (Auth, Articles, etc.)
- `Run()` is called only in the HTTP entry point, never inside the class
- `AllowAll()` is fine for development; use `Allow([...])` in production
- Route params use `:name` syntax — they arrive as `$params["name"]` in the controller
---
## 7. Creating an API Controller
```php
<?php
namespace App\Api;
use Magrathea2\MagratheaApiControl;
use Magrathea2\MagratheaPagination;
use Magrathea2\Exceptions\MagratheaApiException;
use Magrathea2\DB\Query;
use App\Controls\ArticleControl;
use App\Models\Article;
class ArticleApiControl extends MagratheaApiControl {
// GET /articles
public function List(): array {
$articles = ArticleControl::GetWhere(["published" => 1]);
return array_map(fn($a) => $a->ToArray(), $articles);
}
// GET /articles/paginated?page=0 — returning a MagratheaPagination makes
// MagratheaApi::ReturnSuccess() build {success, data, page, count, has_more} automatically.
public function ListPaginated(array $params = []): MagratheaPagination {
$query = Query::Select()
->Obj(new Article())
->Where(["published" => 1])
->Order("created_at DESC");
return ArticleControl::GetPagination($query, page: (int)($params["page"] ?? 0), limit: 20);
}
// GET /articles/:id
public function Read(array $params = []): array {
$article = ArticleControl::GetRowWhere(["id" => $params["id"]]);
if (!$article) {
throw new MagratheaApiException("Article not found", 404);
}
return $article->ToArray();
}
// POST /articles
public function Create(array $data = []): array {
$post = $this->GetPost(); // reads JSON or form body
$article = new Article();
$article->title = $post["title"] ?? "";
$article->body = $post["body"] ?? "";
$article->author_id = $this->GetUserId(); // from JWT
$article->published = false;
$article->created_at = now();
$article->Save();
return $article->ToArray();
}
// PUT /articles/:id
public function Update(array $params = []): array {
$article = ArticleControl::GetRowWhere(["id" => $params["id"]]);
if (!$article) {
throw new MagratheaApiException("Article not found", 404);
}
$put = $this->GetPut();
if (isset($put["title"])) $article->title = $put["title"];
if (isset($put["body"])) $article->body = $put["body"];
$article->updated_at = now();
$article->Save();
return $article->ToArray();
}
// DELETE /articles/:id
public function Delete(array $params = []): bool {
$article = ArticleControl::GetRowWhere(["id" => $params["id"]]);
if (!$article) {
throw new MagratheaApiException("Article not found", 404);
}
return $article->Delete();
}
}
```
**Controller rules:**
- Always throw `MagratheaApiException` for expected errors (404, 400, 403) — the router converts it to JSON automatically
- Use `$this->GetPost()` for POST body, `$this->GetPut()` for PUT body
- Use `$this->GetUserId()` to read the user ID from the decoded JWT token
- Return `->ToArray()` (flat field map) for API responses — never `->ToJson()` (which wraps fields in an outer envelope)
- Never `echo` or `die()` inside controllers
---
## 8. JWT Authentication
### Step 1: Auth controller
```php
<?php
namespace App\Api;
use Magrathea2\MagratheaApiControl;
use Magrathea2\Exceptions\MagratheaApiException;
use Magrathea2\Config;
use App\Controls\UserControl;
class AuthControl extends MagratheaApiControl {
// Override to use config-driven secret
public function GetSecret(): string {
return Config::Instance()->GetConfig("jwt/secret");
}
// POST /auth/login
public function Login(): array {
$post = $this->GetPost();
$user = UserControl::GetRowWhere(["email" => $post["email"] ?? ""]);
if (!$user || !password_verify($post["password"] ?? "", $user->password_hash)) {
throw new MagratheaApiException("Invalid credentials", 401);
}
$token = $this->jwtEncode([
"user_id" => $user->id,
"email" => $user->email,
"role" => $user->role,
"exp" => time() + 86400, // 24 hours
]);
return ["token" => $token, "user" => $user->ToArray()];
}
// Used as base authorization for all protected routes
public function ValidateToken(): bool {
$token = $this->GetAuthorizationToken(); // reads Bearer token
$payload = $this->GetTokenInfo($token); // decodes + verifies
if (!$payload || !isset($payload->user_id)) {
throw new MagratheaApiException("Unauthorized", 0, null, true);
}
$this->userInfo = $payload; // store for downstream use
return true;
}
}
```
### Step 2: Access user in other controllers
```php
public function Create(array $data = []): array {
$userId = $this->GetUserId(); // int|null from token
$info = $this->GetUserInfo(); // full decoded payload object
$role = $info->role ?? "guest";
// ...
}
```
**JWT rules:**
- Always set `"exp"` in the payload
- Never store passwords or sensitive PII in the token
- Always override `GetSecret()` to use a config value, not a hardcoded string
- `GetAuthorizationToken()` throws if the header is missing — call it inside `try/catch` or rely on the route's auth parameter
---
## 9. Caching
`Cache($name, $data=null)` does **not** take the response payload as its second argument. `$data` is an optional key *suffix/discriminator* — it gets appended to `$name` (as `"{$name}-{$data}"`) to build the cache file's handle, e.g. to key the cache by an id or page number. Never pass a JSON blob or the computed result as `$data`.
Calling `$this->Cache($name, $data)` only *peeks*: if a cache file for that key already exists, it outputs it and kills execution (`die`) right there. It never writes anything itself. The actual write-to-disk happens automatically later, when the framework serializes the controller's return value to JSON — no second manual call is needed or correct.
### In a controller (recommended pattern)
```php
public function List(): array {
// Serve from cache if available (outputs and exits on hit).
// Saving happens automatically when the response is emitted.
$this->Cache("articles_published");
$articles = ArticleControl::GetWhere(["published" => 1]);
return array_map(fn($a) => $a->ToArray(), $articles);
}
public function View(array $params = []): array {
// $data suffix keys the cache per-id
$this->Cache("article", $params["id"]);
$article = new ArticleControl($params["id"]);
return $article->ToArray();
}
public function Create(array $data = []): array {
// ... create ...
// Invalidate related cache (same $name/$data used to build the key)
$this->CacheClear("articles_published");
return $article->ToArray();
}
```
### Invalidate by pattern (when many related keys exist)
```php
$this->CacheClearPattern("articles_*");
```
### Cache key naming convention
Always include parameters that affect the result. Prefer the `$data` suffix param for a single discriminator (id, page); bake the rest into `$name`:
```php
$this->Cache("articles_cat_{$categoryId}", $page);
$this->Cache("user_profile", $userId);
"products_active_sort_price"
```
---
## 10. Logging & Debugging
### Logging (production)
```php
use Magrathea2\Logger;
Logger::Instance()->Log("Order #$orderId processed by user #$userId");
try {
// something risky
} catch (\Exception $e) {
Logger::Instance()->LogError($e);
}
Logger::Instance()->SetLogFile("payments")->Log("Payment success: $amount");
```
### Debugging (development only)
```php
use Magrathea2\Debugger;
Debugger::Instance()->Info("Cache miss for: articles_published");
Debugger::Instance()->Add(["user_id" => $id, "role" => $role]);
Debugger::Instance()->Show();
```
---
## 11. Sending Email
### Native mail()
```php
use Magrathea2\MagratheaMail;
$mail = new MagratheaMail();
$mail->SetTo($user->email)
->SetFrom("noreply@myapp.com")
->SetSubject("Welcome!")
->SetHTMLMessage("<h1>Hi!</h1>")
->SetTXTMessage("Hi!")
->Send();
```
### SMTP
```php
use Magrathea2\MagratheaMailSMTP;
$mail = new MagratheaMailSMTP(); // uses [mail] section from config
$mail->SetTo($user->email)->SetSubject("Reset")->SetHTMLMessage($html)->Send();
```
---
## 12. Admin Panel — Entry Point
The admin entry point bootstraps Magrathea (including `StartSession()`), then hands off to `AdminManager`.
```php
<?php
// public/admin.php
use Magrathea2\Admin\AdminManager;
include("bootstrap.php"); // shared MagratheaPHP setup (no Run())
include("../admin/MyAppAdmin.php"); // the Admin class
try {
AdminManager::Instance()->Start(new \App\MyAppAdmin());
} catch (Exception $ex) {
\Magrathea2\p_r($ex);
}
```
**Rules:**
- `bootstrap.php` must call `->StartSession()` for admin auth to work
- `AdminManager::Instance()->Start()` handles auth, rendering, and routing — it does not return
- Never call `Run()` on the API inside an admin entry point
---
## 13. Admin Panel — Admin Class
Extend `\Magrathea2\Admin\Admin` and implement `\Magrathea2\Admin\iAdmin`.
```php
<?php
namespace App;
use Magrathea2\Admin\AdminMenu;
use Magrathea2\Admin\Features\ApiExplorer\ApiExplorer;
use Magrathea2\Admin\Features\AppConfig\AdminFeatureAppConfig;
use App\Admin\ArticleAdmin;
class MyAppAdmin extends \Magrathea2\Admin\Admin implements \Magrathea2\Admin\iAdmin {
private $features = [];
private $apiFeature;
public function Initialize() {
$this->SetTitle("My App Admin");
$this->SetPrimaryColor("#5a2672");
$this->SetAdminLogo(__DIR__ . "/logo.svg"); // absolute path
}
public function Auth($user): bool {
return parent::Auth($user); // uses default Magrathea admin auth
}
public function SetFeatures() {
parent::SetFeatures();
$this->LoadConfig();
$this->LoadFeatures();
$this->LoadApi();
}
public function LoadApi() {
$this->apiFeature = new ApiExplorer();
$this->apiFeature->SetApi(new MyAppApi()); // instantiate the API class
$this->AddFeature($this->apiFeature);
}
public function LoadConfig() {
$this->features["app-config"] = new AdminFeatureAppConfig(true);
$this->features["app-config"]->featureId = "AppConfig";
$this->features["app-config"]->featureName = "Settings";
$this->AddFeature($this->features["app-config"]);
}
public function LoadFeatures() {
$this->AddCrudFeature(new ArticleAdmin());
// add more CRUD features here
}
public function BuildMenu(): AdminMenu {
$menu = new AdminMenu();
$menu->Add($this->features["app-config"]->GetMenuItem());
$menu->Add($menu->CreateTitle("Api"))
->Add($this->apiFeature->GetMenuItem());
$this->AddFeaturesMenu($menu); // auto-adds all CRUD features
$menu->Add(["title" => "Magrathea", "type" => "main"]);
$this->AddMagratheaMenu($menu);
$menu->Add($menu->GetLogoutMenuItem());
return $menu;
}
}
```
**Key methods:**
- `Initialize()` — title, primary color, logo path (called once on load)
- `Auth($user)` — return `parent::Auth($user)` for default Magrathea auth
- `SetFeatures()` — orchestrates LoadConfig / LoadFeatures / LoadApi; always call `parent::SetFeatures()` first
- `LoadApi()` — creates `ApiExplorer`, calls `SetApi(new YourApiClass())`, adds feature
- `LoadFeatures()` — calls `AddCrudFeature()` for each CRUD admin class
- `BuildMenu()` — constructs the sidebar; use `CreateTitle()` for section headers, `AddFeaturesMenu()` to auto-include CRUD items, always end with `GetLogoutMenuItem()`
- `AddFeature()` vs `AddCrudFeature()` — use `AddCrudFeature` for `AdminCrudObject` subclasses, `AddFeature` for everything else
---
## 14. Admin Panel — CRUD Features
Each model gets a companion CRUD admin class extending `AdminCrudObject`.
```php
<?php
namespace App\Admin;
use Magrathea2\Admin\Features\CrudObject\AdminCrudObject;
class ArticleAdmin extends AdminCrudObject {
protected $modelName = "Article";
protected $modelNamespace = "App\\Models\\";
protected $controlName = "ArticleControl";
protected $controlNamespace = "App\\Controls\\";
protected $label = "Articles";
protected $icon = "newspaper"; // Bootstrap Icons name
}
```
**The admin class folder must be registered with `AddCodeFolder()` in bootstrap.**
### Field visibility / permissions
By default `AdminCrudObject` allows full CRUD. Override to restrict:
```php
class ArticleAdmin extends AdminCrudObject {
// ... properties ...
public function CanCreate(): bool { return false; } // read-only list
public function CanEdit(): bool { return false; }
public function CanDelete(): bool { return true; }
}
```
### Bootstrap Icons reference
Common values for `$icon`: `person`, `file-earmark-text`, `card-text`, `pencil`, `trash`, `search`, `newspaper`, `gear`, `box-arrow-right`.
---
## 15. Error Handling
### In API controllers — always use MagratheaApiException
```php
use Magrathea2\Exceptions\MagratheaApiException;
throw new MagratheaApiException("Email is required", 400);
throw new MagratheaApiException("Unauthorized", 401);
throw new MagratheaApiException("You don't have permission", 403);
throw new MagratheaApiException("Resource not found", 404);
throw (new MagratheaApiException("Validation failed", 422))
->SetData(["errors" => $validationErrors]);
try {
// some operation
} catch (\Exception $e) {
throw MagratheaApiException::FromException($e, 500);
}
```
### Global exception handler (optional, add to entry point)
```php
set_exception_handler(function (\Throwable $e) {
Logger::Instance()->LogError($e);
http_response_code(500);
header("Content-Type: application/json");
echo json_encode(["error" => "Internal Server Error"]);
exit;
});
```
---
## 16. Testing
### PHPUnit bootstrap (phpunit.xml)
```xml
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="App">
<directory>tests/</directory>
</testsuite>
</testsuites>
</phpunit>
```
### Mocking for unit tests
```php
use PHPUnit\Framework\TestCase;
use Magrathea2\DB\Database;
use Magrathea2\Config;
class MyTest extends TestCase {
protected function setUp(): void {
Database::Instance()->Mock();
Config::Instance()->SetConfig([
"database" => ["host" => "localhost", "database" => "test"],
"jwt" => ["secret" => "test-secret"],
]);
}
}
```
---
## 17. Complete Application Skeleton
```
my-app/
├── composer.json
├── config/
│ └── magrathea.conf
├── models/
│ └── Article.php
├── controls/
│ └── ArticleControl.php
├── api/
│ ├── AuthControl.php
│ └── ArticleApiControl.php
├── admin/
│ ├── MyAppAdmin.php ← Admin class
│ ├── ArticleAdmin.php ← CRUD feature
│ └── logo.svg
├── public/
│ ├── bootstrap.php ← shared MagratheaPHP setup (returns nothing)
│ ├── api.php ← returns new MyAppApi() (importable)
│ ├── index.php ← require api.php → Run()
│ └── admin.php ← AdminManager::Start(new MyAppAdmin())
├── MyAppApi.php ← extends MagratheaApi
├── cache/
├── logs/
└── vendor/
```
**`public/bootstrap.php`** — shared setup, no output:
```php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Magrathea2\MagratheaPHP;
MagratheaPHP::LoadVendor();
MagratheaPHP::Instance()
->AppPath(__DIR__ . '/..')
->AddCodeFolder("models", "controls", "api", "admin")
->Prod()->Load()->Connect()->StartSession();
```
**`public/api.php`** — importable, returns the configured API:
```php
<?php
require_once __DIR__ . "/bootstrap.php";
return new \App\MyAppApi();
```
**`public/index.php`** — HTTP entry point:
```php
<?php
$api = require __DIR__ . "/api.php";
$api->Run();
```
**`public/admin.php`** — admin entry point:
```php
<?php
use Magrathea2\Admin\AdminManager;
require __DIR__ . "/bootstrap.php";
try {
AdminManager::Instance()->Start(new \App\MyAppAdmin());
} catch (Exception $ex) {
\Magrathea2\p_r($ex);
}
```
---
## 18. Checklist Before Delivering Code
### Bootstrap
- [ ] `MagratheaPHP::LoadVendor()` is called first
- [ ] `AppPath()` is set with `__DIR__` or an absolute path
- [ ] All class folders registered with `AddCodeFolder()` (including `admin/` for admin panels)
- [ ] `->Load()->Connect()` is called before any DB operation
- [ ] `->StartSession()` called when admin panel is used
- [ ] Mode is `->Dev()` for local, `->Prod()` for server
### Models
- [ ] `$dbTable` is set
- [ ] `$dbValues` includes all columns with correct types
- [ ] `$dbPk` is set if primary key is not `"id"`
- [ ] Each `$dbValues` field has an explicit typed public property declaration
- [ ] No `#[\AllowDynamicProperties]` attribute used
- [ ] If the project uses UUIDs as external ids, the field is declared as `"uuid"` type, not hand-rolled
### Controls
- [ ] `$modelName`, `$modelNamespace`, `$dbTable` are all set
- [ ] Namespace matches the actual file location
### API Class
- [ ] Extends `MagratheaApi`
- [ ] Constructor calls `Initialize()`
- [ ] Routes grouped into domain methods
- [ ] `Run()` is NOT called inside the class — only in the HTTP entry point
### API Controllers
- [ ] Every controller extends `MagratheaApiControl`
- [ ] All error cases throw `MagratheaApiException`, not generic exceptions
- [ ] User input never directly interpolated into SQL
- [ ] `GetPost()` / `GetPut()` used (not `$_POST` directly)
- [ ] Returns `->ToArray()` not `->ToJson()` for API responses
### Admin Class
- [ ] Extends `\Magrathea2\Admin\Admin` and implements `\Magrathea2\Admin\iAdmin`
- [ ] `SetFeatures()` calls `parent::SetFeatures()` first
- [ ] `LoadApi()` uses `SetApi(new YourApiClass())` — not `require`
- [ ] `BuildMenu()` ends with `GetLogoutMenuItem()`
- [ ] `AddCrudFeature()` used for `AdminCrudObject` subclasses
### Admin CRUD Features
- [ ] Extends `AdminCrudObject`
- [ ] `$modelName`, `$modelNamespace`, `$controlName`, `$controlNamespace` all set
- [ ] `$label` and `$icon` set for UI
### Config
- [ ] Secrets use `$=ENV_VAR` — never hardcoded
- [ ] Config file at `config/magrathea.conf` relative to `AppPath`
### General
- [ ] `now()` used for datetime fields
- [ ] Cache invalidated when data is mutated
- [ ] Singletons accessed via `::Instance()`, never `new ClassName()`
# Instructions for AI Assistants — MagratheaPHP2
This file is a guide for any AI assistant (Claude, GPT, Gemini, etc.) working on this codebase. Read it before doing any task. It explains the project structure, conventions, key design decisions, and how to approach common tasks.
---
## What This Project Is
**MagratheaPHP2** is a full PHP framework primarily designed for building RESTful APIs. It also ships with an ORM, a query builder, a file-based cache, a logging system, an email system, CSS/JS compression, and an admin panel.
- **Namespace root:** `Magrathea2`
- **Source code:** `src/`
- **Documentation:** `documentation/`
- **Config format:** INI files (`config/magrathea.conf`)
- **PHP version:** 8.0+
- **Database:** MySQL / MariaDB via MySQLi
---
## Source Code Map
```
src/
├── MagratheaPHP.php ← Main bootstrap/entry point (start here)
├── Config.php ← INI-based config
├── ConfigApp.php ← DB-stored key-value config
├── ConfigFile.php ← Config file I/O
├── Singleton.php ← Base singleton pattern
├── MagratheaHelper.php ← Static utility functions
├── MagratheaModel.php ← ORM base model (abstract)
├── MagratheaModelControl.php ← ORM static query interface (abstract)
├── MagratheaApi.php ← RESTful API router
├── MagratheaApiControl.php ← API controller base
├── MagratheaCache.php ← File-based response cache
├── MagratheaMail.php ← Email (native mail())
├── MagratheaMailSMTP.php ← Email (SMTP via PHPMailer)
├── Authentication.php ← JWT token generation
├── Logger.php ← File-based logging
├── Debugger.php ← Debug mode manager
├── _Functions.php ← Global procedural helpers + autoloader
├── _FunctionsDebug.php ← Debug helpers
├── _EnumTrait.php ← Enum trait
├── DB/
│ ├── Database.php ← MySQLi wrapper (Singleton)
│ ├── Query.php ← Fluent SELECT query builder
│ ├── QueryInsert.php ← INSERT query builder
│ ├── QueryUpdate.php ← UPDATE query builder
│ ├── QueryDelete.php ← DELETE query builder
│ ├── QueryHelper.php ← Query utilities
│ └── DatabaseSimulate.php ← Mock database for tests
├── Exceptions/
│ ├── MagratheaException.php ← Base exception
│ ├── MagratheaApiException.php ← API exceptions (carries HTTP status)
│ ├── MagratheaDBException.php ← DB exceptions
│ ├── MagratheaConfigException.php
│ └── MagratheaModelException.php
├── Compressors/
│ ├── MagratheaCompressor.php ← Abstract base
│ ├── CssCompressor.php ← CSS + SCSS → minified CSS
│ └── JavascriptCompressor.php ← JS → minified JS
├── Errors/
│ └── ErrorManager.php
├── Tests/
│ ├── TestsManager.php
│ ├── TestsHelper.php
│ └── phpUnitBootstrap.php
├── Bootstrap/ ← Setup wizard
└── Admin/ ← Admin panel system
├── Admin.php ← Admin config object
├── AdminManager.php ← Admin runtime (Singleton)
├── AdminFeature.php ← Feature base class
├── AdminMenu.php ← Menu builder
├── AdminForm.php ← Form helpers
├── AdminElements.php ← UI element helpers
├── AdminDatabase.php
├── AdminUrls.php
├── AdminUsers.php
├── ObjectManager.php
├── CodeCreator.php
├── CodeManager.php
├── iAdmin.php
├── Install.php
├── Start.php
└── Features/
├── AppConfig/ ← Manage DB-stored config in admin
├── Cache/ ← Cache management
├── CrudObject/ ← Auto-generated CRUD UI
├── FileEditor/ ← Server-side file editor
├── User/ ← Admin user management
├── UserLogs/ ← Admin action log
└── ApiExplorer/ ← API endpoint browser
```
---
## Key Concepts
### 1. Singleton Services
All manager/service classes are singletons. Always access them via `ClassName::Instance()`, never `new ClassName()`:
```php
Database::Instance()
Config::Instance()
Logger::Instance()
Debugger::Instance()
MagratheaCache::Instance()
AdminManager::Instance()
```
### 2. Model + Control Pair
Every database entity has two classes:
- **Model** (`extends MagratheaModel`): instance-level, handles a single row, provides CRUD operations
- **Control** (`extends MagratheaModelControl`): static-only, handles queries returning collections
### 3. Query Builder Doesn't Execute
`Query` only builds SQL strings. Execution is done by `Database::Instance()->QueryAll($query->SQL())` or through the Control methods.
### 4. API Structure
- `MagratheaApi` — the router/runner. Register routes with `->Add()` or `->Crud()`, then call `->Run()`.
- `MagratheaApiControl` — base for controllers. Override `List()`, `Read()`, `Create()`, `Update()`, `Delete()`.
- JWT auth is built-in: use `GetAuthorizationToken()`, `GetTokenInfo()`, `jwtEncode()`, `jwtDecode()`.
### 5. Configuration
Config files use INI format. Multi-environment: `[section:environment]` overrides `[section]` when that environment is active. Environment variable interpolation: `password = $=MY_ENV_VAR`.
---
## Common Tasks
### Adding a new database entity
1. Create `models/MyThing.php` extending `MagratheaModel`
2. Define `$dbTable`, `$dbPk`, `$dbValues`
3. Create `controls/MyThingControl.php` extending `MagratheaModelControl`
4. Set `$modelName`, `$modelNamespace`, `$dbTable`
### Adding a new API endpoint
1. Create/extend `api/MyThingApiControl.php` extending `MagratheaApiControl`
2. Override methods: `List()`, `Read()`, `Create()`, `Update()`, `Delete()`
3. Register in your entry point with `$api->Add(...)` or `$api->Crud(...)`
### Adding admin CRUD
1. Create a class extending `AdminCrudObject` in `admin/`
2. Set `$modelName`, `$modelNamespace`, `$controlName`, `$controlNamespace`, `$label`
3. Register via `$admin->AddFeaturesArray([new MyThingAdminFeature()])`
### Debugging queries
```php
MagratheaPHP::Instance()->Debug(); // enables query logging
Debugger::Instance()->Show(); // dump collected debug info
```
### Caching an API response
In the controller method:
```php
public function List(): array {
$this->Cache("my_cache_key"); // serve cached if exists
$data = MyControl::GetAll();
$this->Cache("my_cache_key", json_encode($data)); // save
return $data;
}
```
---
## Conventions & Rules
| Rule | Reason |
|------|--------|
| Never instantiate Singletons with `new` | Framework guarantees one instance |
| Always call `Query::Clean()` or use `PrepareAndExecute()` for user input | SQL injection prevention |
| Config secrets use `$=ENV_VAR` in the conf file | Keep secrets out of version control |
| Models return `ToJson()` in API responses (not raw `ToArray()`) | `ToJson()` handles relations recursively |
| Call `MagratheaPHP::LoadVendor()` before anything else | Registers the autoloader |
| Use `->Prod()` in production, `->Dev()` in development | Controls error display and debug verbosity |
| API controllers throw `MagratheaApiException` | Automatically converted to structured JSON error |
---
## Documentation
Full documentation lives in `documentation/`:
| File | Topic |
|------|-------|
| `documentation/index.md` | Master index |
| `documentation/getting-started.md` | Quickstart |
| `documentation/core/` | MagratheaPHP, Config, Singleton, Helper, Functions |
| `documentation/database/` | Database, Query Builder, Model, Control |
| `documentation/api/` | MagratheaApi, ApiControl, Authentication |
| `documentation/admin/` | Admin panel and features |
| `documentation/utilities/` | Cache, Mail, Logger, Debugger, Compressors |
| `documentation/exceptions/` | Exception hierarchy |
| `documentation/advanced/` | Design patterns, Testing |
---
## External Dependencies
| Package | Used for |
|---------|---------|
| `firebase/php-jwt` | JWT token encode/decode |
| `phpmailer/phpmailer` | SMTP email |
| `scssphp/scssphp` | SCSS compilation |
| `tedivm/jshrink` | JS minification |
| `components/jquery` | Admin panel jQuery |
| `twbs/bootstrap` | Admin panel Bootstrap |
---
## Things AI Assistants Should Watch Out For
1. **Don't assume `new Database()` is valid** — it's a singleton.
2. **Query Builder methods return `$this`** — they don't execute SQL.
3. **`MagratheaApiException`** is caught by the router and turned into a JSON error — throw it freely in controllers.
4. **`Config::Get()` reads from the `[default]` section** — use `GetConfig("section/key")` for named sections.
5. **`Save()` on a model auto-detects insert vs. update** — based on whether PK is set.
6. **The admin panel requires `session_start()`** — ensure `StartSession()` is called in bootstrap.
7. **Route parameters use `{param}` syntax** in `MagratheaApi::Add()` — they arrive as array in the controller method.
8. **`MagratheaModelControl` methods are all `static`** — don't call them on instances.
9. **`$dbValues` defines the column schema** — fields not listed here are ignored on insert/update.
10. **`AllowAll()` opens CORS to everyone** — use `Allow([...])` in production for specific origins.
---
## Author & Contact
- **Author:** Paulo Henrique Martins — Platypus Technology
- **Official docs:** https://www.platypusweb.com.br/magratheaphp2
- **License:** MIT