# 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](https://en.wikipedia.org/wiki/Cross-site_request_forgery)-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
3. 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
<?php
// autoload everything
require 'load.php';
// code here
```
## Example of JSON API
An example of a JSON API page could be this:
```php
<?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)
```php
// 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;
}
```
```php
// 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:
```php
// 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:
```php
( new Query() )
->from( 'post' )
->insertRow( [
'post_ID' => 1,
'post_title' => 'hello',
] );
```
Declarative way:
```php
insert_row( 'post', [
'post_ID' => 1,
'post_title' => 'hello',
] );
```
## Delete
```php
( 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):
```php
query( "ALTER TABLE ADD COLUMN `hello` ..." );
```
### Select rows (declarative, full list)
```php
// 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)
```php
// 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)
```php
/**
* 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](https://en.wikipedia.org/wiki/Cross-site_request_forgery) you can use `form_action()` and `is_action()`:
```php
<?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
<?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](http://boz.reyboz.it/)
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+**.