Application

Overview

The App class is the heart of JiFramework. It boots the entire framework in one line, manages every component, and provides clean lazy-loaded access to all services through simple property syntax.

  • Class: JiFramework\Core\App\App
  • Bootstrap: $app = new App();

On construction, App automatically:

  • Detects the project root and loads config/jiconfig.php
  • Registers the global error handler and logger
  • Applies rate limiting and IP/country access control
  • Auto-loads all model files from models/*.php
  • Registers factory closures for every other component — none are created until you access them

All Components & Property Names

Access any component via its property name on $app. These are the exact property names as defined in the framework:

Database:

  • $app->dbQueryBuilder for the primary connection
  • $app->db('name')QueryBuilder for a named connection

Auth & Session:

  • $app->authAuth — login, logout, token management
  • $app->sessionManagerSessionManager — raw session operations

Data & Storage:

  • $app->cacheCacheManager — file or database caching
  • $app->fileManagerFileManager — file uploads and management

Security:

  • $app->encryptionEncryption — encrypt and decrypt data
  • $app->rateLimiterRateLimiter — request throttling
  • $app->accessControlAccessControl — IP and country blocking

Validation & Input:

  • $app->validatorValidator — input validation rules

Utilities:

  • $app->dateTimeHelperDateTimeHelper — date/time operations
  • $app->stringHelperStringHelper — string manipulation
  • $app->paginationPaginationHelper — paginated result sets
  • $app->urlUrlHelper — URL generation
  • $app->httpRequestHttpRequestHelper — outbound HTTP calls
  • $app->environmentEnvironmentHelper — server environment info
  • $app->executionTimerExecutionTimer — performance timing
  • $app->languageLanguageManager — translations (requires multi_lang => true)

Infrastructure (always available):

  • $app->loggerLogger — write to application log
  • $app->errorHandlerErrorHandler — registered PHP error handler
  • $app->routerRouter — URL routing (requires router_enabled => true)

Eager vs Lazy Loading

Components are split into two groups to keep boot time minimal.

Eager — created immediately at boot:

  • Logger — must exist before anything can fail
  • ErrorHandler — registered as the global PHP error/exception handler
  • EnvironmentHelper — needed by RateLimiter at boot
  • RateLimiter — enforces limits before any application code runs
  • AccessControl — blocks disallowed IPs/countries before any application code runs

Lazy — created only when first accessed:

Everything else: $app->db, $app->auth, $app->cache, $app->validator, and all other components. They are instantiated on first property access and cached for the rest of the request.

// Only 5 eager components are created at this point
$app = new App();

// QueryBuilder is created here (first access)
$users = $app->db->table('users')->get();

// Same QueryBuilder instance is reused (already cached)
$posts = $app->db->table('posts')->get();

Database Access - Primary Connection

Access the primary database through $app->db. This returns a QueryBuilder instance configured with your primary database credentials.

// Fetch all users
$users = $app->db->table('users')->get();

// With conditions and ordering
$active = $app->db->table('users')
    ->where('status', 'active')
    ->orderBy('created_at', 'DESC')
    ->limit(20)
    ->get();

// Insert and get the new row ID
$id = $app->db->table('posts')->insertGetId([
    'title'      => 'Hello World',
    'body'       => 'Content here',
    'created_at' => date('Y-m-d H:i:s'),
]);

// Update a record
$app->db->table('users')
    ->where('id', $userId)
    ->update(['last_login' => date('Y-m-d H:i:s')]);

Database Access - Named Connections

For multiple databases, define extra connections in jiconfig.php under the databases key and access them with $app->db('name'):

// config/jiconfig.php
return [
    'database' => [
        'host'     => 'localhost',
        'dbname'   => 'main_db',
        'username' => 'root',
        'password' => '',
    ],
    'databases' => [
        'analytics' => [
            'host'     => 'analytics-server',
            'dbname'   => 'stats',
            'username' => 'reader',
            'password' => 'secret',
        ],
    ],
];
// Primary connection (both are equivalent)
$app->db->table('users')->get();
$app->db('primary')->table('users')->get();

// Named connection
$events = $app->db('analytics')->table('events')
    ->where('date', '2026-01-01')
    ->get();

Each connection is cached after first use. The underlying PDO object is reused on every subsequent call to $app->db('name').

redirect()

redirect(string $url): void

Sends an HTTP Location header and immediately terminates the script.

  • $url(string) The URL to redirect to
// Redirect to homepage
$app->redirect('/');

// Redirect with a dynamic segment
$app->redirect('/users/' . $userId . '/profile');

// Post/Redirect/Get pattern ÔÇö prevent double form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $app->db->table('contacts')->insert([
        'name'  => $_POST['name'],
        'email' => $_POST['email'],
    ]);
    $app->redirect('/contact?success=1');
}

exit()

exit(int $statusCode = 200, string $msg = ''): void

Sets the HTTP response code, optionally outputs a message body, and terminates the script. Useful for API endpoints and error responses.

  • $statusCode(int, optional) Valid HTTP status code (100–599). Default: 200
  • $msg(string, optional) Body text to output before exit. Default: ""
// API success response
header('Content-Type: application/json');
$app->exit(200, json_encode(['status' => 'ok', 'data' => $result]));

// Validation failed
header('Content-Type: application/json');
$app->exit(422, json_encode(['errors' => $app->validator->errors()]));

// Forbidden access
$app->exit(403, 'You do not have permission to view this page.');

// Not found
$app->exit(404, 'The requested resource was not found.');

App::getInstance()

App::getInstance(): App

Returns the singleton App instance that was created with new App(). This is used internally by the Model base class to access the database without requiring $app to be passed in explicitly.

// index.php
$app = new App();

// --- anywhere else in your codebase ---
$app = App::getInstance();
$user = $app->db->table('users')->find(1);

In most situations you should pass $app as a parameter rather than calling getInstance(). Reserve it for contexts where injection is not practical, such as static methods or standalone utility classes.

Complete Example - User Registration API

A full example showing multiple framework components working together in a single request lifecycle:

<?php
require __DIR__ . '/vendor/autoload.php';
$app = new App();

header('Content-Type: application/json');

// Only accept POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    $app->exit(405, json_encode(['error' => 'Method not allowed']));
}

// 1. Validate input
$rules = [
    'name'     => 'required|min:2|max:100',
    'email'    => 'required|email',
    'password' => 'required|min:8',
];

if (!$app->validator->validate($_POST, $rules)) {
    $app->exit(422, json_encode(['errors' => $app->validator->errors()]));
}

// 2. Check for duplicate email
$exists = $app->db->table('users')
    ->where('email', strtolower(trim($_POST['email'])))
    ->exists();

if ($exists) {
    $app->exit(409, json_encode(['error' => 'Email already registered']));
}

// 3. Insert new user
$userId = $app->db->table('users')->insertGetId([
    'name'       => $app->stringHelper->sanitize($_POST['name']),
    'email'      => strtolower(trim($_POST['email'])),
    'password'   => password_hash($_POST['password'], PASSWORD_BCRYPT),
    'created_at' => $app->dateTimeHelper->now(),
]);

// 4. Cache a welcome flag (expires in 5 minutes)
$app->cache->set('new_user_' . $userId, true, 300);

// 5. Log the registration event
$app->logger->info('New user registered', [
    'user_id' => $userId,
    'email'   => $_POST['email'],
]);

// 6. Return success
$app->exit(201, json_encode(['status' => 'created', 'id' => $userId]));

json()

json(int $statusCode, array $data): void

Sends a JSON response and terminates the script. Sets the Content-Type: application/json header automatically, encodes the data array with json_encode(), and exits. This is the recommended method for all API and AJAX responses — cleaner than calling header(), json_encode(), and exit manually.

  • $statusCode(int) HTTP status code (100–599). Defaults to 500 with an error payload if the code is out of range.
  • $data(array) Associative or indexed array to encode as the response body.
// 200 OK - return a result
$app->json(200, ['status' => 'ok', 'data' => $result]);

// 201 Created - return the new resource ID
$app->json(201, ['id' => $newId, 'message' => 'Record created']);

// 204 No Content - action succeeded, nothing to return
$app->json(204, []);

// 400 Bad Request
$app->json(400, ['error' => 'Invalid request body']);

// 401 Unauthorized
$app->json(401, ['error' => 'Authentication required']);

// 403 Forbidden
$app->json(403, ['error' => 'You do not have permission']);

// 404 Not Found
$app->json(404, ['error' => 'Resource not found']);

// 422 Validation errors
$app->json(422, ['errors' => $app->validator->errors()]);

// 500 Server error
$app->json(500, ['error' => 'An unexpected error occurred']);

Compared to using exit() for the same result, json() removes boilerplate:

// Using exit() - manual, verbose
header('Content-Type: application/json');
$app->exit(200, json_encode(['data' => $result]));

// Using json() - clean, one line
$app->json(200, ['data' => $result]);

A typical REST endpoint using json() throughout:

<?php
require __DIR__ . '/vendor/autoload.php';
$app = new App();

// Only accept POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    $app->json(405, ['error' => 'Method not allowed']);
}

// Validate CSRF
$app->sessionManager->csrfMiddleware();

// Validate input
$rules = ['name' => 'required|min:2', 'email' => 'required|email'];
if (!$app->validator->validate($_POST, $rules)) {
    $app->json(422, ['errors' => $app->validator->errors()]);
}

// Check for duplicate email
$exists = $app->db->table('users')->where('email', $_POST['email'])->exists();
if ($exists) {
    $app->json(409, ['error' => 'Email already registered']);
}

// Create the user
$id = $app->db->table('users')->insertGetId([
    'name'       => $_POST['name'],
    'email'      => $_POST['email'],
    'created_at' => $app->dateTimeHelper->now(),
]);

$app->json(201, ['id' => $id]);

abort()

abort(int $statusCode, string $message = ''): void

Stop the current request immediately by throwing an HttpException with the given status code. The registered ErrorHandler catches it, logs it, and renders the correct response — HTML error page for browser requests, JSON for API/AJAX requests.

  • $statusCode(int) HTTP status code to respond with (e.g. 401, 403, 404, 500)
  • $message(string, optional) Shown on screen in development mode, always written to the log. Generic message shown in production.
// 404 - resource not found
$post = $app->db->table('posts')->where('id', $id)->first();
if (!$post) {
    $app->abort(404, 'Post not found');
}

// 403 - no permission
if ($post['user_id'] !== $app->sessionManager->get('user_id')) {
    $app->abort(403);
}

// 401 - not logged in
if (!$app->sessionManager->get('user_id')) {
    $app->abort(401, 'Authentication required');
}

// API request - abort() returns JSON automatically
// GET /api/users/99 with Accept: application/json
$app->abort(404, 'User not found');
// Response: HTTP 404
// {"error": {"code": 404, "message": "User not found"}}

Internally, abort() throws JiFramework\Exceptions\HttpException. You can also throw the typed subclasses directly for more expressive code:

use JiFramework\Exceptions\NotFoundException;
use JiFramework\Exceptions\ForbiddenException;
use JiFramework\Exceptions\UnauthorizedException;

throw new NotFoundException('Post not found');     // same as abort(404, ...)
throw new ForbiddenException('Access denied');      // same as abort(403, ...)
throw new UnauthorizedException();                  // same as abort(401)

See the Error Handling page for the full exception hierarchy and custom error template setup.