A PHP micro-framework

1.2.2 2024-08-29 22:31 UTC

This package is auto-updated.

Last update: 2025-06-29 02:00:01 UTC


README

Micro-Framework

Requirements: PHP 5.4+, 7+, 8+

Installation

If you are using Composer, run the command

composer require illogical/lava

Or you can download the archive

Usage

Use the Lava\App class

use Lava\App;

If you downloaded the archive, use the autoloader

require_once '.../Lava/Autoloader.php';

$al = new Lava\Autoloader;
$al->register();

Environment

Lava\App::conf([data]) : accessor

Config

App::conf([
    'charset' => 'utf-8',             // encoding for HTTP headers
    'type'    => 'html',              // default type
    'home'    => '/path-to-home',     // home folder
    'pub'     => '/pub-uri',          // public folder
    'safe'    => [
        // default values
        'algo' => 'md5',              // hashing algorithm
        'sign' => '',                 // signature
        'salt' => '0123456789abcdef', // salt character set
    ],
    'storage' => [
        // default storage, named "0"
        [
            'driver'   => 'PDO',
            'dsn'      => 'mysql:.../mysqld.sock;dbname=test;',
            'username' => 'root',
            'password' =>  NULL,
        ],
        'db2' => [...],
        ...
    ],
]);

echo App::conf()->charset; # utf-8

Lava\App::env() : accessor

Environment

echo       App::env()->method;    # GET
var_export(App::env()->accept()); # array (0 => 'text/html', 1 => '*/*')

Lava\App::args() : accessor

Variables

Value priority: custom, POST, GET

// URL: http://example.com/sandbox/?foo=3&bar=4&foo=5

echo       App::args()->foo;    # 5
var_export(App::args()->foo()); # array (0 => '3', 1 => '5')

Lava\App::cookie() : accessor

Cookies

Offsets for expire:

  • s - second
  • m - minute
  • h - hour
  • D - day
  • W - week
  • M - month
  • Y - year
// setting
App::cookie()->foo = 'bar';
App::cookie()->bar = [1, 2, 3];

// read
echo       App::cookie()->foo;    # bar
var_export(App::cookie()->bar()); # array (0 => '1', 1 => '2', 2 => '3')

// additional parameters: expire, path, domain, secure
App::cookie()->foo('bar', '1M');  // expire = 1 month

Lava\App::host([scheme [, subdomain]]) : host

Returns the name of the host

If scheme is TRUE, then the current scheme

echo App::host();      # host
echo App::host(TRUE);  # http://host
echo App::host('ftp'); # ftp://host

Lava\App::home([node, ...]) : home

Returns the home folder

If not set in the config, then the current folder where the script is running

echo App::home();             # /path-to-home
echo App::home('foo', 'bar'); # /path-to-home/foo/bar

Lava\App::pub([node, ...]) : pub

Returns the public folder

If not set in the config, then the current folder

echo App::pub();             # /pub-uri
echo App::pub('foo', 'bar'); # /pub-uri/foo/bar

Lava\App::uri([path|route [, data [, append]]]) : uri

Returns URI

Variables from data will be appended as query_string

The append flag adds the current query_string

// URL: http://example.com/sandbox/?zzz=456

echo App::uri();                        # /sandbox/
echo App::uri('foo', ['bar' => 123]);   # /sandbox/foo?bar=123
echo App::uri('/foo', 'bar=123', TRUE); # /foo?zzz=456&bar=123

Lava\App::url([path|route [, data [, append]]]) : url

Returns URL

// URL: http://example.com/sandbox/?zzz=456

echo App::url();                        # http://example.com/sandbox/
echo App::url('foo', ['bar' => 123]);   # http://example.com/sandbox/foo?bar=123
echo App::url('/foo', 'bar=123', TRUE); # http://example.com/foo?zzz=456&bar=123

Routes

Lava\App::route([rule [, cond]]) : route

Placeholder :name corresponds to the full fragment ([^\/]+)

Placeholder #name matches the name ([^\/]+?)(?:\.\w*)?

Placeholder *name matches the remainder (.+)

