Tutorial Fat Free Framework

Part 5: Configuration & Utilities

Config files, caching, sessions, and the plugin ecosystem

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.

Prerequisites
You should have F3 working from Part 1 and understand the basics of routing, templates, and databases from Parts 2-4.

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)
INI Format Notes
Strings: Don't need quotes unless they contain spaces or special characters.

Arrays: Use comma separation: colors = red,blue,green

Nested arrays: Use dot notation: db.host = localhost

Comments: Lines starting with ; or # are ignored.

2. Configuration Sections

INI files support four special sections:

SectionPurposeExample
[globals]Set F3 variablesapp.name = My Blog
[routes]Define routesGET / = Home->index
[maps]Route maps/api = Api\Handler
[redirects]Redirect rulesGET /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
Security: Never Commit Secrets
Never put database passwords, API keys, or secrets in config files that are committed to version control. Use environment variables or separate files excluded from git (.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);
Cache Backends
F3 supports these cache backends:

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');
Don't Cache Personalized Pages
F3 caches by URL only — it doesn't know about user sessions. If you cache a page that shows different content for logged-in users, everyone will see the same cached version.

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
);
When to Cache Queries
Cache queries that:
• 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');
});
Security: Use password_hash()
The example above uses 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.

Dictionary Keys Are Global
Dictionary keys become F3 variables. If you have a dictionary key 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');
More Plugins Available
F3 also includes: Audit (security validation), Auth (authentication), Image (image processing/CAPTCHA), SMTP (email), Test (unit testing).

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

LevelBehaviorWhen to Use
0Suppresses stack trace logs entirelyProduction
1Logs errors with file and line numberTesting
2Adds class names and function namesDevelopment
3Adds detailed object informationDeep debugging
// Development: show everything
$f3->set('DEBUG', 3);

// Production: hide errors from users
$f3->set('DEBUG', 0);
Production Security
Stack traces reveal file paths, database queries, and sometimes credentials. Always set 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

ProblemCauseFix
Config not loadingWrong file path or syntax errorUse absolute path or relative to index.php; check INI syntax
Cache not workingCACHE not enabled or directory not writableSet CACHE to true; ensure tmp/cache/ is writable
Session not persistingSession not started or cookies disabledUse SESSION variable; check browser cookie settings
Translation keys showing as-isLOCALES not set or wrong LANGUAGESet LOCALES path; verify dictionary file exists for that language
Stack trace visible in browserDEBUG set to non-zero in productionSet 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.