diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..fd206b2
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+DB_HOST=
+DB_NAME=
+DB_USER=
+DB_PASSWORD=
+SHOW_ERRORS=1
diff --git a/.gitignore b/.gitignore
index 6af376f..24e8192 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-vendor
-
-composer.lock
\ No newline at end of file
+.env
+/vendor
\ No newline at end of file
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..7c4ca3a
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,2 @@
+RewriteEngine On
+RewriteRule (.*) public/$1
\ No newline at end of file
diff --git a/App/Config.php b/App/Config.php
deleted file mode 100644
index 4f5859b..0000000
--- a/App/Config.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-namespace App;
-
-/**
- * Application configuration
- *
- * PHP version 7.0
- */
-class Config
-{
-
-    /**
-     * Database host
-     * @var string
-     */
-    const DB_HOST = 'your-database-host';
-
-    /**
-     * Database name
-     * @var string
-     */
-    const DB_NAME = 'your-database-name';
-
-    /**
-     * Database user
-     * @var string
-     */
-    const DB_USER = 'your-database-user';
-
-    /**
-     * Database password
-     * @var string
-     */
-    const DB_PASSWORD = 'your-database-password';
-
-    /**
-     * Show or hide error messages on screen
-     * @var boolean
-     */
-    const SHOW_ERRORS = true;
-}
diff --git a/App/Controllers/Home.php b/App/Controllers/Home.php
deleted file mode 100644
index 8fc4571..0000000
--- a/App/Controllers/Home.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-namespace App\Controllers;
-
-use \Core\View;
-
-/**
- * Home controller
- *
- * PHP version 7.0
- */
-class Home extends \Core\Controller
-{
-
-    /**
-     * Show the index page
-     *
-     * @return void
-     */
-    public function indexAction()
-    {
-        View::renderTemplate('Home/index.html');
-    }
-}
diff --git a/App/Models/User.php b/App/Models/User.php
deleted file mode 100644
index 2edb9b1..0000000
--- a/App/Models/User.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-namespace App\Models;
-
-use PDO;
-
-/**
- * Example user model
- *
- * PHP version 7.0
- */
-class User extends \Core\Model
-{
-
-    /**
-     * Get all the users as an associative array
-     *
-     * @return array
-     */
-    public static function getAll()
-    {
-        $db = static::getDB();
-        $stmt = $db->query('SELECT id, name FROM users');
-        return $stmt->fetchAll(PDO::FETCH_ASSOC);
-    }
-}
diff --git a/App/Views/404.html b/App/Views/404.html
deleted file mode 100644
index de09ea4..0000000
--- a/App/Views/404.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Page not found{% endblock %}
-
-{% block body %}
-
-    <h1>Page not found</h1>
-    <p>Sorry, that page doesn't exist.</p>
-
-{% endblock %}
diff --git a/App/Views/500.html b/App/Views/500.html
deleted file mode 100644
index 8925d98..0000000
--- a/App/Views/500.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Error{% endblock %}
-
-{% block body %}
-
-    <h1>An error occurred</h1>
-    <p>Sorry, an error has occurred.</p>
-
-{% endblock %}
diff --git a/App/Views/Home/index.html b/App/Views/Home/index.html
deleted file mode 100644
index 59d06a4..0000000
--- a/App/Views/Home/index.html
+++ /dev/null
@@ -1,9 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Home{% endblock %}
-
-{% block body %}
-
-    <h1>Welcome</h1>
-
-{% endblock %}
diff --git a/App/Views/base.html b/App/Views/base.html
deleted file mode 100644
index 08e2e58..0000000
--- a/App/Views/base.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="UTF-8">
-    <title>{% block title %}{% endblock %}</title>
-</head>
-<body>
-    {% block body %}
-    {% endblock %}
-</body>
-</html>
diff --git a/Core/Controller.php b/Core/Controller.php
deleted file mode 100644
index 4ce2d51..0000000
--- a/Core/Controller.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-namespace Core;
-
-/**
- * Base controller
- *
- * PHP version 7.0
- */
-abstract class Controller
-{
-
-    /**
-     * Parameters from the matched route
-     * @var array
-     */
-    protected $route_params = [];
-
-    /**
-     * Class constructor
-     *
-     * @param array $route_params  Parameters from the route
-     *
-     * @return void
-     */
-    public function __construct($route_params)
-    {
-        $this->route_params = $route_params;
-    }
-
-    /**
-     * Magic method called when a non-existent or inaccessible method is
-     * called on an object of this class. Used to execute before and after
-     * filter methods on action methods. Action methods need to be named
-     * with an "Action" suffix, e.g. indexAction, showAction etc.
-     *
-     * @param string $name  Method name
-     * @param array $args Arguments passed to the method
-     *
-     * @return void
-     */
-    public function __call($name, $args)
-    {
-        $method = $name . 'Action';
-
-        if (method_exists($this, $method)) {
-            if ($this->before() !== false) {
-                call_user_func_array([$this, $method], $args);
-                $this->after();
-            }
-        } else {
-            throw new \Exception("Method $method not found in controller " . get_class($this));
-        }
-    }
-
-    /**
-     * Before filter - called before an action method.
-     *
-     * @return void
-     */
-    protected function before()
-    {
-    }
-
-    /**
-     * After filter - called after an action method.
-     *
-     * @return void
-     */
-    protected function after()
-    {
-    }
-}
diff --git a/Core/Error.php b/Core/Error.php
deleted file mode 100644
index e95ec56..0000000
--- a/Core/Error.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-namespace Core;
-
-/**
- * Error and exception handler
- *
- * PHP version 7.0
- */
-class Error
-{
-
-    /**
-     * Error handler. Convert all errors to Exceptions by throwing an ErrorException.
-     *
-     * @param int $level  Error level
-     * @param string $message  Error message
-     * @param string $file  Filename the error was raised in
-     * @param int $line  Line number in the file
-     *
-     * @return void
-     */
-    public static function errorHandler($level, $message, $file, $line)
-    {
-        if (error_reporting() !== 0) {  // to keep the @ operator working
-            throw new \ErrorException($message, 0, $level, $file, $line);
-        }
-    }
-
-    /**
-     * Exception handler.
-     *
-     * @param Exception $exception  The exception
-     *
-     * @return void
-     */
-    public static function exceptionHandler($exception)
-    {
-        // Code is 404 (not found) or 500 (general error)
-        $code = $exception->getCode();
-        if ($code != 404) {
-            $code = 500;
-        }
-        http_response_code($code);
-
-        if (\App\Config::SHOW_ERRORS) {
-            echo "<h1>Fatal error</h1>";
-            echo "<p>Uncaught exception: '" . get_class($exception) . "'</p>";
-            echo "<p>Message: '" . $exception->getMessage() . "'</p>";
-            echo "<p>Stack trace:<pre>" . $exception->getTraceAsString() . "</pre></p>";
-            echo "<p>Thrown in '" . $exception->getFile() . "' on line " . $exception->getLine() . "</p>";
-        } else {
-            $log = dirname(__DIR__) . '/logs/' . date('Y-m-d') . '.txt';
-            ini_set('error_log', $log);
-
-            $message = "Uncaught exception: '" . get_class($exception) . "'";
-            $message .= " with message '" . $exception->getMessage() . "'";
-            $message .= "\nStack trace: " . $exception->getTraceAsString();
-            $message .= "\nThrown in '" . $exception->getFile() . "' on line " . $exception->getLine();
-
-            error_log($message);
-
-            View::renderTemplate("$code.html");
-        }
-    }
-}
diff --git a/Core/Model.php b/Core/Model.php
deleted file mode 100644
index 94ea06b..0000000
--- a/Core/Model.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-namespace Core;
-
-use PDO;
-use App\Config;
-
-/**
- * Base model
- *
- * PHP version 7.0
- */
-abstract class Model
-{
-
-    /**
-     * Get the PDO database connection
-     *
-     * @return mixed
-     */
-    protected static function getDB()
-    {
-        static $db = null;
-
-        if ($db === null) {
-            $dsn = 'mysql:host=' . Config::DB_HOST . ';dbname=' . Config::DB_NAME . ';charset=utf8';
-            $db = new PDO($dsn, Config::DB_USER, Config::DB_PASSWORD);
-
-            // Throw an Exception when an error occurs
-            $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
-        }
-
-        return $db;
-    }
-}
diff --git a/Core/Router.php b/Core/Router.php
deleted file mode 100644
index 85b3746..0000000
--- a/Core/Router.php
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-
-namespace Core;
-
-/**
- * Router
- *
- * PHP version 7.0
- */
-class Router
-{
-
-    /**
-     * Associative array of routes (the routing table)
-     * @var array
-     */
-    protected $routes = [];
-
-    /**
-     * Parameters from the matched route
-     * @var array
-     */
-    protected $params = [];
-
-    /**
-     * Add a route to the routing table
-     *
-     * @param string $route  The route URL
-     * @param array  $params Parameters (controller, action, etc.)
-     *
-     * @return void
-     */
-    public function add($route, $params = [])
-    {
-        // Convert the route to a regular expression: escape forward slashes
-        $route = preg_replace('/\//', '\\/', $route);
-
-        // Convert variables e.g. {controller}
-        $route = preg_replace('/\{([a-z]+)\}/', '(?P<\1>[a-z-]+)', $route);
-
-        // Convert variables with custom regular expressions e.g. {id:\d+}
-        $route = preg_replace('/\{([a-z]+):([^\}]+)\}/', '(?P<\1>\2)', $route);
-
-        // Add start and end delimiters, and case insensitive flag
-        $route = '/^' . $route . '$/i';
-
-        $this->routes[$route] = $params;
-    }
-
-    /**
-     * Get all the routes from the routing table
-     *
-     * @return array
-     */
-    public function getRoutes()
-    {
-        return $this->routes;
-    }
-
-    /**
-     * Match the route to the routes in the routing table, setting the $params
-     * property if a route is found.
-     *
-     * @param string $url The route URL
-     *
-     * @return boolean  true if a match found, false otherwise
-     */
-    public function match($url)
-    {
-        foreach ($this->routes as $route => $params) {
-            if (preg_match($route, $url, $matches)) {
-                // Get named capture group values
-                foreach ($matches as $key => $match) {
-                    if (is_string($key)) {
-                        $params[$key] = $match;
-                    }
-                }
-
-                $this->params = $params;
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Get the currently matched parameters
-     *
-     * @return array
-     */
-    public function getParams()
-    {
-        return $this->params;
-    }
-
-    /**
-     * Dispatch the route, creating the controller object and running the
-     * action method
-     *
-     * @param string $url The route URL
-     *
-     * @return void
-     */
-    public function dispatch($url)
-    {
-        $url = $this->removeQueryStringVariables($url);
-
-        if ($this->match($url)) {
-            $controller = $this->params['controller'];
-            $controller = $this->convertToStudlyCaps($controller);
-            $controller = $this->getNamespace() . $controller;
-
-            if (class_exists($controller)) {
-                $controller_object = new $controller($this->params);
-
-                $action = $this->params['action'];
-                $action = $this->convertToCamelCase($action);
-
-                if (preg_match('/action$/i', $action) == 0) {
-                    $controller_object->$action();
-
-                } else {
-                    throw new \Exception("Method $action in controller $controller cannot be called directly - remove the Action suffix to call this method");
-                }
-            } else {
-                throw new \Exception("Controller class $controller not found");
-            }
-        } else {
-            throw new \Exception('No route matched.', 404);
-        }
-    }
-
-    /**
-     * Convert the string with hyphens to StudlyCaps,
-     * e.g. post-authors => PostAuthors
-     *
-     * @param string $string The string to convert
-     *
-     * @return string
-     */
-    protected function convertToStudlyCaps($string)
-    {
-        return str_replace(' ', '', ucwords(str_replace('-', ' ', $string)));
-    }
-
-    /**
-     * Convert the string with hyphens to camelCase,
-     * e.g. add-new => addNew
-     *
-     * @param string $string The string to convert
-     *
-     * @return string
-     */
-    protected function convertToCamelCase($string)
-    {
-        return lcfirst($this->convertToStudlyCaps($string));
-    }
-
-    /**
-     * Remove the query string variables from the URL (if any). As the full
-     * query string is used for the route, any variables at the end will need
-     * to be removed before the route is matched to the routing table. For
-     * example:
-     *
-     *   URL                           $_SERVER['QUERY_STRING']  Route
-     *   -------------------------------------------------------------------
-     *   localhost                     ''                        ''
-     *   localhost/?                   ''                        ''
-     *   localhost/?page=1             page=1                    ''
-     *   localhost/posts?page=1        posts&page=1              posts
-     *   localhost/posts/index         posts/index               posts/index
-     *   localhost/posts/index?page=1  posts/index&page=1        posts/index
-     *
-     * A URL of the format localhost/?page (one variable name, no value) won't
-     * work however. (NB. The .htaccess file converts the first ? to a & when
-     * it's passed through to the $_SERVER variable).
-     *
-     * @param string $url The full URL
-     *
-     * @return string The URL with the query string variables removed
-     */
-    protected function removeQueryStringVariables($url)
-    {
-        if ($url != '') {
-            $parts = explode('&', $url, 2);
-
-            if (strpos($parts[0], '=') === false) {
-                $url = $parts[0];
-            } else {
-                $url = '';
-            }
-        }
-
-        return $url;
-    }
-
-    /**
-     * Get the namespace for the controller class. The namespace defined in the
-     * route parameters is added if present.
-     *
-     * @return string The request URL
-     */
-    protected function getNamespace()
-    {
-        $namespace = 'App\Controllers\\';
-
-        if (array_key_exists('namespace', $this->params)) {
-            $namespace .= $this->params['namespace'] . '\\';
-        }
-
-        return $namespace;
-    }
-}
diff --git a/Core/View.php b/Core/View.php
deleted file mode 100644
index 0e18ab2..0000000
--- a/Core/View.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-namespace Core;
-
-/**
- * View
- *
- * PHP version 7.0
- */
-class View
-{
-
-    /**
-     * Render a view file
-     *
-     * @param string $view  The view file
-     * @param array $args  Associative array of data to display in the view (optional)
-     *
-     * @return void
-     */
-    public static function render($view, $args = [])
-    {
-        extract($args, EXTR_SKIP);
-
-        $file = dirname(__DIR__) . "/App/Views/$view";  // relative to Core directory
-
-        if (is_readable($file)) {
-            require $file;
-        } else {
-            throw new \Exception("$file not found");
-        }
-    }
-
-    /**
-     * Render a view template using Twig
-     *
-     * @param string $template  The template file
-     * @param array $args  Associative array of data to display in the view (optional)
-     *
-     * @return void
-     */
-    public static function renderTemplate($template, $args = [])
-    {
-        static $twig = null;
-
-        if ($twig === null) {
-            $loader = new \Twig\Loader\Filesystemloader(dirname(__DIR__) . '/App/Views');
-            $twig = new \Twig\Environment($loader);
-        }
-
-        echo $twig->render($template, $args);
-    }
-}
diff --git a/README.md b/README.md
index a076db0..3b558a4 100644
--- a/README.md
+++ b/README.md
@@ -1,128 +1,23 @@
-# Welcome to the PHP MVC framework
+# Welcome to the PHP MVC Framework
 
 This is a simple MVC framework for building web applications in PHP. It's free and [open-source](LICENSE).
 
-It was created for the [Write PHP like a pro: build an MVC framework from scratch](https://davehollingworth.net/phpmvcg) course. The course explains how the framework is put together, building it step-by-step, from scratch. If you've taken the course, then you'll already know how to use it. If not, please follow the instructions below.
+It was created for the [Write PHP Like a Pro: Build an MVC Framework from Scratch](https://davehollingworth.com/go/phpmvc/) course.
 
-## Starting an application using this framework
+That course explains how the framework is put together, building it step-by-step, from scratch. If you've taken the course, then you'll already know how to use it.
 
-1. First, download the framework, either directly or by cloning the repo.
-1. Run **composer update** to install the project dependencies.
-1. Configure your web server to have the **public** folder as the web root.
-1. Open [App/Config.php](App/Config.php) and enter your database configuration data.
-1. Create routes, add controllers, views and models.
-
-See below for more details.
-
-## Configuration
-
-Configuration settings are stored in the [App/Config.php](App/Config.php) class. Default settings include database connection data and a setting to show or hide error detail. You can access the settings in your code like this: `Config::DB_HOST`. You can add your own configuration settings in here.
-
-## Routing
-
-The [Router](Core/Router.php) translates URLs into controllers and actions. Routes are added in the [front controller](public/index.php). A sample home route is included that routes to the `index` action in the [Home controller](App/Controllers/Home.php).
-
-Routes are added with the `add` method. You can add fixed URL routes, and specify the controller and action, like this:
-
-```php
-$router->add('', ['controller' => 'Home', 'action' => 'index']);
-$router->add('posts/index', ['controller' => 'Posts', 'action' => 'index']);
-```
-
-Or you can add route **variables**, like this:
-
-```php
-$router->add('{controller}/{action}');
-```
-
-In addition to the **controller** and **action**, you can specify any parameter you like within curly braces, and also specify a custom regular expression for that parameter:
-
-```php
-$router->add('{controller}/{id:\d+}/{action}');
-```
-
-You can also specify a namespace for the controller:
-
-```php
-$router->add('admin/{controller}/{action}', ['namespace' => 'Admin']);
-```
-
-## Controllers
-
-Controllers respond to user actions (clicking on a link, submitting a form etc.). Controllers are classes that extend the [Core\Controller](Core/Controller.php) class.
-
-Controllers are stored in the `App/Controllers` folder. A sample [Home controller](App/Controllers/Home.php) included. Controller classes need to be in the `App/Controllers` namespace. You can add subdirectories to organise your controllers, so when adding a route for these controllers you need to specify the namespace (see the routing section above).
-
-Controller classes contain methods that are the actions. To create an action, add the **`Action`** suffix to the method name. The sample controller in [App/Controllers/Home.php](App/Controllers/Home.php) has a sample `index` action.
+If not, please look at the example code in the [config](config) and [src/App](src/App) folders for examples of how to use it.
 
-You can access route parameters (for example the **id** parameter shown in the route examples above) in actions via the `$this->route_params` property.
+## DISCLAIMER
+This framework was developed as a **teaching example** on the aforementioned course. The idea is that by developing this framework on that course, you'll understand how frameworks like Laravel work. No warranty is provided. For more details please see the [licence](LICENSE).
 
-### Action filters
+## Starting an Application using this Framework
 
-Controllers can have **before** and **after** filter methods. These are methods that are called before and after **every** action method call in a controller. Useful for authentication for example, making sure that a user is logged in before letting them execute an action. Optionally add a **before filter** to a controller like this:
-
-```php
-/**
- * Before filter. Return false to stop the action from executing.
- *
- * @return void
- */
-protected function before()
-{
-}
-```
-
-To stop the originally called action from executing, return `false` from the before filter method. An **after filter** is added like this:
-
-```php
-/**
- * After filter.
- *
- * @return void
- */
-protected function after()
-{
-}
-```
-
-## Views
-
-Views are used to display information (normally HTML). View files go in the `App/Views` folder. Views can be in one of two formats: standard PHP, but with just enough PHP to show the data. No database access or anything like that should occur in a view file. You can render a standard PHP view in a controller, optionally passing in variables, like this:
-
-```php
-View::render('Home/index.php', [
-    'name'    => 'Dave',
-    'colours' => ['red', 'green', 'blue']
-]);
-```
-
-The second format uses the [Twig](http://twig.sensiolabs.org/) templating engine. Using Twig allows you to have simpler, safer templates that can take advantage of things like [template inheritance](http://twig.sensiolabs.org/doc/templates.html#template-inheritance). You can render a Twig template like this:
-
-```php
-View::renderTemplate('Home/index.html', [
-    'name'    => 'Dave',
-    'colours' => ['red', 'green', 'blue']
-]);
-```
-
-A sample Twig template is included in [App/Views/Home/index.html](App/Views/Home/index.html) that inherits from the base template in [App/Views/base.html](App/Views/base.html).
-
-## Models
-
-Models are used to get and store data in your application. They know nothing about how this data is to be presented in the views. Models extend the `Core\Model` class and use [PDO](http://php.net/manual/en/book.pdo.php) to access the database. They're stored in the `App/Models` folder. A sample user model class is included in [App/Models/User.php](App/Models/User.php). You can get the PDO database connection instance like this:
-
-```php
-$db = static::getDB();
-```
-
-## Errors
-
-If the `SHOW_ERRORS` configuration setting is set to `true`, full error detail will be shown in the browser if an error or exception occurs. If it's set to `false`, a generic message will be shown using the [App/Views/404.html](App/Views/404.html) or [App/Views/500.html](App/Views/500.html) views, depending on the error.
-
-## Web server configuration
-
-Pretty URLs are enabled using web server rewrite rules. An [.htaccess](public/.htaccess) file is included in the `public` folder. Equivalent nginx configuration is in the [nginx-configuration.txt](nginx-configuration.txt) file.
+1. First, download the framework, either directly or by cloning the repo.
+1. Configure your web server to have the **public** folder as the web root.
+1. Make a copy of the [.env.example](.env.example) file and call it .env, and enter your database configuration data.
+1. Create routes, controllers, views, models, middleware etc.
 
----
+## Build This Framework From Scratch
 
-Signup for the course [here](https://davehollingworth.net/phpmvcg) and understand how this framework is built from scratch, putting it all together step by step.
+Signup for the course [here](https://davehollingworth.com/go/phpmvc/) and understand how this framework is built from scratch, putting it all together step by step.
diff --git a/composer.json b/composer.json
deleted file mode 100644
index de0df26..0000000
--- a/composer.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-    "require": {
-        "twig/twig": "~3.0"
-    },
-    "autoload": {
-        "psr-4": {
-            "Core\\": "Core/",
-            "App\\": "App/"
-        }
-    }
-}
diff --git a/config/middleware.php b/config/middleware.php
new file mode 100644
index 0000000..5e10c07
--- /dev/null
+++ b/config/middleware.php
@@ -0,0 +1,5 @@
+<?php
+
+return [
+    "example" => \App\Middleware\MiddlewareExample::class
+];
\ No newline at end of file
diff --git a/config/routes.php b/config/routes.php
new file mode 100644
index 0000000..a678b7e
--- /dev/null
+++ b/config/routes.php
@@ -0,0 +1,25 @@
+<?php
+
+$router = new Framework\Router;
+
+// Homepage example
+$router->add("/", ["controller" => "home", "action" => "index"]);
+
+// Catch-all example
+$router->add("/{controller}/{action}");
+
+// Examples with custom route variables
+$router->add("/{title}/{id:\d+}/{page:\d+}", ["controller" => "products", "action" => "showPage"]);
+$router->add("/product/{slug:[\w-]+}", ["controller" => "products", "action" => "show"]);
+
+// Example with namespace
+$router->add("/admin/{controller}/{action}", ["namespace" => "Admin"]);
+
+// Example with HTTP method
+$router->add("/{controller}/{id:\d+}/destroy", ["action" => "destroy", "method" => "post"]);
+
+// Example with middleware
+$router->add("/{controller}/{id:\d+}/show", ["action" => "show", "middleware" => "example"]);
+$router->add("/{controller}/{id:\d+}/edit", ["action" => "edit", "middleware" => "one|two"]);
+
+return $router;
\ No newline at end of file
diff --git a/config/services.php b/config/services.php
new file mode 100644
index 0000000..affa3aa
--- /dev/null
+++ b/config/services.php
@@ -0,0 +1,19 @@
+<?php
+
+$container = new Framework\Container;
+
+// Database example
+$container->set(App\Database::class, function() {
+
+    return new App\Database($_ENV["DB_HOST"], $_ENV["DB_NAME"], $_ENV["DB_USER"], $_ENV["DB_PASSWORD"]);
+
+});
+
+// Viewer example
+$container->set(Framework\TemplateViewerInterface::class, function() {
+
+    return new Framework\MVCTemplateViewer;
+
+});
+
+return $container;
\ No newline at end of file
diff --git a/logs/.keep b/logs/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/nginx-configuration.txt b/nginx-configuration.txt
deleted file mode 100644
index 1f6bf9a..0000000
--- a/nginx-configuration.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# nginx configuration to remove the question mark from the URL but maintain the query string
-
-location / { 
-    if (!-f $request_filename){
-        set $rule_0 1$rule_0;
-    }   
-    if (!-d $request_filename){
-        set $rule_0 2$rule_0;
-    }
-    if ($rule_0 = "21"){
-        rewrite ^/(.*)$ /index.php?$1 last;
-    }   
-}
\ No newline at end of file
diff --git a/public/.htaccess b/public/.htaccess
index c823dae..b7e2dcd 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -1,10 +1,5 @@
-# Remove the question mark from the request but maintain the query string
 RewriteEngine On
-
-# Uncomment the following line if your public folder isn't the web server's root
-# RewriteBase /
-
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteCond %{REQUEST_FILENAME} !-l
-RewriteRule ^(.*)$ index.php?$1 [L,QSA]
+RewriteRule ^ index.php
diff --git a/public/example.css b/public/example.css
new file mode 100644
index 0000000..7918fe3
--- /dev/null
+++ b/public/example.css
@@ -0,0 +1,5 @@
+/* Example CSS file */
+html {
+    font-family: sans-serif;
+    margin: 1em;
+}
\ No newline at end of file
diff --git a/public/index.php b/public/index.php
index 2e7ef24..29f0f18 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,32 +1,33 @@
 <?php
 
-/**
- * Front controller
- *
- * PHP version 7.0
- */
-
-/**
- * Composer
- */
-require dirname(__DIR__) . '/vendor/autoload.php';
-
-
-/**
- * Error and Exception handling
- */
-error_reporting(E_ALL);
-set_error_handler('Core\Error::errorHandler');
-set_exception_handler('Core\Error::exceptionHandler');
-
-
-/**
- * Routing
- */
-$router = new Core\Router();
-
-// Add the routes
-$router->add('', ['controller' => 'Home', 'action' => 'index']);
-$router->add('{controller}/{action}');
-    
-$router->dispatch($_SERVER['QUERY_STRING']);
+declare(strict_types=1);
+
+define("ROOT_PATH", dirname(__DIR__));
+
+spl_autoload_register(function (string $class_name) {
+
+    require ROOT_PATH . "/src/" . str_replace("\\", "/", $class_name) . ".php";
+
+});
+
+$dotenv = new Framework\Dotenv;
+
+$dotenv->load(ROOT_PATH . "/.env");
+
+set_error_handler("Framework\ErrorHandler::handleError");
+
+set_exception_handler("Framework\ErrorHandler::handleException");
+
+$router = require ROOT_PATH . "/config/routes.php";
+
+$container = require ROOT_PATH . "/config/services.php";
+
+$middleware = require ROOT_PATH . "/config/middleware.php";
+
+$dispatcher = new Framework\Dispatcher($router, $container, $middleware);
+
+$request = Framework\Request::createFromGlobals();
+
+$response = $dispatcher->handle($request);
+
+$response->send();
\ No newline at end of file
diff --git a/src/App/Controllers/Admin/Users.php b/src/App/Controllers/Admin/Users.php
new file mode 100644
index 0000000..10d70b6
--- /dev/null
+++ b/src/App/Controllers/Admin/Users.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers\Admin;
+
+class Users
+{
+    public function index()
+    {
+        echo "Hello from a namespaced controller";
+    }
+}
\ No newline at end of file
diff --git a/src/App/Controllers/Home.php b/src/App/Controllers/Home.php
new file mode 100644
index 0000000..5e10293
--- /dev/null
+++ b/src/App/Controllers/Home.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers;
+
+use Framework\Controller;
+use Framework\Response;
+
+class Home extends Controller
+{
+    public function index(): Response
+    {
+        return $this->view("Home/index.mvc.php");
+    }
+}
diff --git a/src/App/Controllers/Products.php b/src/App/Controllers/Products.php
new file mode 100644
index 0000000..7539452
--- /dev/null
+++ b/src/App/Controllers/Products.php
@@ -0,0 +1,124 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers;
+
+use App\Models\Product;
+use Framework\Exceptions\PageNotFoundException;
+use Framework\Controller;
+use Framework\Response;
+
+// Example controller
+class Products extends Controller
+{
+    public function __construct(private Product $model)
+    {
+    }
+
+    public function index(): Response
+    {
+        $products = $this->model->findAll();
+
+        return $this->view("Products/index.mvc.php", [
+            "products" => $products,
+            "total" => $this->model->getTotal()
+        ]);
+    }
+
+    public function show(string $id): Response
+    {
+        $product = $this->getProduct($id);
+
+        return $this->view("Products/show.mvc.php", [
+            "product" => $product
+        ]);
+    }
+
+    public function edit(string $id): Response
+    {
+        $product = $this->getProduct($id);
+
+        return $this->view("Products/edit.mvc.php", [
+            "product" => $product
+        ]);
+    }
+
+    public function new(): Response
+    {
+        return $this->view("Products/new.mvc.php");
+    }
+
+    public function create(): Response
+    {
+        $data = [
+            "name" => $this->request->post["name"],
+            "description" => empty($this->request->post["description"]) ? null : $this->request->post["description"]
+        ];
+
+        if ($this->model->insert($data)) {
+
+            return $this->redirect("/products/{$this->model->getInsertID()}/show");
+
+        } else {
+
+            return $this->view("Products/new.mvc.php", [
+                "errors" => $this->model->getErrors(),
+                "product" => $data
+            ]);
+
+        }
+    }
+
+    public function update(string $id): Response
+    {
+        $product = $this->getProduct($id);
+        
+        $product["name"] = $this->request->post["name"];
+        $product["description"] = empty($this->request->post["description"]) ? null : $this->request->post["description"];
+
+        if ($this->model->update($id, $product)) {
+
+            return $this->redirect("/products/{$id}/show");
+
+        } else {
+
+            return $this->view("Products/edit.mvc.php", [
+                "errors" => $this->model->getErrors(),
+                "product" => $product
+            ]);
+
+        }
+    }
+    
+    public function delete(string $id): Response
+    {
+        $product = $this->getProduct($id);
+
+        return $this->view("Products/delete.mvc.php", [
+            "product" => $product
+        ]);
+    }
+
+    public function destroy(string $id): Response
+    {
+        $product = $this->getProduct($id);
+
+        $this->model->delete($id);
+
+        return $this->redirect("/products/index");
+    }
+
+    private function getProduct(string $id): array
+    {
+        $product = $this->model->find($id);
+
+        if ($product === false) {
+
+            throw new PageNotFoundException("Product not found");
+
+        }
+
+        return $product;
+    }
+}
\ No newline at end of file
diff --git a/src/App/Database.php b/src/App/Database.php
new file mode 100644
index 0000000..33b265b
--- /dev/null
+++ b/src/App/Database.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App;
+
+use PDO;
+
+class Database
+{
+    private ?PDO $pdo = null;
+
+    public function __construct(private string $host,
+                                private string $name,
+                                private string $user,
+                                private string $password)
+    {
+    }
+
+    public function getConnection(): PDO
+    {
+        if ($this->pdo === null) {
+
+            $dsn = "mysql:host={$this->host};dbname={$this->name};charset=utf8;port=3306";
+
+            $this->pdo = new PDO($dsn, $this->user, $this->password, [
+                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+            ]);
+        }
+
+        return $this->pdo;
+    }
+}
\ No newline at end of file
diff --git a/src/App/Middleware/ChangeRequestExample.php b/src/App/Middleware/ChangeRequestExample.php
new file mode 100644
index 0000000..09ae4a7
--- /dev/null
+++ b/src/App/Middleware/ChangeRequestExample.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use Framework\Request;
+use Framework\Response;
+use Framework\RequestHandlerInterface;
+use Framework\MiddlewareInterface;
+
+class ChangeRequestExample implements MiddlewareInterface
+{
+    public function process(Request $request, RequestHandlerInterface $next): Response
+    {
+        $request->post = array_map("trim", $request->post);
+
+        $response = $next->handle($request);
+
+        return $response;
+    }
+}
\ No newline at end of file
diff --git a/src/App/Middleware/ChangeResponseExample.php b/src/App/Middleware/ChangeResponseExample.php
new file mode 100644
index 0000000..71eb21a
--- /dev/null
+++ b/src/App/Middleware/ChangeResponseExample.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use Framework\Request;
+use Framework\Response;
+use Framework\RequestHandlerInterface;
+use Framework\MiddlewareInterface;
+
+class ChangeResponseExample implements MiddlewareInterface
+{
+    public function process(Request $request, RequestHandlerInterface $next): Response
+    {
+        $response = $next->handle($request);
+
+        $response->setBody($response->getBody() . " hello from the middleware");
+
+        return $response;
+    }
+}
\ No newline at end of file
diff --git a/src/App/Middleware/RedirectExample.php b/src/App/Middleware/RedirectExample.php
new file mode 100644
index 0000000..9c9fad5
--- /dev/null
+++ b/src/App/Middleware/RedirectExample.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use Framework\Request;
+use Framework\Response;
+use Framework\RequestHandlerInterface;
+use Framework\MiddlewareInterface;
+
+class RedirectExample implements MiddlewareInterface
+{
+    public function __construct(private Response $response)
+    {
+    }
+
+    public function process(Request $request, RequestHandlerInterface $next): Response
+    {
+        $this->response->redirect("/products/index");
+
+        return $this->response;
+    }
+}
\ No newline at end of file
diff --git a/src/App/Models/Product.php b/src/App/Models/Product.php
new file mode 100644
index 0000000..15f593f
--- /dev/null
+++ b/src/App/Models/Product.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Models;
+
+use Framework\Model;
+use PDO;
+
+// Example model
+class Product extends Model
+{
+    // protected $table = "products";
+
+    protected function validate(array $data): void
+    {
+        if (empty($data["name"])) {
+            
+            $this->addError("name", "Name is required");
+
+        }
+    }
+
+    public function getTotal(): int
+    {
+        $sql = "SELECT COUNT(*) AS total
+                FROM product";
+
+        $conn = $this->database->getConnection();
+
+        $stmt = $conn->query($sql);
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        return (int) $row["total"];
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/Container.php b/src/Framework/Container.php
new file mode 100644
index 0000000..85f4b0d
--- /dev/null
+++ b/src/Framework/Container.php
@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+use ReflectionClass;
+use Closure;
+use ReflectionNamedType;
+use InvalidArgumentException;
+
+class Container
+{
+    private array $registry = [];
+
+    public function set(string $name, Closure $value): void
+    {
+        $this->registry[$name] = $value;
+    }
+
+    public function get(string $class_name): object
+    {
+        if (array_key_exists($class_name, $this->registry)) {
+
+            return $this->registry[$class_name]();
+
+        }
+
+        $reflector = new ReflectionClass($class_name);
+
+        $constructor = $reflector->getConstructor();
+
+        $dependencies = [];
+
+        if ($constructor === null) {
+
+            return new $class_name;
+
+        }
+
+        foreach ($constructor->getParameters() as $parameter) {
+
+            $type = $parameter->getType();
+
+            if ($type === null) {
+
+                throw new InvalidArgumentException("Constructor parameter 
+                      '{$parameter->getName()}' 
+                      in the $class_name class 
+                      has no type declaration");
+
+            }
+
+            if ( ! ($type instanceof ReflectionNamedType)) {
+
+                throw new InvalidArgumentException("Constructor parameter
+                      '{$parameter->getName()}' 
+                      in the $class_name class is an invalid type: '$type' 
+                      - only single named types supported");
+
+            }
+
+            if ($type->isBuiltIn()) {
+
+                throw new InvalidArgumentException("Unable to resolve
+                      constructor parameter '{$parameter->getName()}'
+                      of type '$type' in the $class_name class");
+
+            }
+
+            $dependencies[] = $this->get((string) $type);
+
+        }
+
+        return new $class_name(...$dependencies);
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/Controller.php b/src/Framework/Controller.php
new file mode 100644
index 0000000..93946a6
--- /dev/null
+++ b/src/Framework/Controller.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+abstract class Controller
+{
+    protected Request $request;
+
+    protected Response $response;
+
+    protected TemplateViewerInterface $viewer;
+    
+    public function setResponse(Response $response): void
+    {
+        $this->response = $response;
+    }
+
+    public function setRequest(Request $request): void
+    {
+        $this->request = $request;
+    }
+
+    public function setViewer(TemplateViewerInterface $viewer): void
+    {
+        $this->viewer = $viewer;
+    }
+
+    protected function view(string $template, array $data = []): Response
+    {
+        $this->response->setBody($this->viewer->render($template, $data));
+
+        return $this->response;        
+    }
+
+    protected function redirect(string $url): Response
+    {
+        $this->response->redirect($url);
+
+        return $this->response;
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/ControllerRequestHandler.php b/src/Framework/ControllerRequestHandler.php
new file mode 100644
index 0000000..cf2483c
--- /dev/null
+++ b/src/Framework/ControllerRequestHandler.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class ControllerRequestHandler implements RequestHandlerInterface
+{
+    public function __construct(private Controller $controller,
+                                private string $action,
+                                private array $args)
+    {
+    }
+
+    public function handle(Request $request): Response
+    {
+        $this->controller->setRequest($request);
+
+        return ($this->controller)->{$this->action}(...$this->args);
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/Dispatcher.php b/src/Framework/Dispatcher.php
new file mode 100644
index 0000000..cca6cbe
--- /dev/null
+++ b/src/Framework/Dispatcher.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+use ReflectionMethod;
+use Framework\Exceptions\PageNotFoundException;
+use UnexpectedValueException;
+
+class Dispatcher
+{
+    public function __construct(private Router $router,
+                                private Container $container,
+                                private array $middleware_classes)
+    {
+    }
+
+    public function handle(Request $request): Response
+    {
+        $path = $this->getPath($request->uri);
+
+        $params = $this->router->match($path, $request->method);
+
+        if ($params === false) {
+
+            throw new PageNotFoundException("No route matched for '$path' with method '{$request->method}'");
+
+        }
+
+        $action = $this->getActionName($params);
+        $controller = $this->getControllerName($params);
+
+        $controller_object = $this->container->get($controller);
+
+        $controller_object->setViewer($this->container->get(TemplateViewerInterface::class));
+
+        $controller_object->setResponse($this->container->get(Response::class));
+
+        $args = $this->getActionArguments($controller, $action, $params);
+
+        $controller_handler = new ControllerRequestHandler($controller_object,
+                                                           $action,
+                                                           $args);
+
+        $middleware = $this->getMiddleware($params);
+
+        $middleware_handler = new MiddlewareRequestHandler($middleware,
+                                                           $controller_handler);
+
+        return $middleware_handler->handle($request);
+    }
+
+    private function getMiddleware(array $params): array
+    {
+        if ( ! array_key_exists("middleware", $params)) {
+
+            return [];
+
+        }
+        
+        $middleware = explode("|", $params["middleware"]);
+
+        array_walk($middleware, function(&$value) {
+
+            if ( ! array_key_exists($value, $this->middleware_classes)) {
+
+                throw new UnexpectedValueException("Middleware '$value' not found in config settings");
+
+            }
+
+            $value = $this->container->get($this->middleware_classes[$value]);
+
+        });
+
+        return $middleware;
+    }
+
+    private function getActionArguments(string $controller, string $action, array $params): array
+    {
+        $args = [];
+        
+        $method = new ReflectionMethod($controller, $action);
+
+        foreach ($method->getParameters() as $parameter) {
+
+            $name = $parameter->getName();
+
+            $args[$name] = $params[$name];
+
+        }
+
+        return $args;
+    }
+
+    private function getControllerName(array $params): string
+    {
+        $controller = $params["controller"];
+
+        $controller = str_replace("-", "", ucwords(strtolower($controller), "-"));
+
+        $namespace = "App\Controllers";
+
+        if (array_key_exists("namespace", $params)) {
+
+            $namespace .= "\\" . $params["namespace"];
+
+        }
+
+        return $namespace . "\\" . $controller;
+    }
+
+    private function getActionName(array $params): string
+    {
+        $action = $params["action"];
+
+        $action = lcfirst(str_replace("-", "", ucwords(strtolower($action), "-")));
+
+        return $action;
+    }
+
+    private function getPath(string $uri): string
+    {
+        $path = parse_url($uri, PHP_URL_PATH);
+
+        if ($path === false) {
+        
+            throw new UnexpectedValueException("Malformed URL: '$uri'");
+        
+        }        
+
+        return $path;
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/Dotenv.php b/src/Framework/Dotenv.php
new file mode 100644
index 0000000..b48411a
--- /dev/null
+++ b/src/Framework/Dotenv.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class Dotenv
+{
+    public function load(string $path): void
+    {
+        $lines = file($path, FILE_IGNORE_NEW_LINES);
+
+        foreach ($lines as $line) {
+
+            list($name, $value) = explode("=", $line, 2);
+
+            $_ENV[$name] = $value;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/ErrorHandler.php b/src/Framework/ErrorHandler.php
new file mode 100644
index 0000000..00498c9
--- /dev/null
+++ b/src/Framework/ErrorHandler.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+use ErrorException;
+use Throwable;
+use Framework\Exceptions\PageNotFoundException;
+
+class ErrorHandler
+{
+    public static function handleError(
+        int $errno,
+        string $errstr,
+        string $errfile,
+        int $errline
+    ): bool
+    {
+        throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
+    }
+
+    public static function handleException(Throwable $exception): void
+    {
+        if ($exception instanceof PageNotFoundException) {
+    
+            http_response_code(404);
+    
+            $template = "404.php";
+    
+        } else {
+        
+            http_response_code(500);
+    
+            $template = "500.php";
+    
+        }
+    
+        if ($_ENV["SHOW_ERRORS"]) {
+    
+            ini_set("display_errors", "1");
+    
+        } else {
+    
+            ini_set("display_errors", "0");
+    
+            ini_set("log_errors", "1");
+    
+            require dirname(__DIR__, 2) . "/views/$template";
+    
+        }
+    
+        throw $exception;
+    }    
+}
diff --git a/src/Framework/Exceptions/PageNotFoundException.php b/src/Framework/Exceptions/PageNotFoundException.php
new file mode 100644
index 0000000..1943452
--- /dev/null
+++ b/src/Framework/Exceptions/PageNotFoundException.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Framework\Exceptions;
+
+use DomainException;
+
+class PageNotFoundException extends DomainException
+{
+}
\ No newline at end of file
diff --git a/src/Framework/MVCTemplateViewer.php b/src/Framework/MVCTemplateViewer.php
new file mode 100644
index 0000000..eb72ea4
--- /dev/null
+++ b/src/Framework/MVCTemplateViewer.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class MVCTemplateViewer implements TemplateViewerInterface
+{
+    public function render(string $template, array $data = []): string
+    {
+        $views_dir = dirname(__DIR__, 2) . "/views/";
+
+        $code = file_get_contents($views_dir . $template);
+
+        if (preg_match('#^{% extends "(?<template>.*)" %}#', $code, $matches) === 1) {
+
+            $base = file_get_contents($views_dir . $matches["template"]);
+
+            $blocks = $this->getBlocks($code);
+
+            $code = $this->replaceYields($base, $blocks);
+
+        }
+
+        $code = $this->loadIncludes($views_dir, $code);
+
+        $code = $this->replaceVariables($code);
+
+        $code = $this->replacePHP($code);
+
+        extract($data, EXTR_SKIP);
+
+        ob_start();
+
+        eval("?>$code");
+
+        return ob_get_clean();
+    }
+
+    private function replaceVariables(string $code): string
+    {
+        return preg_replace("#{{\s*(\S+)\s*}}#", "<?= htmlspecialchars(\$$1 ?? '') ?>", $code);
+    }
+
+    private function replacePHP(string $code): string
+    {
+        return preg_replace("#{%\s*(.+)\s*%}#", "<?php $1 ?>", $code);
+    }
+
+    private function getBlocks(string $code): array
+    {
+        preg_match_all("#{% block (?<name>\w+) %}(?<content>.*?){% endblock %}#s", $code, $matches, PREG_SET_ORDER);
+
+        $blocks = [];
+
+        foreach ($matches as $match) {
+
+            $blocks[$match["name"]] = $match["content"];
+
+        }
+
+        return $blocks;
+    }
+
+    private function replaceYields(string $code, array $blocks): string
+    {
+        preg_match_all("#{% yield (?<name>\w+) %}#", $code, $matches, PREG_SET_ORDER);
+
+        foreach ($matches as $match) {
+
+            $name = $match["name"];
+
+            $block = $blocks[$name];
+
+            $code = preg_replace("#{% yield $name %}#", $block, $code);
+
+        }
+
+        return $code;
+    }
+
+    private function loadIncludes(string $dir, string $code): string
+    {
+        preg_match_all('#{% include "(?<template>.*?)" %}#', $code, $matches, PREG_SET_ORDER);
+
+        foreach ($matches as $match) {
+
+            $template = $match["template"];
+
+            $contents = file_get_contents($dir . $template);
+
+            $code = preg_replace("#{% include \"$template\" %}#", $contents, $code);
+
+        }
+
+        return $code;
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/MiddlewareInterface.php b/src/Framework/MiddlewareInterface.php
new file mode 100644
index 0000000..cef2285
--- /dev/null
+++ b/src/Framework/MiddlewareInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Framework;
+
+interface MiddlewareInterface
+{
+    public function process(Request $request, RequestHandlerInterface $next): Response;    
+}
\ No newline at end of file
diff --git a/src/Framework/MiddlewareRequestHandler.php b/src/Framework/MiddlewareRequestHandler.php
new file mode 100644
index 0000000..9d30a5b
--- /dev/null
+++ b/src/Framework/MiddlewareRequestHandler.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class MiddlewareRequestHandler implements RequestHandlerInterface
+{
+    public function __construct(private array $middlewares,
+                                private ControllerRequestHandler $controller_handler)
+    {
+    }
+
+    public function handle(Request $request): Response
+    {
+        $middleware = array_shift($this->middlewares);
+
+        if ($middleware === null) {
+            
+            return $this->controller_handler->handle($request);
+
+        }
+
+        return $middleware->process($request, $this);
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/Model.php b/src/Framework/Model.php
new file mode 100644
index 0000000..4aa1676
--- /dev/null
+++ b/src/Framework/Model.php
@@ -0,0 +1,178 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+use PDO;
+use App\Database;
+
+abstract class Model
+{
+    protected $table;
+
+    protected array $errors = [];
+
+    public function update(string $id, array $data): bool
+    {
+        $this->validate($data);
+
+        if ( ! empty($this->errors)) {
+            return false;
+        }
+        
+        $sql = "UPDATE {$this->getTable()} ";
+
+        unset($data["id"]);
+
+        $assignments = array_keys($data);
+
+        array_walk($assignments, function (&$value) {
+            $value = "$value = ?";
+        });
+
+        $sql .= " SET " . implode(", ", $assignments);
+
+        $sql .= " WHERE id = ?";
+
+        $conn = $this->database->getConnection();
+
+        $stmt = $conn->prepare($sql);
+
+        $i = 1;
+
+        foreach ($data as $value) {
+
+            $type = match(gettype($value)) {
+                "boolean" => PDO::PARAM_BOOL,
+                "integer" => PDO::PARAM_INT,
+                "NULL" => PDO::PARAM_NULL,
+                default => PDO::PARAM_STR
+            };
+
+            $stmt->bindValue($i++, $value, $type);
+
+        }
+
+        $stmt->bindValue($i, $id, PDO::PARAM_INT);
+
+        return $stmt->execute();        
+    }
+
+    protected function validate(array $data): void
+    {
+    }
+
+    public function getInsertID(): string
+    {
+        $conn = $this->database->getConnection();
+
+        return $conn->lastInsertId();
+    }
+
+    protected function addError(string $field, string $message): void
+    {
+        $this->errors[$field] = $message;
+    }
+
+    public function getErrors(): array
+    {
+        return $this->errors;
+    }
+
+    private function getTable(): string
+    {
+        if ($this->table !== null) {
+
+            return $this->table;
+
+        }
+
+        $parts = explode("\\", $this::class);
+
+        return strtolower(array_pop($parts));
+    }
+
+    public function __construct(protected Database $database)
+    {
+    }
+
+    public function findAll(): array
+    {
+        $pdo = $this->database->getConnection();
+
+        $sql = "SELECT *
+                FROM {$this->getTable()}";
+
+        $stmt = $pdo->query($sql);
+
+        return $stmt->fetchAll(PDO::FETCH_ASSOC);
+    }
+
+    public function find(string $id): array|bool
+    {
+        $conn = $this->database->getConnection();
+
+        $sql = "SELECT *
+                FROM {$this->getTable()}
+                WHERE id = :id";
+
+        $stmt = $conn->prepare($sql);
+
+        $stmt->bindValue(":id", $id, PDO::PARAM_INT);
+
+        $stmt->execute();
+
+        return $stmt->fetch(PDO::FETCH_ASSOC);
+    }
+
+    public function insert(array $data): bool
+    {
+        $this->validate($data);
+
+        if ( ! empty($this->errors)) {
+            return false;
+        }
+
+        $columns = implode(", ", array_keys($data));
+        $placeholders = implode(", ", array_fill(0, count($data), "?"));
+
+        $sql = "INSERT INTO {$this->getTable()} ($columns)
+                VALUES ($placeholders)";
+
+        $conn = $this->database->getConnection();
+
+        $stmt = $conn->prepare($sql);
+
+        $i = 1;
+
+        foreach ($data as $value) {
+
+            $type = match(gettype($value)) {
+                "boolean" => PDO::PARAM_BOOL,
+                "integer" => PDO::PARAM_INT,
+                "NULL" => PDO::PARAM_NULL,
+                default => PDO::PARAM_STR
+            };
+
+            $stmt->bindValue($i++, $value, $type);
+
+        }
+
+        return $stmt->execute();
+    }
+
+    public function delete(string $id): bool
+    {
+        $sql = "DELETE FROM {$this->getTable()}
+                WHERE id = :id";
+
+        $conn = $this->database->getConnection();
+
+        $stmt = $conn->prepare($sql);
+
+        $stmt->bindValue(":id", $id, PDO::PARAM_INT);
+
+        return $stmt->execute();
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/PHPTemplateViewer.php b/src/Framework/PHPTemplateViewer.php
new file mode 100644
index 0000000..0aa668a
--- /dev/null
+++ b/src/Framework/PHPTemplateViewer.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class PHPTemplateViewer implements TemplateViewerInterface
+{
+    public function render(string $template, array $data = []): string
+    {
+        extract($data, EXTR_SKIP);
+
+        ob_start();
+
+        require dirname(__DIR__, 2) . "/views/$template";
+
+        return ob_get_clean();
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/Request.php b/src/Framework/Request.php
new file mode 100644
index 0000000..4f5b9ba
--- /dev/null
+++ b/src/Framework/Request.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class Request
+{
+    public function __construct(public string $uri,
+                                public string $method,
+                                public array $get,
+                                public array $post,
+                                public array $files,
+                                public array $cookie,
+                                public array $server)
+    {
+    }
+
+    public static function createFromGlobals()
+    {
+        return new static(
+            $_SERVER["REQUEST_URI"],
+            $_SERVER["REQUEST_METHOD"],
+            $_GET,
+            $_POST,
+            $_FILES,
+            $_COOKIE,
+            $_SERVER
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/RequestHandlerInterface.php b/src/Framework/RequestHandlerInterface.php
new file mode 100644
index 0000000..4d89555
--- /dev/null
+++ b/src/Framework/RequestHandlerInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Framework;
+
+interface RequestHandlerInterface
+{
+    public function handle(Request $request): Response;
+}
\ No newline at end of file
diff --git a/src/Framework/Response.php b/src/Framework/Response.php
new file mode 100644
index 0000000..84d88a0
--- /dev/null
+++ b/src/Framework/Response.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class Response
+{
+    private string $body = "";
+
+    private array $headers = [];
+
+    private int $status_code = 0;
+
+    public function setStatusCode(int $code): void
+    {
+        $this->status_code = $code;
+    }
+    
+    public function redirect(string $url): void
+    {
+        $this->addHeader("Location: $url");
+    }
+
+    public function addHeader(string $header): void
+    {
+        $this->headers[] = $header;
+    }
+
+    public function setBody(string $body): void
+    {
+        $this->body = $body;
+    }
+    
+    public function getBody(): string
+    {
+        return $this->body;
+    }
+
+    public function send(): void
+    {
+        if ($this->status_code) {
+        
+            http_response_code($this->status_code);
+        }
+
+        foreach ($this->headers as $header) {
+
+            header($header);
+        }
+
+        echo $this->body;
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/Router.php b/src/Framework/Router.php
new file mode 100644
index 0000000..c1fc40c
--- /dev/null
+++ b/src/Framework/Router.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Framework;
+
+class Router
+{
+    private array $routes = [];
+
+    public function add(string $path, array $params = []): void
+    {
+        $this->routes[] = [
+            "path" => $path,
+            "params" => $params
+        ];
+    }
+
+    public function match(string $path, string $method): array|bool
+    {
+        $path = urldecode($path);
+        
+        $path = trim($path, "/");
+        
+        foreach ($this->routes as $route) {
+        
+            $pattern = $this->getPatternFromRoutePath($route["path"]);
+
+            if (preg_match($pattern, $path, $matches)) {
+
+                $matches = array_filter($matches, "is_string", ARRAY_FILTER_USE_KEY);
+
+                $params = array_merge($matches, $route["params"]);
+
+                if (array_key_exists("method", $params)) {
+
+                    if (strtolower($method) !== strtolower($params["method"])) {
+
+                        continue;
+
+                    }
+
+                }
+
+                return $params;
+            }
+        }
+
+        return false;
+    }
+
+    private function getPatternFromRoutePath(string $route_path): string
+    {
+        $route_path = trim($route_path, "/");
+
+        $segments = explode("/", $route_path);
+
+        $segments = array_map(function(string $segment): string {
+
+            if (preg_match("#^\{([a-z][a-z0-9]*)\}$#", $segment, $matches)) {
+
+                return "(?<" . $matches[1] . ">[^/]*)";
+
+            }
+
+            if (preg_match("#^\{([a-z][a-z0-9]*):(.+)\}$#", $segment, $matches)) {
+
+                return "(?<" . $matches[1] . ">" . $matches[2] . ")";
+
+            }
+
+            return $segment;
+
+        }, $segments);
+
+        return "#^" . implode("/", $segments) . "$#iu";
+    }
+}
\ No newline at end of file
diff --git a/src/Framework/TemplateViewerInterface.php b/src/Framework/TemplateViewerInterface.php
new file mode 100644
index 0000000..ebd1fc5
--- /dev/null
+++ b/src/Framework/TemplateViewerInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Framework;
+
+interface TemplateViewerInterface
+{
+    public function render(string $template, array $data = []): string;    
+}
\ No newline at end of file
diff --git a/vendor/autoload.php b/vendor/autoload.php
deleted file mode 100644
index 877560b..0000000
--- a/vendor/autoload.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-// autoload.php @generated by Composer
-
-require_once __DIR__ . '/composer/autoload_real.php';
-
-return ComposerAutoloaderInitbf02bf6c8d134df9a8ad40e5d0abe2d9::getLoader();
diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php
deleted file mode 100644
index fce8549..0000000
--- a/vendor/composer/ClassLoader.php
+++ /dev/null
@@ -1,445 +0,0 @@
-<?php
-
-/*
- * This file is part of Composer.
- *
- * (c) Nils Adermann <naderman@naderman.de>
- *     Jordi Boggiano <j.boggiano@seld.be>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Composer\Autoload;
-
-/**
- * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
- *
- *     $loader = new \Composer\Autoload\ClassLoader();
- *
- *     // register classes with namespaces
- *     $loader->add('Symfony\Component', __DIR__.'/component');
- *     $loader->add('Symfony',           __DIR__.'/framework');
- *
- *     // activate the autoloader
- *     $loader->register();
- *
- *     // to enable searching the include path (eg. for PEAR packages)
- *     $loader->setUseIncludePath(true);
- *
- * In this example, if you try to use a class in the Symfony\Component
- * namespace or one of its children (Symfony\Component\Console for instance),
- * the autoloader will first look for the class under the component/
- * directory, and it will then fallback to the framework/ directory if not
- * found before giving up.
- *
- * This class is loosely based on the Symfony UniversalClassLoader.
- *
- * @author Fabien Potencier <fabien@symfony.com>
- * @author Jordi Boggiano <j.boggiano@seld.be>
- * @see    http://www.php-fig.org/psr/psr-0/
- * @see    http://www.php-fig.org/psr/psr-4/
- */
-class ClassLoader
-{
-    // PSR-4
-    private $prefixLengthsPsr4 = array();
-    private $prefixDirsPsr4 = array();
-    private $fallbackDirsPsr4 = array();
-
-    // PSR-0
-    private $prefixesPsr0 = array();
-    private $fallbackDirsPsr0 = array();
-
-    private $useIncludePath = false;
-    private $classMap = array();
-    private $classMapAuthoritative = false;
-    private $missingClasses = array();
-    private $apcuPrefix;
-
-    public function getPrefixes()
-    {
-        if (!empty($this->prefixesPsr0)) {
-            return call_user_func_array('array_merge', $this->prefixesPsr0);
-        }
-
-        return array();
-    }
-
-    public function getPrefixesPsr4()
-    {
-        return $this->prefixDirsPsr4;
-    }
-
-    public function getFallbackDirs()
-    {
-        return $this->fallbackDirsPsr0;
-    }
-
-    public function getFallbackDirsPsr4()
-    {
-        return $this->fallbackDirsPsr4;
-    }
-
-    public function getClassMap()
-    {
-        return $this->classMap;
-    }
-
-    /**
-     * @param array $classMap Class to filename map
-     */
-    public function addClassMap(array $classMap)
-    {
-        if ($this->classMap) {
-            $this->classMap = array_merge($this->classMap, $classMap);
-        } else {
-            $this->classMap = $classMap;
-        }
-    }
-
-    /**
-     * Registers a set of PSR-0 directories for a given prefix, either
-     * appending or prepending to the ones previously set for this prefix.
-     *
-     * @param string       $prefix  The prefix
-     * @param array|string $paths   The PSR-0 root directories
-     * @param bool         $prepend Whether to prepend the directories
-     */
-    public function add($prefix, $paths, $prepend = false)
-    {
-        if (!$prefix) {
-            if ($prepend) {
-                $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
-                    $this->fallbackDirsPsr0
-                );
-            } else {
-                $this->fallbackDirsPsr0 = array_merge(
-                    $this->fallbackDirsPsr0,
-                    (array) $paths
-                );
-            }
-
-            return;
-        }
-
-        $first = $prefix[0];
-        if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
-
-            return;
-        }
-        if ($prepend) {
-            $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
-                $this->prefixesPsr0[$first][$prefix]
-            );
-        } else {
-            $this->prefixesPsr0[$first][$prefix] = array_merge(
-                $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
-            );
-        }
-    }
-
-    /**
-     * Registers a set of PSR-4 directories for a given namespace, either
-     * appending or prepending to the ones previously set for this namespace.
-     *
-     * @param string       $prefix  The prefix/namespace, with trailing '\\'
-     * @param array|string $paths   The PSR-4 base directories
-     * @param bool         $prepend Whether to prepend the directories
-     *
-     * @throws \InvalidArgumentException
-     */
-    public function addPsr4($prefix, $paths, $prepend = false)
-    {
-        if (!$prefix) {
-            // Register directories for the root namespace.
-            if ($prepend) {
-                $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
-                    $this->fallbackDirsPsr4
-                );
-            } else {
-                $this->fallbackDirsPsr4 = array_merge(
-                    $this->fallbackDirsPsr4,
-                    (array) $paths
-                );
-            }
-        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
-            // Register directories for a new namespace.
-            $length = strlen($prefix);
-            if ('\\' !== $prefix[$length - 1]) {
-                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
-            }
-            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
-        } elseif ($prepend) {
-            // Prepend directories for an already registered namespace.
-            $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
-                $this->prefixDirsPsr4[$prefix]
-            );
-        } else {
-            // Append directories for an already registered namespace.
-            $this->prefixDirsPsr4[$prefix] = array_merge(
-                $this->prefixDirsPsr4[$prefix],
-                (array) $paths
-            );
-        }
-    }
-
-    /**
-     * Registers a set of PSR-0 directories for a given prefix,
-     * replacing any others previously set for this prefix.
-     *
-     * @param string       $prefix The prefix
-     * @param array|string $paths  The PSR-0 base directories
-     */
-    public function set($prefix, $paths)
-    {
-        if (!$prefix) {
-            $this->fallbackDirsPsr0 = (array) $paths;
-        } else {
-            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
-        }
-    }
-
-    /**
-     * Registers a set of PSR-4 directories for a given namespace,
-     * replacing any others previously set for this namespace.
-     *
-     * @param string       $prefix The prefix/namespace, with trailing '\\'
-     * @param array|string $paths  The PSR-4 base directories
-     *
-     * @throws \InvalidArgumentException
-     */
-    public function setPsr4($prefix, $paths)
-    {
-        if (!$prefix) {
-            $this->fallbackDirsPsr4 = (array) $paths;
-        } else {
-            $length = strlen($prefix);
-            if ('\\' !== $prefix[$length - 1]) {
-                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
-            }
-            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
-        }
-    }
-
-    /**
-     * Turns on searching the include path for class files.
-     *
-     * @param bool $useIncludePath
-     */
-    public function setUseIncludePath($useIncludePath)
-    {
-        $this->useIncludePath = $useIncludePath;
-    }
-
-    /**
-     * Can be used to check if the autoloader uses the include path to check
-     * for classes.
-     *
-     * @return bool
-     */
-    public function getUseIncludePath()
-    {
-        return $this->useIncludePath;
-    }
-
-    /**
-     * Turns off searching the prefix and fallback directories for classes
-     * that have not been registered with the class map.
-     *
-     * @param bool $classMapAuthoritative
-     */
-    public function setClassMapAuthoritative($classMapAuthoritative)
-    {
-        $this->classMapAuthoritative = $classMapAuthoritative;
-    }
-
-    /**
-     * Should class lookup fail if not found in the current class map?
-     *
-     * @return bool
-     */
-    public function isClassMapAuthoritative()
-    {
-        return $this->classMapAuthoritative;
-    }
-
-    /**
-     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
-     *
-     * @param string|null $apcuPrefix
-     */
-    public function setApcuPrefix($apcuPrefix)
-    {
-        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
-    }
-
-    /**
-     * The APCu prefix in use, or null if APCu caching is not enabled.
-     *
-     * @return string|null
-     */
-    public function getApcuPrefix()
-    {
-        return $this->apcuPrefix;
-    }
-
-    /**
-     * Registers this instance as an autoloader.
-     *
-     * @param bool $prepend Whether to prepend the autoloader or not
-     */
-    public function register($prepend = false)
-    {
-        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
-    }
-
-    /**
-     * Unregisters this instance as an autoloader.
-     */
-    public function unregister()
-    {
-        spl_autoload_unregister(array($this, 'loadClass'));
-    }
-
-    /**
-     * Loads the given class or interface.
-     *
-     * @param  string    $class The name of the class
-     * @return bool|null True if loaded, null otherwise
-     */
-    public function loadClass($class)
-    {
-        if ($file = $this->findFile($class)) {
-            includeFile($file);
-
-            return true;
-        }
-    }
-
-    /**
-     * Finds the path to the file where the class is defined.
-     *
-     * @param string $class The name of the class
-     *
-     * @return string|false The path if found, false otherwise
-     */
-    public function findFile($class)
-    {
-        // class map lookup
-        if (isset($this->classMap[$class])) {
-            return $this->classMap[$class];
-        }
-        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
-            return false;
-        }
-        if (null !== $this->apcuPrefix) {
-            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
-            if ($hit) {
-                return $file;
-            }
-        }
-
-        $file = $this->findFileWithExtension($class, '.php');
-
-        // Search for Hack files if we are running on HHVM
-        if (false === $file && defined('HHVM_VERSION')) {
-            $file = $this->findFileWithExtension($class, '.hh');
-        }
-
-        if (null !== $this->apcuPrefix) {
-            apcu_add($this->apcuPrefix.$class, $file);
-        }
-
-        if (false === $file) {
-            // Remember that this class does not exist.
-            $this->missingClasses[$class] = true;
-        }
-
-        return $file;
-    }
-
-    private function findFileWithExtension($class, $ext)
-    {
-        // PSR-4 lookup
-        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
-
-        $first = $class[0];
-        if (isset($this->prefixLengthsPsr4[$first])) {
-            $subPath = $class;
-            while (false !== $lastPos = strrpos($subPath, '\\')) {
-                $subPath = substr($subPath, 0, $lastPos);
-                $search = $subPath . '\\';
-                if (isset($this->prefixDirsPsr4[$search])) {
-                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
-                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
-                        if (file_exists($file = $dir . $pathEnd)) {
-                            return $file;
-                        }
-                    }
-                }
-            }
-        }
-
-        // PSR-4 fallback dirs
-        foreach ($this->fallbackDirsPsr4 as $dir) {
-            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
-                return $file;
-            }
-        }
-
-        // PSR-0 lookup
-        if (false !== $pos = strrpos($class, '\\')) {
-            // namespaced class name
-            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
-                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
-        } else {
-            // PEAR-like class name
-            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
-        }
-
-        if (isset($this->prefixesPsr0[$first])) {
-            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
-                if (0 === strpos($class, $prefix)) {
-                    foreach ($dirs as $dir) {
-                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
-                            return $file;
-                        }
-                    }
-                }
-            }
-        }
-
-        // PSR-0 fallback dirs
-        foreach ($this->fallbackDirsPsr0 as $dir) {
-            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
-                return $file;
-            }
-        }
-
-        // PSR-0 include paths.
-        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
-            return $file;
-        }
-
-        return false;
-    }
-}
-
-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- */
-function includeFile($file)
-{
-    include $file;
-}
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
deleted file mode 100644
index f27399a..0000000
--- a/vendor/composer/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-
-Copyright (c) Nils Adermann, Jordi Boggiano
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is furnished
-to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php
deleted file mode 100644
index 7a91153..0000000
--- a/vendor/composer/autoload_classmap.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-// autoload_classmap.php @generated by Composer
-
-$vendorDir = dirname(dirname(__FILE__));
-$baseDir = dirname($vendorDir);
-
-return array(
-);
diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php
deleted file mode 100644
index b7fc012..0000000
--- a/vendor/composer/autoload_namespaces.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-// autoload_namespaces.php @generated by Composer
-
-$vendorDir = dirname(dirname(__FILE__));
-$baseDir = dirname($vendorDir);
-
-return array(
-);
diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
deleted file mode 100644
index be9cb21..0000000
--- a/vendor/composer/autoload_psr4.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-// autoload_psr4.php @generated by Composer
-
-$vendorDir = dirname(dirname(__FILE__));
-$baseDir = dirname($vendorDir);
-
-return array(
-    'Twig\\' => array($vendorDir . '/twig/twig/src'),
-    'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
-    'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
-    'Core\\' => array($baseDir . '/Core'),
-    'App\\' => array($baseDir . '/App'),
-);
diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php
deleted file mode 100644
index abc574d..0000000
--- a/vendor/composer/autoload_real.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-// autoload_real.php @generated by Composer
-
-class ComposerAutoloaderInitbf02bf6c8d134df9a8ad40e5d0abe2d9
-{
-    private static $loader;
-
-    public static function loadClassLoader($class)
-    {
-        if ('Composer\Autoload\ClassLoader' === $class) {
-            require __DIR__ . '/ClassLoader.php';
-        }
-    }
-
-    /**
-     * @return \Composer\Autoload\ClassLoader
-     */
-    public static function getLoader()
-    {
-        if (null !== self::$loader) {
-            return self::$loader;
-        }
-
-        spl_autoload_register(array('ComposerAutoloaderInitbf02bf6c8d134df9a8ad40e5d0abe2d9', 'loadClassLoader'), true, true);
-        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
-        spl_autoload_unregister(array('ComposerAutoloaderInitbf02bf6c8d134df9a8ad40e5d0abe2d9', 'loadClassLoader'));
-
-        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
-        if ($useStaticLoader) {
-            require_once __DIR__ . '/autoload_static.php';
-
-            call_user_func(\Composer\Autoload\ComposerStaticInitbf02bf6c8d134df9a8ad40e5d0abe2d9::getInitializer($loader));
-        } else {
-            $map = require __DIR__ . '/autoload_namespaces.php';
-            foreach ($map as $namespace => $path) {
-                $loader->set($namespace, $path);
-            }
-
-            $map = require __DIR__ . '/autoload_psr4.php';
-            foreach ($map as $namespace => $path) {
-                $loader->setPsr4($namespace, $path);
-            }
-
-            $classMap = require __DIR__ . '/autoload_classmap.php';
-            if ($classMap) {
-                $loader->addClassMap($classMap);
-            }
-        }
-
-        $loader->register(true);
-
-        if ($useStaticLoader) {
-            $includeFiles = Composer\Autoload\ComposerStaticInitbf02bf6c8d134df9a8ad40e5d0abe2d9::$files;
-        } else {
-            $includeFiles = require __DIR__ . '/autoload_files.php';
-        }
-        foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequirebf02bf6c8d134df9a8ad40e5d0abe2d9($fileIdentifier, $file);
-        }
-
-        return $loader;
-    }
-}
-
-function composerRequirebf02bf6c8d134df9a8ad40e5d0abe2d9($fileIdentifier, $file)
-{
-    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
-        require $file;
-
-        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
-    }
-}
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
deleted file mode 100644
index 8521872..0000000
--- a/vendor/composer/installed.json
+++ /dev/null
@@ -1,215 +0,0 @@
-[
-    {
-        "name": "symfony/polyfill-ctype",
-        "version": "v1.17.0",
-        "version_normalized": "1.17.0.0",
-        "source": {
-            "type": "git",
-            "url": "https://github.com/symfony/polyfill-ctype.git",
-            "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
-        },
-        "dist": {
-            "type": "zip",
-            "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
-            "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
-            "shasum": ""
-        },
-        "require": {
-            "php": ">=5.3.3"
-        },
-        "suggest": {
-            "ext-ctype": "For best performance"
-        },
-        "time": "2020-05-12T16:14:59+00:00",
-        "type": "library",
-        "extra": {
-            "branch-alias": {
-                "dev-master": "1.17-dev"
-            }
-        },
-        "installation-source": "dist",
-        "autoload": {
-            "psr-4": {
-                "Symfony\\Polyfill\\Ctype\\": ""
-            },
-            "files": [
-                "bootstrap.php"
-            ]
-        },
-        "notification-url": "https://packagist.org/downloads/",
-        "license": [
-            "MIT"
-        ],
-        "authors": [
-            {
-                "name": "Gert de Pagter",
-                "email": "BackEndTea@gmail.com"
-            },
-            {
-                "name": "Symfony Community",
-                "homepage": "https://symfony.com/contributors"
-            }
-        ],
-        "description": "Symfony polyfill for ctype functions",
-        "homepage": "https://symfony.com",
-        "keywords": [
-            "compatibility",
-            "ctype",
-            "polyfill",
-            "portable"
-        ],
-        "funding": [
-            {
-                "url": "https://symfony.com/sponsor",
-                "type": "custom"
-            },
-            {
-                "url": "https://github.com/fabpot",
-                "type": "github"
-            },
-            {
-                "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                "type": "tidelift"
-            }
-        ]
-    },
-    {
-        "name": "symfony/polyfill-mbstring",
-        "version": "v1.17.0",
-        "version_normalized": "1.17.0.0",
-        "source": {
-            "type": "git",
-            "url": "https://github.com/symfony/polyfill-mbstring.git",
-            "reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
-        },
-        "dist": {
-            "type": "zip",
-            "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
-            "reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
-            "shasum": ""
-        },
-        "require": {
-            "php": ">=5.3.3"
-        },
-        "suggest": {
-            "ext-mbstring": "For best performance"
-        },
-        "time": "2020-05-12T16:47:27+00:00",
-        "type": "library",
-        "extra": {
-            "branch-alias": {
-                "dev-master": "1.17-dev"
-            }
-        },
-        "installation-source": "dist",
-        "autoload": {
-            "psr-4": {
-                "Symfony\\Polyfill\\Mbstring\\": ""
-            },
-            "files": [
-                "bootstrap.php"
-            ]
-        },
-        "notification-url": "https://packagist.org/downloads/",
-        "license": [
-            "MIT"
-        ],
-        "authors": [
-            {
-                "name": "Nicolas Grekas",
-                "email": "p@tchwork.com"
-            },
-            {
-                "name": "Symfony Community",
-                "homepage": "https://symfony.com/contributors"
-            }
-        ],
-        "description": "Symfony polyfill for the Mbstring extension",
-        "homepage": "https://symfony.com",
-        "keywords": [
-            "compatibility",
-            "mbstring",
-            "polyfill",
-            "portable",
-            "shim"
-        ],
-        "funding": [
-            {
-                "url": "https://symfony.com/sponsor",
-                "type": "custom"
-            },
-            {
-                "url": "https://github.com/fabpot",
-                "type": "github"
-            },
-            {
-                "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                "type": "tidelift"
-            }
-        ]
-    },
-    {
-        "name": "twig/twig",
-        "version": "v3.0.3",
-        "version_normalized": "3.0.3.0",
-        "source": {
-            "type": "git",
-            "url": "https://github.com/twigphp/Twig.git",
-            "reference": "3b88ccd180a6b61ebb517aea3b1a8906762a1dc2"
-        },
-        "dist": {
-            "type": "zip",
-            "url": "https://api.github.com/repos/twigphp/Twig/zipball/3b88ccd180a6b61ebb517aea3b1a8906762a1dc2",
-            "reference": "3b88ccd180a6b61ebb517aea3b1a8906762a1dc2",
-            "shasum": ""
-        },
-        "require": {
-            "php": "^7.2.5",
-            "symfony/polyfill-ctype": "^1.8",
-            "symfony/polyfill-mbstring": "^1.3"
-        },
-        "require-dev": {
-            "psr/container": "^1.0",
-            "symfony/phpunit-bridge": "^4.4|^5.0"
-        },
-        "time": "2020-02-11T15:33:47+00:00",
-        "type": "library",
-        "extra": {
-            "branch-alias": {
-                "dev-master": "3.0-dev"
-            }
-        },
-        "installation-source": "dist",
-        "autoload": {
-            "psr-4": {
-                "Twig\\": "src/"
-            }
-        },
-        "notification-url": "https://packagist.org/downloads/",
-        "license": [
-            "BSD-3-Clause"
-        ],
-        "authors": [
-            {
-                "name": "Fabien Potencier",
-                "email": "fabien@symfony.com",
-                "homepage": "http://fabien.potencier.org",
-                "role": "Lead Developer"
-            },
-            {
-                "name": "Twig Team",
-                "role": "Contributors"
-            },
-            {
-                "name": "Armin Ronacher",
-                "email": "armin.ronacher@active-4.com",
-                "role": "Project Founder"
-            }
-        ],
-        "description": "Twig, the flexible, fast, and secure template language for PHP",
-        "homepage": "https://twig.symfony.com",
-        "keywords": [
-            "templating"
-        ]
-    }
-]
diff --git a/views/404.php b/views/404.php
new file mode 100644
index 0000000..12d52a8
--- /dev/null
+++ b/views/404.php
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Page not found</title>
+</head>
+<body>
+
+    <h1>Page not found</h1>
+
+</body>
+</html>
diff --git a/views/500.php b/views/500.php
new file mode 100644
index 0000000..a8f80b8
--- /dev/null
+++ b/views/500.php
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Error</title>
+</head>
+<body>
+
+    <h1>An Error Occurred</h1>
+
+</body>
+</html>
diff --git a/views/Home/index.mvc.php b/views/Home/index.mvc.php
new file mode 100644
index 0000000..a893209
--- /dev/null
+++ b/views/Home/index.mvc.php
@@ -0,0 +1,9 @@
+{% extends "base.mvc.php" %}
+
+{% block title %}Home{% endblock %}
+
+{% block body %}
+
+<h1>Home</h1>
+
+{% endblock %}
\ No newline at end of file
diff --git a/views/Products/delete.mvc.php b/views/Products/delete.mvc.php
new file mode 100644
index 0000000..27f3957
--- /dev/null
+++ b/views/Products/delete.mvc.php
@@ -0,0 +1,19 @@
+{% extends "base.mvc.php" %}
+
+{% block title %}Delete Product{% endblock %}
+
+{% block body %}
+
+<h1>Delete Product</h1>
+
+<form method="post" action="/products/{{ product["id"] }}/destroy">
+
+<p>Delete this product?</p>
+
+<button>Yes</button>
+
+</form>
+
+<p><a href="/products/{{ product["id"] }}/show">Cancel</a></p>
+
+{% endblock %}
\ No newline at end of file
diff --git a/views/Products/edit.mvc.php b/views/Products/edit.mvc.php
new file mode 100644
index 0000000..f827094
--- /dev/null
+++ b/views/Products/edit.mvc.php
@@ -0,0 +1,17 @@
+{% extends "base.mvc.php" %}
+
+{% block title %}Edit Product{% endblock %}
+
+{% block body %}
+
+<h1>Edit Product</h1>
+
+<form method="post" action="/products/{{ product["id"] }}/update">
+
+{% include "Products/form.mvc.php" %}
+
+</form>
+
+<p><a href="/products/{{ product["id"] }}/show">Cancel</a></p>
+
+{% endblock %}
\ No newline at end of file
diff --git a/views/Products/form.mvc.php b/views/Products/form.mvc.php
new file mode 100644
index 0000000..fe02eba
--- /dev/null
+++ b/views/Products/form.mvc.php
@@ -0,0 +1,11 @@
+<label for="name">Name</label>
+<input type="text" id="name" name="name" value="{{ product["name"] }}">
+
+{% if (isset($errors["name"])): %}
+    <p>{{ errors["name"] }}</p>
+{% endif; %}
+
+<label for="description">Description</label>
+<textarea id="description" name="description">{{ product["description"] }}</textarea>
+
+<button>Save</button>
\ No newline at end of file
diff --git a/views/Products/index.mvc.php b/views/Products/index.mvc.php
new file mode 100644
index 0000000..2635de0
--- /dev/null
+++ b/views/Products/index.mvc.php
@@ -0,0 +1,23 @@
+{% extends "base.mvc.php" %}
+
+{% block title %}Products{% endblock %}
+
+{% block body %}
+
+<h1>Products</h1>
+
+<a href="/products/new">New Product</a>
+
+<p>Total: {{ total }}</p>
+
+{% foreach ($products as $product): %}
+
+    <h2>
+        <a href="/products/{{ product["id"] }}/show">
+            {{ product["name"] }}
+        </a>
+    </h2>
+    
+{% endforeach; %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/views/Products/new.mvc.php b/views/Products/new.mvc.php
new file mode 100644
index 0000000..79cd14b
--- /dev/null
+++ b/views/Products/new.mvc.php
@@ -0,0 +1,15 @@
+{% extends "base.mvc.php" %}
+
+{% block title %}New Product{% endblock %}
+
+{% block body %}
+
+<h1>New Product</h1>
+
+<form method="post" action="/products/create">
+
+{% include "Products/form.mvc.php" %}
+
+</form>
+
+{% endblock %}
\ No newline at end of file
diff --git a/views/Products/show.mvc.php b/views/Products/show.mvc.php
new file mode 100644
index 0000000..490a885
--- /dev/null
+++ b/views/Products/show.mvc.php
@@ -0,0 +1,15 @@
+{% extends "base.mvc.php" %}
+
+{% block title %}Product{% endblock %}
+
+{% block body %}
+
+<h1>{{ product["name"] }}</h1>
+
+<p>{{ product["description"] }}</p>
+
+<p><a href="/products/{{ product["id"] }}/edit">Edit</a></p>
+
+<p><a href="/products/{{ product["id"] }}/delete">Delete</a></p>
+
+{% endblock %}
\ No newline at end of file
diff --git a/views/base.mvc.php b/views/base.mvc.php
new file mode 100644
index 0000000..7128df4
--- /dev/null
+++ b/views/base.mvc.php
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>{% yield title %}</title>
+    <meta charset="UTF-8">
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
+</head>
+<body>
+
+{% yield body %}
+
+</body>
+</html>
