Qaton Documentation

Introduction

Qaton is an absolute minimal MVC framework (and is not intended compete with full-fledged frameworks such as Laravel). Qaton's primary goal is to provide MVC support at the most basic and elemental level possible while providing developers with the maximum amount of flexibility and access to rapid development tools. Because of it's simplicity, Qaton runs extremely fast (almost as if one were running vanilla PHP). Although Qaton is a minimal system, it is easily extended and is able to support all kinds of projects (with no limit to size). However, Qaton primarily targets small to medium sized projects in situations where a full-blown framework would not only be overkill but also a liability. Qaton has been developed based on the fundamental belief that the latest versions of PHP (after 7.4) have all optimizations and functionality built-in naively to build robust applications without additional framework overhead (provided that the projects are within Qaton's project scope). The latest version of Qaton was developed for PHP 7.4. (A PHP 8 version is on the way)

The MVC (Model-Controller-View) architectural pattern which Qaton is built on aims at separating applications into three major logical components. If you are not familiar with this pattern, you will soon get an idea of how it works when you begin working with Qaton. It's a good idea to do a bit of research on your own about MVC starting with Mozilla's MVC doc. In addition, Qaton's default data models are also powered by Qaton's own FileDatabase which is a JSON based ORM style RDBMS system designed with version control in mind. FileDatabase is extremely fast as well as long as your use case falls within Qaton's scope (small to medium sized projects). For more information about FileDatabase, see below.

NOTE: Qaton is still at its infancy and is at the proof of concept stage. The project is seeking support from anyone who is willing to help it advance and mature.
IMPORTANT: This documentation is still under development.

Config & Install

Getting Started (Development Only)

There are several ways to get started quickly for development. However, these methods are not recommended for a production setup.

METHOD 1: Quick Start using Git and Docker

Use this method if you have very little experience and do not have a development environment with PHP or a server enabled.

Note: You do not need to use Docker if you have another solution (such as a virtual machine, other type of containers) or prefer to use PHP directly from your host environment. If that is the case, Method 2 might be more suited for you.

  • Clone the qatonapp repository with git clone https://github.com/virxnet/qatonapp.git or your favourite Git management tool. Alternatively, if you don't have Git installed, you can download the ZIP file of this repository https://github.com/aspvirx/qatonapp/archive/refs/heads/master.zip and extract it somewhere on your computer.

  • Enter the project directory using your terminal (e.g.: cd qatonapp or cd master).

  • You may want to delete the .git directory if you plan on using your own version control repository for this project. Learning more about Git is recommended

  • Install the Docker Desktop/Engine for your operating system. Once, installed make sure it is running by launching the desktop (or command line) application. See Docker documentation for instructions on the complete setup process.

  • Provide executable permission to the qaton file which is located at the root directory of the project. This file expects to be executed with BASH. If you are on Linux or Mac, it will run painlessly as long as you have given it permission to do so. To do that, open a terminal, make sure you are in the same directory and run:

chmod +x qaton

Note: If you are on Microsoft Windows you will need to setup Microsoft WSL2. Also check Docker WSL2 documentation for Docker requirements.

  • Using a BASH terminal, execute the Qaton Development Helper to install and start the container on the default port 8888

./qaton start

or if you wish to specify a different port, edit the qaton.env file or run qaton (replacing [PortNumber] with the desired port:

./qaton serve [PortNumber]

example:

./qaton serve 3000

Note: if the above commands fail, try running bash qaton instead.

  • If successful, you should be taken to the development environment after the setup process completes. You can now type qpm anytime to access the Qaton Project Manager tool and run development commands.

  • On your host machine, you should be able to visit http://localhost:8888 to view the web application using an internet browser like Firefox, Google Chrome, etc... (If you changed the port number, apply that here too. e.g. http://localhost:3000)


 

METHOD 2: Quick Install using Composer

Use this method if you already have a development environment readily installed with the minimum requirements available. Essentially, a properly configured web server (such as Apache2/nginx) and with URL rewrite enabled (mod_rewrite with Apache2) and pointing all HTTP requests to public/index.php. The correct version of PHP (with the required extensions), Packgist's composer, etc... should also all be available on the system.

Create a project with PHP composer
composer create-project virx/qatonapp:"dev-master" [ProjectDir] --remove-vcs
Set the right permissions
cd [ProjectDir]
chmod +x qaton
chmod +x qpm

Begin Development (That's it! You're Ready!)

That's all you need to do for Method 2.

Notes and Troubleshooting:

Remember that there are two ways to get started with development quickly. The quickest way is using Docker Desktop/Engine. However, if you started the project using composer (Method 2) but still wish to use Docker, you can follow the instructions outlined in Method 1 by only skipping the first step (git clone/zip download). Otherwise, if your environment has already been setup correctly, then you should already be able to begin development at this point. If you have any issues, consider the following tips for troubleshooting:
  • Your web server's document root is correctly pointed to the public directory
  • You have enabled URL rewriting so everything points to the index.php file
  • PHP is the correct version and is properly configured
  • You have set the right permissions for qpm and qaton

Qaton Project Manager qpm

Type qpm in your BASH terminal window to view your development options.

 

Production Setup

Setting up Qaton for production is very simple. Qaton was intentionally designed for quick and easy deployment on any server with the minimal requirements. For example, you do not need access to a command line to run a Qaton application. In fact, you could run it on any server with the correct PHP version, PHP extensions and URL rewriting enabled. Qaton development should always be isolated to your local machine and should never be performed on the actual production server itself. The use of Packagist's composer is also strongly discouraged on the production server, even if you do have access to the command line. Of course, there are several steps you will need to perform before you put your application into production to ensure security and performance. That may vary from one environment to another and is beyond the scope of this documentation. However, as far as Qaton is concerned you will nee to follow these steps:

  1. Upload the files to your server and set appropriate permissions to all files globally to make sure that they are secure. However, enable write permissions to the server process for your Database directory (if using Qaton's buit-in FileDatabase) and any other storage directories you might have setup.
  2. Ensure that you have disabled all debugging related options in the config.php file
  3. Perform some quality assrance and penetesting to ensure that everything is secure and working as expected. If you do not know what you are doing or are new to web development, hire a professional to perform this whole process.

 

Configuration

By default, Qaton does not need to be configured to work correctly. Almost all config items are detected automatically with the possible exception of your public directory. However, there will be situations where you might want to apply custom configuration items to override the system's default behaviour. In such cases, you may do so by editing the $APP_CONFIG array found in the config.php file found at the root directory. Below is a list of config items that you can customize:

Config Key Description
APP_PATH
string
Path to your App directory. The default setting would work out of the box (typically /var/www/App for example)

BASE_PATH
string

Base path of the public web server root directory. This is where your initial index.php file will live.
APP_PATH_RELATIVE_TO_BASE
boolean
Tells Qaton to treat the APP_PATH as a relative path (true) or absolute path (false)

APP_PATHS
array

CONTROLLERS
string
Path to your Controllers directory
VIEWS
string
Path to your Views directory
DATABASE
string
Path to your Database directory
FILEDATABASE
string
Path to your FileDatabase directory
FILEDATABASE_MIGRATIONS
string
Path to your FileDatabase migrations directory
TEMPLATES
string
Path to your Templates directory
BLUEPRINTS
string
Path to your Blueprints directory
CONFIG
string
Path to your Config directory
MODELS
string
Path to your Models directory
HELPERS
string
Path to your Helpers directory
STORAGE
string
Path to your file Storage directory
CACHE
string
Path to your Cache directory
APP_VIEWS_SPECIAL_PATHS
array

These paths are part of the Qaton templates system

COMMON
string
Path to template common files
LAYOUTS
string
Path to template layout files
SECTIONS
string
Path to template sections
APP_PUBLIC_PATH
string
Public path of the application (relative)
APP_PUBLIC_ASSETS_PATH
string
Public path where assets are stored (relative to APP_PUBLIC_PATH)
APP_DATABASE_TYPE
string
The database driver to use. This is set to FileDatabase by default and is currently the only driver that's supported. However, this does not mean that you can not simply use Eloquent, Doctrine, RedBean or some other ORM by using them with any other project. This setting applied primarily to the automated defaults that come with Qaton for your convenience.
APP_DATABASE
array

These are the database settings. The settings you apply here will depend on the APP_DATABASE_TYPE that has been defined.

FileDatabase:

NAME
string
Name of the active FileDatabase database to use by default. The preset value is default_database
MIGRATIONS
string
Name of the FileDatabase database to use for storing migration data. the present value is filedatabase_migrations
APP_DATABASE_OPTIONS
array

These are extended options to pass to the database. The settings you apply here will depend on the APP_DATABASE_TYPE that has been defined.

FileDatabase:

LOGGING
bool
FileDatabase will log everything it's doing. This is disabled by default for performance but should be set to true if you are trying to debug something.
DEBUG_PRINT_QATON
string
Outputs FileDatabase logs and error messages directly to the Qaton Debug Console output. This will not do anything if you have disabled LOGGING.
DEBUG_PRINT_HTML
string
Outputs FileDatabase logs to the screen as HTML. Very useful for debugging things quickly.
APP_AUTO_USE_DATABASE
boolean
Determines of Qaton should assume that it's okay to use the default database settings in case a database connection is requested.
APP_AUTH
array

Defines values for the authentication system that has been built into Qaton. You do not need to use this feature at all, it has only been provided for your convenience and you can write your own if necessary or use a third-party solution instead.

USERS_TABLE
string
The database table name where user information will be stored.
USER_MODEL
string
The name of the User model to use.
ACTIVE_USERS_TABLE
string
The name of the Active Users table where active sessions will be stored.
INITIAL_USER_DEFAULTS
array

These settings will be used to create the first default user when invoked via qpm.

LEVEL
integer
Default is 1 which is the highest admin level. You can change this to anything but then you will not be able to login as that user into the admin panel if you install that also using qpm.
USERNAME
string
Default is admin. This is the user name of the first user.
EMAIL
string
Default is admin@example.com. This is the email address you associated with the first user and will also be used to login.
PASSWORD
string
Default is password. This is the password of the first user.
FIRSTNAME
string
Default is Admin. First Name of the user.
LASTNAME
string
Default is Prime. Last Name of the user.
SESSION_NAME
string
Default is QatonActiveUser. Session name to use for Auth sessions
COOKIE_NAME
string
Default is QatonUserAuthenticationSession. Cookie name to use for Auth sessions
COOKIE_EXPIRY
integer
Default is 172800. How long the cookie should live for (in seconds).
APP_DASHBOARD_NAME
string
Default is admin. Name of the admin dashboard (this will also be the path)
APP_DEFAULT_CONTROLLER
string
Default is index. This should not be changed except under special circumstances. It is the default controller name.
APP_DEFAULT_MIGRATION_CLASS
string
Default is AppMigration. This class name will be appended to all migration class names that are generated by qpm.
APP_DEFAULT_METHOD
string
Default is index. This is the name of the method that will be called by default when at a controller's root route.
APP_FALLBACK_CONTROLLER
string
Default is Errors/Error404 and will be created for you automatically which you can edit as you see fit. This is the controller which will be called when you reach a 404 page not found event.
APP_PROTOCOL
string
Default is auto. However this can be forced to serve http:// or https:// or any other protocol. All URLs will then use this.
APP_URL_SUB_DIR
string
Default is /. You may want to change this if you have installed Qaton in a sub directory under the domain root. For example if your web server serves it's root at /var/www/html and you install Qaton's public dir at /var/www/html/something/something_else/test_app/ then you should set this value to /something/something_else/test_app/
APP_SERVER_USER
string
Default is www-data which is the username used on most Linux web servers. This setting will be used to chown files when qpm creates them for you.
APP_SERVER_GROUP
string
Default is www-data which is the username used on most Linux web servers. This setting will be used to chown files when qpm creates them for you.
APP_DEV_CHMOD
number
Default is 777. This setting will be used to chmod files when qpm creates them for you.This should only be used during development. You should not use qpm in production anyway, but if you do then you should set this to something more restrictive.
APP_DEBUG
boolean
Default is true. Enables or disables the Qaton Debug Console output and tells Qaton to do debugging related things. This should never be enabled on a production server.
APP_PRODUCTION_MODE
boolean
Default is false. Enables or disables production mode and should be enabled when the app is in production. This tells Qaton that the app is in production and should do things to keep things safe.
APP_OUTPUT_MODE
string
Default is html. This tells Qaton to output everything as HTML content. Changing this to text or json for example will result in such output.
   

 

Qaton Project Manager

The Qaton Project Manager is what makes development with Qaton extremely fast and easy. With a few qpm commands, you could setup an entire web site in minutes. It is also possible to manage your database if you are using the default FileDatabase system. Once your environment has been setup correctly, simply type the following in a BASH terminal from your project directory:

php qpm

If that does not work, it means you have not set things up properly yet. So verify that you have done so first. Also for your own convenience you may want to include your project directory into your PATH. This may vary in some environments but generally, you may edit the .bashrc file located in your home directory. At the bottom of this file, simply add the following in a new line at the very bottom of the file (assuming your project path is /var/www/ for example):

PATH=$PATH:/var/www/

Then, after you restart your session (or open a new one) you can simply type qpm without calling php qpm. If everything is working as expected, you should see something like this:

Command Modules

When you type qpm you will be greeted with a list of available command modules. It will also be possible to create your own Qaton commands in the future. Each command has it's own help documentation. Simply type the name of the command followed by the help flag to view it. For example:

qpm Create --help

You will see qpm being utilized throughout this documentation. However, the best way to learn about qpm is to try it out on your own.

Routing

Routing is the process of directing HTTP requests to the relevant backend component that is meant to handle the request. For example, if a user calls http://example.com/jello, then you will want some internal handler designed to serve that request. With Qaton, routing is handled completely automatically based on file & directory structure, class methods and parameters. If you have used other web development frameworks in the past (such as Laravel or VueJS for example) you might be familiar with the process of writing routes to tell your application how to handle incoming requests from a web browser or other HTTP client. However, that is not necessary with Qaton. In fact, a manual router has been excluded from Qaton intentionally for to reduce development overhead and mimic how a static site might work (almost). I say almost because Qaton also handles PHP class methods as part of the process.

This approach to routing will also ensure that your files are organized in a logical and consistent way so that you will not get lost in your own code. If you are using qpm (as you should) for development, then everything will be done for you automatically. Let's take our jello example from earlier and create a web page to demonstrate this using qpm:

qpm Create page jello

This one simple command will take care of a number of things for you automatically (all of which are documented below). However, for the sake of understanding how routing works, lets take a look at this jello page and even go a step further to understand how it work. When we executed that command, although a number of things were done for you in the background, it is the Controller file (and path) which will be used to determine routing. If you look inside your application directory, there should now be a new file called Jello.php inside the /Controllers/ path. It should look something like this:

namespace App\Controllers;

class Jello
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        $data = [];
        $this->view->section("sections/jello", "layouts/default", $data);
    }
}

For example sake, let's assume your server is running at http://localhost:8888. Now, if you visit http://localhost:8888/jello the routing system will automatically fetch this Jello.php file, instantiate the Jello controller class and use the index() method by default to render the output. The auto generated page would look something like this:

If I edit this file and modify the index() method by adding a simple echo output, like so:

public function index()
{
    echo "Jello World!";
}

When you visit the page now, you will get an output like this instead:


Routing Rules

The routing process is extremely simple, but also quite flexible. In the example above, we looked at a simple page. However, you can achieve a lot more organization using directory structure and methods.

Directories
/directory_path/[Controller].php
/directory_path/[Index Controller].php

Organizing your routes by directories gives you a clean project structure so that you will never get lost in your own code. It's going to be very easy to find your files based on the path provided. For example http://example.com/foo/bar/baz could be pointing to /Controllers/foo/bar/Baz.php. I say could because of method calls which can do the same thing, which is also documented below.

Directory routing is as simple as that. Directory routing also support index files just like how standard web servers serve static content. So if you wish to display something when a user visits http://example.com/foo/bar/ then all you need to do is have an index controller at /Controllers/foo/bar/Index.php. So the learning curve is minimal if you follow this approach.

Directory structure dept is only limited by your environment and request limits. It would be ideal to have a reasonably shallow structure though.

Application Default Controller (Index)

This index file we have been talking about is identified as the Default Controller. In the configuration, you can override this name "index" to anything you like with APP_DEFAULT_CONTROLLER. For example, most people might prefer to call this "home" instead. Qaton will look for this default controller if the request path leads to a directory (including the root path). So a request to http://example.com for instance would look for /Controllers/Index.php and this applies to all sub directories. If no Default Controller is found, a 404 "not found" error will be issued instead.

Application Default Method (Index)

The Default Method is called whenever a request route matches a controller file. The index method is also the only method in the Qaton routing system which will not accept parameters. So parameters are not allowed in any default controller file. By default, the this method is called "index" but it can be configured with APP_DEFAULT_METHOD to anything you prefer instead.

Controllers
/directory_path/[Controller].php/[Method]/[Paramenter]/[Parameter]...

Controllers are described in detail in the Controller's section. However, as far as routing is concerned, whenever a request matches a Controller file, Qaton will attempt to serve it if the following are true:

  • The controller has a matching class name
  • A default method exists (index)

So if we take the example from earlier, http://example.com/foo/bar/baz there are several possible scenarios if you are not using a simple directory route.

Controllers/Foo.php
Controllers/foo/Bar.php
Controllers/foo/bar/Baz.php

In this case, if all these files exist, the priority goes to the highest parent. So in this case, Qaton will look at grandpa Foo for all these routes:

http://example.com/foo - will work if an index() method exists in Foo.php
http://example.com/foo/bar - will render a 404 unless a method named bar() exists inside Foo.php
http://example.com/foo/bar/baz - will render a 404 unless a method named bar() exists and expects one required parameter to pass baz into

Now if I delete Foo.php (not the directory named foo, just the file), then the highest parent will be Bar.php and the same rules will apply.

http://example.com/foo - a 404 will be issued unless an Index.php file exists inside the foo directory.
http://example.com/foo/bar - will work if an index() method exists in Bar.php (otherwise a 404 as usual)
http://example.com/foo/bar/baz - will render a 404 unless a method named baz() exists

Methods & Parameters
[Controller].php/[Method]/[Paramenter]/[Parameter]...

Methods are described in detail in the Method's section. However, as far as routing is concerned, the examples above should already illustrate how they work. Qaton will use a method and pass parameters if the following are true:

  • The active controller has a matching method name
  • The matching method has matching required parameters if they were part of the request.

Let's take a look at this example:

namespace App\Controllers;

class Foo
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        echo "Jello World!";
    }

    public function bar($bar_name)
    {
        echo $bar_name;
    }
}

