illogical / lava
A PHP micro-framework
Requires
- php: >=5.4.0
- ext-json: *
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; } }