You can add a restriction on environment variables in the additional cond conditions

If the rule does not start with a slash, it will be appended to the public folder Lava\App::pub()

// URL: http://example.com/foo1.bar/foo2.bar/foo3.bar/foo4.bar
App ::route('/:node1/#node2/*node3')
    ->to   (function($node1, $node2, $node3) { // handler
        echo $node1;                           #  foo1.bar
        echo $node2;                           #  foo2
        echo $node3;                           #  foo3.bar/foo4.bar
    });
// route search
App::routes_match();

// environment constraint
App::route('/foo', [
    'method'     => ['GET', 'HEAD'], // if the method is GET or HEAD
    'user_addr'  => '127.0.0.1',     // and the user is local
    'user_agent' => '/^Mozilla/',    // and the browser is Mozilla
]);

// method restriction only
App::route('/foo', 'DELETE');

Lava\App::route_get([rule]) : route

Restrict the route to the GET method

App::route_get ('/foo');
// analog
App::route     ('/foo', 'GET');

Lava\App::route_post([rule]) : route

App::route_post('/foo');

Lava\App::routes_match() : result

Executes handlers for matched routes

If the handler returns TRUE, it continues checking the rest of the routes, otherwise it stops checking and returns the result of the handler

App::routes_match();

route->cond(cond) : route

Add an environment constraint to the route

App ::route('/foo')
    ->cond (['user_addr' => '/^192\.168\./']);

route->name(name) : route

Used to convert a route to a path

// URL: http://example.com/foo/123
App ::route('/foo/#id')
    ->name ('route_name')
    ->to   (function($id) {
        echo App::uri('route_name', ['id' => $id + 1]); #  /foo/124
    });

route->to(mixed) : route

Route handler

Default method Lava\App::env()->method

// function
App::route('/foo')->to(function() {echo 'hello';});

// class|namespace, method
App::route('/foo')->to('Controller\Foo', 'bar');

// file, method
// the class name must match the file name
// an instance of the Foo class will be created and the bar method will be called
App::route('/foo')->to('controller/Foo.php', 'bar');

// file, class|namespace, method
App::route('/foo')->to('controller/Foo.php', 'Ctrl\Foo', 'bar');
// if the class is different from the file name or if we need to specify a namespace

Rendering

Lava\App::render(handlers) : has_handler

Executes a handler with type Lava\App::type(), if none exists, then with index 0

If there is no type of the requested data, Lava\App::conf()->type is used

If the type is json and there is a Lava\App::args()->callback value, returns JSONP

App::route('/page')->to(function() {
    App::render([
        'html' => 'HTML CONTENT',
        'json' => ['bar' => 123],
        function () {
            echo 'OTHER TYPE: ' . App::type();
        },
    ]);
});

// URL: http://example.com/page.html
App::routes_match(); # HTML CONTENT

// URL: http://example.com/page.json
App::routes_match(); # {"bar":123}

// URL: http://example.com/page.xml
App::routes_match(); # OTHER TYPE: xml

// if Lava\App::conf()->type == 'html'
// URL: http://example.com/page
App::routes_match(); # HTML CONTENT

Lava\App::redirect([url|uri|route [, data [, append]]]) : void

Appends to the Location header

App::redirect('/foo');

Security

Lava\App::safe()->uuid() : uuid

Returns UUID

You can specify the hashing algorithm in the config, the default is md5

echo App::safe()->uuid(); # 055fb982653fef1ae76bde78b10f7221

Lava\App::safe()->uuid_signed() : [signed_uuid, uuid]

Returns the signed UUID

You can specify the signature in the config, defaults to an empty string

list($signed, $uuid) = App::safe()->uuid_signed();

echo $signed; # 31bd185d9b3929eb56ae6e4712b73962dcd6b2b55b5287117b9d65380f4146e3
echo $uuid;   # 31bd185d9b3929eb56ae6e4712b73962

Lava\App::safe()->check(signed_uuid) : uuid

Checks the signed UUID

echo App::safe()->check($signed); # 31bd185d9b3929eb56ae6e4712b73962

Lava\App::safe()->salt(size) : random_string