Let's assume this file exists at the root controllers path. This will be the highest parent considered for all requests matching /foo in the URL. The following scenarios will be true:

http://example.com/foo - will output "Jello World!" because the default method is used
http://example.com/foo/bar - will result in a 404 because even though the method exists, it expects a parameter named $bar_name
http://example.com/foo/bar/baz - will output "Baz" because the request passed the value "baz" from the URL to the parameter $bar_name

Now if we remove the parameter, something else occurs:

namespace App\Controllers;

class Foo
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        echo "Jello World!";
    }

    public function bar()
    {
        echo "I am bar!";
    }
}

http://example.com/foo - will output "Jello World!" because the default method is used
http://example.com/foo/bar - will output "I am bar!" because the method exists and does not expect any parameters
http://example.com/foo/bar/baz - will result in a 404 because even though the method exists, there is no matching route (no parameters were expected)

Fallback

The fallback controller configured with APP_FALLBACK_CONTROLLER is what an user will see when no route matches the request. In other words, Page Not Found (404). Appropriate headers will also be issued. This can be set to any Controller you desire as well. The default one being called will be found in Controllers/Errors/Error404 but if this also does not exist, there is another fallback within the Qaton Core itself which you should not modify. If you wish to update the default 404 message, simply edit the one within your app or update the configuration to point to another.

