Configuration

PointArt uses a .env file for configuration. Copy .env.example to .env and set your values.

Namespaces PointStart\Core — Env, App, ClassLoader, HttpResponses

Environment Variables

.env KeyDefaultDescription
APP_DEBUGfalseShow stack traces on error
DB_DRIVERmysqlmysql, pgsql, or sqlite
DB_HOSTlocalhostDatabase host
DB_PORT3306Database port (5432 for pgsql)
DB_DATABASEpointartDatabase name
DB_USERNAMEDatabase user
DB_PASSWORDDatabase password
DB_CHARSETutf8mb4Charset (MySQL only)
DB_PATHPath to SQLite file (SQLite only)
CORS_ENABLEDfalseEnable CORS headers
CORS_ALLOWED_ORIGINS*Comma-separated allowed origins, or *
CORS_ALLOWED_METHODSGET,POST,OPTIONSComma-separated allowed methods
CORS_ALLOWED_HEADERSContent-Type,Authorization,X-Requested-WithComma-separated allowed headers
CORS_ALLOW_CREDENTIALSfalseAllow credentials (cookies, auth headers)
CORS_MAX_AGE86400Preflight cache duration in seconds
CSRF_ENABLEDtrueEnable CSRF token validation for POST form requests
UPDATER_ENABLEDfalseEnable the built-in framework updater route
UPDATER_SECRETSecret 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.

Credentials + wildcard origin If 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
Namespaces PointStart\Core — Cors, Csrf
Global 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

FunctionEquivalent
return404()httpError(404)
return401()httpError(401)
return403()httpError(403)
return405()httpError(405)

Error Behaviour

ScenarioBehaviour
No route matchedAutomatic 404 — clean error page
Controller calls httpError()Renders that code's error page
Uncaught exception + APP_DEBUG=trueException class, message, and full stack trace
Uncaught exception + APP_DEBUG=falseClean 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):

  1. ClassLoader scans app/components/, app/models/, app/repositories/
  2. Uses Reflection to read #[Router], #[Route], #[Service] attributes
  3. Builds route + service registry
  4. Serializes to cache/registry.ser

Subsequent requests (cache hit):

  1. Unserializes cache/registry.ser
  2. Registers autoloader — no class files loaded yet
  3. 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.

Remember If you add a new controller, route, or service and it doesn't appear — clear the cache.

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>
Do not place assets outside app/public/ Files in app/views/, app/components/, etc. are blocked by .htaccess and will return 403.

Design Constraints

That covers the full framework. Want to help improve it? Contributing →