Core Boxx – PHP Modular Development Framework

Core Boxx is a lightweight PHP modular development framework. The whole idea is not to be another bloated framework with tons of unused features. So it is built around the concept of modularity, load only what you need. It is more like “predefined system modules you can expand on”, rather than a full-fledged framework.

 

TABLE OF CONTENTS

Download & Notes Modules Simple Tutorial
More Quick Reference

 

 

DOWNLOAD & NOTES

First, here are the download links and a quick “setup guide” for the impatient folks who don’t want to read through everything.

 

LICENSE & DOWNLOAD

Core Boxx is released under the MIT License. You are free to use it for personal and commercial projects, modify as you see fit. On the condition that the software is provided “as-is”. There are no warranties provided and “no strings attached”. Code Boxx and the authors are not liable for any claims, damages, or liabilities.

Download Core Boxx | GitHub | Source Forge

 

SYSTEM REQUIREMENTS

  • LAMP/WAMP/MAMP/XAMPP
  • Apache Mod Rewrite
  • PHP MYSQL PDO Extension
  • At least PHP 8.0

Core Boxx has not been extensively tested, but it is developed and works on a WAMP8 (Windows Apache MySQL PHP 8) server.

 

QUICKSTART

  1. Just access index.php in your browser to launch the installer. This will automatically create a project database with a dummy options table and generate the necessary .htaccess and api/.htaccess files.
  2. Done! You now have a basic framework. Do check out the HTML-Javascript template if you need a kickstart with the client-side too. It is built on Bootstrap and Material Icons.

 

 

CORE BOXX MODULES

Go ahead, check out the Core Boxx modules. Download the ones you need, plug them into Core Boxx. Do some of your own customizations, and help yourself speed up development.

 

  1. HTML Javascript Template – Built with Boostrap and Material Icons. PWA ready.
  2. Mail – Simple email module.
  3. Users – User register, login, logout, forgot password, and admin functions.
  4. One Time Password – For added security.
  5. Resumable Upload – Resumable uploads with FlowJS and FlowPHP.
  6. Dynamic Content – Store and retrieve contents from the database.
  7. Likes & Dislikes Reaction – Or was it “upvote and downvote”?
  8. Comments – Allow registered users to comment on your posts/products/images/videos.

 

SIMPLE TUTORIAL

Don’t worry, Core Boxx does not have some crazy documentation that takes months to read. Just this page. Let us walk through a quick example of creating a dummy content module

 

TUTORIAL CODE DOWNLOAD

Click here to download the tutorial code.

 

STRUCTURE OVERVIEW

The folders layout in Core Boxx:

  • api The API endpoint to handle AJAX calls, mobile app requests, server-to-server communications.
  • assets Public CSS, Javascript, images, and whatever else.
  • pages HTML pages and templates.
  • lib Core and library files should be kept in this folder.

Then, there are 5 “base library files” in the lib folder.

  • CORE-config.php The mandatory “system settings” file.
  • CORE-go.php Where it starts.
  • LIB-Core.php The core library itself.
  • LIB-DB.php Default MYSQL PDO database module.
  • LIB-Route.php To resolve pretty URL.

Lastly, the not-so-important files:

  • CORE-install.php The “one-page installation script”. Feel free to reuse this in your own project to create an installer.
  • INSTALL-index.foo The actual and proper index.php.
  • LIB-Options.php Optional library. If you want to use the database to keep some settings that the user can update by themselves.

 

 

STEP 1) RUN THE INSTALLER

1A) CONFIG FILE

lib/CORE-config.php
// (A) HOST
define("HOST_BASE", "http://localhost/");
 
// (B) DATABASE
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8");
define("DB_USER", "root");
define("DB_PASSWORD", "");

Captain Obvious step 1, the installer should have automatically changed the settings in the config file. But you can update or add more of your own settings at any time.

 

1B) HTACCESS FILES

Just make sure that .htaccess and api/.htaccess are created. If you migrate or change the URL path, update HOST_BASE in the config file and run:

  • require "lib/CORE-go.php";
  • $_CORE->load("Route");
  • $_CORE->Route->init();

This will regenerate the .htaccess files.

 

STEP 2) CREATE DATABASE TABLES

lib/SQL-contents.sql
CREATE TABLE `content` (
  `id` int(11) NOT NULL,
  `content` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
ALTER TABLE `content`
  ADD PRIMARY KEY (`id`);
 
ALTER TABLE `content`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

Next, we will create the database tables. This is just a simple dummy content table with only 2 fields:

  • id The primary key, auto-increment.
  • content The content.

 

 

STEP 3) DEVELOP THE LIBRARY MODULE