Routing with Request & Response Headers

By default all output is HTML. Output will be rendered with the appropriate headers with content type set to text/html. However, if you are building a simple API or making command line calls in the terminal or working with JavaScript AJAX requests (that need plain text or JSON responses etc...), then you can manually request the response headers by setting the "Accept" header in your request. In those cases, you will not want HTML responses.

You may always manually override the default HTML headers by asking for JSON or plain-text like so:

Content-Type: application/json

or

Content-Type: text/plain

any other requests or not specifying one at all will default to:

Content-Type: text/html

For example, if you are testing an API call using the cUrl tool, you might do this:

curl -H "Accept: application/json" http://example.com/jello

or

curl -H "Accept: text/plain" http://example.com/jello

Let's take a look at our Jello controller as an example by sending out a JSON response of a simple array. First disable debugging in the config.php file so that your response does not get crowded/corrupted with debugging messages. Then do this:

namespace App\Controllers;

class Jello
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        $years = [2019, 2020, 2021];
        echo json_encode($years);
    }
}


Now, manually set your request headers in your browser. For Firefox, you may consult this page to learn how. You could also use a browser extension. Now, when you make the request, a JSON aware browser such as Firefox will automatically parse the JSON for you like so:

Or a less intelligent browser such as Google Chrome (at the time of this writing) would render:

However, in both cases the browser is aware that the content is JSON because the headers are set. You could also test this using an API testing tool to further explore this functionality.

 

Controllers

If you have read the Routing section, you should already be somewhat familiar with how these work. Essentially, a Controller will contain the application logic (heart and brains of your custom logic) which will drive your app. All your calculations and computations should be housed in controllers. To avoid being redundant, the routing specific aspects of a controller will not be repeated here. Please see that section for routing specific details. Controllers have a few rules that you need to abide by, but first let's create a standalone controller using qpm:

qpm Create controller hello

This will create the empty controller file Hello.php at the root /Controllers/ directory and will look like this:

namespace App\Controllers;

class Hello
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        //
    }
}

The namespace is very important as well, since this must match with the file path. For instance:

qpm Create controller a/b/c/d

Would create the file /Controllers/aa/bb/cc/Dd.php and will look like:

namespace App\Controllers\Aa\Bb\Cc;

class Dd
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        //
    }
}

Qaton takes care of the naming rules automatically when creating the file, but if you ever do this manually, then you will need to follow these same rules:

Controller Rules
  1. Controller file's first letter needs to be caplitalized
  2. The controller's file name must match the class name
  3. No special characters are allowed
  4. The first letter must be alphabetical A-Z
  5. The namespace must map with the file path
  6. Must have an default method or at least one method defined
  7. Only public methods are considered by the router. Other private, static and protected methods for example may be declared for other uses but will be ignored by the router.
  8. Method names must all be lower case and alpanumeric only
