Create a Login System with FatFreeFramework and Medoo

php login mysql apache medoo fatfreeeframework

Login systems are an integral part of any user-centric website. Whether you are building a blog or a to-do app, you need to have a login system to authenticate users and serve login-protected pages. Here in this tutorial, I am going to walk you through a tutorial on creating a very simple login system. Whether you are using PHP or not, you can use the logic in your own application. I have put up the code on GitHub as well. Feel free to use them on any project.

1. Getting Started


For this tutorial, we would use FatFreeFramework, a very lightweight yet robust PHP micro-framework. On the database layer, we would use Medoo, a PHP class that provides easy API to access data from a host of databases (MySQL in our case). As for the view layer (i.e. the HTML), we would use Tabler, a Bootstrap 4 based admin panel.


I will assume that you have PHP, MySQL, Apache installed and you have a basic understanding of Object Oriented Programming  in PHP (however this is not mandatory).

Application Logic

Our app is very simple: Users can login via localhost/login, sign-up from localhost/signup, logout from localhost/logout. Upon logging-in, users will be redirected to localhost/admin. This last page is login protected. So, if a non-loggedin user tries to access /admin, he would be redirected to /login

2. Setting up the database

First, create a MySQL database called signin_example (or whatever catches your fancy). Then, run these commands to create a users table:

SET time_zone = '+00:00';
SET foreign_key_checks = 0;

CREATE TABLE `users` (
  `email` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `firstname` varchar(255) NOT NULL,
  `lastname` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)

It creates a new table called users with six fields: id, email, password, firstname, lastname and created_at. id field is our primary key and is set to auto-increament. created_at field will be pre-filled with CURRENT_TIMESTAMP.

3. Starting with FatFreeFramework

Download the FatFreeFramework from the official GitHub repo, and unzip it. Once unzipped, you’d have the following file-structure:

├── ui
│   ├── css
│   ├── images
├── lib
│   ...
├── composer.json
├── config.ini
├── index.php

Here, ui/ is where we’d keep our html files. lib/ is where core FatFreeFramework files live. config.ini contains all our configurations.

The default file-structure is good, but I prefer a more isolated approach. Let us create a new directory called app/ and another called vendors/. Inside app/ we would keep all our application specific logic. And inside vendors/ will be third-party libaries (like Medoo). Also, copy config.ini to app/. So finally, our directory structure is like:

├── app
|   ├── config.ini
├── ui
│   ├── css
│   ├── images
├── lib
|   ...
├── index.php
├── composer.json

Also, download the Medoo.php file from the website and put it inside vendors/.

4. Configurations

Let’s put all our configs inside app/config.ini:




Change dbname, dbtype etc accordingly.

Next, create a new file called routes.ini inside app/:

GET /login=AuthController->login
GET /logout=AuthController->logout
POST /login=AuthController->auth
GET /signup=AuthController->signup
POST /signup=AuthController->signup
GET /admin=AdminController->profile

Recall from our application logic, we are just setting up the routes of our application here. As regards the controller classes, we would create them in a while.

Next step in our config is setting up .htaccess. Create a new file .htaccess file inside the root directory and put these inside:

RewriteEngine On
RewriteRule ^(tmp)\/|\.ini$ - [R=404]

RewriteCond %{REQUEST_FILENAME} !-l
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L,QSA]
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]

I copied these from F3’s API doc, and slightly changed to serve our purpose.

5. index.php

index.php is our main file. It does all the work for us. Replace the boilerplate index.php with this one:

// Kickstart the framework

/* Medoo */

/* Load configuration */

/* Load routes */

