- suckless-php: Another KISS framework that sucks less
- Why using suckless-php
- When not using suckless-php
- Requirements
- Framework installation
- Quick start project
- Understand what happens
- Configure database
- All available configurations for your load.php file
- Troubleshooting
- Example of JSON API
- Execute a database query
- Insert
- Delete
- Database row → object (mapping)
- File upload
- CSRF protection
- Example of login page
- Unit tests
- License
Welcome in suckless-php
suckless-php: Another KISS framework that sucks less
You discovered suckless-php! My amazing keep-it-simple-and-stupid laser cannon that I used to develop dozen of very-different Content Management Systems made from scratch in my life.
Why using suckless-php
With this framework I was able to create a lot of relational content management systems with these features:
- stateless (it means it's very easy to scale)
- efficient (very tiny footprint both in disk and memory)
- CSRF-safe
- on demand resource loader (e.g. DB connection is instantiated only if you use it etc.)
- object-oriented query builder (supporting also very complex queries)
- micro-ORM implementation
- associative options WordPress-style
- database table prefix (you can run multiple instances in a single database)
- login, user roles, user permissions, role inheritance
- secure file uploads (MIME type checks etc.)
- multi-language (using the widely used GNU Gettext native or high-level)
Everything is made in just ~15 short PHP files. It's damn small for all these features!
When not using suckless-php
- do not use suckless-php if you hate programming
- do not use suckless-php if you expect something complete with all the bells and whistles that you can give to a _luser_ to manage a website in every it's damn component without a programmer (in that case you need a shitty, scary and giant complete content management system, you need to host something like 12MB of source code, often that you must fill with other crapware plugins, etc. In this case the suckless-php framework is not your solution. This framework is incompatible with people who love waste of resources, computation and global warming).
Requirements
You should have basic knowledges about a webserver with PHP and MySQL (or MariaDB obviously).
Run your favorite webserver. I use a simple (G)LAMP machine:
- GNU/Linux
- Apache or nginx
- PHP
- MySQL or MariaDB
This can't be a guide for how to install a webserver. Anyway if it's your first time, try with a clean Debian GNU/Linux installation, and then:
sudo apt install apache2 mysql-server libapache2-mod-php php-mysql
If you plan to allow users to upload files, install the libmagic-dev package.
That's all.
Framework installation
The framework can be saved inside a read-only directory of your choice.
I usually want it in the /usr/share/php/suckless-php path, in order to keep it in one place for every project:
sudo git clone https://gitpull.it/source/suckless-php.git /usr/share/php/suckless-php
But instead you may want the framework inside your project or whenever you want. It will also work.
Quick start project
We have created a template project (boilerplate) for you. You can just try it and then come back here for more instructions!
Boilerplate:
https://gitpull.it/source/suckless-php-boilerplate/
Quick start documentation
You can configure everything, but let's assume that:
- /var/www is your DocumentRoot of your webserver's virtual host
- /var/www/project will contain your example website project (HTML, CSS, JavaScript, PHP files)
- http://localhost/project is what you want to visit to see that project
- /usr/share/php/suckless-php/load.php should exist
Spoiler! In the next steps you will create:
- /var/www/project/load.php - your database configuration file
- /var/www/project/load-post.php - your website configuration file
- /var/www/project/index.php - your homepage
Let's start creating a MySQL database and a MySQL user with a password.
Go to your project (/var/www/project) and create a configuration file. Call it load.php and save this:
<?php // database credentials $username = 'Your database username'; $password = 'The password of your database user'; $database = 'Your database name'; $charset = 'utf8mb4'; $location = 'localhost'; // table prefix, if any $prefix = ''; // absolute pathname to this directory without trailing slash define( 'ABSPATH', __DIR__ ); // relative URL to this directory (or an empty string) without trailing slash define( 'ROOT', '/project' ); // load the framework require '/usr/share/php/suckless-php/load.php';
Now create also a load-post.php file with something like:
<?php // this file is automagically called after something called your 'load.php' // in this file you can use every suckless-php function to describe your project // register some custom privileges register_permissions('subscribed', [ 'add-comment', 'page-vote', ] ); // the superuser inherit from the subscribed and has also other permissions inherit_permissions('superadmin', 'subscribed', [ 'do-wonderful-things', ] ); // register some useful JavaScript/CSS files to be used later register_js( 'jquery', 'media/jquery-min.js'); register_css( 'my-style', 'media/style.css'); // register some menu entries // etc. (we will see features next)
Now create also your homepage in index.php:
<?php // autoload everything require 'load.php'; // test a query $row = query_row('SELECT NOW() AS now'); // print something echo "If the database credentials works, the time is: {$row->now}";
Now just visit http://localhost/project/ to check if everything works.
Understand what happens
Just as a note. With the above example, when you visit http://localhost/project/ this happens:
- The user requests http://localhost/project
- /var/www/project/index.php is executed and then it requires your 'load.php' (see the comment // autoload everything above)
- /var/www/project/load.php is executed and loads the framework (see the comment / load the framework above)
- /usr/share/php/suckless-php/load.php is executed and quits, requiring your load-post.php
- /var/www/project/load-post.php is executed and ends
- /var/www/project/load.php is executed and loads the framework (see the comment / load the framework above)
- /var/www/project/index.php continue it's execution as normal
- You see If the database credentials works, the time is: ...
That's all.
As you note, you decide whenever you want to use the framework or not. Just put a require 'load.php'; as the first line of your PHP page to use the framework stuff.
Configure database
If you want these features:
- login/logout
- user roles and permissions
- associative options
Just import the file [example-schema.sql](https://gitpull.it/source/suckless-php/browse/master/examples/extras/example-schema.sql) provided in the examples folder of this framework. Adjust the table prefixes for your needs.
All available configurations for your load.php file
Mandatory configurations:
- $username (string) The database username.
- $password (string) The password of the database username.
- $database (string) The database name.
- $charset (string) The database charset.
- $location (string) The database host location.
- $prefix (string) The database table prefix.
- ABSPATH (string) The absolute pathname of your site (usually = __DIR__). No trailing slash.
- ROOT (string) The absolute request pathname (something as /blog if you visit http://localhost/blog for your homepage, or simply an empty string). No trailing slash.
Extra constants you can define:
- DEBUG (bool, default false): enable verbose debugging.
- DEBUG_QUERIES (bool, default false): the queries are logged, and also printed when in DEBUG mode.
- PROTOCOL (string, default to http:// or https://): builds the URL constant.
- DOMAIN (string, default to your domain name): builds the URL constant.
- URL (string, default is PROTOCOL . DOMAIN . ROOT) the absolute URL to your public home directory
Some advanced constants:
- REQUIRE_LOAD_POST (mixed, default to ABSPATH . '/load-post.php'): the pathname to your load-post.php file. If false it's never loaded.
Troubleshooting
You should know how to inspect your webserver logs. Usually just:
sudo tail -f /var/log/apache2/error.log
In the next examples you may also want to debug your queries. Just put these inside your /var/www/project/load.php:
define( 'DEBUG_QUERIES', true ); define( 'DEBUG', true );
This is useful only during development to show on video every database query before its execution. Remember to force to false in production.
Example of JSON API
Creating a JSON API page is very simple. This is an example that can be saved as /var/www/project/api.php
<?php // autoload everything require 'load.php'; // do some pre-conditions if( empty( $_GET['ping'] ) ) { // die with an HTTP status code 400 Bad Request and show a message json_error( 400, 'missing-ping', "Please specify the 'ping' argument in your query string" ); } json( [ 'success' => true, 'pong' => $_GET['ping'], ] );
Now try to visit:
- http://localhost/project/api.php
- http://localhost/project/api.php?ping=foo
Note that to optimize the data transfer, false and null values and empty objects will be automagically removed from the JSON. It's a feature. Handle these cases from JavaScript.
Note that in this example you do not use the database so no database connection was established.
Execute a database query
Note that the database connection is established automagically only if you need it. Now some examples.
Execute a query using the Query builder (object-oriented)
// get a pure array of rows from an example 'post' database table $posts = ( new Query() ) ->from( 'post' ) ->queryResults(); // do something with your data foreach( $posts as $post ) { echo $post->id; }
// get a Generator of rows from an example 'post' database table (see PHP generators) $generator = ( new Query() ) ->from( 'post' ) ->queryGenerator(); // do something with your data foreach( $posts as $post ) { echo $post->id; }
Another more complex example:
// get a pure array of rows from an example 'post' database table // where the 'post_author_id' = $author_id (int comparison) // where the 'post_status' can be 'private' or 'stub' or 'deleted' // where the 'post_date' is before the current MySQL date // LEFT join to the user ON (user.user_ID = post.post_author_id) $posts = ( new Query() ) ->from( 'post' ) ->whereStr( 'post_title', $title ) ->whereInt( 'post_author_id', $author_id ) ->whereSomethingIn( 'post_status', [ 'private', 'stub', 'deleted' ] ) ->where( 'post_date < NOW()' ) ->joinOn( 'LEFT', 'user', 'user.user_ID', 'post.post_author_id' ) ->queryResults(); // do something with your data foreach( $posts as $post ) { echo $post->id; }
Note that a method that gives a generator is more efficient over a method that returns a complete array of results, if:
- You do not need the full list (you need just one at time)
- You do not need to read the list multiple times.
Insert
Object-oriented way:
( new Query() ) ->from( 'post' ) ->insertRow( [ 'post_ID' => 1, 'post_title' => 'hello', ] );
Declarative way:
insert_row( 'post', [ 'post_ID' => 1, 'post_title' => 'hello', ] );
Delete
( new Query() ) ->from( 'post' ) ->whereStr( 'post_author', 'jhon' ) ->whereInt( 'post_stub, 1 ) ->delete();
Query (declarative way)
This is useful to run strange queries that does not return stuff (remember to sanitize your data):
query( "ALTER TABLE ADD COLUMN `hello` ..." );
Select rows (declarative, full list)
// results as a pure array $posts = query_results( "SELECT post_title FROM wp_post" ); // as example, let's expose them in JSON json( $posts );
Note that query_results() is better over query_generator() if:
- You need the full list (e.g. to count them, etc.)
- You want to read the list multiple times
Select rows (declarative, generator)
// generator of results $posts = query_generator( "SELECT post_title FROM wp_post" ); if( $posts->valid() ) { foreach( $posts as $post ) { echo $post->post_title; } }
Note that query_generator() is better over query_results() if:
- You do not need the full list (but just one at time)
- You do not need to read the list multiple times.
Database row → object (mapping)
/** * Declaration of my custom class */ class Post extends Queried { /** * Table name without table prefix */ const T = 'post'; /** * An example method * * @return string */ public function getTitle() { return $this->post_title; } } // now you have a query builder $post = Post::factory() ->whereInt( 'post_ID', 1 ) ->queryRow(); // do stuff if( $post ) { // this method exists $title = $post->getTitle(); }
File upload
File uploading is one of the most painful stuff ever full of security nightmares. We tried to keep it simple and secure.
This is just an example:
<?php $uploader = new FileUploader( 'my-filename', [ 'category' => [ 'image', 'video' ], 'max-filesize' =>10000000, // 10~ megabytes ] ); // check if the user uploaded the file if( $uploader->fileChoosed() ) { // save the file somewhere $ok = $uploader->uploadTo( '/var/www/directory/name' ); // print an error message if( !$ok ) { die( $uploader->getErrorMessage() ); } } ?> <form method="post" enctype="multipart/form-data"> <input type="file" name="my-filename" /> <button type="submit">Submit</button> </form>
Some available file categories:
- image
- audio
- video
- document
Check the class class-MimeTypes.php for further details about the default categories.
You can use register_mimetypes( $category, $mimetypes ) inside your load-post.php to register additional mime types to existing categories or to add additional categories.
CSRF protection
To both identify a form and secure it against Cross-site request forgery you can use form_action() and is_action():
<?php ... if( is_action( 'save-user' ) ) { echo "submitted"; } ?> <form method="post"> <?php form_action( 'save-user' ) ?> <button type="submit">Save</button> </form>
Example of login page
<?php ... if( is_action( 'try-login' ) ) { login(); } if( is_logged() ) { echo "Welcome!"; } ?> <form method="post"> <?php form_action( 'try-login' ) ?> <input type="text" name="user_uid" /> <input type="password" name="user_password" /> <button type="submit">Login</button> </form>
Unit tests
phpunit --bootstrap=phpunit/load.php --testdox phpunit
License
Copyright (c) 2015-2019 Valerio Bozzolan
This is a Free as in Freedom project. It comes with ABSOLUTELY NO WARRANTY. You are welcome to redistribute it under the terms of the GNU General Public License v3+.
- Last Author
- valerio.bozzolan
- Last Edited
- Nov 7 2020, 01:25