Hidden Features

Controller files also have some hidden magic. When they are instantiated by the Qaton routing system, certain properties are added for convenience by default. While this is an unconventional approach, there was no reason not to do it. For example, all form submissions sent to a page are automatically registered under $this->request including request queries from the URL itself. For example:

namespace App\Controllers;

class Jello
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        echo $this->request->get['name'];
    }
}

When requesting this file, you might see an error message similar to this:

Notice: Trying to access array offset on value of type null in /var/www/App/Controllers/Jello.php on line 14

However, if you pass name in the query string, like so: http://example.com/jello?name=Antony, you will see the word "Antony" on the screen because the request was captured. The same is true for POST type form requests.

Views

Support for views are also hidden and the View class is also automatically instantiated and added to the Controller. See the Views section for more details on how they work. It is possible to create a View file automatically when creating a Controller using qpm like so:

qpm Create controller example --view

This will create the Example.php controller and connect it to the view file that was generated automatically like so:

namespace App\Controllers;

class Example
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        $data = [];
        $this->view->render("/example", $data);
    }
}

The view file itself would be saved to the /Views/ directory as /Views/example.php and contain some basic HTML to render that page.

Which you can preview on your browser by visiting the associated URL. For example: http://example.com/example

Views

Views hold all your application's presentation level HTML markup (or potentially other output as well). Qaton views are completely PHP driven and are designed this way intentionally. Most other modern PHP frameworks implement some sort of template parsing layer to process views. For example, Laravel uses a template engine called Blade which is extremely versatile. However, it also adds a layer of complexity and overhead that can be a liability for the type of projects that Qaton is targeted towards. PHP itself is a template engine itself, so it only make sense to harness the full power of PHP to process and render templates. This is what Qaton achieves. You might have already seen a few views at work in this documentation. However, Qaton views are capable of a lot more. Let's start with the basics first. You can create a simple standalone view with qpm by running:
qpm Create view [view_name]

By default, this file will only contain some simple placeholder HTML. The file itself does not do anything unless it's called from a Controller file as documented in the Controllers section above. However, views are also able to call other views and work systematically enabling you to build a modular structure. This means that it is possible to have layouts and reusable sections and components withing your code significantly elimination redundancy and unnecessary repetition.

Methods
Section
section(string $section = null, string $layout = null, array $data = [])
Sections enable you to have clean and organized code for individual pages or components. A section always points to a view collection (a directory) where all the relevant parts for that section will live with a primary default view. The default view is always named main.php within the section directory. The main view can then yield() other parts within that same section into itself. The main view itself will be wrapped in the layout view (if defined). The data array can be an array of values you want to pass into the view. Each key in this array will be converted into a variable in the actual view itself. This should usually be called from the controller itself.

Render
render(string $view = null, array $data = [])

Render is the simplest method. It simply calls a view and then processes it along with the data array. The keys in the data array will be converted into variables in the view itself.

Yield
yield(string $sub_section_view = null)

The yield method works with sections and layouts. As long as the sub_section_view that is being called exists, then it will be merged into the calling view (usually the section's main view) but it could also be other views which have been called using yield itself. Any variables that were included using $data when the section itself was called would also be accessible within these views.

Fetch
fetch(string $view = null, array $data = [])

Fetch works just like render() except that it returns the processed result rather than output it to the browser.

isActive()

isActive($match, $class = 'active')

Checks if the page path is currently active and returns the class name ('active' by default) or null or true if class is set false.

baseUrl()

Returns a fully formed base path URL for the application either auto generated based on intelligent detection or as defined within the configuration.

pageName()

Return the name of the active page.

Layouts

Layouts are a convenient way of managing HTML templates. A layout is typically a complete HTML document with the variable components within it being called using other methods such as render(), yield(), fetch(), etc... which means a layout will typically begin with a HTML DOCTYPE declaration and end with a . The skeletal structure and layout of the template will be defined in a layout view and then be called with a section() where the section's components can be yield() into the document based on context.

Common

Common views are meant to store components you might repeatedly use throughout your application. For example a menu could be a common view which can be called in by other views.

Sections

Generally when building web applications or web pages, sections along with layout would be the solution to use when rendering HTML output. Sections allow code to be neatly organized and included into parent views on demand. Let's take a look at the Jello page we created earlier which will demonstrate how this work. When that page was created using qpm, a number of things took place. We already looked at the controller:

/Controllers/Jello.php

namespace App\Controllers;

class Jello
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        $data = [];
        $this->view->section("sections/jello", "layouts/default", $data);
    }
}

If you look at the index() method, you see that it's calling the section named "jello" and the layout named "default" and passing an empty array named $data. That section named "jello" is actually a directory with one file called main.php living inside it.