/* Set up medoo */
use Medoo\Medoo;
$md_db = new Medoo([
    'database_type' => $f3->get('dbtype'),
    'database_name' => $f3->get('dbname'),
    'server' => $f3->get('dbhost'),
    'username' => $f3->get('dbuser'),
    'password' => $f3->get('dbpass'),
    'charset' => 'utf8'

/* Load files */

// Let's take off!

Let’s break things.

  1. We first load the FatFreeFramework by including lib/base.php
  2. Then we include Medoo.
  3. We tell the framework to load app/config.ini for configuration purpose.
  4. We also load the routes from app/routes.ini
  5. Next, we set up Medoo. Note here that we are fetching the configs from config.ini by $f3->get(some_config_name). This is provided by F3’s get method.
  6. We load the controllers from app/controllers (which we will create next). Important thing to note here is that by setting AUTOLOAD we tell F3 to load all files inside app/controllers. Do read the documentation page for details.
  7. Finally, we run our app with $f3->run();

At this point if we run the app, we would see an error. That’s because we have no files inside app/controllers that can be AUTOLOADed.


Let’s first set the HTML files which will be served according to the routes. Download the Tabler package from Github. After you unzip the package, you’d have several HTML files. We won’t need them all. Let’s copy: empty.html, login.html, register.htmlprofile.html and the entire assets/directory. Paste all of these into ui/ directory of our app.

Our view system will work this way:

  1. There will be a layout.html which is the base.
  2. Inside layout.html, we will include other smaller chunks of HTML, according to the application logic.
  3. For example, we will have a small login.html file that contains only the login form. And when the user visits /login, layout.html will output the form along with other content.

Now, if you open empty.html, or login.html (the ones we copied from the Tabler package), you’d notice that all the code until <div class='page'> is same for all three files. So, let us create our base layout.html by deleting all the content after <div class="page"> (but keeping the closing </div> , body and html tags. So our layout.html will look like:

<!doctype html>
<html lang="en" dir="ltr">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta http-equiv="Content-Language" content="en" />
    <meta name="msapplication-TileColor" content="#2d89ef">
    <meta name="theme-color" content="#4188c9">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <link rel="icon" href="{{ @BASE }}/ui/favicon.ico" type="image/x-icon"/>
    <link rel="shortcut icon" type="image/x-icon" href="{{ @BASE }}/ui/favicon.ico" />
    <!-- Generated: 2018-04-16 09:29:05 +0200 -->
    <title>{{ @title }}</title>
    <link rel="stylesheet" href="">
    <link rel="stylesheet" href=",300i,400,400i,500,500i,600,600i,700,700i&amp;subset=latin-ext">
    <script src="{{ @BASE }}/ui/assets/js/require.min.js"></script>
          baseUrl: '{{ @BASE }}/ui/'
    <!-- Dashboard Core -->
    <link href="{{ @BASE }}/ui/assets/css/dashboard.css" rel="stylesheet" />
    <script src="{{ @BASE }}/ui/assets/js/dashboard.js"></script>
    <!-- c3.js Charts Plugin -->
    <link href="{{ @BASE }}/ui/assets/plugins/charts-c3/plugin.css" rel="stylesheet" />
    <script src="{{ @BASE }}/ui/assets/plugins/charts-c3/plugin.js"></script>
    <!-- Google Maps Plugin -->
    <link href="{{ @BASE }}/ui/assets/plugins/maps-google/plugin.css" rel="stylesheet" />
    <script src="{{ @BASE }}/ui/assets/plugins/maps-google/plugin.js"></script>
    <!-- Input Mask Plugin -->
    <script src="{{ @BASE }}/ui/assets/plugins/input-mask/plugin.js"></script>
  <body class="">
    <div class="page">
        <include href="{{ @inc }}" />

In the above code, I have made a few changes:

  1. I replaced all ./ with {{ @BASE }}/ui/ F3’s builtin @BASE variable outputs the correct URL of our application.
  2. I have replaced the default title with {{ @title }} We will set this variable to the correct title in our Controller classes.
  3. I also added <include href="{{ @inc }}" /> This inc which we will set in our controllers, is the name of the smaller chunks of HTML (for example, login.html, register.html etc).

Next, from the login.html file, delete everything upto <div class="page"> and the closing body and HTML tags. Same for register.html and profile.html.  Check this directory, for details.

7. Controllers

Base Controller

From the routes.ini file, you can see are going to have two main controllers: AdminController and AuthController. But let’s first create a Controller.php (inside app/controllers/) which we will use to extend later.

//! Base controller
class Controller {
  //! HTTP route pre-processor
  function beforeroute($f3) {
  //! HTTP route post-processor
  function afterroute() {
    // Render HTML layout
    echo Template::instance()->render('layout.html');
  //! Instantiate class
  function __construct() {
    global $md_db;
    $this->db = $md_db;

Here, we create $f3 as an instance of F3’s Base class. Also, note that we have set global $md_db;. We did so, because we are going to need the database object for all our controllers. And when we will extend this Controller class, all our controllers will have access to it.

Also note that we have created an afterroute() method. This method will run every time a route is accessed from a controller extending this one. Inside afterroute(), we have used F3’s Template class to render layout.html.


Next, create a new file called AdminController.php inside app/controller/ and put these inside:

class AdminController extends Controller {
    //! HTTP route pre-processor
    function beforeroute($f3) {
        if (empty($f3->get('SESSION.user_id'))){
            $f3->set('flash', 'Please login to continue');
    function profile($f3){
        $f3->set('title', 'Profile');
        $f3->set('inc', 'profile.html');

This controller is as simple as the last one: it extends the base Controller class. The beforeroute() method checks if the user is logged in by checking the existence of SESSION.user_id. If not, it redirects to /login after setting a flash variable. Note the SESSION variable here. It is the PHP equivalent of SESSION global. Read the documentation for more details.

Also note, we have set title and inc. The title variable will be used inside the <title> tag. The inc variable is the name of the specific file which will be loaded for the route (in this case, it is profile.html)


This is our main controller. Let us start by extending the base Controller class:

class AuthController extends Controller {
    function __construct() {
        global $md_db;
        $this->db = $md_db;


Nothing fancy here. We have set $md_db to global to access the database object inside our controller.

Now, recall from our routes.ini, AuthController has a login method which is mapped to /login route. Let’s create this method (inside the class):

function login($f3){
        $f3->set('title', 'Login');

Here, we clear the session (if the user is visiting the login page, there is no point in keeping him logged in). We set COOKIE.sent to true. This variable will be checked later. Finally, we set the title of the login page and serve login.html.

Let’s also create the logout method (which is mapped to /logout):

function logout($f3){

We just clear the session and redirect the user to /login route.

Now, let’s create the signup method (inside AuthController class):

function signup($f3){
            $email = filter_var($f3->get(''), FILTER_SANITIZE_EMAIL);
            $firstname = filter_var($f3->get('POST.firstname'),FILTER_SANITIZE_STRING);
            $lastname = filter_var($f3->get('POST.lastname'),FILTER_SANITIZE_STRING);
            $password = filter_var($f3->get('POST.password'),FILTER_SANITIZE_STRING);
            $confirm_pass = filter_var($f3->get('POST.confirm_pass'),FILTER_SANITIZE_STRING);
            if($password != $confirm_pass){
                $f3->set('flash', 'Please enter the same password twice');
            } else if(in_array('', [$email, $firstname, $lastname, $password])){
                $f3->set('flash', 'Please fill in all the required fields');
            } else if(!filter_var($email, FILTER_VALIDATE_EMAIL)){
                $f3->set('flash','Please enter valid email address');
            } else if($this->db->has('users',['email'=> $email])){
                $f3->set('flash', 'User already exists.');
            } else {
                $hashed_pass = password_hash($password, PASSWORD_BCRYPT);
                $this->db->insert('users', [
                    'email' => $email,
                    'firstname' => $firstname,
                    'lastname' => $lastname,
                    'password' => $hashed_pass
                    $f3->set('flash', 'Account created. Please login to continue');
        $f3->set('title', 'Sign Up');

Here we first check if a form is being submitted (with POST). If so, we sanitize all the data using PHP’s builtin filter_var method. Then we run a few checks to validate the data. If validation fails, at any level, we set flash variable with the correct message. Finally, if the validation succeeds, we insert a new record in our database after using PHP’s password_hash method and redirect the user to login page.

Couple of things to note in the above code:

  1. We have used Medoo’s has method to check if user with same email address already exists in our database.
  2. We have also used the insert method. Do read the corresponding documentation pages for a better understanding of how they work.

Finally, we need to actually login the users via auth method. (Recall from our routes.ini that AuthController->auth is called everytime POST request to /auth is made. So let’s create the auth method (inside AuthController class):

function auth($f3){
        $f3->set('flash', 'Cookies must be enabled in order to login.');
    } else {
        $email = $f3->get('');
        $password = $f3->get('POST.password');
        $hashed_pass = password_hash($password, PASSWORD_BCRYPT);
        $user = $this->db->select('users', ['id', 'email', 'password'], [ 'email' => $email]);
        if(!empty($user)){ // user is found
            if(!password_verify($password, $user[0]['password'])){ // pass mismatch
                $f3->set('flash', 'Password mismatch. Please try again.');
            } else { // all okay
                $f3->set('SESSION.user_id', $user[0]['id']);
        } else { // no user
            $f3->set('flash', 'User doesn\'t exist. Please enter valid email.');
  1. Recall from our login method that we have set a COOKIE.sent variable. Inside auth method, we first check if the variable is there. This check is done to make sure the browser has support for cookies.
  2. Then we fetch the email and password (which are sent when use clicks the submit button on login page).
  3. Next, we create a hashed password from the password the user submitted.
  4. Then we check if the user with such email and password actually exists in our database (with Medoo’s select method).
  5. If no such user exists, we just set a message and continue with $this->login($f3) (at the bottom.)
  6. If a user exists, we set a SESSION variable user_id and redirect the user to /admin.

That is all! Your login system should be working now.


This is a very basic user login system. You can extend upon it to suit your needs. If you have any question, do let me know in the comments or use the issue tracker on Github.