In Part 4, you added a database to your app. Now you have routes, templates, and persistence — but everything is hardcoded. Database credentials are in your PHP file, there's no caching, and every user sees the same language.
This part fixes that. You'll learn how to externalize configuration, enable caching for performance, handle user sessions, support multiple languages, and use F3's built-in plugins.
1. Configuration Files
Instead of hardcoding values in your PHP, F3 lets you use configuration files. The most common format is INI — simple key-value pairs that non-developers can also edit.
Creating a Config File
# config.ini
[globals]
app.name = My Blog
DEBUG = 3
db.path = data/app.db
[routes]
GET / = Home->index
GET /about = Page->about Loading the Config
// Load config file
$f3->config('config.ini');
// Access values anywhere
$appName = $f3->get('app.name'); // "My Blog"
$debug = $f3->get('DEBUG'); // 3 (F3's built-in debug variable) Arrays: Use comma separation:
colors = red,blue,greenNested arrays: Use dot notation:
db.host = localhostComments: Lines starting with
; or # are ignored.2. Configuration Sections
INI files support four special sections:
| Section | Purpose | Example |
|---|---|---|
| [globals] | Set F3 variables | app.name = My Blog |
| [routes] | Define routes | GET / = Home->index |
| [maps] | Route maps | /api = Api\Handler |
| [redirects] | Redirect rules | GET /old = /new |
# Complete config example
[globals]
app.name = My Blog
DEBUG = 0
db.path = data/app.db
[routes]
GET / = Home->index
GET /about = Page->about
GET /blog/@slug = Blog->show
[redirects]
GET /old-page = /new-page
GET /archive = /blog 3. Environment-Specific Configuration
Real apps have different settings for development and production. Load config files conditionally from PHP:
// Load base config
$f3->config('config.ini');
// Load environment-specific config
$env = getenv('APP_ENV') ?: 'dev';
if ($env === 'production') {
$f3->config('config/prod.ini');
} else {
$f3->config('config/dev.ini');
} # config/dev.ini
[globals]
DEBUG = 3
db.path = data/dev.db # config/prod.ini
[globals]
DEBUG = 0
db.path = /var/lib/mysql/prod.db .gitignore).For production, consider setting secrets via environment:
$_ENV['DB_PASSWORD'] or getenv('DB_PASSWORD')4. The Cache Engine
F3 has a built-in cache engine that can store pages, query results, and variables. Caching reduces server load by avoiding repeated work.
Enabling Cache
// Enable cache with auto-detection
// Tries APC/WinCache first, then falls back to filesystem
$f3->set('CACHE', true);
// Or specify backend explicitly
$f3->set('CACHE', 'memcache=localhost:11211');
// Disable cache
$f3->set('CACHE', false); • APC (Linux) — auto-detected, fastest single-server option
• WinCache (Windows IIS) — auto-detected on Windows
• XCache — auto-detected if installed
• Memcached — specify explicitly:
CACHE=memcache=localhost:11211• Redis — specify explicitly:
CACHE=redis=localhost:6379• Filesystem — automatic fallback, stores in
tmp/cache/When you set
CACHE to true, F3 checks for APC, WinCache, or XCache. If none is found, it uses the filesystem.5. Route-Level Caching
The simplest way to cache is at the route level. Add a third argument to $f3->route() specifying how long (in seconds) the cached page should last.
// Cache for 10 minutes (600 seconds)
$f3->route('GET /about', 'Page->about', 600);
// Cache for 1 hour
$f3->route('GET /blog', 'Blog->index', 3600);
// No caching (default)
$f3->route('GET /contact', 'Page->contact'); Rule: Only cache pages that are identical for all users.
6. Query Caching
You can also cache expensive database queries. Add a third argument to $db->exec() with the TTL in seconds.
// Cache query results for 24 hours
$categories = $db->exec(
'SELECT * FROM categories ORDER BY name',
null,
86400 // 24 * 60 * 60
);
// Cache with parameters
$posts = $db->exec(
'SELECT * FROM posts WHERE status = ?',
['published'],
3600 // 1 hour
); • Are executed frequently
• Return data that changes rarely
• Are slow to execute (complex JOINs, aggregations)
Don't cache queries that:
• Return user-specific data
• Need to be real-time (stock prices, etc.)
7. Session Handling
F3 handles sessions automatically. When you access the SESSION variable, F3 starts the session for you. SESSION maps directly to PHP's $_SESSION.
Basic Session Usage
// Set session values
$f3->set('SESSION.user_id', 42);
$f3->set('SESSION.user_name', 'Dimas');
// Get session values
$userId = $f3->get('SESSION.user_id');
// Check if value exists
if ($f3->exists('SESSION.user_id')) {
echo 'User is logged in';
}
// Clear a session value
$f3->clear('SESSION.user_id'); Simple Authentication Pattern
// Login route
$f3->route('POST /login', function($f3) {
$email = $f3->get('POST.email');
$password = $f3->get('POST.password');
// Verify credentials (use password_hash in real apps)
$user = new \DB\SQL\Mapper($f3->get('DB'), 'users');
$user->load(['email=? AND password=?', $email, md5($password)]);
if ($user->dry()) {
$f3->set('error', 'Invalid credentials');
$f3->reroute('/login');
return;
}
// Store user in session
$f3->set('SESSION.user_id', $user->id);
$f3->set('SESSION.user_name', $user->name);
$f3->reroute('/dashboard');
}); // Protected route
$f3->route('GET /dashboard', function($f3) {
if (!$f3->exists('SESSION.user_id')) {
$f3->reroute('/login');
return;
}
$f3->set('user_name', $f3->get('SESSION.user_name'));
echo \Template::instance()->render('views/dashboard.htm');
}); md5() for simplicity. In production, always use password_hash() and password_verify():// Store: password_hash($password, PASSWORD_DEFAULT)// Verify: password_verify($input, $stored_hash)8. Multilingual Support (i18n)
F3 has built-in multilingual support. Create dictionary files with translations and use them in your templates.
Creating Dictionary Files
// dict/en.php
return [
'welcome' => 'Welcome to My Blog',
'read_more' => 'Read more',
'login' => 'Log in',
'logout' => 'Log out',
]; // dict/id.php (Indonesian)
return [
'welcome' => 'Selamat Datang di Blog Saya',
'read_more' => 'Baca selengkapnya',
'login' => 'Masuk',
'logout' => 'Keluar',
]; Setting Up i18n
// Point to dictionary folder
$f3->set('LOCALES', 'dict/');
// Set language explicitly (or let F3 detect from browser)
$f3->set('LANGUAGE', 'id'); // Indonesian Using Translations in Templates
<!-- views/home.htm -->
<h1>{{ @welcome }}</h1>
<a href="/login">{{ @login }}</a> F3 automatically loads the dictionary and makes the keys available as variables. Switch languages by changing the LANGUAGE variable.
welcome and also set $f3->set('welcome', 'something') in your code, the last one wins. Use a prefix to avoid conflicts:// dict/en.php
'i18n.welcome' => 'Welcome'Then access as
{{ @i18n.welcome }} in templates.9. Useful Plugins
F3 ships with several built-in plugins. Here are the most useful ones:
Log — Write to Log Files
use \Log;
$logger = new Log('app.log');
$logger->write('User logged in: dimas');
$logger->write('Page visited: /about', 'Y-m-d H:i:s'); // 2nd arg is date format Web — HTTP Client
use \Web;
// Fetch external API
$response = Web::instance()->request('https://api.example.com/data');
$data = json_decode($response['body'], true);
echo $data['result']; Markdown — Convert Markdown to HTML
use \Markdown;
$md = '## Hello World
This is **bold** and *italic*.';
$html = Markdown::instance()->convert($md);
// <h2>Hello World</h2><p>This is <strong>bold</strong> and <em>italic</em>.</p> Image — CAPTCHA and Image Processing
use \Image;
// Generate CAPTCHA
$img = new Image();
$img->captcha('fonts/CoolFont.ttf', 16, 5, 'SESSION.captcha_code');
$img->render();
// The session variable stores the expected answer
$captchaCode = $f3->get('SESSION.captcha_code'); See the full list at fatfreeframework.com/plug-ins
10. Debug & Error Handling
F3 has a built-in debug system with four verbosity levels, and a customizable error handler.
Debug Levels
| Level | Behavior | When to Use |
|---|---|---|
| 0 | Suppresses stack trace logs entirely | Production |
| 1 | Logs errors with file and line number | Testing |
| 2 | Adds class names and function names | Development |
| 3 | Adds detailed object information | Deep debugging |
// Development: show everything
$f3->set('DEBUG', 3);
// Production: hide errors from users
$f3->set('DEBUG', 0); DEBUG to 0 in production.Custom Error Handler
$f3->set('ONERROR', function($f3) {
$code = $f3->get('ERROR.code');
$text = $f3->get('ERROR.text');
// Log the error
$logger = new \Log('errors.log');
$logger->write("[$code] $text");
// Show custom error page
http_response_code($code);
echo \Template::instance()->render("errors/$code.htm");
}); Triggering Errors Manually
// Trigger a 404 error
$f3->error(404);
// Trigger with custom message
$f3->error(403, 'Access denied. Please contact admin.'); Putting It Together
Here's how a real app might use all these features together:
# config.ini
[globals]
app.name = My Blog
DEBUG = 3
db.path = data/app.db # config/routes.ini
[routes]
GET / = Blog->index
GET /post/@slug = Blog->show
GET /login = Auth->loginForm
POST /login = Auth->login
GET /logout = Auth->logout
[redirects]
GET /archive = /posts <?php
require 'vendor/autoload.php';
$f3 = \Base::instance();
// Load configuration
$f3->config('config.ini');
$f3->config('config/routes.ini');
// Enable caching
$f3->set('CACHE', true);
// Set up locales
$f3->set('LOCALES', 'dict/');
// Debug mode (disable in production!)
$f3->set('DEBUG', 3);
// Connect database
$db = new \DB\SQL('sqlite:' . $f3->get('db.path'));
$f3->set('DB', $db);
// Custom error handler
$f3->set('ONERROR', function($f3) {
$code = $f3->get('ERROR.code');
echo '<h1>Error ' . $code . '</h1>';
echo '<p>' . htmlspecialchars($f3->get('ERROR.text')) . '</p>';
});
$f3->run(); Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Config not loading | Wrong file path or syntax error | Use absolute path or relative to index.php; check INI syntax |
| Cache not working | CACHE not enabled or directory not writable | Set CACHE to true; ensure tmp/cache/ is writable |
| Session not persisting | Session not started or cookies disabled | Use SESSION variable; check browser cookie settings |
| Translation keys showing as-is | LOCALES not set or wrong LANGUAGE | Set LOCALES path; verify dictionary file exists for that language |
| Stack trace visible in browser | DEBUG set to non-zero in production | Set DEBUG to 0 in production config |
What's Next?
You now have a fully configured F3 application. In Part 6: Real-World Tips, the final part, you'll learn:
- When to use F3 vs Laravel/Symfony (honest comparison)
- Performance tuning and profiling
- Error handling and logging best practices
- Testing your F3 application
- Deployment checklist for production
A framework is only as good as your understanding of it. Let's wrap up with practical wisdom for production in Part 6.
Caching: fatfreeframework.com/optimization
Plugins: fatfreeframework.com/plug-ins