About blackCurrant PHP
blackCurrant PHP - a minimal PHP 8.1+ framework. Barebones, fast, secure.
How a request flows
index.phpregisters the autoloader and callsBoot::run().Bootloads config, sends security headers, starts the session, captures the request.Url::parse()splits the path intocurrentClass/currentFunction/param.Dispatchermaps/about→App\Controllers\About.- The controller calls
$this->view->render('index'), the View engine finds the template via cascade. Controller::renderPage()wraps the body inlayouts/appwith header + footer.- Boot echoes the output. Add
?_speed=1from a safe IP for a per-stage profile.
Template cascade
When rendering index from the About controller, View looks in order:
app/Views/about/index.php← this page (most specific)app/Views/index.php(project default)core/Views/index.php(framework fallback)
Override only what you need; the rest cascades up.
Where things live
your-site/
├── public/ ← *** WEB DOCUMENT ROOT ***
│ ├── index.php front controller (BC_ROOT = parent)
│ ├── .htaccess short rewrite + headers
│ ├── css/ site.css, bc-banner.css, bc-components.css, bc-home.css,
│ │ bc-theme-editor.css, bc-debug-viewer.css, bc-update.css,
│ │ theme.css (generated)
│ ├── js/ bc-banner.js, bc-nav.js, bc-features-toc.js,
│ │ bc-theme-editor.js, bc-debug-clear.js
│ ├── build/ Tailwind output (manifest.json + hashed files)
│ ├── uploads/ source images for the ImageProxy
│ └── images/ brand assets, logos
├── storage/ persistent state OUTSIDE webroot
│ ├── theme.json palette saved by /theme
│ └── uploads/ private user uploads (only proxied)
├── cache/ writable, denied from HTTP
│ └── images/ auto-generated proxy variants
├── logs/ writable, denied from HTTP
├── core/ framework code - treat as vendor
│ ├── Mvc/ Controller, View, Model, ApiController, AdminController, Seo
│ ├── Http/ Request, Response, Csrf, Headers, Pipeline, RateLimiter
│ │ └── Middleware/ MaintenanceMode, RateLimit, VerifyCsrf, Compress, ImageProxy
│ ├── Routing/ Url (locale-aware), Dispatcher
│ ├── Db/ Connection, Database, Paginator
│ ├── Cache/ Redis / APCu / File / Null
│ ├── Security/ Sanitize, CleanInput, Session, Auth
│ ├── Debug/ Logger, Profiler, ErrorHandler, LogViewer
│ ├── Ab/ Bucket
│ ├── Cli/ CLI kernel + commands (bin/bc: serve, ai:call, core:push, …)
│ ├── Ai/ AI agent layer: Capability, Registry, Gateway · Mcp/ Http/ Identity/ Discovery/
│ ├── Update/ CoreSync - push core/ to other installs on this drive
│ └── Views/ layouts/app, partials/seo, partials/seo_body, system/*
├── app/ ← YOUR code goes here
│ ├── Controllers/ page controllers (Home, About, Contact, …)
│ ├── Models/ data-access classes
│ ├── Views/ templates
│ │ ├── layouts/ page shells (app, admin)
│ │ ├── partials/ shared chunks (header, nav, footer)
│ │ └── <controller>/ per-controller views
│ ├── Api/V1/ JSON API endpoints
│ ├── Admin/ admin-gated pages
│ ├── Ai/ agent capabilities (skills) - exposed via MCP / REST / CLI
│ └── Http/Middleware/ custom middleware (optional)
├── config/config.php all configuration
├── .env secrets (gitignored)
├── index.php legacy entrypoint - kept for back-compat
│ injects "Insecure deployment" banner
│ if reached (i.e. docroot wasn't switched
│ to public/ yet)
└── .htaccess legacy hardened rewrites + denies
Decision table - where new files go
| You're adding… | Put it at |
|---|---|
| A new page | app/Controllers/<Name>.php + app/Views/default/<name>/index.php |
| A new API endpoint | app/Api/V1/<Name>.php |
| A new admin screen | app/Admin/<Name>.php + app/Views/default/admin/<name>/index.php |
| An AI agent capability | app/Ai/<Name>.php (implements Capability) + register in config.php → ai.capabilities |
| A database model | app/Models/<Name>.php |
| A shared partial | app/Views/default/partials/<name>.php |
| A layout | app/Views/default/layouts/<name>.php |
| Custom HTTP middleware | app/Http/Middleware/<Name>.php + register in config.php → middleware |
| Hand-written CSS | public/css/<name>.css → <link href="/css/<name>.css"> |
| Plain JS (CSP-safe) | public/js/<name>.js → <script src="/js/<name>.js" defer> |
| Source images for the proxy | public/uploads/<…>.jpg → <?= img('/uploads/<…>.jpg', width: 800) ?> |
| Brand SVG / logos | public/images/<name>.svg - passed through unchanged |
| Private user uploads | storage/uploads/<…> - outside webroot, only proxy can serve |
| Persistent state (non-DB) | storage/<name>.json - gitignored |
| Tailwind input | resources/css/app.css → builds to public/build/app.css |
| A framework feature | STOP - touching core/ needs review |
Where things must NOT go
| Don't | Why |
|---|---|
Anything in core/ | Treated as vendor. App-level changes go in app/. |
New routes in the root index.php | Legacy entrypoint - kept only for back-compat. Target public/index.php. |
Secrets / source under public/ | public/ is the webroot. .env, config, models, vendor must stay above it. |
.env in any deployed webroot | One misconfigured rule away from leaking. Fix: docroot = public/. |