Returns a random string of the given length

You can change the list of available characters in the config, default is 0123456789abcdef

echo App::safe()->salt(16); # f8da4f571ec3de9d

Validation

Lava\App::is_valid(val, tests) : bool_result

Tests:

  • tinyint[:unsigned]

  • smallint[:unsigned]

  • mediumint[:unsigned]

  • integer[:unsigned]

  • bigint[:unsigned]

  • numeric[:precision[:scale]]

  • boolean

  • string[:min_size[:max_size]]

  • char[:size]

  • email

  • url

  • ipv4

  • date

  • time

  • datetime

  • lt[:num]

  • lte[:num]

  • gt[:num]

  • gte[:num]

  • bool

  • array

  • regexp

  • function

// the string is between 1 and 20 characters and matches Email
echo App::is_valid('me@example.com', ['string:1:20', 'email']); # TRUE

Storage\PDO

Lava\Storage::source('PDO', opts) : storage

Creation

$storage = Lava\Storage::source('PDO', [
    'dsn'      => 'mysql:unix_socket=...mysqld.sock;dbname=name',
    'username' => 'root',
    'password' => '',
]);

storage->exec(query[, bind]) : row_count

Runs the SQL query and returns the number of rows involved in its execution

$storage->exec('DELETE FROM users');

$storage->exec(
    'INSERT INTO users (login, email) VALUES (?, ?)',
    ['username', 'abc@mail']
);

$storage->exec(
    'INSERT INTO users (login, email) VALUES (:login, :email)',
    ['login' => 'username', 'email' => 'abc@mail']
);

storage->fetch(query[, bind]) : row

Fetching a row from the result set

$user = $storage->fetch('SELECT * FROM users WHERE id = ?', 123);

storage->fetch_all(query[, bind[, index]]) : rows

Fetching all rows from the result set

index is used to specify the name of the field whose value will become the index

$users = $storage->fetch_all('SELECT * FROM users');

storage->last_insert_id() : id

ID of the last inserted row

$id = $storage->last_insert_id();

storage->error() : error_info

Driver-defined error message

$error = $storage->error();

storage->factory([target]) : factory

Query factory

factory->get([index]) : rows

Data selection

// index data with id value
$data = $storage->factory('users')->get('id');
# query: SELECT * FROM `users`

factory->one() : row

Selecting one record

$data = $storage->factory('users')->one();
# query: SELECT * FROM `users` LIMIT ?
# bind:  1

factory->columns(expression) : factory

Columns or calculations

$data = $storage
    ->factory('users')
    // columns(expression)
    ->columns('id')
    // columns([alias => expression, ...])
    ->columns(['full_name' => 'CONCAT(first_name, " ", last_name)'])
    ->get();
# query: SELECT `id`, CONCAT(first_name, " ", last_name) AS `full_name` FROM `users`

factory->*join(target, relations[, bind]) : factory

Table joins

$data = $storage
    ->factory('users')
    ->join('profiles', 'id')
    ->left_join('sessions', 'sessions.user_id = users.id')
    ->right_join('roles', ['roles.user_id' => 'users.id', 'roles.id' => '?'], 123)
    ->get();
# query: SELECT * FROM `users`
#        JOIN `profiles` USING (`id`)
#        LEFT JOIN `sessions` ON sessions.user_id = users.id
#        RIGHT JOIN `roles` ON `roles`.`user_id` = `users`.`id` AND `roles`.`id` = ?
# bind:  123

factory->filter*(expression) : factory

Data filtering: filter_eq, filter_ne, filter_lt, filter_gt, filter_lte, filter_gte, filter_like, filter_not_like, filter_in, filter_not_in, filter_between, filter_is_null, filter_is_not_null, filter_raw

Context change: filter_and, filter_or, filter_not

filter works depending on context, for data as filter_eq, for closures as filter_and

$data = $storage
    ->factory('users')
    // filter(key, val)
    ->filter_eq('id', 123)
    // filter(expression)
    ->filter_ne(['name' => 'guest', 'email' => 'test'])
    ->get();