/Views/sections/jello/main.php

Now if you look at the layout named default, you will notice something interesting:

/Views/layouts/default.php

The yeild('main') here is instructing the Qaton to render the action section's sub view named main (i.e. the main.php we just looked at). When you create your own views, you can name them anything you like and call them in this way. You also see the render() methods being called for common/header and common/footer. Let's take a look at those as well:

/Views/common/header.php

/Views/common/footer.php

If you study these files, you will see how wonderfully they work together giving you advnced control of your templates and views. Nothing is impossible and best of all, it's powered by PHP so you get maximum speed and control without an additional layer of complexity. There are pros and cons to this approach, but for the scope of projects that Qaton is aimed at, it's all pros.

Database

Qaton supports any kind of Database PHP supports. It should be fairly easy to extend Qaton to utilize ORM solutions as well. Doctrine, Eloquent, Propel, RedBean, etc... are all very easy to integrate to Qaton. However, Qaton by default comes with it's own FIleDatabase. You don't have to use it if you don't want to, and there will be many use cases where it will not be the right tool anyway. However, for small to medium sized web sites with mostly content, it would suit your needs just fine.

FileDatabase

FileDatabase is a JSON based flat-file NoSQL database equipped with a object-relational mapper (ORM) style interface. It's has been designed with version control in mind, so you can actually use Git or other such solutions to keep track of data changes. FileDatabase does not aim to replace databases like MySQL, MSSQL, or even MongoDB, Firebase, etc... However, it does aim to replace SQLite (and other similar solutions) by offering a NoSQL, plain-text based approach gear at small to medium sized websites. FileDatabase allows you to interact with the database directly or using Models for greater consistency, context and control.

Quick Start

The quickest way to start is to simply use qpm to create a Model and Migration for your first table. For this example, let's imagine that we want to create a small database collection of songs:

qpm Create model Song --migration

Notice how the model name is singular. This is the recommended naming convention for Models. The following files will be generated when this command is run:

/Models/Song.php

namespace App\Models;

use VirX\Qaton\Models\FileDatabaseModel;

class Song extends FileDatabaseModel
{
    protected $table = 'songs';
}

As you can see, Qaton has also converted the singular 'song' to 'plural 'songs'. Please note that at the present time, this process does not always work as expected. For example, for the word 'quiz', Qaton will come up with 'quizs' rather than 'quizzes'. You might want to adjust such instances manually, however improving that mechanism is definitely on the roadmap.

/Database/FileDatabase/Migrations/[unique_stamp]_song.php

// a migration for Song

namespace App\Database\FileDatabase\Migrations;

use VirX\Qaton\Migration;

class AppMigration[UniqueStamp]Song extends Migration
{
    public function up()
    {
        $this->db->table('songs')->create([
            'name' => [
                'type' => 'string',
                'null' => false,
                'default' => 'Untitled'
            ],
            // ...
        ], ['timestamps' => true]);
    }

    public function down()
    {
        $this->db->table('songs')->drop();
    }
}

Now you can edit this migration file with the table structure we desire. For this example, lets create the following fields:

  • title - string value
  • year - integer value
  • lyrics - text block
  • artist - foreign id (will point to another table)
public function up()
    {
        $this->db->table('songs')->create([
            'title' => [
                'type' => 'string',
                'null' => false,
                'default' => 'Untitled'
            ],
            'year' => [
                'type' => 'integer',
                'null' => true
            ],
            'lyrics' => [
                'type' => 'text',
                'null' => true,
            ],
            'artist' => [
                'type' => 'foreign',
                'foreign' => 'artists',
                'key' => 'id'
            ],
        ], ['timestamps' => true]);
    }

This migration once customized and updated in this can can now be run. However, let's also create the a table to store the artists:

qpm Create model Artist --migration

/Database/FileDatabase/Migrations/[unique_stamp]_artist.php

namespace App\Database\FileDatabase\Migrations;

use VirX\Qaton\Migration;

class AppMigration[UniqueStamp]Artist extends Migration
{
    public function up()
    {
        $this->db->table('artists')->create([
            'name' => [
                'type' => 'string',
                'null' => false,
                'default' => 'Untitled'
            ]
        ], ['timestamps' => true]);
    }

    public function down()
    {
        $this->db->table('artists')->drop();
    }
}

Now to view these migrations, we could run:

qpm Migrate list

To install them, we could run:

qpm Migrate install

At this point, you should have two empty tables. We can use these tables as examples when exploring the ORM interface as we move forward. First, lets look at some of the possible data types and properties that are available in FileDatabase.

 

Data Types

FileDatabase provides some of the common data types you might already be familiar with if you have ever used other databases. However, FileDatabase also introduces some special types which are exclusive to this engine and provide convenient ways of manage common types of data.

string

Similar to a VARCHAR and used for storing short strings.

Properties:

  • null (boolean): weather to allow null values or not
  • default (string): default value to insert if no value was provided
integer

Integer values

Properties:

  • null (boolean): weather to allow null values or not
  • default (string): default value to insert if no value was provided
float, double

Float and double values

Properties:

  • null (boolean): weather to allow null values or not
  • default (string): default value to insert if no value was provided
text, html, md

Large text fields (used for bulky text fields). The help and md types will also support auto parsing in the future.

Properties:

  • null (boolean): weather to allow null values or not
  • default (string): default value to insert if no value was provided
timestamp

Timestamp

Properties:

  • null (boolean): weather to allow null values or not
foreign (although stable and functional, this is still a work in progress and is very basic at the moment)

