Configuration
PointArt uses a .env file for configuration. Copy .env.example to .env and set your values.
PointStart\Core — Env, App, ClassLoader, HttpResponses
Environment Variables
.env Key | Default | Description |
|---|---|---|
APP_DEBUG | false | Show stack traces on error |
DB_DRIVER | mysql | mysql, pgsql, or sqlite |
DB_HOST | localhost | Database host |
DB_PORT | 3306 | Database port (5432 for pgsql) |
DB_DATABASE | pointart | Database name |
DB_USERNAME | — | Database user |
DB_PASSWORD | — | Database password |
DB_CHARSET | utf8mb4 | Charset (MySQL only) |
DB_PATH | — | Path to SQLite file (SQLite only) |
CORS_ENABLED | false | Enable CORS headers |
CORS_ALLOWED_ORIGINS | * | Comma-separated allowed origins, or * |
CORS_ALLOWED_METHODS | GET,POST,OPTIONS | Comma-separated allowed methods |
CORS_ALLOWED_HEADERS | Content-Type,Authorization,X-Requested-With | Comma-separated allowed headers |
CORS_ALLOW_CREDENTIALS | false | Allow credentials (cookies, auth headers) |
CORS_MAX_AGE | 86400 | Preflight cache duration in seconds |
CSRF_ENABLED | true | Enable CSRF token validation for POST form requests |
UPDATER_ENABLED | false | Enable the built-in framework updater route |
UPDATER_SECRET | — | Secret key required to access the updater (details) |
CORS
CORS headers are disabled by default — opt-in for backend development or cross-origin API access. Configure entirely via .env:
CORS_ENABLED=true
CORS_ALLOWED_ORIGINS=*
CORS_ALLOWED_METHODS=GET,POST,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Requested-With
CORS_ALLOW_CREDENTIALS=false
CORS_MAX_AGE=86400
When enabled, headers are set on every response. OPTIONS preflight requests are handled entirely by the framework (returns 204 No Content) — no controller is involved.
allow_credentials is true, browsers reject Access-Control-Allow-Origin: *. Replace the wildcard with a specific origin.
CSRF
CSRF protection is enabled by default for all POST requests from HTML forms. JSON API requests (Content-Type: application/json) skip the check automatically. A POST without a valid token returns 403 Forbidden.
In forms
Use csrf_field() to output a hidden token input inside any form:
<form method="POST" action="/user/create">
<?= csrf_field() ?>
<input type="text" name="name">
<button type="submit">Create</button>
</form>
For AJAX requests, read the raw token with csrf_token() and send it in the X-CSRF-Token header:
fetch('/api/data', {
method: 'POST',
headers: { 'X-CSRF-Token': '<?= csrf_token() ?>' },
body: JSON.stringify(data),
});
Exempting a route
Set csrfExempt: true on #[Route] to bypass the check for a specific endpoint:
#[Route('/webhook', HttpMethod::POST, csrfExempt: true)]
public function webhook(): array {
// No CSRF token required — webhook sender authenticates another way
}
Disabling CSRF globally
Set in .env:
CSRF_ENABLED=false
PointStart\Core — Cors, CsrfGlobal helpers —
csrf_token(), csrf_field()
Error Handling
PointArt provides built-in error handling for HTTP errors and uncaught exceptions.
httpError()
// In a controller — render a clean error page and stop
httpError(403, 'You do not have permission.');
return '';
Sets the HTTP response code and renders a styled HTML error page. Supported codes with built-in messages: 400, 401, 403, 404, 405, 408, 409, 422, 429, 500, 502, 503.
Convenience Wrappers
| Function | Equivalent |
|---|---|
return404() | httpError(404) |
return401() | httpError(401) |
return403() | httpError(403) |
return405() | httpError(405) |
Error Behaviour
| Scenario | Behaviour |
|---|---|
| No route matched | Automatic 404 — clean error page |
Controller calls httpError() | Renders that code's error page |
Uncaught exception + APP_DEBUG=true | Exception class, message, and full stack trace |
Uncaught exception + APP_DEBUG=false | Clean 500 error page |
Caching
PointArt scans app/ on the first request and serializes the route and service registry to cache/registry.ser. Every subsequent request reads from cache — no scanning, no Reflection.
Cache Flow
First request (no cache):
- ClassLoader scans
app/components/,app/models/,app/repositories/ - Uses Reflection to read
#[Router],#[Route],#[Service]attributes - Builds route + service registry
- Serializes to
cache/registry.ser
Subsequent requests (cache hit):
- Unserializes
cache/registry.ser - Registers autoloader — no class files loaded yet
- On dispatch: only the matched controller's file is loaded
Clearing the Cache
ClassLoader::clearCache();
Or delete cache/registry.ser manually. The cache rebuilds automatically on the next request.
Static Assets
Place all public files (CSS, JS, images, fonts) inside app/public/. The .htaccess blocks direct access to everything else under app/.
app/
└── public/
├── css/
│ └── style.css
└── js/
└── main.js
Reference assets with an absolute path from the web root:
<link rel="stylesheet" href="/app/public/css/style.css">
<script src="/app/public/js/main.js"></script>
app/public/
Files in app/views/, app/components/, etc. are blocked by .htaccess and will return 403.
Design Constraints
- No Composer required
- No template engine — views are plain
.phpfiles - No CLI — no code generation, no migration runner
- PHP 8.1+ required for
#[Attribute]syntax - Single process — no async, no workers, designed for shared hosting
That covers the full framework. Want to help improve it? Contributing →