# query: SELECT * FROM `users` WHERE (`id` = ? AND (`name` != ? AND `email` != ?))
# bind:  123, 'guest', 'test'

$data = $storage
    ->factory('users')
    ->filter_or(['name' => 'guest', 'email' => 'test'])
    ->get();
# query: SELECT * FROM `users` WHERE (`name` = ? OR `email` = ?)
# bind:  'guest', 'test'

$data = $storage
    ->factory('users')
    ->filter_or(function($filter) {
        // in a closure without the filter_ prefix
        $filter ->like('name', '%test%')
                ->and(function($filter) {
                    $filter->gt('id', 10)->lte('id', 20);
                })
                ->is_not_null('email');
    })
    ->get();
# query: SELECT * FROM `users` WHERE (`name` LIKE ? OR (`id` > ? AND `id` <= ?) OR `email` IS NOT ?
# bind:  '%test%', 10, 20, NULL

$data = $storage
    ->factory('users')
    ->filter_not(function($filter) {
        $filter ->or(['name' => 'guest', 'email' => 'test'])
                ->in('role_id', [2, 4, 6]);
    })
    ->get();
# query: SELECT * FROM `users` WHERE NOT ((`name` = ? OR `email` = ?) AND `role_id` IN (?, ?, ?))
# bind:  'guest', 'test', 2, 4, 6

// subqueries

$data = $storage
    ->factory('users')
    ->filter_in('id', $storage->factory('sessions')->columns('user_id')->filter('active', 1))
    ->get();
# query: SELECT * FROM `users` WHERE `id` IN (SELECT `user_id` FROM `sessions` WHERE `active` = ?)
# bind:  1

factory->group_by(expression) : factory

Grouping

$data = $storage
    ->factory('users')
    ->columns(['role_id', 'count' => 'COUNT(*)'])
    ->group_by('role_id')
    ->get();
# query: SELECT `role_id`, COUNT(*) AS `count` FROM `users` GROUP BY `role_id`

factory->having*(expression) : factory

Similar to filter

Filtering data: having_eq, having_ne, having_lt, having_gt, having_lte, having_gte, having_like, having_not_like, having_in, having_not_in, having_between, having_is_null, having_is_not_null, having_raw

Context change: having_and, having_or, having_not

having works depending on context, for data as having_eq, for closures as having_and

$data = $storage
    ->factory('users')
    ->columns(['role_id', 'count' => 'COUNT(*)'])
    ->group_by('role_id')
    ->having_or(function($having) {
        $having->gt('count', 2)->lte('count', 5);
    })
    ->get();
# query: SELECT `role_id`, COUNT(*) AS `count` FROM `users` GROUP BY `role_id` HAVING (`count` > ? OR `count` <= ?)
# bind:  2, 5

factory->order_by*(expression) : factory

Sorting

$data = $storage
    ->factory('users')
    ->order_by('name')
    ->order_by_desc('email')
    ->get();
# query: SELECT * FROM `users` ORDER BY `name` ASC, `email` DESC

factory->limit(count[, offset]) : factory

Limits the number of records returned

$data = $storage
    ->factory('users')
    ->limit(10)
    ->get();
# query: SELECT * FROM `users` LIMIT ?
# bind:  10

factory->add(data[, update]) : row_count

Inserting data

$users   = $storage->factory('users');

$users->add([
    'login' => 'username',
    'email' => 'abc@mail',
]);
# query: INSERT INTO `users` SET `login` = ?, `email` = ?
# bind:  'username', 'abc@mail'

$archive = $storage->factory('archive_users');

$users->add($archive);
# query: INSERT INTO `users` SELECT * FROM `archive_users`

$users->columns(['login', 'email'])->add(
    $archive->columns(['username', 'email'])->filter_lt('id', 123)
);
# query: INSERT INTO `users` (`login`, `email`)
#        SELECT `username`, `email` FROM `archive_users` WHERE `id` < ?
# bind:  123

$storage
    ->factory('log')
    ->add(['name' => 'access', 'count' => 1], ['count = count + 1']);