Used to create relationships with other tables.

Properties:

  • null (boolean): weather to allow null values or not
  • foreign (string): foreign table name
  • key (string): column name to associate value with
masked

This is equal to a string type field, however, in query results it will not display the contents. Instead it will display something like ***** to hide it. To reveal the contents, unmask() must be issued.

Properties:

  • null (boolean): weather to allow null values or not
  • default (strin(g): default value to insert if no value was provided
hashed

Encrypts the contents (like a password for example). It can never be retrieved, but if the value is known it can be tested.

TODO: more documentation on this

 

Accessing the Database

Ideally you should not access the database directly, you should use Models instead. However, if you need to interact with the database directly, then you may simply initialize FileDatabase using the Db facade like so from any controller:

namespace App\Controllers;

use VirX\Qaton\Db;

class Index
{
    public function __construct()
    {
        //
    }

    public function index()
    {
        $db = Db::init(_env('QATON_CONFIG'));
    }
}

Notice that use Virx\Qaton\Db has been instructed at the top. You need to pass the QATON_CONFIG from your environment into the Db::init() static method which will return the database object for you to interact with.

 

Methods
table(string $table)

Set the active table context. You should only do this if interacting with the database directly using the Db facade to initialize it or from within a migration. When using a model, this part is taken care of for you automatically behind the scenes.

$db = Db::init(_env('QATON_CONFIG'));
$songs = $db->table('songs');
$artists = $db->table('artists');

 

create(array $columns, array $options = array())

Create table schema.

$db->table('artists')->create([
    'name' => [
        'type' => 'string',
        'null' => false,
        'default' => 'Untitled'
    ]
], ['timestamps' => true]);

 

insert(array $data)

Insert values into table

// Insert just one record
$artists->insert([
    'name' => 'A-ha'
]);

// Insert multiple records at once
$artists->insert([[
    'name' => 'Michael Jackson'
],[
    'name' => 'Madonna'
],[
    'name' => 'Duran Duran'
]]);

 

get()

Get all matching results

$artists->get();

If you examine the response, you should get results similar to this:

array(4) {
  [0]=>
  array(7) {
    ["name"]=>
    string(4) "A-ha"
    ["created_on"]=>
    int(1624776557)
    ["updated_on"]=>
    int(1624776557)
    ["deleted_on"]=>
    NULL
    ["id"]=>
    int(1)
    ["created_on_text"]=>
    string(19) "2021-06-27 06:49:17"
    ["updated_on_text"]=>
    string(19) "2021-06-27 06:49:17"
  }
  [1]=>
  array(7) {
    ["name"]=>
    string(15) "Michael Jackson"
    ["created_on"]=>
    int(1624776810)
    ["updated_on"]=>
    int(1624776810)
    ["deleted_on"]=>
    NULL
    ["id"]=>
    int(2)
    ["created_on_text"]=>
    string(19) "2021-06-27 06:53:30"
    ["updated_on_text"]=>
    string(19) "2021-06-27 06:53:30"
  }
  [2]=>
  array(7) {
    ["name"]=>
    string(7) "Madonna"
    ["created_on"]=>
    int(1624776810)
    ["updated_on"]=>
    int(1624776810)
    ["deleted_on"]=>
    NULL
    ["id"]=>
    int(3)
    ["created_on_text"]=>
    string(19) "2021-06-27 06:53:30"
    ["updated_on_text"]=>
    string(19) "2021-06-27 06:53:30"
  }
  [3]=>
  array(7) {
    ["name"]=>
    string(11) "Duran Duran"
    ["created_on"]=>
    int(1624776810)
    ["updated_on"]=>
    int(1624776810)
    ["deleted_on"]=>
    NULL
    ["id"]=>
    int(4)
    ["created_on_text"]=>
    string(19) "2021-06-27 06:53:30"
    ["updated_on_text"]=>
    string(19) "2021-06-27 06:53:30"
  }
}

 

first()

Get the first matching result

$artists->first();

The result would only contain the first matching record:

array(7) {
  ["name"]=>
  string(4) "A-ha"
  ["created_on"]=>
  int(1624776557)
  ["updated_on"]=>
  int(1624776557)
  ["deleted_on"]=>
  NULL
  ["id"]=>
  int(1)
  ["created_on_text"]=>
  string(19) "2021-06-27 06:49:17"
  ["updated_on_text"]=>
  string(19) "2021-06-27 06:49:17"
}

 

select(string $column)

Select a specific column to be returned instead of all. These can be chained to select multiple columns.

// Select one column
$artists->select('name')->first();

Where the result would be:

array(1) {
  ["name"]=>
  string(4) "A-ha"
}

or

// Select multiple columns
$artists->select('id')
        ->select('name')
        ->select('created_on')
        ->first();

Which would result in:

array(4) {
  ["id"]=>
  int(1)
  ["name"]=>
  string(4) "A-ha"
  ["created_on"]=>
  int(1624776557)
  ["created_on_text"]=>
  string(19) "2021-06-27 06:49:17"
}

Notice that the dynamic field created_on_text was also included. This can be disabled in the advanced settings which should also slightly improve performance. Also note that you can not select dynamic fields, but only the actual one which it's based on. In this case that is created_on which is what created_on_text is generated from.

where(string $column, $value_or_operator, $value_with_operator = false)
Where conditions to match when retrieving table data. The where() method can be used in two different ways. The first way is to pass a value as the second parameter which will be tested for a direct match by default. The second way is to specify an operator as the second parameter and pass the value to be tested against as the third parameter. Where methods can not be chained at the moment but this will be added in the future as well.

Without an operator:

$artists->where('id', 3)->get();

Would return

array(1) {
  [0]=>
  array(7) {
    ["name"]=>
    string(7) "Madonna"
    ["created_on"]=>
    int(1624776810)
    ["updated_on"]=>
    int(1624776810)
    ["deleted_on"]=>
    NULL
    ["id"]=>
    int(3)
    ["created_on_text"]=>
    string(19) "2021-06-27 06:53:30"
    ["updated_on_text"]=>
    string(19) "2021-06-27 06:53:30"
  }
}

There are several operators available:

 =  value equal to
!=  value not equal to
>  value greater than
<  value less than
>=  value greater than or equal to
<=  value less than our equal to
like  value string match (useful for simple text searches)

 

$artists->where('id', '>', 2)->get();

Would result in:

array(2) {
  [0]=>
  array(7) {
    ["name"]=>
    string(7) "Madonna"
    ["created_on"]=>
    int(1624776810)
    ["updated_on"]=>
    int(1624776810)
    ["deleted_on"]=>
    NULL
    ["id"]=>
    int(3)
    ["created_on_text"]=>
    string(19) "2021-06-27 06:53:30"
    ["updated_on_text"]=>
    string(19) "2021-06-27 06:53:30"
  }
  [1]=>
  array(7) {
    ["name"]=>
    string(11) "Duran Duran"
    ["created_on"]=>
    int(1624776810)
    ["updated_on"]=>
    int(1624776810)
    ["deleted_on"]=>
    NULL
    ["id"]=>
    int(4)
    ["created_on_text"]=>
    string(19) "2021-06-27 06:53:30"
    ["updated_on_text"]=>
    string(19) "2021-06-27 06:53:30"
  }
}

 

offset(int $offset = 1)

Offset the starting point of retrieval.

$artists->offset(2)->get();

 

limit(int $limit = 0)
Limit the number of records to retrieve
$artists->limit(2)->get()

 

update(array $data)

Update values in table

$artists->where('id', 1)->update([
    'name' => 'Toto'
])

 

unmask()

If there are columns with masked data, return them unmasked

withHashed()

Get with hashed columns. By default, hashed columns are not included in get() or first() retrievals because they are encrypted data which are useless to reading purposes most of the time.

verifyHashed(string $column, string $value)

Verify a value with a hashed column to see if it matches (such as an user input password)

withForeign(string $column)

Include foreign table data for column

allForeign()

Include foreign table data for all columns

getForeign()

(to be documented)

withPivot(string $table, string $column)

Use an intermediary table as a pivot using a specific column
(to be documented further)

explain()

Return the schema structure of a table

clone()

Clone a row and duplicate it

drop()

Drop table

delete(bool $timestamps = true)

Delete matching rows (soft delete only)

purge()

Completely delete matching rows

table_exists(string $table)

Check if table exists

table_is_empty(string $table)

Check if table is empty

count_rows()

Count number of active (non-soft deleted) rows on a table

count_all_rows()

Count all rows on a table including deleted ones

paginate(bool $manage_request_offset=false)

Automatically paginate results and optionally generate request key helpers

pages(int $pages)

Number of pages when paginating. To be used with paginate() only.

log()

Return the log if logging was enabled

errors()

Return errors if error logging was enabled

Models

Models

Migrations

Migrations

Templates

Templates

Public Assets

Public Assets

Extend & Libraries

Extend

Conventions

Conventions

Best Practices

Best Practices

Requirements & Environment

Minimum Requirements:

  • PHP 7.4+

Environment:

At this time Qaton has only been tested on LAMP based systems (e.g. Debian, Ubuntu, CentOS, etc...). However, all code has been written mindfully to function well with all other platforms as well. Therefore, it is likely that Qaton will run just fine on a Microsoft IIS server for example. However, there may be a few configuration tweaks that you might need to make in order for that to happen. The following is an example of an Ubuntu environment where Qaton is guaranteed to function as expected:

Example:

Ubuntu 18.04 (amd64) server with root access via terminal/ssh (this could be a VPS, VM or container) with packages apache2, libapache2-mod-php, php-mbstring and composer installed. In order to do this, if you are on an Ubuntu system that is less than 20.04, then you need to run the following as the root user (or using sudo):

apt install software-properties-common
add-apt-repository ppa:ondrej/php

If you have a Ubuntu 20.04 or higher, you could skip that step (although running is it harmless as regardless) and then run the following:

apt update
apt install apache2 php7.4 libapache2-mod-php7.4 php7.4-mbstring composer

Since this is a fresh installation, let's setup apache2 by enabling the rewrite and headers mods and update it's public directory to reflect Qaton's default configuration (otherwise you may also edit Qaton's config instead to match your apache2 installation instead):

a2enmod rewrite headers
cd /var/
rm -rf www/html

This is when you might install Qaton. For example, if using composer to perform your installation you would do this:

composer create-project virx/qatonapp:"dev-master" www --remove-vcs

Now we can complete apache2's permissions and configuration update:

chown -Rv www-data:www-data www

# I'm using nano to edit the file, but you can edit it with something else: 

nano /etc/apache2/sites-available/000-default.conf

Update the config file to match the following (or as you need it according to your setup):


        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/public
        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
        
                Options Indexes FollowSymLinks MultiViews
                AllowOverride All
                Require all granted
    

Finally restart apache2:

service apache2 restart

Production Environment:

Please note that Packagist Composer is NOT recommended by us for production deployment. When deploying Qaton for production use, please set things up manually. However, this is only a suggestion.

Debugging

Roadmap

TODO / Roadmap

Roadmap