lib/LIB-Content.php
<?php
class Content extends Core {
  // (A) SAVE CONTENT
  function save ($content, $id=null) {
    if ($id===null) {
      $this->DB->insert("content", ["content"], [$content]);
    } else {
      $this->DB->update("content", ["content"], "`id`=?", [$content, $id]);
    }
    return true;
  }
 
  // (B) GET ALL CONTENT
  function getAll () {
    return $this->DB->fetchAll("SELECT * FROM `content`");
  }
 
  // (C) GET CONTENT
  function get ($id) {
    return $this->DB->fetch("SELECT * FROM `content` WHERE `id`=?", [$id]);
  }
}

With the database table in place, the next step is to create the library itself.

  • Since we are creating a content module, we create a corresponding lib/LIB-Content.php – Take note of the capital C.
  • Set the class name to the same as the file name, and remember to extend the core – class Content extends Core.
  • Add the content functions. In this dummy library, we have 3 simple functions:
    • function save() Add or update content.
    • function get() Get content by ID.
    • function getAll() Get all content.
  • Take note of how we “link back” and use functions from the database library $this->DB->insert(), $this->DB->update(), $this->DB->fetch().
  • If you are curious about how this works, dissect function load() and class Core in lib/LIB-Core.php in your own free time… If not, just know that you can use $this->core->XXX to access the core library.

P.S. Modules can also access each other. For example, we can add a new function to send a piece of content to a specified email:

function mailTo ($email, $id) {
  $this->core->load("Mail");
  return $this->core->Mail->send([
    "to" => $email,
    "subject" => "Test",
    "body" => $this->get($id)
  ]);
}

Yep, that is the whole idea of a modular framework – We load modules only when required, and use them to help each other.

 

 

STEP 4) DEVELOP THE API ENDPOINT

api/API-content.php
<?php
switch ($_REQ) {
  // (A) INVALID REQUEST
  default:
    $_CORE->respond(0, "Invalid request", null, null, 400);
    break;
 
  // (B) GET ALL
  case "getAll":
    $_CORE->autoGETAPI("Content", "getAll");
    break;
 
  // (C) SAVE CONTENT
  case "save":
    $_CORE->autoAPI("Content", "save");
    break;
  }
}
  • To call the API in Core Boxx, we send a POST or GET request to http://site.com/api/MODULE/REQUESTMODULE will be parsed into $_MOD and REQUEST into $_REQ.
  • In this example, the content endpoint http://site.com/api/content will map to api/API-content.php.
  • The request $_REQ is to be manually handled in the script itself. As above, we use a switch ($_REQ) to specify the available commands.
  • The rest is automatic magic.
    • $_CORE->autoAPI("Content", "save") calls $_CORE->Content->save(), maps all the post parameters, and replies with the system standard JSON response $_CORE->respond().
    • $_CORE->autoGETAPI() is the cousin of $_CORE->autoAPI() and should be used on functions that return an array of data.

 

STEP 5) ADD HTML PAGES

5A) LIST CONTENT PAGE

pages/PAGE-list.php
<?php require PATH_PAGES . "TEMPLATE-top.php"; ?>
<!-- (A) JAVASCRIPT FETCH ALL CONTENT -->
<script>
function getAll () {
  fetch("<?=HOST_API_BASE?>content/getAll/", { method: "POST" })
  .then(res=>res.json())
  .then((res) => {
    let wrap = document.getElementById("wrap");
    if (res.data==null) { wrap.innerHTML = "No content"; }
    else { for (let c of res.data) {
      let row = document.createElement("div");
      row.innerHTML = `<a href="<?=HOST_BASE?>addedit/${c["id"]}">${c["content"]}</a>`;
      wrap.appendChild(row);
    }}
  });
}
window.onload = getAll;
</script>
 
<!-- (B) ADD NEW CONTENT -->
<a href="<?=HOST_BASE?>addedit">Add Content</a>
 
<!-- (C) CONTENT LIST -->
<div id="wrap"></div>
<?php require PATH_PAGES . "TEMPLATE-bottom.php"; ?>
  • Take note of how the URL-to-file resolves:
    • pages/PAGE-list.php will be accessible at http://site.com/list.
    • pages/PAGE-hello.php will be accessible at http://site.com/hello.
    • pages/PAGE-hello-world.php will be accessible at http://site.com/hello/world.
  • Don’t want to restrict which HTML/CSS/JS frameworks you guys can use, so this is just raw HTML/CSS/Javascript. Feel free to use your preferred framework in your own project.

 