# query: INSERT INTO `log` SET `name` = ?, `count` = ?
#        ON DUPLICATE KEY UPDATE count = count + 1
# bind:  'access', 1

factory->set(data) : row_count

Update data

$data = $storage
    ->factory('users')
    ->filter('id', 123)
    ->set(['name' => 'bar']);
# query: UPDATE `users` SET `name` = ? WHERE `id` = ?
# bind:  'bar', 123

factory->del() : row_count

Deleting data

$data = $storage
    ->factory('users')
    ->filter('id', 123)
    ->del();
# query: DELETE FROM `users` WHERE `id` = ?
# bind:  123

factory->count([key]) : count

Returns the number of expressions

$data = $storage
    ->factory('users')
    ->filter_gt('id', 123)
    ->count('email');
# query: SELECT COUNT(`email`) AS `val` FROM `users` WHERE `id` > ?
# bind:  123

factory->min(key) : value

Returns the minimum value

$data = $storage
    ->factory('users')
    ->filter_like('email', '%@mail%')
    ->min('id');
# query: SELECT MIN(`id`) AS `val` FROM `users` WHERE `email` LIKE ?
# bind:  '%@mail%'

factory->max(key) : value

Returns the maximum value

$data = $storage
    ->factory('users')
    ->filter_in('role_id', [2, 3])
    ->max('id');
# query: SELECT MAX(`id`) AS `val` FROM `users` WHERE `role_id` IN (?, ?)
# bind:  2, 3

factory->avg(key) : value

Returns the average value

$data = $storage
    ->factory('users')
    ->avg('id');
# query: SELECT AVG(`id`) AS `val` FROM `users`

factory->sum(key) : value

Returns the sum of values

$data = $storage
    ->factory('users')
    ->sum('id');
# query: SELECT SUM(`id`) AS `val` FROM `users`

Model\SQL

use Lava\Model\SQL;

class User extends SQL {

    // meta data
    protected static

        // optionally, storage name
        // name from Lava\App::conf()->storage()
        // default is "0"
        $storage = '0',

        // optionally, table name
        // defaults to a class name in snake case with an "s" at the end
        $table   = 'users',

        // optionally, the name of the primary key
        // defaults to "id"
        $id      = 'id',

        // columns description
        $columns = [
            'key' => [
                // value cannot be NULL
                'not_null' => FALSE|TRUE,
                // value is unique
                'unique'   => FALSE|TRUE,
                // default value
                'default'  => VALUE,
                // validation tests
                'valid'    => [tests],
            ],
            ...
        ],

        // optionally, default limit
        // if not specified at selection
        // unrestricted by default
        $limit   = 0;
}

Creation

$user = new User;

$user->name  = 'foo';
$user->email = 'mail@example.com';

$user->save();

Selecting

// selecting one object by ID
$user  = User::one(123);

echo $user->name;

// selecting a collection by condition
$users = User::find(['active' => TRUE])->get();

foreach ($users as $user) {
    echo $user->name;
}

Default values

class Session extends SQL {

    // method name must start with the prefix "column_default_"
    public static function column_default_created () {
        return date('Y-m-d H:i:s');
    }
}

$session = new Session;

echo $session->created; // now

Relationship

class User extends SQL {

    public function sessions () {
        return $this
            ->has_many(Session::classname());
    }

    // additional filters
    public function active_sessions () {
        return $this
            ->sessions()
            ->filter(['active' => TRUE]);
    }
}

class Session extends SQL {

    public function user () {
        return $this->belongs_to(User::classname());
    }
}

$user   = User::one(123);

// property returns a collection
foreach ($user->sessions as $session) {
    echo $session->id;
}

// method returns resultset
$active = $user ->sessions()
                ->filter(['active' => TRUE])
                ->get();

// similarly
$active = $user ->active_sessions;

Import & Export

class Session extends SQL {

    public static function import ($data) {

        if (isset($data['ip'])) {
            $data['ip'] = long2ip($data['ip']);
        }

        return $data;
    }

    public static function export ($data) {

        if (isset($data['ip'])) {
            $data['ip'] = ip2long($data['ip']);
        }

        return $data;
    }
}