Simple php framework is a light weight (< 1mb), expressive, easy to configure, and less ambiguous framework. It comes packed with most things you need and can be easily extended to cater your needs through its simple syntax and vanilla codebase. This was built to tackle one problem at start, and that was a dynamic way to create RestApi using some ideas from GraphQL and just make api calls more fun and easy to setup.
In a traditional app, you will need to create several routes just to fetch data. With simple-php-framework, it creates a graph of all your routes so you can fetch related information from different tables, records etc with just one request and one endpoint.
Another advantage of using simple-php-framework is its views component. You do not need to use custom tags i.e blade syntax inside a php file anymore, just use the variable(s) assigned to the script and you are good to go, example shown below.
This package contains:
- Simple and fast routing mechanism for json, post and get request.
- Easy to use views loader for public / private files
- Simple but powerful Authentication mechanism i.e header control, os, jwt and more.
- Expressive database ORM
- Graph mapping / live documentation / schema of all Apis.
- Out of the box utilities for file upload, compression, complex to simple data manipulation functions, agonistic mail functionalities, mail templates and more
Simple php framework lets you build robust applications.
Every request gets routed to the index.php
page by default. So all routes, auth guards and more can be configured here.
- Edit the file in
apache_file.txt
to suit your needs. - Deploy your project
- The composer.json is just to tell the server that you are using php scripting language i.e heroku, digital ocean deploy
- Make sure to give proper permissions to the folder, uploads directory and editable folders, index.php with
read & write access
if necessary / under a firewall. - Navigate to your url index page so the function can run itself.
- This function creates the
.htaccess
with the right path toautoload.php
and deletes itself fromindex.php
afterwards by default. Reload your browser to see the effect.
Local deploy:
- Clone project using
xampp/lampp
to./htdocs
directory by opening a terminal in the directory, thengit clone https://github.com/johnsonfash/simple-php-framework.git
- To open up power shell on
htdocs
, navigate tohtdocs
,ctrl + shift + right click
on the folder. - On mac, navigate to
htdocs
, double finger touch on the folder to open the menu list, and clickopen terminal on folder
. - Create a database with the name
simple-db
, or change theconst db
fromenum/graph.php
to a database name you created. - Import the
sample-db.sql
file on the database, and follow the steps given above. Open upview/test.html
on your browser, i.ehttp://localhost/simple-php-framework
and theninspect
to see the logged result.
Sample of index.php
routes below. Goto enum/graph.php
to change runDirectory. On localhost: '/simple-php-framework/'
, on server: '/'
header::options(); //import for api request with OPTIONS i.e upload to send out a status "OK" to continue
router::get('/login', function () {
$data = ['a'];
return view::load('view/login.php', $data); // simply use the variable $data inside of login.php without issues.
});
router::json('/customer', function ($data, $user_id) {
if ($user_id) {
return typehandler::start($data, $user); //typehandler handles all graphed apis by using one route.
}
}, function () {
return header::auth(); //authenticates a user using i.e jwt and passes the details to main function i.e $user;
});
router::post('/upload', function ($id) {
if ($id) {
return typehandler::upload($id); //upload files handler
}
}, function () {
return header::auth();
});
router::get('/graph', function () {
return typehandler::view(); //api documentation. Very useful for frontend developers
});
You can simply use an apache / nginx config file instead of .htaccess
, but for those using shared hosting, this will be quite useful.
The default .htaccess file comes with the basic configuration to get started.
- Reroute of all request to index.php.
- Configurable access control you can tweak to manage all Api access
- Allowed headers, and exposed headers which can be useful for token and jwt on the frontend.
Lets start by defining the functionality of one
1. Header always set Access-Control-Expose-Headers
This lets you attach custom headers to api request, i.e the frontend can have access to read backend headers like token, jwt etc. This is incredibly useful to separate the returned api data from token, jwt, cookies and more which should sit at the head anyways, check information here for more.
You can defined all the constants you will use throughout the app here. One advantage of this is the autocomplete feature, if you use the right IDE i.e vs-code.
The model, controllers, and views folders lets you define:
- Model - database function calls that returns a result
- Controller -Â handles the frontend input, calls the right model function, manipulate data and sends the response back to the user.
- View - handles loading a script to view, attaching variables with data, so they can be used within the view script.
Samples are shown in each folder to get started.
The core is where the magic happens. auth, plugin, http request function, mail, jwt, os methods, utils, upload, router, graphql validation, assignment methods to right api and more are here. Codebase is straight forward and can be configured to suit your needs.
Database drivers are defined here.
By default, this comes with mysqli
ORM to get started, but you can easily configure it to support postgres
and more.
NOTE:Â for security, every query()
apart from raw();
uses prepared statement. While data are sanitised for all request by default, be sure not to include frontend user input in raw();
unless you are sure of what you are doing.
Samples below:
use database\db;
$db = db::connect();
$get_a_user = $db->query()->table('customers', ['email', 'first_name','created_at'])->where('id', 1)->first();
$insert_a_ser = $db->query()->table('customers')->insert([
'email' => $email, 'first_name' => 'john'
]);
$get_all_users = $db->query()->table('customers', array $columns)->getAll();
$update_a_user = $db->query()->table('customers')->where('id', $id)->update([
'first_name' => $first_name
]);
$limit_and_order = $db->query()->table('otp')->where('otp', $otp, 'type', 'customer_password', 'user_type', 'customer')->orderBy('id', 'desc')->limit(1)->first();
$limit_and_order_type_2 = $db->query()->table('otp')->where(['otp' => $otp, 'type' => 'customer_password', 'user_type' => 'customer'])->orderBy('id', 'desc')->limit(1)->first();
$delete = $db->query()->table('otp')->where('otp', $variables->otp)->delete();
$pagination = $db->query()->table('customers')->where('email LIKE', '@gmail')->limit($variables->size, $variables->page)->orderBy('id', 'DESC')->getAll();
$dangerous_raw_query_to_array_return = $db->raw("SELECT COUNT(CASE WHEN status = 'active' THEN 1 END) as 'active', COUNT(CASE WHEN status = 'blocked' THEN 1 END) as 'blocked', COUNT(CASE WHEN status = 'pending' THEN 1 END) as 'pending' FROM customers WHERE user_type = 'starters'");
The ./handler directory holds the controlhandler.php
& typehandler.php
.
Make sure to register all your controller classes in the ./handler/controlhandler.php
Also register all your graphql/types script in the ./handler/typehandler.php
Samples have been prepared to give you a good understanding how this works.
The graphql types folder is where you define your api schema, make minute input validations and return schema. A sample is show below.
By default, the variable name of every type constant must match the name of a controller function to execute. Which means every type must go along with a class function that execute them, examples are given in the controllers
and graphql/types
folders respectively.
NOTE: Make sure the variable name of type constants are unique, else, an error.
You can also setup constraints on the database to make this nested queries even better.
<?php
namespace graphql\types;
use enum\graph;
interface testType
{
const getUser = [
graph::input => [
"id" => graph::integerNotNull, //a required input field to execute this function
],
graph::return => [
"id" => graph::integer,
"email" => graph::string,
"username" => graph::string,
"first_name" => graph::string,
"last_name" => graph::string,
"age" => graph::integer,
"created_at" => graph::string,
"address" => [ //nexted query within this type,
graph::type => "getAddress",
graph::input => [
"user_id" => "parent.id" //parent means the main thread value i.e ['id'=> 1, 'email' => '[email protected]' ...]. you are mapping this id from parent to input parameter of the next query type. The nexted query must have an input field called 'user_id'
]
]
]
];
const getAddress = [
graph::input => [
"id" => graph::integerNotNull,
"user_id" => graph::integer // meaning you cant make direct api call to this without defining the id input, but user_id is optional. Only backend chain call can omit this feature, i.e getUser attach getAddress without calling id
],
graph::return => [
"id" => graph::integer,
"user_id" => graph::integer,
"country" => graph::string,
"state" => graph::string,
"lga" => graph::string,
"address" => graph::string,
"created_at" => graph::string,
"geodata" => [
graph::type => "getGeoData",
graph::input => [
"address_id" => "parent.id"
]
]
]
];
const getGeoData = [
graph::input => [
"id" => graph::integerNotNull,
"address_id" => graph::integer
],
graph::return => [
"id" => graph::integer,
"address_id" => graph::integer,
"user_id" => graph::integer,
"lat" => graph::string,
"lng" => graph::string,
"created_at" => graph::string,
"bio" => [
graph::type => "getBio",
graph::input => [
"user_id" => "parent.user_id"
]
]
]
];
const getBio = [
graph::input => [
"id" => graph::integerNotNull,
"user_id" => graph::integer
],
graph::return => [
"id" => graph::integer,
"user_id" => graph::integer,
"user_type" => graph::string,
"bio" => graph::string,
"likes" => graph::string,
"created_at" => graph::string,
"timeline" => [
graph::type => "getTimeline",
graph::input => [
"user_id" => "parent.user_id"
]
]
]
];
const getTimeline = .......
By default, every controller has a $parent
value (if called as a nested query or empty array: []
, if its the main query. $columns
:(required columns defined from the frontend), $variables
which can be []
or defined by the frontend if needed for the request. $middleware_data
can be meta data like user_id
from authenticated route or null
for public routes.
By default every controller must return an array
which must include a 'data'
associative key. It is recommended you use the build_res
utility function offered in the test.php sample to build your response object, and also graph::data
, graph::error
, graph::errorMessage
for autocomplete features and avoiding mistakes.
Traits are used instead of classes for good implementation reasons. Make use of trait in controllers.
<?php
namespace controllers;
use core\utils;
use enum\graph;
use model\user as ModelUser;
trait user
{
public static function getUser($parent, $columns, $variables, $middleware_data)
{
$res = utils::build_res();
$input = utils::validate($variables);
if ($input[graph::error]) {
return $res->get_res($input);
}
$model = new ModelTest();
//just a sample, might not be need here, or overkill
$old_user = $model->checkEmail(@$variables->email);
$user = $model->getUser($variables->id, $columns);
return $res->get_res([graph::data => $user]);
}
......
}
Making api request is simple and straight forward, simply goto the PUBLIC_URL/graph
to see the api type
, input
and return
values to make your api call.
The /graph
shows all the available endpoints
to the graph, the input
parameters and return
values. Sample below:
{
"description": {
"maximum": "you can set the maximum nested query in the enum 'maxNextedQuery'",
"requirement_1": "nexted queries should include type & optional input",
"note_1": "please make sure to include & handle all type of input for both main queries and nexted queries",
"note_2": "you can choose to include a description & name key on your API service type if you want",
"warning_1": "auth fields like password and token must be managed internally and not exposed to the GRAPH VIEW return keys",
"type": "used to specify the controller for a nexted query",
"input": "used map input from the main query to the input of the nexted query e.g [next_query.input => main_query.input]"
},
"types": {
"getAddress": {
"input": {
"id": "integer",
"user_id": "integer"
},
"return": {
"id": "integer",
"user_id": "integer",
"country": "string",
"state": "string",
"lga": "string",
"address": "string",
"created_at": "string",
"geodata": {
"id": "integer",
"address_id": "integer",
"user_id": "integer",
"lat": "string",
"lng": "string",
"created_at": "string",
"bio": {
"id": "integer",
"user_id": "integer",
"user_type": "string",
"bio": "string",
"likes": "string",
"created_at": "string",
"timeline": {.... }
}
}
}
}
}
}
},
"getBio": {
"input": {
"id": "integer!",
"user_id": "integer"
},
.......
}
}
Check the sample below on making a rest api request, or goto view/test.html
to see a sample
const variables = { id: 1 };
const query = {
type: "getUser",
return: {
id: "integer", //you can use short(1 letter) or long key type. i.e "string" === "s", "boolean" === "b"
email: "string",
username: "string",
first_name: "s",
last_name: "s",
age: "i",
created_at: "s",
address: {
id: "i",
country: "s",
state: "s",
lga: "s",
address: "s",
geodata: {
id: "i",
lat: "s",
lng: "s",
bio: {
id: "i",
bio: "s",
likes: "s",
timeline: {
id: "i",
header: "s",
detail: "s",
},
},
},
},
},
};
fetch("http://localhost/simple-php-framework/test", {
method: "post",
headers: {
"content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
})
.then((d) => {
return d.json();
})
.then((v) => console.log(v))
.catch((e) => console.log(e.message));