5B) MANUAL ROUTE

index.php
/* (B) ADD YOUR OWN MANUAL ROUTES IF YOU WANT */
$_CORE->Route->set([
  "addedit/*" => "PAGE-addedit.php"
]);

For this page, let us create a manual wildcard route first. That is, http://site.com/addedit/WHAT/EVER/ will all route to PAGE-addedit.php.

 

5C) ADD/EDIT CONTENT PAGE

pages/PAGE-addedit.php
<?php
// (A) SPLIT PATH
// $_PATH[0] SHOULD BE THE CONTENT ID
$_PATH = explode("/", rtrim($_PATH, "/"));
$pass = count($_PATH)==1;
 
// (B) GET CONTENT
if ($pass) { $pass = is_numeric($_PATH[0]); }
if ($pass) {
  $_CORE->load("Content");
  $content = $_CORE->Content->get($_PATH[0]);
  $pass = is_array($content);
} 
 
// (C) INVALID
if (!$pass) {
  $_CORE->Route->load("PAGE-404.php", "", 404);
  exit();
}
 
// (D) HTML FORM
require PATH_PAGES . "TEMPLATE-top.php"; ?>
<!-- (D1) JAVASCRIPT -->
<script>
function save () {
  // FORM DATA
  var data = new FormData();
  data.append("content", document.getElementById("ctext").value);
  var id = document.getElementById("cid").value;
  if (id!="") { data.append("id", id); }
 
  // FETCH CALL API
  fetch("<?=HOST_API_BASE?>content/save/", { method: "POST", body: data })
  .then(res=>res.json())
  .then((res) => {
    console.log(res);
    if (res.more) {
      location.href = "<?=HOST_BASE?>addedit/" + res.more;
    } else {
      alert(res.message);
    }
  });
  return false;
}
</script>
 
<!-- (D2) ADD/EDIT CONTENT FORM -->
<h1><?=isset($content)?"EDIT":"NEW"?> CONTENT</h1>
<form onsubmit="return save()">
  ID : <input type="number" id="cid" readonly value="<?=isset($content["id"])?$content["id"]:""?>"/>
  Text : <textarea id="ctext" required><?=isset($content["content"])?$content["content"]:""?></textarea>
  <input type="submit" value="Save"/>
</form>
 
<!-- (D3) "BACK TO LIST" -->
<a href="<?=HOST_BASE?>list">Back to list</a>
<?php require PATH_PAGES . "TEMPLATE-bottom.php"; ?>

This page is a little busier, but essentially:

  • (A & B) We split the URL file path, and use it as the content ID. No content ID will literally mean “add content”, while having a provided ID means “edit content”.
  • (C) Throw the “file not found” error on invalid “edit content”.
  • (D) Self-explanatory. Just an HTML form, and we are sending the data to the API endpoint to be saved.

 

 

MORE CORE BOXX

The above simple tutorial covered basic development. Here are a couple more extras and examples to take note of.

 

A) SUPPORTING CORS FOR API CALLS

lib/GO.php
// define("API_CORS", false); // no cors support
// define("API_CORS", true); // any domain + mobile apps
// define("API_CORS", "site-a.com"); // this domain only
// define("API_CORS", ["site-a.com", "site-b.com"]);

For you guys who have to work with multiple sites, or even support mobile apps – Remember to enable CORS in lib/GO.php. The easiest way is to set API_CORS to true… But that is somewhat a security risk – You decide.

 

B) URL ROUTES

index.php
// (B) ADD YOUR MANUAL ROUTES
$_CORE->Route->set([
  "/" => "myhome.php",
  "mypage/", "page.php",
  "products/*", "myproducts.php"
);

If you poke around the scripts, you should have found this one… Yep, the engine default is to map site.com/NAME to pages/PAGE-NAME.php. To “override” and manually define which file to load, use $_CORE->route->add("PATH/", "SCRIPT IN PAGES FOLDER").

P.S. Exact routes have the higher precedence. That is, between "products/toys" and "products/*", it will resolve to "products/toys".

 

C) REGISTERED USERS ONLY – EXAMPLE

C1) WILDCARD MAP TO CHECK SCRIPT

index.php
$_CORE->Route->set([
  "admin/*" => "ADM-check.php"
]);

Want to restrict some pages to registered users only? Start by doing a wildcard map to your “check script”.

 

C2) PERMISSION CHECK

pages/ADM-check.php
// (A) NO PERMISSION
if (!isset($_SESSION["user"])) {
  // REDIRECT TO LOGIN PAGE OR STRAIGHT EXIT
}
 
