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
Access any component via its property name on $app. These are the exact property names as defined in the framework:
Database:
$app->db—QueryBuilderfor the primary connection$app->db('name')—QueryBuilderfor a named connection
Auth & Session:
$app->auth—Auth— login, logout, token management$app->sessionManager—SessionManager— raw session operations
Data & Storage:
$app->cache—CacheManager— file or database caching$app->fileManager—FileManager— file uploads and management
Security:
$app->encryption—Encryption— encrypt and decrypt data$app->rateLimiter—RateLimiter— request throttling$app->accessControl—AccessControl— IP and country blocking
Validation & Input:
$app->validator—Validator— input validation rules
Utilities:
$app->dateTimeHelper—DateTimeHelper— date/time operations$app->stringHelper—StringHelper— string manipulation$app->pagination—PaginationHelper— paginated result sets$app->url—UrlHelper— URL generation$app->httpRequest—HttpRequestHelper— outbound HTTP calls$app->environment—EnvironmentHelper— server environment info$app->executionTimer—ExecutionTimer— performance timing$app->language—LanguageManager— translations (requiresmulti_lang => true)
Infrastructure (always available):
$app->logger—Logger— write to application log$app->errorHandler—ErrorHandler— registered PHP error handler$app->router—Router— URL routing (requiresrouter_enabled => true)
Components are split into two groups to keep boot time minimal.
Eager — created immediately at boot:
Logger— must exist before anything can failErrorHandler— registered as the global PHP error/exception handlerEnvironmentHelper— needed by RateLimiter at bootRateLimiter— enforces limits before any application code runsAccessControl— 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();
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')]);
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(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(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
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.
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(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(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.