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
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
- Just access
index.php
in your browser to launch the installer. This will automatically create a project database with a dummyoptions
table and generate the necessary.htaccess
andapi/.htaccess
files. - 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.
- HTML Javascript Template – Built with Boostrap and Material Icons. PWA ready.
- Mail – Simple email module.
- Users – User register, login, logout, forgot password, and admin functions.
- One Time Password – For added security.
- Resumable Upload – Resumable uploads with FlowJS and FlowPHP.
- Dynamic Content – Store and retrieve contents from the database.
- Likes & Dislikes Reaction – Or was it “upvote and downvote”?
- 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 properindex.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
// (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
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
<?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 capitalC
. - 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()
andclass Core
inlib/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
<?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/REQUEST
–MODULE
will be parsed into$_MOD
andREQUEST
into$_REQ
. - In this example, the content endpoint
http://site.com/api/content
will map toapi/API-content.php
. - The request
$_REQ
is to be manually handled in the script itself. As above, we use aswitch ($_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
<?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 athttp://site.com/list
.pages/PAGE-hello.php
will be accessible athttp://site.com/hello
.pages/PAGE-hello-world.php
will be accessible athttp://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
/* (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
<?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
// 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
// (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
$_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
// (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
$_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
$_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
// (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")
inlib/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)
Loads lib/LIB-$module.php
, and extends it to $_CORE->$module = new $module();
$module
– String, module to load.
$_CORE->load("Users");
Checks if the specified $module
is loaded.
$module
– String, module to check.
if ($_CORE->loaded("Users")) { ... }
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
orGET
. Defaults toPOST
.
$users = $_CORE->autoCall("Users", "getAll");
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
orGET
. Defaults toPOST
.
$_CORE->autoAPI("Users", "save");
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
orGET
. Defaults toPOST
.
$_CORE->autoAPI("Users", "save");
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);
Nothing to see here. This one is used to handle errors globally.
Creates an alphanumeric string.
$length
– Integer, number of bytes. Defaults to 8.
$password = $_CORE->random(10);
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
] */
HTTP redirect.
$page
– Page or path.$url
– Defaults toHOST_BASE
.
if (!isset($_SESSION["user"])) {
$_CORE->redirect("login/");
}
DATABASE CORE (LIB-DB.PHP)
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;
Used in conjunction with start()
.
$pass
– Boolean, commit or rollback?
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]
);
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"
);
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...]
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]
);
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]
);
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"]
);
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]
);
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]
);
{“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.
SQLite works a little differently. You will need to rebuild your own database module.
https://www.sqlitetutorial.net/sqlite-php/
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.
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.
Got it – Updated
good topic
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}
https://code-boxx.com/php-curl-cookies/
Save the session cookie on login. Send the cookie on subsequent calls.
This seems a very elegant, starting point. Thanks
Is there a forum or further help facility, for questions not covered here?
Sorry, I don’t have the time nor capacity to manage yet another free help forum… Just report any bugs or post requests here.
https://code-boxx.com/faq/#help