// (B) OK - LOAD PAGE
$_CORE->Route->pathload($_PATH, "ADMIN");

Yep, just do a check against the session or cookie – Whichever you are using to track user login. Then, use $_CORE->Route->pathload() to help you auto-resolve the proper page… Or you can devise your own way to resolve pages.

 

D) REGISTERED USERS ONLY – ANOTHER EXAMPLE

index.php
$_CORE->Route->run(function ($_PATH) {
  // EXAMPLE - TO LOGIN PAGE IF NOT SIGNED IN
  if (!isset($_SESSION["user"]) && $_PATH!="/login") {
    header("Location: ".HOST_BASE."login");
    exit();
  }
 
  // MUST RETURN PATH
  return $_PATH;
});

Want to “protect all pages” except for the login page? Simply define a hook on run() – This will run just before resolving the path-to-file.

P.S. You can also use this to tweak the path.

 

E) CATEGORY & PAGINATION – EXAMPLE

E1) WILDCARD MAP TO HANDLER SCRIPT

index.php
$_CORE->Route->set([
  "products/*" => "PDT-list.php"
]);

Want to use the URL for pagination or show different products? Use the wildcard once again.

 

E2) HANDLER SCRIPT LOAD

pages/PDT-list.php
// (A) $_PATH IS THE CURRENT URL PATH
// E.G. toys/3
print_r($_PATH);
 
// (B) MAYBE EXPLODE INTO AN ARRAY FIRST?
$parts = explode($_PATH, "/");
// $parts[0] - category
// $parts[1] - page number
if (count($parts)!=2) { $_CORE->Route->load("PAGE-404", null, 404); }

In your handler script, deal with $_PATH as you see fit.

 

F) SETTINGS IN DATABASE

Most of the system settings and config are already stored in lib/CORE-config.php. But if you want to use the database to keep “user-customizable settings”:

  • Enable $_CORE->load("Options") in lib/CORE-go.php.
  • Manually enter your own settings into the options table.

If you do not need this, the options table and LIB-Options.php can be deleted.

 

QUICK REFERENCE

With that, we have come to the end of the “quickstart tutorial”, and here is a summary of the core functions (plus quick examples).

 

CORE ENGINE (LIB-CORE.PHP)

function load($module)

Loads lib/LIB-$module.php, and extends it to $_CORE->$module = new $module();

  • $module – String, module to load.
$_CORE->load("Users");
function loaded($module)

Checks if the specified $module is loaded.

  • $module – String, module to check.
if ($_CORE->loaded("Users")) { ... }
function autoCall($module, $function, $mode)

Automatically map POST or GET variables to the specified module function, and run it.

  • $module – String, module to load.
  • $function – String, function to call.
  • $mode – String, POST or GET. Defaults to POST.
$users = $_CORE->autoCall("Users", "getAll");
function autoAPI($module, $function, $mode)

Automatically map POST or GET variables to the specified module function, and respond after running it.

  • $module – String, module to load.
  • $function – String, function to call.
  • $mode – String, POST or GET. Defaults to POST.
$_CORE->autoAPI("Users", "save");
function autoGETAPI($module, $function, $mode)

Automatically map POST or GET variables to the specified module (get entries) function, and respond after running it.

  • $module – String, module to load.
  • $function – String, function to call.
  • $mode – String, POST or GET. Defaults to POST.
$_CORE->autoAPI("Users", "save");
function respond($status, $msg, $data, $more, $http, $exit)

Formats and outputs a standard JSON encoded string.

  • $status – Boolean, 1, 0, or invent your own set of status code.
  • $msg – String, system message.
  • $data – Data if any.
  • $more – Additional data, if any.
  • $http – Optional, HTTP response code.
  • $exit – Optional, stop processing after JSON string output. Defaults to true.
$_CORE->respond(0, "An error has occurred!", null, null, 500);
function ouch($ex)

Nothing to see here. This one is used to handle errors globally.

function random($length)

Creates an alphanumeric string.

  • $length – Integer, number of bytes. Defaults to 8.
$password = $_CORE->random(10);
function paginator($entries, $now)

Calculates pagination.

  • $entries – Integer, the total number of entries.
  • $now – Integer, the current page number.
$entries = 1234; // DO YOUR OWN SELECT COUNT(*) FROM `TABLE`
$now = 4; // CURRENT PAGE
$page = $_CORE->paginator($entries, $now);
/* $page = [
  "entries" => TOTAL NUMBER OF ENTRIES
  "total" => TOTAL NUMBER OF PAGES
  "now" => CURRENT PAGE
  "x" => USE THIS TO LIMIT X,Y YOUR SQL QUERY
  "y" => USE THIS TO LIMIT X,Y YOUR SQL QUERY
] */
function redirect ($page, $url)

