Raptor Framework - Full Documentation
codesaur/raptor - A multi-layered, multi-tenant PHP CMS framework built on PSR standards.
Table of Contents
- Introduction
- Installation
- Configuration (.env)
- Architecture
- Middleware Pipeline
- Modules - one subsection per module
- Template System
- Routing
- Controller
- Model
- Testing
- Usage Examples
1. Introduction
codesaur/raptor is a PHP framework built on PSR-7/PSR-15 middleware standards. It ships with two apps by default - Web (public site) and Dashboard (admin panel) - and a developer can add as many app layers as the project needs.
Key Features
- PSR-7/PSR-15 middleware-based architecture
- JWT + Session authentication
- Multi-tenant organizations with RBAC (Role-Based Access Control)
- Multi-language support (Localization)
- CMS modules: News, Pages, Files, References, Settings
- Shop module (Products, Orders, Reviews)
- MySQL or PostgreSQL supported
- SQL file-based database migration system
- codesaur/template custom template engine (Twig-style syntax)
- OpenAI integration (moedit editor)
- Image optimization (GD)
- PSR-3 logging system
- Email delivery (Brevo API, SMTP, PHP mail)
- PSR-14 Event Dispatcher system (Discord, email, logging via listeners)
- Discord webhook notifications (via event listener)
- SEO: Search, Sitemap, XML Sitemap, RSS feed
- Spam protection (honeypot, HMAC token, rate limiting, Cloudflare Turnstile)
- CSRF protection (CsrfMiddleware, csrfFetch)
- Shared hosting / WAF compatibility (cPanel/LiteSpeed/mod_security): HTTP method override, body encoding
- File-based DB cache (PSR-16 SimpleCache) with auto-invalidation
- Contact form with message management
- News article comments with 1-level reply
- Product reviews with star rating (1-5)
- Admin email notifications for contact messages, orders, comments, reviews (configurable per channel)
- Trash module for recovering deleted content records
codesaur Ecosystem
Raptor works together with these codesaur packages:
| Package | Purpose |
|---|---|
codesaur/http-application | PSR-15 Application, Middleware base |
codesaur/http-message | PSR-7 ServerRequest / Response implementations |
codesaur/router | Router base class (pulled in by http-application) |
codesaur/dataobject | PDO-based ORM (Model, LocalizedModel) |
codesaur/template | Template engine (Twig-style syntax, autoescape) |
codesaur/http-client | HTTP client (OpenAI, Brevo mail API, Discord webhook, Turnstile verification) |
codesaur/container | PSR-11 Dependency Injection Container |
2. Installation
Requirements
- PHP 8.2.1+
- Composer
- MySQL or PostgreSQL
- PHP extensions:
ext-gd,ext-intl
Install via Composer
composer create-project codesaur/raptor my-project
The Composer setup-env script (runs on create-project, composer install and composer update) will:
- Auto-copy
.env.exampleto.env(if.envis not already present) - Auto-generate the
RAPTOR_JWT_SECRETkey (only when it is missing, empty, or still the placeholder - a real existing secret is never overwritten)
If
.envwas not created, copy it manually withcp docs/conf.example/.env.example .envand setRAPTOR_JWT_SECRETyourself.
Manual Installation
git clone https://github.com/codesaur-php/Raptor.git my-project
cd my-project
composer install
composer install runs the same setup-env script and creates .env automatically.
3. Configuration
All .env configuration options explained:
Environment & App
# Application name
CODESAUR_APP_NAME=raptor
# Environment mode: development or production
CODESAUR_APP_ENV=development
# Timezone (optional)
#CODESAUR_APP_TIME_ZONE=Asia/Ulaanbaatar
- In
developmentmode, errors are displayed on screen and written tologs/code.log - In
productionmode, errors are only written tologs/code.log
Database
RAPTOR_DB_DRIVER=mysql
RAPTOR_DB_HOST=localhost
RAPTOR_DB_NAME=raptor
RAPTOR_DB_USERNAME=root
RAPTOR_DB_PASSWORD=
RAPTOR_DB_CHARSET=utf8mb4
RAPTOR_DB_COLLATION=utf8mb4_unicode_ci
RAPTOR_DB_PERSISTENT=false
RAPTOR_DB_DRIVER-mysql(default) orpgsql; see "Database Driver Selection" in section 5- In a new environment you MUST create the empty database yourself - Raptor only connects to an existing database, it never creates one (e.g. on MySQL:
CREATE DATABASE raptor CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;) - All tables inside it and the initial seed data (permissions, roles, translations, menu, sample content) are created automatically by Raptor's Model classes on first run
- Never create the tables by hand - the code will not work against a mismatched schema
JWT (JSON Web Token)
RAPTOR_JWT_ALGORITHM=HS256
RAPTOR_JWT_LIFETIME=2592000
RAPTOR_JWT_SECRET=auto-generated
RAPTOR_JWT_SECRET- Auto-generated 128-character (64-byte hex) key by Composer scriptRAPTOR_JWT_LIFETIME- Token validity in seconds (2592000 = 30 days)
Security
RAPTOR_PASSWORD_RESET_MINUTES=10
RAPTOR_SIGNUP_VERIFY_HOURS=72
RAPTOR_PASSWORD_RESET_MINUTES- validity of a password-reset link and the per-email resend cooldown, in minutesRAPTOR_SIGNUP_VERIFY_HOURS- validity of the signup e-mail verification link, in hours
WAF Compatibility (mod_security)
RAPTOR_WAF_BODY_ENCODING=true
RAPTOR_WAF_BODY_ENCODING- Whentrue(default),csrfFetch()base64-encodes form field values so a mod_security-style WAF cannot flag HTML/JS-like rich-text in the POST body;BodyEncodingMiddlewaredecodes them server-side. Setfalseon hosts without a body-inspecting WAF. See the "Shared Hosting / WAF Compatibility" section inCLAUDE.mdfor the full mechanism.
The session cookie lifetime is set to 30 days by Raptor in
SessionMiddleware(session_set_cookie_params(...)) - a client-side cookie that, in practice, keeps an admin logged in even after closing the browser. The server-side session file cleanup (session.gc_maxlifetime), however, follows the host's php.ini rather than the framework; to store session files reliably and have them cleaned up on schedule, configure it per host - see SESSION-LIFETIME.md.
"Why does my PUT/DELETE request show up as POST in the browser?"
This is expected, not a bug. When you open DevTools -> Network and submit a dashboard form declared method="PUT" (or PATCH/DELETE), you will see the request go out as POST. csrfFetch() deliberately rewrites these verbs to POST and carries the real verb in the X-HTTP-Method-Override request header, because many shared hosts (cPanel/LiteSpeed + mod_security) block PUT/PATCH/DELETE at the web-server level before PHP ever runs. On the server, MethodOverrideMiddleware reads that header and restores the real method before routing, so GET_PUT('/news/{id}') still dispatches to its PUT handler.
How to confirm what is happening in DevTools -> Network -> (the request) -> Headers:
- Request method:
POST(the tunnel) X-HTTP-Method-Override: PUT- the real verb the route receivesX-Body-Encoding: base64- present when body encoding is on; the form body shows as base64 instead of readable HTML (this is also intentional - see below)
The URL and the JSON response are identical to a native PUT, so nothing downstream changes.
Disabling body encoding
If your host has no body-inspecting WAF and you would rather see/inspect raw form bodies:
- Site-wide: set
RAPTOR_WAF_BODY_ENCODING=falsein.env.Controller::template()then emits<meta name="waf-body-encoding" content="0">andcsrfFetch()stops encoding. (The server-side decode is header-gated, so it simply never triggers - safe either way.) - Custom layouts: the
<meta name="waf-body-encoding">tag is the only channel that carries this flag to the browser. If you do not use the shippeddashboard.html, you must include it, or the=falsesetting is silently ignored on the client. - One-off raw request:
csrfFetch()has no per-call opt-out. To send a single request with the verb and body verbatim, call plainfetch()instead - but add theX-CSRF-TOKENheader yourself (getCsrfToken()) for CSRF-protected routes. This is mainly for calling non-dashboard/external endpoints; for dashboard routes prefercsrfFetch().
Note: method override (the verb -> POST rewrite) has no off switch - it is always on but is a no-op unless the override header is present, so it never needs disabling. To hit an endpoint with a genuine PUT/DELETE verb, the host must allow those verbs and you must use a client other than csrfFetch() (e.g. plain fetch()).
RAPTOR_MAIL_FROM=noreply@codesaur.net
#RAPTOR_MAIL_FROM_NAME="Raptor Notification"
#RAPTOR_MAIL_REPLY_TO=
#RAPTOR_MAIL_REPLY_TO_NAME=
# Transport: brevo (default), smtp, mail
#RAPTOR_MAIL_TRANSPORT=brevo
#RAPTOR_MAIL_BREVO_APIKEY=
# SMTP settings (when transport=smtp)
#RAPTOR_SMTP_HOST=smtp.gmail.com
#RAPTOR_SMTP_PORT=465
#RAPTOR_SMTP_USERNAME=
#RAPTOR_SMTP_PASSWORD=
#RAPTOR_SMTP_SECURE=ssl
# Admin e-mail notifications (an address = on, empty = off)
RAPTOR_CONTACT_EMAIL_TO=
RAPTOR_ORDER_EMAIL_TO=
RAPTOR_COMMENT_EMAIL_TO=
RAPTOR_REVIEW_EMAIL_TO=
send()selects transport based onRAPTOR_MAIL_TRANSPORTenv var (brevo/smtp/mail)RAPTOR_*_EMAIL_TO- recipient of the admin notification for contact messages, orders, comments and reviews; an empty value turns that notification off.system_codercan edit them from the module index pages (SettingsController::updateEnv())
OpenAI
#RAPTOR_OPENAI_API_KEY=sk-your-api-key-here
#RAPTOR_OPENAI_MODEL=gpt-5-mini
#RAPTOR_OPENAI_VISION_MODEL=gpt-5.1
- Used by the moedit editor's AI button (
AIHelper) RAPTOR_OPENAI_MODEL/RAPTOR_OPENAI_VISION_MODEL- optional model overrides for the HTML and vision modes (defaultsgpt-5-mini/gpt-5.1)
Image Optimization
RAPTOR_CONTENT_IMG_MAX_WIDTH=1920
RAPTOR_CONTENT_IMG_QUALITY=90
- CMS image uploads are optimized using the GD extension
Cloudflare Turnstile
#RAPTOR_TURNSTILE_SITE_KEY=
#RAPTOR_TURNSTILE_SECRET_KEY=
- Optional: If not set, Turnstile widget is not shown and server-side check is skipped
- Used by
SpamProtectionTraitfor CAPTCHA verification on public forms
Discord Notifications
#RAPTOR_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
- Optional: If empty or not set, no notifications are sent
- Used by
DiscordNotifierservice for system event notifications
Server Configuration
CRITICAL: the web server document root MUST be
public_html/, never the project root.public_html/is the only directory meant to be web-served; everything else (application/,protected/,database/,vendor/,.env,logs/, ...) lives one level above it and must stay unreachable by URL. Pointing the docroot at the project root would expose source code, the.envsecrets, uploaded files, and session data. This single rule is the primary, server-agnostic protection (Apache and Nginx alike) - the.htaccess/ nginxdenyrules are only a secondary fallback for exactly this misconfiguration. On shared hosting, set the domain's document root to.../public_html; the example configs below already do this.
Example configuration files for Apache and Nginx are available in docs/conf.example/:
| File | Description |
|---|---|
.env.example | Environment variables reference |
.htaccess.example | Apache URL rewrite and HTTPS redirect |
.nginx.conf.example | Nginx server block (HTTP, HTTPS, PHP-FPM) |
CI/CD
The framework includes 2 GitHub Actions workflows:
CI (.github/workflows/ci.yml)
Default workflow included in the repository. Runs code quality checks on every push and pull request to main:
composer validate --strict- validate composer.json- PHP syntax check - every
.phpfile underapplication/andpublic_html/ - Merge conflict markers - detect
<<<<<<<,=======,>>>>>>> - Debug statements -
var_dump,dd,print_rwarnings composer dump-autoload --strict-psr- autoload verification
Deploy (.github/workflows/deploy.yml)
Unified deploy workflow with 2 deploy jobs - FTP and SSH - plus a small check-targets pre-check job that detects which secrets/variables are set. Each deploy job runs only when its required secrets/variables are configured. All configured jobs run in parallel.
A / B below are jobs of this workflow and cover virtually every environment - shared hosting, VPS, cloud VM, dedicated. C is outside the workflow: a fallback used only when neither A nor B can reach the server.
Execution flow:
Push to main -> CI workflow runs -> Success -> Deploy workflow starts
-> Failure -> Deploy is skipped
The deploy workflow uses a workflow_run trigger to wait for the CI workflow result. Deploy starts only when CI succeeds (conclusion == 'success'). If CI fails, deploy is skipped - broken code never reaches the server. If no deploy secrets/variables are configured (e.g. developer clone), the three deploy jobs are silently skipped (only check-targets runs).
A) FTP Deploy
For any server with FTP access (shared hosting, VPS, dedicated). Add the following secrets in Settings -> Secrets and variables -> Actions -> Secrets:
| Secret | Description | Example |
|---|---|---|
FTP_HOST | FTP server address | ftp.example.com |
FTP_USERNAME | FTP username | user@example.com |
FTP_PASSWORD | FTP password | |
FTP_SERVER_DIR | Target directory on server | / |
B) SSH Deploy
For Linux servers with SSH access (VPS, cloud VM, dedicated). Add the following secrets in Settings -> Secrets and variables -> Actions -> Secrets:
| Secret | Description | Example |
|---|---|---|
SSH_HOST | Server address | example.com or 1.2.3.4 |
SSH_USERNAME | SSH username | deploy |
SSH_KEY | SSH private key (full content of id_rsa) | |
SSH_DEPLOY_DIR | Target directory on server | /var/www/myproject |
SSH_PORT | (optional) SSH port, default: 22 | 22 |
C) cPanel Git Deploy (fallback - only when neither A nor B can reach the server)
The two paths above are the standard ones. Being hosted on cPanel does NOT by itself mean you need this path - if the host offers FTP (A) or SSH (B) access, use those.
A few rare environments, however, are unreachable by any GitHub Actions job: cPanel shared hosting with SSH/Terminal disabled and no externally reachable FTP. A real-world example of such an environment is the shared hosting operated by the National Data Center of Mongolia for government agency web portals. Only in that case, use the scaffold built on cPanel's own Git + cron:
| File | Role |
|---|---|
docs/conf.example/.cpanel.yml.example | Copy to repo root as .cpanel.yml - cPanel Git deploy task list |
docs/conf.example/auto-deploy.sh.example | Copy to deploy/auto-deploy.sh, run via cron |
Full guide: docs/mn/CPANEL.md
Note: The deploy workflow requires CI (ci.yml) to exist. If CI workflow is removed, deploy will not trigger.
Excluded from deployment
.env- Create and configure manually on the server- Runtime folder contents -
cache/,logs/,protected/,public_html/public/anddatabase/migrations/deploy as folders with their guard files (.htaccessetc.) and are created on the server, but their runtime contents (cache entries, logs, uploaded files, migration SQL) are never uploaded, overwritten or deleted by a deploy. Each deploy path enforces this with its own mechanism, so the three filter lists indeploy.ymlintentionally differ - see the comments there before "harmonizing" them. docs/,tests/- Development only.github/,nbproject/,phpunit.xml,.git*,error_log- Repository, IDE and test-runner filesvendor/- Built during the workflow withcomposer install/update --no-dev
4. Architecture
Two-Layer Structure
public_html/index.php (Entry point)
|
|-- /dashboard/* -> Dashboard\Application (Admin Panel)
| |-- Middleware: ErrorHandler -> MethodOverride -> BodyEncoding -> Session -> JWT -> Container -> Localization -> Settings (CSRF is per-route)
| |-- Routers: Login, Users, Organization, RBAC, Localization, Contents (news, pages, references, settings, messages, comments), Logs, Migration, Trash, Template, Home, Shop, Manual, Development, File, Badge
| \-- Controllers -> Templates -> HTML Response
|
|-- /{xx}/* -> Web\Application mounted on /{xx} (xx = two-letter code of a non-default active language; sets the 'language_prefix' attribute)
|
\-- /* -> Web\Application (Public Website, default language - no prefix)
|-- Middleware: ExceptionHandler -> MethodOverride -> BodyEncoding -> Container -> Session -> Localization -> Settings
|-- Router: WebRouter (/, /page, /news, /contact, /products, /cart, /order, /search, /sitemap, /rss, /session/language, /session/contact-send, /session/cart/add, /session/cart/update, /session/order, /session/news/{id}/comment, /session/product/{id}/review, ...)
\-- Controllers -> Templates -> HTML Response
Request Flow
Browser -> index.php -> .env -> ServerRequest
-> Application selection (by URL path)
-> Middleware chain (in order)
-> Router match
-> Controller::action()
-> Model (DB)
-> FileTemplate (codesaur/template) -> render()
-> HTML Response -> Browser
Directory Structure
Raptor follows the MVC pattern but uses a modular (package-by-feature)
organization: each module bundles its Controller, Model, and templates together in
one folder, rather than splitting them across separate layer directories (there is
no top-level Models/, Controllers/, or Views/). Adding or removing a feature
is as simple as copying or deleting one folder.
application/ holds two apps: dashboard/ (admin panel application, which also
hosts the shared platform modules - base Controller, middlewares, models - used
by web) and web/ (public website application). See the "Directory Structure"
section of the root README.md for the app-level layout. Each module's folder
location and classes are documented in its own subsection of section 6.
Everything outside
vendor/is yours. Aftercomposer create-project,application/,public_html/,database/,tests/,docs/and the config files all become part of your project - freely modify, delete, or rewrite them to fit your needs. There is no separate "framework core" layer:application/holds two apps,dashboard/andweb/, and both are plain project code. The default codebase is a baseline covering a developer's common needs, so adapt the code directly to your own detailed requirements. Only thevendor/*packages are Composer-managed dependencies (updated viacomposer update); leave those untouched.
5. Middleware Pipeline
Middleware are PSR-15 standard layers that process request/response. Registration order matters!
The PDO connection is opened once in public_html/index.php via
\Dashboard\DatabaseConnection::connect() and reaches the Application as the
request's pdo attribute.
Dashboard Middleware
| # | Middleware | Purpose |
|---|---|---|
| 1 | ErrorHandler | Returns errors as JSON/HTML |
| 2 | MethodOverrideMiddleware | Restores PUT/PATCH/DELETE from X-HTTP-Method-Override (WAF verb-block workaround). Runs before Session/routing so the real verb is visible everywhere |
| 3 | BodyEncodingMiddleware | base64-decodes form fields sent with X-Body-Encoding (WAF body-inspection workaround) |
| 4 | SessionMiddleware | Starts and manages PHP session |
| 5 | JWTAuthMiddleware | Validates JWT and creates User object |
| 6 | ContainerMiddleware | Injects DI Container |
| 7 | LocalizationMiddleware | Determines language and translations |
| 8 | SettingsMiddleware | Injects system settings |
CsrfMiddlewareis NOT in the app-wide pipeline - it is attached per-route to each mutating route in the router (see 6.20).
Web Middleware
| # | Middleware | Purpose |
|---|---|---|
| 1 | ExceptionHandler | Renders error pages using templates |
| 2 | MethodOverrideMiddleware | Restores PUT/PATCH/DELETE from X-HTTP-Method-Override (shared with the dashboard) |
| 3 | BodyEncodingMiddleware | base64-decodes form fields sent with X-Body-Encoding (header-gated, no effect on plain web forms) |
| 4 | ContainerMiddleware | DI Container |
| 5 | SessionMiddleware | Session for the /session/* routes (contact form, order, comment, review spam state) |
| 6 | LocalizationMiddleware | Language from the URL prefix only (/en/...), default language when there is no prefix - no session key |
| 7 | SettingsMiddleware | Settings (logo, title, footer) |
Database Driver Selection
The driver is chosen via the RAPTOR_DB_DRIVER variable in .env
(mysql or pgsql). \Dashboard\DatabaseConnection::connect() reads it
and returns the corresponding PDO instance:
# MySQL (default)
RAPTOR_DB_DRIVER=mysql
# PostgreSQL
RAPTOR_DB_DRIVER=pgsql
6. Modules
6.1 Authentication
Classes: LoginRouter, LoginController, JWTAuthMiddleware, SessionMiddleware, User
- JWT + Session combined authentication
- Login / Logout / Forgot password / Signup
- Password reset tokens are single-use: consumed via
deactivateById()on successful reset (not deleted) - the admin requests modal shows them with used / expired / ready states - Organization selection (multi-org users)
- JWT stored in
$_SESSION['RAPTOR_JWT'] Userobject contains profile, organization, RBAC permissions
6.2 User Management
Classes: UsersRouter, UsersController, UsersModel
- User CRUD (Create, Read, Update, Deactivate/soft delete)
- Passwords stored using bcrypt hash
- Profile fields: username, email, phone, first_name, last_name
- Avatar image upload
6.3 Organization
Classes: OrganizationRouter, OrganizationController, OrganizationModel, OrganizationUserModel
- Organization CRUD
- User-organization relationship management
- One user can belong to multiple organizations
- Topbar organization switcher: users with more than one organization switch via a dropdown on the topbar brand area (search filter appears above 10 organizations)
system_codercan switch into ANY active organization - access is derived from the role, no membership rows are created
6.4 RBAC (Access Control)
Classes: RBACRouter, RBACController, ActAsController, RBAC, Role, Roles, Permissions, RolePermission, UserRole
- Create and manage roles
- Create and manage permissions
- Role-Permission assignments
- User-Role assignments
- Act As (role preview): picks a role from the account dropdown and uses the dashboard (menu, buttons, actions) with that role's permissions -
system_coder: any role except coder,system_rbacholders: roles within their own permissions, other users with 2+ roles: their own roles; changes are real and attributed to the user, ends after 1 hour, shown with a banner and audited indashboard_log - Check permissions in controllers:
// Check if user has "admin" role in system organization
$this->isUser('system_admin');
// Check if user has "news_edit" permission
$this->isUserCan('news_edit');
6.5 Files
Classes: FileRouter, FileController, FilesController, FilesModel, ProtectedFilesController
- File upload (native JS, FormData)
- Image optimization (GD)
- Files organized by module/table
- MIME type detection
- Protected files (authenticated users only, authorizeRead() hook)
6.6 Content - News
Classes: NewsController, NewsModel
- News CRUD (hard delete with Trash backup)
- Cover image upload
- File attachments
- Publish date management
- View count (read_count)
- Content editing with moedit editor
- Sample data reset: clear seed data and restart with ID=1 (
reset()method)
6.7 Content - Pages
Classes: PagesController, PagesModel
- Page CRUD (hard delete with Trash backup) with simplified single-form interface (no type wizard)
- Parent-child structure (multi-level navigation menu)
positionfield for orderingtypefield: free text with dropdown suggestions, defaultmenu; pages whose type ismenuor ends with-menuappear in the public navigation. Parent/child is set through the parent dropdown (parent_id), not throughtype- Parent pages (pages with children) automatically hide content fields (description, content, link, featured) in edit form
is_featuredfield: featured pages in footer (auto-reset to 0 when page becomes a parent)linkfield: URL or local path with frontend + backend validation (isValidLink())read()guards: published check, parent check, link redirect- SEO slug generation (
generateSlug) - File attachments
- Sample data reset: clear seed data and restart with ID=1 (
reset()method)
6.8 Content - References
Classes: ReferencesController, ReferenceModel, TemplateService (e-mail template loader, cached)
- Reference tables (key-value style)
- Multi-language (LocalizedModel)
- Dynamic table names
6.9 Content - Settings
Classes: SettingsController, SettingsModel, SettingsMiddleware
- System-wide settings (multi-language)
- Site title, logo, description
- Favicon, Apple Touch Icon
- Contact information (phone, email, address)
- Footer information (copyright, social links)
SettingsMiddlewareinjects settings into request attributes
6.10 Localization
Classes: LocalizationRouter, LocalizationController, LanguageModel, TextModel, LocalizationMiddleware
- Add / edit / remove languages
- Translation text management (key -> value)
- Language resolution: URL prefix (
language_prefix, public web) -> session (RAPTOR_LANGUAGE_CODE, dashboard only) -> default language - Use in Templates:
{{ 'key'|text }}
6.11 Logging
Classes: LogsRouter, LogsController, Logger
- PSR-3 standard logging system
- Logs stored in database
- Log levels: emergency, alert, critical, error, warning, notice, info, debug
- Auto-captures server request metadata
- Auto-captures authenticated user info
- Error log viewer tab (system_coder users) - view the PHP error log file configured in
error_log(logs/code.logby default) directly in the Access Logs page
6.12 Mail
Classes: Mailer
send()selects transport viaRAPTOR_MAIL_TRANSPORTenv var (brevo/smtp/mail)- HTML messages, CC/BCC, attachments
6.13 Template (Dashboard UI)
Classes: TemplateRouter, TemplateController, DashboardTrait, MenuModel
- Dashboard layout rendering via
DashboardTrait::dashboardTemplate() - Sidebar menu with i18n, permissions, parent/child hierarchy (
MenuModel) - Menu management CRUD (insert, update, hard delete with Trash backup)
- SweetAlert2, motable, moedit JS components
- Responsive Bootstrap 5 design
6.14 Shop (E-Commerce)
Classes: ProductsController, OrdersController, ReviewsController, ShopRouter (unified router for products + orders + reviews), ProductsModel, ProductOrdersModel, ReviewsModel
- Product CRUD (hard delete with Trash backup) with slug generation, excerpt extraction
- Product fields: price, sale_price, SKU, barcode, sizes, colors, stock, category, featured, review toggle
- Order management (
products_orderstable) with customer info and status tracking - Web cart: session cart (
Web\Shop\Cart,/cart,/session/cart/add,/session/cart/update) checked out as one multi-product order (itemsJSON price snapshot +total); an order always covers the whole cart - the product page has a single "Add to cart" button whose dialog offers "Continue shopping", "View cart" or "Checkout". Customer accounts / social login are not part of the framework - see WEB-MEMBERSHIP.md - Product reviews with star rating (1-5) and written comments
- Reviews displayed in product detail view (both web and dashboard)
- Media gallery on web product page (thumbnail strip + large preview for images/video/audio)
- Sample product data seeded on first run
- PSR-14 event-driven notifications for new orders, status changes, and reviews
- Admin email notification for new orders (configurable: toggle + recipient email,
system_coderonly)
6.15 Reviews (Product Reviews)
Classes: ReviewsController, ReviewsModel (dashboard, registered via ShopRouter), ShopController::reviewSubmit() (web)
- Public review form on product detail pages (when
review=1) - Star rating (1-5) with written review text
- Guest reviews with name and optional email
- Spam protection via
SpamProtectionTrait(honeypot, HMAC, rate limiting, Turnstile) - Average rating and review count displayed on product listing cards
- Dashboard: reviews shown in products-view with delete support
- Dashboard: reviews index accessible from products-index header link
- Badge: new reviews appear as
info(cyan) badge on products sidebar item - Admin email notification (configurable: toggle + recipient email,
system_coderonly, disabled by default) - Web-side review submission via
/session/product/{id}/review
6.16 Event System & Notification
Classes: EventDispatcher, ListenerProvider, ContentEvent, UserEvent, OrderEvent, DevRequestEvent, DiscordListener
- PSR-14 Event Dispatcher system replaces direct Discord calls
- Event classes:
ContentEvent,UserEvent,OrderEvent,DevRequestEvent ListenerProviderregisters listeners (currentlyDiscordListener)DiscordListenersends Discord webhook notifications for all event types- Controllers dispatch events via
$this->dispatch(new ContentEvent(...))helper DiscordNotifierstores admin name and dashboard URL (injected viaContainerMiddleware)- Notification types: user signup request, user approval, new order, order status change, new dev request / dev request response, new contact message, new news comment, new product review, settings update, content actions (insert, update, delete, publish)
- The listener branches on the event's
actionstring and silently ignores unknown values - a dispatcher must use exactly the actions listed indocs/en/api.mdfor each event class (tests/Unit/Notification/EventDispatchServiceTest.phpscans for mismatches) - Color-coded Discord embed messages
- Configured via
RAPTOR_DISCORD_WEBHOOK_URLenv variable - Gracefully skips if webhook URL is not set or listener is unavailable
6.17 Development Tools
Classes: DevelopmentRouter, DevRequestController, DevRequestModel, DevResponseModel
- Development request tracking system (submit requests, respond, view history)
- Lives in
application/dashboard/development/(Dashboard\Developmentnamespace) - Any authenticated user can create requests and can view, respond to and delete only their own or assigned ones
- Users with the
system_developmentpermission manage all requests (view, respond, delete any)
6.18 Site Service (Web)
Classes: SearchController, SeoController
- Full-text search across pages, news, and products (
SearchController,/search?q=) - Human-readable sitemap with hierarchical page tree
- XML sitemap (
/sitemap.xml) for search engines - RSS 2.0 feed (
/rss) with latest news and products robots.txt- Search engine bot instructions included inpublic_html/
robots.txt
A pre-configured robots.txt is included at public_html/robots.txt. It controls which bots can access your site:
- Allowed: Googlebot, Bingbot, YandexBot, Baiduspider
- Blocked: SEO scrapers (MJ12bot, SemrushBot, AhrefsBot, DotBot, Bytespider, PetalBot)
- AI bots: Allowed by default (GPTBot, ClaudeBot, etc.). Uncomment lines to block
- Dashboard:
/dashboard/is disallowed for all bots - Sitemap: Update the
Sitemap:line with your actual domain
Sitemap: https://example.com/sitemap.xml
6.19 Spam Protection
Classes: SpamProtectionTrait
- Honeypot hidden field detection
- HMAC token validation with timestamp
- Rate limiting per action (login 2s, signup 5s, forgot 10s)
- Form expiration (1 hour max)
- Minimum fill time check (1 s for login/signup/forgot, 2-3 s for the public forms)
- Cloudflare Turnstile CAPTCHA support (enabled when
RAPTOR_TURNSTILE_SECRET_KEYis set in.env) - Link spam filter (blocks text with excessive URLs)
- Applied to login, signup, forgot password, contact, comment, review, and order forms
6.20 CSRF Protection
Classes: CsrfMiddleware
- Per-route middleware (NOT app-wide). Attached to each mutating route in the router via
->middleware([CsrfMiddleware::class])(withuse Dashboard\CsrfMiddleware;) - The middleware ONLY validates: it compares
$_SESSION['CSRF_TOKEN']against theX-CSRF-TOKENheader and returns 403 on mismatch - GET/HEAD/OPTIONS requests pass through without validation (protects the GET side of
GET_POST/GET_PUTcompound routes) - Token is generated at login and stored in
$_SESSION['CSRF_TOKEN']. As a fallback for old sessions,Controller::template()generates it when an authorized user has none and the session is writable - Token delivered to frontend via
<meta name="csrf-token">indashboard.html(Controller::template()reads it from session) - Client JS sends token via
X-CSRF-TOKENheader usingcsrfFetch()wrapper - For new modules: every mutating route MUST add
->middleware([CsrfMiddleware::class])(NOT automatic); only login routes are exempt. On the client, usecsrfFetch()instead offetch()for all state-changing requests
6.21 Database Migration
Classes: MigrationRunner, MigrationController, MigrationRouter, MigrationSecurityScanner
- File-based forward-only SQL migration system; state lives on disk (no tracking table)
database/migrations/is git-ignored - per-environment upload- Per-user folder:
{userId}-{username}/holds pending files,{userId}-{username}/ran/holds applied files - Coders upload
.sqlvia the dashboard (max = min(10 MB, php.ini post_max_size, upload_max_filesize)); Apply runs the file and moves it toran/on success MigrationSecurityScannerflags writes against sensitive tables (users,rbac_*,organizations*,localization_language,raptor_menu) and DCL (GRANT/REVOKE/CREATE-DROP-ALTER USER); a soft warning requires the coder to typeCONFIRMbefore apply proceeds- Advisory lock (
GET_LOCK/pg_try_advisory_lock) prevents concurrent apply - Every upload/apply/delete is logged to
dashboard_logwith SHA-256, statement count, and warning count - Protected: only
system_coderusers can access the dashboard - Parent
database/.htaccess(deny from all) protection blocks direct browser access to SQL files
6.22 Messages (Contact Form)
Classes: MessagesController, MessagesModel (dashboard), ContactController (web)
- Public contact form (
/contact) with spam protection - Contact form submissions stored in database
- Dashboard interface for viewing and managing messages
- View message details in modal dialog
- Hard delete with Trash backup (replaced soft delete/deactivate)
- Event-driven notification on new contact message (Discord via PSR-14 listener)
- Admin email notification (configurable: toggle + recipient email,
system_coderonly) - Web-side
ContactControllerhandles form display and submission via/session/contact-send
6.23 Comments (News)
Classes: CommentsController, CommentsModel (dashboard), NewsController::commentSubmit() (web)
- Public comment form on news article pages
- 1-level reply support (parent_id for replies to top-level comments)
- Guest comments with name and email fields
- Authenticated users auto-fill name/email from profile
- Spam protection via
SpamProtectionTrait(honeypot, HMAC, rate limiting, Turnstile) - Dashboard: comments shown in news-view with reply and delete support
- Dashboard: comments index accessible from news-index header link
- Badge: new comments appear as
info(cyan) badge on news sidebar item - Hard delete with Trash backup (replaced soft delete/deactivate)
- Admin email notification (configurable: toggle + recipient email,
system_coderonly, disabled by default) - Web-side comment submission via
/session/news/{id}/comment
6.24 Badge System (Sidebar Badges)
Classes: Dashboard\Badge\BadgeController, Dashboard\Badge\BadgeRouter, Dashboard\Badge\AdminBadgeSeenModel (application/dashboard/badge/)
- Colored badge pills on sidebar menu items showing unseen activity counts per admin
- Reads from existing
*_logtables - no separate event table - Multi-tenant:
orgScopedModules()scopes listed modules' badges to the viewing admin's organization, filtering by the record's org (record_organization_idin log context) with the actor's org as fallback;system_coderand admins viewing from the system organization (isSystemWideViewer()) see all organizations - Badge colors: green (create), info/cyan (new comment or review), blue (update), red (delete)
- Up to 4 badges per module, shown left to right in green-info-blue-red order
- Filters by admin permissions (PERMISSION_MAP) and excludes admin's own actions
- First-time users get 30-day lookback
- File-count badges for manual and migrations (non-log based)
- JS:
initSidebarBadges()indashboard.jsfetches and renders badges on page load
6.25 Dashboard Home
Classes: HomeRouter, HomeController, SearchController, WebLogStatsController, WebLogStats, PhpSecurityStatus
- Dashboard home page with system overview
- Topbar quick icons (search | language | theme): search modal (Ctrl+K) across news, pages, products, orders, users, organizations, dev-requests, messages, comments, and reviews (RBAC-filtered, each source gated by its module's index permission or row-level filter); language dropdown (session-persisted); light/dark theme dropdown (instant, no reload); account dropdown (name / email / organization header, My profile, Logout). On mobile (<768px) the topbar keeps only sidebar toggle | brand | search | account; language and theme move to the bottom of the offcanvas sidebar, and the organization switcher opens as a full-width panel under the topbar
- Web visit statistics with chart data, top pages/news/products, IP addresses
- System log statistics per
*_logtable (today/week/total counts) web_log_cachetable for performance optimization- Server PHP security notice (
system_coderonly, home page only): the running PHP version against a known advisory, and - when vulnerable - the SAPI, php.ini path and the actual values of the interim mitigation settings with a per-SAPI hint. See PHP-SECURITY.md
6.26 Dashboard Manual
Classes: ManualRouter, ManualController
- Lists all help/manual HTML files grouped by module with language variants
- Displays specific manual with language fallback to English
- Manual files stored in
application/dashboard/manual/as{name}-manual-{lang}.html
6.27 AI Helper (moedit)
Classes: AIHelper
- OpenAI API integration for moedit WYSIWYG editor
- HTML mode: content enhancement with Bootstrap 5 components - model via
RAPTOR_OPENAI_MODEL(.env, defaultgpt-5-mini) - Vision mode: OCR/image text recognition - model via
RAPTOR_OPENAI_VISION_MODEL(.env, defaultgpt-5.1) - Endpoint:
POST /dashboard/content/moedit/ai - Requires
RAPTOR_OPENAI_API_KEYin.env
6.28 Seed and Initial Data
Classes: PermissionsSeed, RolePermissionSeed, MenuSeed, TextInitial, ReferenceInitial, NewsSamples, PagesSamples, ProductsSamples
- Automatically populate database on fresh installs via Model
__initial()methods - Permissions: 18+ system permissions with
system_prefix - Roles: coder, admin, manager, editor, viewer with permission assignments
- Menu: 4-section dashboard sidebar (Contents, Shop, System, Coder - the last visible to system_coder only) with i18n titles
- Translations: 100+ system UI keywords in MN/EN
- Reference templates: 11+ email templates (password reset, notifications, order confirmation) + ToS/PP
- Sample data: demo news (6), pages (14+), products (4) - removable via dashboard "Reset" button
6.29 Trash
Classes: TrashRouter, TrashController, TrashModel
- Stores JSON snapshots of content records right after their hard deletion (
deleteById()first, thenTrashModel::store()) - Content models have no
is_activecolumn; deletion goes throughdeleteById()+ Trash for: News, Pages, Products, Orders, Reviews, Comments, Messages, Files, References, DevRequests, Menus, Texts, Languages - Users, Organizations and Forgot (password-reset tokens) keep the
is_activecolumn (soft delete / token consumption); a deactivated user or organization can then be hard-deleted into Trash - Dashboard interface for viewing, inspecting, and managing deleted records
- Restore: returns a record to its source table. Tries the original ID first to preserve FK references, falls back to auto-increment on PRIMARY KEY conflict; aborts with an admin-friendly message on UNIQUE collisions (slug, keyword, code, sku, etc.); LocalizedModel
_contentrows are restored alongside the primary row - Dual restore audit logging: each restore writes to both
trash_log(full audit) and the channel named by the trash record'slog_tablecolumn, so the entry shows up in Logger Protocol on the record's view/update page. Controllers pass the log channel name directly toTrashModel::store()(e.g.ReviewsController->'products',ReferencesController->'content') - Empty trash to permanently remove all records
- Restricted to the
system_coderrole
7. Template System
Raptor uses the FileTemplate class from the codesaur/template package - a lightweight custom engine with Twig-style syntax (NOT the actual twig/twig library).
Base Variables
When calling template() from a controller, these variables are automatically added:
| Variable | Description |
|---|---|
user | Authenticated User object (may be null) |
index | Script path (subdirectory support) |
localization | Language and translation data |
csrf_token | CSRF token for the csrf-token meta tag |
waf_body_encoding | '1' / '0' flag for the waf-body-encoding meta tag |
Custom Filters (registered by Controller)
| Filter | Usage | Description |
|---|---|---|
text | {{ 'key'|text }} | Get translation text |
link | {{ 'route'|link({'id': 5}) }} | Generate URL from route name |
pattern | {{ 'page-view'|pattern }} | Route pattern with its placeholders kept (/dashboard/pages/view/{id}) for client-side JS |
basename | {{ path|basename }} | Extract filename (content templates rendered via webTemplate()) |
Autoescape
Every {{ }} string is HTML-escaped by default (codesaur/template ^5). Print real HTML with |raw ({{ record['content']|raw }}, {{ text|nl2br|raw }}, {{ rows|json_encode|raw }} inside scripts); use |e('js') for values placed in JS string literals. Markup objects and nested template objects (the layouts' {{ content }}) are never escaped; call setAutoEscape(false) only for non-HTML output such as e-mail subjects.
Twig features NOT supported
Use these alternatives in codesaur/template:
| Twig (unsupported) | Replacement |
|---|---|
{% for i in 1..5 %} | {% for i in range(1, 5) %} |
**, // operators | use *, / |
is divisible by, is same as | not available |
loop.revindex, loop.parent | not available (use loop.length - loop.index0) |
{% verbatim %}, {% include %}, {% extends %} | not available |
|date(format='Y-m-d') | |date('Y-m-d') (positional only) |
in/not inmembership,ends with,matches(regex), andis even/is oddARE supported - e.g.{% if type in ['image', 'video'] %}.
Example
<!-- Translation -->
<h1>{{ 'welcome'|text }}</h1>
<!-- Route link -->
<a href="{{ 'page'|link({'slug': page.slug}) }}">{{ page.title }}</a>
<!-- User check (object method calls supported) -->
{% if user is not null and user.can('system_content_index') %}
<p>Hello, {{ user.profile.first_name }}!</p>
{% endif %}
<!-- Language switcher -->
{% for code, language in localization.language %}
<a href="{{ 'language'|link({'code': code}) }}">{{ language.title }}</a>
{% endfor %}
8. Routing
Raptor uses the Router class from the codesaur/router package (pulled in by codesaur/http-application).
Defining Routes
class MyRouter extends \codesaur\Router\Router
{
public function __construct()
{
// GET route
$this->GET('/path', [Controller::class, 'method'])->name('route-name');
// POST route
$this->POST('/path', [Controller::class, 'method'])->name('route-name');
// PUT route (full resource update)
$this->PUT('/path/{uint:id}', [Controller::class, 'method'])->name('route-name');
// PATCH route (partial update - single field, status toggle)
$this->PATCH('/path/{uint:id}/status', [Controller::class, 'method'])->name('route-name');
// DELETE route
$this->DELETE('/path', [Controller::class, 'method'])->name('route-name');
// GET + POST (form display + submit)
$this->GET_POST('/path', [Controller::class, 'method'])->name('route-name');
// GET + PUT (edit form)
$this->GET_PUT('/path/{uint:id}', [Controller::class, 'method'])->name('route-name');
}
}
Dynamic Parameters
| Pattern | Description | Example |
|---|---|---|
{slug} | String parameter (ASCII, URL-safe) | /page/{slug} |
{int:id} | Signed integer | /offset/{int:id} |
{uint:id} | Unsigned integer | /page/{uint:id} |
{float:price} | Float | /price/{float:price} |
{utf8:name} | UTF-8 string (Cyrillic etc.) | /tag/{utf8:name} |
Registering Routers
In your Application class:
$this->use(new MyRouter());
Route Name Optimization
Only use ->name('route-name') when the route name is actually referenced via:
{{ 'route-name'|link }}/{{ 'route-name'|pattern }}in Templates$this->generateRouteLink('route-name')/$this->redirectTo('route-name')in PHP controllers
Routes that are never referenced by name do not need ->name(), reducing unnecessary overhead.
9. Controller
Base Controller (Dashboard\Controller)
All controllers extend Dashboard\Controller. Available methods:
| Method | Description |
|---|---|
$this->pdo | PDO connection |
getUser() | Authenticated user (User|null) |
getUserId() | User ID |
isUserAuthorized() | Is authenticated |
isUser($role) | Check RBAC role |
isUserCan($permission) | Check RBAC permission |
getLanguageCode() | Active language code |
getLanguages() | All languages list |
text($key) | Translation text |
template($file, $vars) | Template object |
respondJSON($data, $code) | JSON response |
redirectTo($route, $params) | Redirect |
log($table, $level, $msg) | Write log entry |
dispatch($event) | Dispatch a PSR-14 event |
generateRouteLink($name, $params) | Generate URL |
getContainer() | DI Container |
getService($id) | Get service |
hasService($id) | Service exists? |
invalidateCache(...$keys) | Delete cache keys ({code} placeholder iterates all languages) |
getMountPath() | Mount path of the running app (/dashboard) |
setLanguageCode($code) | Store the language in the session (no-op for web) |
Example: Writing a New Controller
namespace Dashboard\Products;
class ProductsController extends \Dashboard\Controller
{
public function index()
{
// Check permission
if (!$this->isUserCan('product_read')) {
throw new \Error('Access denied', 403);
}
// Use model
$model = new ProductsModel($this->pdo);
$products = $model->getRows();
// Render template
$tpl = $this->template(__DIR__ . '/index.html', [
'products' => $products
]);
$tpl->render();
}
public function store()
{
$body = $this->getRequest()->getParsedBody();
$model = new ProductsModel($this->pdo);
$record = $model->insert($body);
$id = $record['id'];
// Write log - use the standard `record_id` key so the entry shows up
// in the record's Logger Protocol on its view/update page.
$this->log('products', \Psr\Log\LogLevel::INFO, 'Product added', [
'action' => 'create',
'record_id' => $id
]);
// JSON response
$this->respondJSON(['status' => 'success', 'id' => $id]);
}
}
10. Model
Raptor uses the Model classes from the codesaur/dataobject package.
Model (single language)
use codesaur\DataObject\Column;
use codesaur\DataObject\Model;
class ProductsModel extends Model
{
public function __construct(\PDO $pdo)
{
$this->setInstance($pdo);
$this->setColumns([
(new Column('id', 'bigint'))->primary(),
new Column('name', 'varchar', 255),
new Column('price', 'decimal', '10,2'),
(new Column('is_active', 'tinyint'))->default(1),
new Column('created_at', 'datetime'),
]);
$this->setTable('products');
}
}
LocalizedModel (multi-language)
use codesaur\DataObject\Column;
use codesaur\DataObject\LocalizedModel;
class CategoriesModel extends LocalizedModel
{
public function __construct(\PDO $pdo)
{
$this->setInstance($pdo);
// Primary table columns
$this->setColumns([
(new Column('id', 'bigint'))->primary(),
(new Column('is_active', 'tinyint'))->default(1),
]);
// Per-language content columns
$this->setContentColumns([
new Column('title', 'varchar', 255),
new Column('description', 'text'),
]);
$this->setTable('categories');
}
}
Key Methods
| Method | Description |
|---|---|
insert($record) | Insert a record, returns the inserted row (LocalizedModel: insert($record, $content)) |
updateById($id, $record) | Update by ID (LocalizedModel: updateById($id, $record, $content)) |
getById($id) | Get single row by primary key |
existsById($id) | Row exists? |
countRows($condition) | Count rows matching a SELECT condition |
hasColumn($name) | Column declared on the model? |
assertColumn($name) | Throws InvalidArgumentException for an undeclared column (safe GROUP BY / selection) |
orderBy($column, $dir) | Driver-quoted ORDER BY fragment; throws for an undeclared column or a direction other than ASC/DESC (LocalizedModel returns the p. / c. alias) |
deleteById($id) | Hard delete by ID (used for content modules) |
deactivateById($id, $record) | Soft delete by ID (Users/Organizations soft delete, also consumes used Forgot tokens) |
getRowWhere($with_values) | Get single row by WHERE key=value conditions |
getRow($condition) | Get single row with SELECT condition |
getRows($condition) | Get multiple rows with SELECT condition |
getName() | Get table name |
LocalizedModel Data Structure
LocalizedModel::getRows() returns:
[
1 => [
'id' => 1,
'is_active' => 1,
'localized' => [
'mn' => ['title' => 'Mongolian title', 'description' => '...'],
'en' => ['title' => 'English title', 'description' => '...'],
]
],
// ...
]
11. Testing
Raptor includes a PHPUnit 11 test suite with unit and integration tests.
Requirements
composer install # installs phpunit dev dependency
Running Tests
# All tests
composer test
# Unit tests only
composer test:unit
# Integration tests only
composer test:integration
Configuration
The .env.testing file contains test environment settings. Integration tests use a separate test database (raptor_test).
RAPTOR_DB_NAME=raptor_test
Test Structure
Selected files - tests/Unit/ holds one folder per area (Authentication, Content, Controller, Exception, Localization, Log, Middleware, Migration, Notification, Router, Template, Trash, Web):
tests/
|-- bootstrap.php # Test environment setup
|-- Support/
| |-- RaptorTestCase.php # Base class for unit tests
| \-- IntegrationTestCase.php # Base class for integration tests
|-- Unit/
| |-- Authentication/
| | \-- UserTest.php # User::is(), User::can() tests
| |-- Controller/
| | \-- ControllerTextTest.php # Controller::text() tests
| |-- Migration/
| | \-- MigrationSecurityScannerTest.php # Sensitive SQL pattern checks
| |-- Notification/
| | \-- EventDispatchServiceTest.php # Event action / listener contract
| \-- ...
\-- Integration/
|-- Model/
| |-- UsersModelTest.php # User CRUD tests
| |-- OrganizationModelTest.php # Organization tests
| \-- SignupModelTest.php # Signup tests
|-- RBAC/
| \-- RolesPermissionsTest.php # RBAC seed data verification
|-- Authentication/
| \-- JWTAuthTest.php # JWT encode/decode tests
\-- Migration/
\-- MigrationRunnerIntegrationTest.php # File-based migration runner tests
Key Features
- Transaction isolation - Each integration test runs inside a transaction that is rolled back on teardown. Test data never affects the real database
- RaptorTestCase - Provides mock request and user factory helpers (
createAdmin(),createCoder(),createGuest()) - IntegrationTestCase - Static PDO connection shared across test class, auto-creates test database if not exists
Writing a New Test
namespace Tests\Unit;
use Tests\Support\RaptorTestCase;
class MyTest extends RaptorTestCase
{
public function test_example(): void
{
$user = $this->createAdmin();
$this->assertTrue($user->can('system_user_index'));
}
}
12. Usage Examples
Adding a New Router
- Create the Router class:
// application/dashboard/mymodule/MyModuleRouter.php
namespace Dashboard\MyModule;
use Dashboard\CsrfMiddleware;
class MyModuleRouter extends \codesaur\Router\Router
{
public function __construct()
{
// Paths are mount-naive: the app is mounted on /dashboard in public_html/index.php
$this->GET('/mymodule', [MyModuleController::class, 'index'])->name('mymodule');
$this->GET_POST('/mymodule/insert', [MyModuleController::class, 'insert'])
->name('mymodule-insert')
->middleware([CsrfMiddleware::class]);
}
}
- Register the namespace in
composer.json:
{
"autoload": {
"psr-4": {
"Dashboard\\MyModule\\": "application/dashboard/mymodule/"
}
}
}
Then regenerate the autoloader:
composer dump-autoload
- Register the Router inside
Dashboard\Application's constructor, next to the other$this->use(...)router registrations:
// application/dashboard/Application.php (inside __construct)
$this->use(new MyModule\MyModuleRouter()); // New router
Adding a Public Web Page
// application/web/WebRouter.php
$this->GET('/catalog', [HomeController::class, 'catalog'])->name('catalog');
// application/web/HomeController.php
public function catalog()
{
$code = $this->getLanguageCode();
$model = new ProductsModel($this->pdo);
// code IN (:code, '*') - language-neutral records show on every language
$products = $model->getRows([
'WHERE' => "published=1 AND code IN (:code, '*')",
'PARAM' => [':code' => $code]
]);
$this->webTemplate(__DIR__ . '/catalog.html', ['products' => $products])->render();
}
Switching Database
Change the driver via RAPTOR_DB_DRIVER in .env:
# MySQL (default)
RAPTOR_DB_DRIVER=mysql
# Switch to PostgreSQL
RAPTOR_DB_DRIVER=pgsql
\Dashboard\DatabaseConnection::connect() reads this value and returns the matching PDO.
Next Steps
- API Reference - Detailed API reference for all classes and methods
- Discussions - Ask questions, share ideas, get help
- codesaur ecosystem - Other packages