Page MenuHomeGitPull.it

Welcome in suckless-php
Updated 1,628 Days AgoPublic

Version 2 of 21: You are viewing an older version of this document, as it appeared on Nov 14 2019, 01:37.

suckless-php: Another PHP framework that sucks less

You discovered suckless-php! My laser cannon that I used to develop dozen of very-different Content Management Systems made from scratch in my life.

Benefits

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!

Requirements

You should have basic knowledges about a webserver with PHP and MySQL (or MariaDB as well).

Run your favorite webserver. I use a simple GLAMP machine (GNU/Linux + Apache or nginx + PHP + MySQL or MariaDB).

If you plan to allow users to upload files, install the libmagic-dev package.

Framework installation

If you can be root drop the framework in a shared read-only directory like /usr/share/php/suckless-php:

sudo git clone https://gitpull.it/source/suckless-php.git /usr/share/php/suckless-php

That's it.

To be honest, you can put the framework wherever you want but it's good to have it in just one place for multiple projects.

Quick start

Let's assume that:

  • /var/www is the DocumentRoot for your virtual host
  • /var/www/project will contain our example website project
  • http://localhost/project is what you want to visit to see this project
  • /usr/share/php/suckless-php is this framework

Create 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 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)

OK. Now its the time to create your first file 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.

The load workflow

  1. The user requests http://localhost/project

2.0 /var/www/project/index.php is executed (where you require 'load.php')
2.1 /var/www/project/load.php is executed (where you require the framework)
2.2. /usr/share/php/suckless-php/load.php is excecuted (where it requires your load-post.php)
2.3. /var/www/project/load-post.php is executed and ends

  1. The index.php continue its execution as normal

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.

That's all!

Now it's the time to use the framework. Remember that to use it, your page (like your index.php) should start with something like:

<?php
// autoload everything
require 'load.php';

// code here

Example of JSON API

An example of a JSON API page could be this:

<?php
// autoload everything
require 'load.php';

// do some pre-conditions
if( !isset( $_GET['ping'] ) ) {

	// die with an HTTP status code 400 (Bad Request) and show a message
	json_error( 400, 'missing-ping', "Please specify the 'ping' GET argument" );
}

json( [
	'success' => true,
	'pong'    => $_GET['ping'],
] );

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.

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();
}

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>

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 14 2019, 01:37

Event Timeline

valerio.bozzolan created this object with edit policy "All Users".
valerio.bozzolan edited the content of this document. (Show Details)
valerio.bozzolan edited the content of this document. (Show Details)