HTTP redirect.

  • $page – Page or path.
  • $url – Defaults to HOST_BASE.
if (!isset($_SESSION["user"])) {
  $_CORE->redirect("login/");
}

 

DATABASE CORE (LIB-DB.PHP)

function __construct()
Connects to the database when the object is created.
function __destruct()
Closes the database connection when done.
function start()

Auto-commit off.

$this->DB->start();
$pass = $this->DB->update(SQL 1);
if ($pass) { $pass = $this->DB->update(SQL 2); }
$this->DB->end($pass);
return $pass;
function end($pass)

Used in conjunction with start().

  • $pass – Boolean, commit or rollback?
function query($sql, $data)

Runs an SQL query.

  • $sql – String, SQL query to run.
  • $data – Array, data to feed into query.
$this->DB->query(
  "DELETE FROM `users` WHERE `id`=?", [$id]
);
function fetchAll($sql, $data, $key)

Fetch multiple rows of data.

  • $sql – String, SQL query to run.
  • $data – Array, data to feed into query.
  • $key – String, use this column as the key of the array. Optional.
$users = $this->DB->fetchAll(
  "SELECT * FROM `users` WHERE `age`<?",
  [30], "user_id"
);
function fetchKV($sql, $data, $key, $value)

Fetch multiple rows of data, sorted into KEY => VALUE.

  • $sql – String, SQL query to run.
  • $data – Array, data to feed into query.
  • $key – String, use this column as the key of the array.
  • $value – String, use this column as the value of the array.
$users = $this->DB->fetchKV(
  "SELECT * FROM `users`", null, "user_id", "user_email"
);
// $users = [ID => EMAIL, ID => EMAIL, ETC...]
function fetch($sql, $data)

Fetch a single row of data.

  • $sql – String, SQL query to run.
  • $data – Array, data to feed into query.
$user = $this->DB->fetch(
  "SELECT * FROM `users` WHERE `id`=?", [$id]
);
function fetchCol($sql, $data)

Fetch a single column of data.

  • $sql – String, SQL query to run.
  • $data – Array, data to feed into query.
$email = $this->DB->fetchCol(
  "SELECT `user_email` FROM `users` WHERE `id`=?", [$id]
);
function insert($table, $fields, $data, $replace)

Insert or replace data into the specified database table.

  • $table – String, target table.
  • $fields – Array, name of fields.
  • $data – Array, data to feed into query.
  • $replace – Boolean, replace instead of insert?
$this->DB->insert("users", 
  ["user_email", "user_password"]
  ["john@doe.com", "123", "jane@doe.com", "456"]
);
function update($table, $fields, $where, $data)

Run update query.

  • $table – String, target table.
  • $fields – Array, name of fields.
  • $where – String, the “WHERE” clause.
  • $data – Array, data to feed into query.
$this->DB->update("table",
  ["user_email", "user_pass"],
  "`user_id`=?",
  ["joy@doe.com", "PASSWORD", 1]
);
function delete($table, $where, $data)

Run delete query.

  • $table – String, target table.
  • $where – String, the “WHERE” clause.
  • $data – Array, data to feed into query.
$this->DB->delete("content",
  "`content_id`=?", [123]
);

10 thoughts on “Core Boxx – PHP Modular Development Framework”

  1. {“status”:0,”message”:”Call to a member function execute() on bool”,”code”:0,”file”:”C:\\xampp\\htdocs\\box\\lib\\LIB-DB.php”,”line”:40}

    I keep getting that, i want to use sqlite as a database.

      1. Thank you! I will do my research for that and try to build it. I want SQLite since my most of the project built with this framework will be small projects or prototypes that I want to move fast from server to server or locally.

  2. Thanks much for this. Found a small typo:
    In the line below, instead of User.php, it should be Users.php:

    Since we are creating a user module, we create a corresponding lib/User.php – Take note of the capital U.

  3. Thanks again for your FREE work. It is helpful & appreciated.

    I am attempting to login via the API from external site, but cannot seem to login via cURL. Although I set CURLOPT_POSTFIELDS, the email & password do not seem to be available as $_POST variables in the session.php script under api folder.
    To try debug, I have tried to force with test data directly, but even then the mysite/api/users/verify returns {“status”:0,”message”:”Please sign in first”,”data”:null,”page”:null}

  4. This seems a very elegant, starting point. Thanks
    Is there a forum or further help facility, for questions not covered here?

Leave a Comment

Your email address will not be published.