DONE: resolved #1, resolved #3, resolved #4, resolved #5, resolved #7, resolved #9, resolved #10, resolved #12

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2025-01-27 17:10:20 +07:00
parent a69c2bbcdd
commit f56de9b053
14 changed files with 1293 additions and 684 deletions

View File

@ -1,5 +1,5 @@
# Ebaboba database
A lightweight database in pure PHP<br>
A lightweight database by pure PHP<br>
At the moment the project is a modified RFC 4180

View File

@ -1,11 +1,12 @@
{
"name": "mirzaev/csv",
"description": "Lightweight library for creating CSV databases",
"homepage": "https://git.mirzaev.sexy/mirzaev/csv",
"type": "library",
"name": "mirzaev/ebaboba",
"description": "Lightweight binary database by pure PHP",
"homepage": "https://git.svoboda.works/mirzaev/ebaboba",
"type": "database",
"keywords": [
"csv",
"database"
"binary",
"plain",
"lightweight"
],
"readme": "README.md",
"license": "WTFPL",
@ -19,8 +20,8 @@
],
"support": {
"email": "arsen@mirzaev.sexy",
"wiki": "https://git.mirzaev.sexy/mirzaev/csv/wiki",
"issues": "https://git.mirzaev.sexy/mirzaev/csv/issues"
"wiki": "https://git.svoboda.works/mirzaev/ebaboba/wiki",
"issues": "https://git.svoboda.works/mirzaev/ebaboba/issues"
},
"minimum-stability": "stable",
"require": {
@ -28,12 +29,12 @@
},
"autoload": {
"psr-4": {
"mirzaev\\csv\\": "mirzaev/csv/system/"
"mirzaev\\ebaboba\\": "mirzaev/ebaboba/system/"
}
},
"autoload-dev": {
"psr-4": {
"mirzaev\\csv\\tests\\": "mirzaev/csv/tests"
"mirzaev\\ebaboba\\tests\\": "mirzaev/ebaboba/tests"
}
}

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "70ef8045ba581d96d3a68483b6031a33",
"content-hash": "d05285edd5fdf816383617183f6d6c38",
"packages": [],
"packages-dev": [],
"aliases": [],

View File

@ -1,240 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\csv;
// Files of the project
use mirzaev\csv\traits\file;
// Built-in libraries
use exception;
/**
* Database
*
* Comma-Separated Values by RFC 4180
*
* @see https://tools.ietf.org/html/rfc4180 RFC 4180
* @see https://en.wikipedia.org/wiki/Create,_read,_update_and_delete CRUD
*
* @package mirzaev\csv
*
* @var string FILE Path to the database file
* @var array $columns Database columns
*
* @method void __construct(array|null $columns) Constructor
* @method void create() Write to the database file
* @method array|null read(int $amount, int $offset, bool $backwards, ?callable $filter) Read from the database file
* @method void update() @todo create + tests
* @method void delete() @todo create + teste
* @method void __set(string $name, mixed $value) Write the parameter
* @method mized __get(string $name) Read the parameter
* @method void __unset(string $name) Delete the parameter
* @method bool __isset(string $name) Check for initializing the parameter
*
* @license http://www.wtfpl.net Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class database
{
use file {
file::read as protected file;
}
/**
* File
*
* Path directories to the file will not be created automatically to avoid
* checking the existence of all directories on every read or write operation.
*
* @var string FILE Path to the database file
*/
public const string FILE = 'database.csv';
/**
* Columns
*
* This property is used instead of adding a check for the presence of the first row
* with the designation of the column names, as well as reading these columns,
* which would significantly slow down the library.
*
* @see https://www.php.net/manual/en/function.array-combine.php Used when creating a record instance
*
* @var array $columns Database columns
*/
public protected(set) array $columns;
/**
* Constructor
*
* @param string ...$columns Columns
*
* @return void
*/
public function __construct(string ...$columns)
{
// Initializing columns
if (!empty($columns)) $this->columns = $columns;
}
/**
* Initialize
*
* Checking for existance of the database file and creating it
*
* @return bool Is the database file exists?
*/
public static function initialize(): bool
{
if (file_exists(static::FILE)) {
// The database file exists
// Exit (success)
return true;
} else {
// The database file is not exists
// Creating the database file and exit (success/fail)
return touch(static::FILE);
}
}
/**
* Create
*
* Create records in the database file
*
* @param record $record The record
* @param array &$errors Buffer of errors
*
* @return void
*/
public static function write(record $record, array &$errors = []): void
{
try {
// Opening the database file
$file = fopen(static::FILE, 'c');
if (flock($file, LOCK_EX)) {
// The file was locked
// Writing the serialized record to the database file
fwrite($file, $record->serialize());
// Applying changes
fflush($file);
// Unlocking the file
flock($file, LOCK_UN);
}
// Deinitializing unnecessary variables
unset($serialized, $record, $before);
// Closing the database file
fclose($file);
} catch (exception $e) {
// Write to the buffer of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
/**
* Read
*
* Read records in the database file
*
* @param int $amount Amount of records
* @param int $offset Offset of rows for start reading
* @param bool $backwards Read from end to beginning?
* @param callable|null $filter Filter for records function($record, $records): bool
* @param array &$errors Buffer of errors
*
* @return array|null Readed records
*/
public static function read(int $amount = 1, int $offset = 0, bool $backwards = false, ?callable $filter = null, array &$errors = []): ?array
{
try {
// Opening the database file
$file = fopen(static::FILE, 'r');
// Initializing the buffer of readed records
$records = [];
// Continuing reading
offset:
foreach (static::file(file: $file, offset: $offset, rows: $amount, position: 0, step: $backwards ? -1 : 1) as $row) {
// Iterating over rows
if ($row === null) {
// Reached the end or the beginning of the file
// Deinitializing unnecessary variables
unset($row, $record, $offset);
// Closing the database file
fclose($file);
// Exit (success)
return $records;
}
// Initializing record
$record = new record($row)->combine($this);
if ($record) {
// Initialized record
if ($filter === null || $filter($record, $records)) {
// Filter passed
// Writing to the buffer of readed records
$records[] = $record;
}
}
}
// Deinitializing unnecessary variables
unset($row, $record);
if (count($records) < $amount) {
// Fewer rows were read than requested
// Writing offset for reading
$offset += $amount;
// Continuing reading (enter to the recursion)
goto offset;
}
// Deinitializing unnecessary variables
unset($offset);
// Closing the database file
fclose($file);
// Exit (success)
return $records;
} catch (exception $e) {
// Write to the buffer of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -1,237 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\csv;
// Files of the project
use mirzaev\csv\database;
/**
* CSV
*
* Comma-Separated Values by RFC 4180
*
* @see https://tools.ietf.org/html/rfc4180 RFC 4180
*
* @package mirzaev\csv
*
* @var array $parameters Parameters of the record
*
* @method void __construct(string|null $row) Constructor
* @method string static serialize() Convert record instance to values for writing into the database
* @method void static unserialize(string $row) Convert values from the database and write to the record instance
* @method void __set(string $name, mixed $value) Write the parameter
* @method mized __get(string $name) Read the parameter
* @method void __unset(string $name) Delete the parameter
* @method bool __isset(string $name) Check for initializing the parameter
*
* @license http://www.wtfpl.net Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class record
{
/**
* Parameters
*
* Mapped with database::COLUMN
*
* @var array $parameters Parameters of the record
*/
public protected(set) array $parameters = [];
/**
* Constructor
*
* @param mixed $parameters Parameter of the record
*
* @return void
*/
public function __construct(mixed ...$parameters)
{
// Initializing parameters
if (!empty($parameters)) $this->parameters = $parameters;
}
/**
* Columns
*
* Combine parameters of the record with columns of the database
* The array of parameters of the record will become associative
*
* @return static The instance from which the method was called (fluent interface)
*/
public function columns(database $database): static
{
// Combining database columns with record parameters
$this->parameters = array_combine($database->columns, $this->parameters);
// Exit (success)
return $this;
}
/**
* Serialize
*
* Convert record instance to values for writing into the database
*
* @return string Serialized record
*/
public function serialize(): string
{
// Declaring the buffer of generated row
$serialized = '';
foreach ($this->parameters as $value) {
// Iterating over parameters
// Generating row by RFC 4180
$serialized .= ',' . preg_replace('/(?<=[^^])"(?=[^$])/', '""', preg_replace('/(?<=[^^]),(?=[^$])/', '\,', $value ?? ''));
}
// Trimming excess first comma in the buffer of generated row
$serialized = mb_substr($serialized, 1, mb_strlen($serialized));
// Exit (success)
return $serialized;
}
/**
* Deserialize
*
* Convert values from the database and write to the record instance
*
* @param string $row Row from the database
*
* @return array Deserialized record
*/
public static function deserialize(string $row): array
{
// Separating row by commas
preg_match_all('/(.*)(?>(?<!\\\),|$)/Uu', $row, $matches);
// Deleting the last matched element (i could not come up with a better regular expression)
array_pop($matches[1]);
// Generating parameters by RFC 4180
foreach ($matches[1] as &$match) {
// Iterating over values
// Declaring buffer of the implementated value
$buffer = null;
if ($match === 'null' || empty($match)) {
// Null
// Writing to the matches buffer
$match = null;
} else if (($buffer = filter_var($match, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE)) !== null) {
// Boolean
// Writing to the matches buffer
$match = $buffer;
} else if (($buffer = filter_var($match, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE)) !== null) {
// Integer
// Writing to the matches buffer
$match = $buffer;
} else if (($buffer = filter_var($match, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE)) !== null) {
// Float
// Writing to the matches buffer
$match = $buffer;
} else {
// String
// Deinitializing unnecessary variables
unset($buffer);
// Removing quotes from both sides (trim() is not suitable here)
$unquoted = preg_replace('/"(.*)"/u', '$1', $match);
// Unescaping commas
$commaded = preg_replace('/\\\,/', ',', $unquoted);
// Unescaping quotes (by RFC 4180)
$quoted = preg_replace('/""/', '"', $commaded);
// Removing line break characters
/* $unbreaked = preg_replace('/[\n\r]/', '', $quoted); */
// Removing spaces from both sides
/* $unspaced = trim($unbreaked); */
// Writing to the matches buffer
$match = $quoted;
}
// Deinitializing unnecessary variables
unset($buffer);
}
// Exit (success)
return $matches[1];
}
/**
* Write
*
* Write the parameter
*
* @param string $name Name of the parameter
* @param mixed $value Content of the parameter
*
* @return void
*/
public function __set(string $name, mixed $value = null): void
{
// Writing the parameter and exit (success)
$this->parameters[$name] = $value;
}
/**
* Read
*
* Read the parameter
*
* @param string $name Name of the parameter
*
* @return mixed Content of the parameter
*/
public function __get(string $name): mixed
{
// Reading the parameter and exit (success)
return $this->parameters[$name];
}
/**
* Delete
*
* Delete the parameter
*
* @param string $name Name of the parameter
*
* @return void
*/
public function __unset(string $name): void
{
// Deleting the parameter and exit (success)
unset($this->parameter[$name]);
}
/**
* Check for initializing
*
* Check for initializing the parameter
*
* @param string $name Name of the parameter
*
* @return bool Is the parameter initialized?
*/
public function __isset(string $name): bool
{
// Checking for initializing the parameter and exit (success)
return isset($this->parameters[$name]);
}
}

View File

@ -1,103 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\csv\traits;
// Built-in libraries
use Exception as exception,
Generator as generator;
/**
* File
*
* @package mirzaev\csv\traits
*
* @method static generator|null|false read($file, int $offset, int $rows, int $position, int $step, array &$errors) Read the file
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait file
{
/**
* Read
*
* Read the file
*
* @param resource $file Pointer to the file (fopen())
* @param int $rows Amount of rows for reading
* @param int $offset Offset of rows for start reading
* @param int $position Initial cursor position on a row
* @param int $step Reading step
* @param array &$errors Buffer of errors
*
* @return generator|null|false
*/
private static function read($file, int $rows = 10, int $offset = 0, int $position = 0, int $step = 1, array &$errors = []): generator|null|false
{
try {
while ($offset-- > 0) {
do {
// Iterate over symbols of the row
// The end (or the beginning) of the file reached (success)
if (feof($file)) break;
// Moving the cursor to next position on the row
fseek($file, $position += $step, SEEK_END);
// Reading a character of the row
$character = fgetc($file);
// Is the character a carriage return? (end or start of the row)
} while ($character !== PHP_EOL);
}
while ($rows-- > 0) {
// Reading rows
// Initializing of the buffer of row
$row = '';
// Initializing the character buffer to generate $row
$character = '';
do {
// Iterate over symbols of the row
// The end (or the beginning) of the file reached (success)
if (feof($file)) break;
// Building the row
$row = $step > 0 ? $row . $character : $character . $row;
// Moving the cursor to next position on the row
fseek($file, $position += $step, SEEK_END);
// Reading a character of the row
$character = fgetc($file);
// Is the character a carriage return? (end or start of the row)
} while ($character !== PHP_EOL);
// Exit (success)
yield empty($row) ? null : $row;
}
// Exit (success)
return null;
} catch (exception $e) {
// Write to the buffer of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
}

View File

@ -1,92 +0,0 @@
<?php
use mirzaev\csv\database,
mirzaev\csv\record;
// Importing files of thr project and dependencies
require(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
// Initializing the counter of actions
$action = 0;
// Initializing the test database
$database = new database(
'empty_value_at_the_beginning',
'name',
'second_name',
'age_between_quotes',
'true',
'empty_value',
'empty_value_between_quotes',
'number',
'null',
'null_between_quotes',
'float_between_quotes',
'float',
'float_long',
'float_with_two_dots',
'string_with_doubled_quotes',
'string_with_doubled_quotes_twice',
'string_with_space_at_the_beginning',
'string_with_escaped_comma',
'string_with_unicode_symbols'
);
echo '[' . ++$action . "] Created the database instance with columns\n";
// Initializing the test record with the test row
$record = new record(...record::deserialize(',"Arsen","Mirzaev","23",true,,"",100,null,"null","102.1",300.34,1001.23145,5000.400.400,"test ""value""","another"" test "" value with ""two double quotes pairs"" yeah"," starts with space","has\, an escaped comma inside","unicode символы"'));
echo '[' . ++$action . "] Created the record with the test row\n";
// Initializing the counter of tests
$test = 0;
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[0] === null ? 'SUCCESS' : 'FAIL') . "] The empty value at the beginning is need to be null\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[1] === 'Arsen' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Arsen\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[2] === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Mirzaev\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[3] === '23' ? 'SUCCESS' : 'FAIL') . "] The age between quotes value is need to be \"23\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[4] === true ? 'SUCCESS' : 'FAIL') . "] The value is need to be true\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[5] === null ? 'SUCCESS' : 'FAIL') . "] The empty value is need to be null\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[6] === '' ? 'SUCCESS' : 'FAIL') . "] The empty value between quotes is need to be \"\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[7] === 100 ? 'SUCCESS' : 'FAIL') . "] The value is need to be 100 integer\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[8] === null ? 'SUCCESS' : 'FAIL') . "] The null value is need to be null\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[9] === 'null' ? 'SUCCESS' : 'FAIL') . "] The null value between quotes is need to be \"null\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[10] === '102.1' ? 'SUCCESS' : 'FAIL') . "] The float value between quotes is need to be \"102.1\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[11] === 300.34 ? 'SUCCESS' : 'FAIL') . "] The float value is need to be 300.34 float \n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[12] === 1001.23145 ? 'SUCCESS' : 'FAIL') . "] The long float value is need to be 1001.23145 float\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[13] === '5000.400.400' ? 'SUCCESS' : 'FAIL') . "] The float value with two dots is need to be \"5000.400.400\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[14] === 'test "value"' ? 'SUCCESS' : 'FAIL') . "] The value with quotes is need to be \"test \"value\"\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[15] === 'another" test " value with "two double quotes pairs" yeah' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"another\" test \" value with \"two double quotes pairs\" yeah\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[16] === ' starts with space' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \" starts with space\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[17] === 'has, an escaped comma inside' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"has, an escaped comma inside\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[18] === 'unicode символы' ? 'SUCCESS' : 'FAIL') . "] The valueis need to be \"unicode символы\" string\n";
// Combining database columns with record parameters
$record->columns($database);
echo '[' . ++$action . "] Combined database columns with record parameters\n";
// Reinitializing the counter of tests
$test = 0;
echo '[' . ++$action . '][' . ++$test . '][' . ($record->empty_value_at_the_beginning === null ? 'SUCCESS' : 'FAIL') . "] The empty value at the beginning is need to be null\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->name === 'Arsen' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Arsen\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Mirzaev\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->age_between_quotes === '23' ? 'SUCCESS' : 'FAIL') . "] The age between quotes value is need to be \"23\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->true === true ? 'SUCCESS' : 'FAIL') . "] The value is need to be true\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->empty_value === null ? 'SUCCESS' : 'FAIL') . "] The empty value is need to be null\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->empty_value_between_quotes === '' ? 'SUCCESS' : 'FAIL') . "] The empty value between quotes is need to be \"\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->number === 100 ? 'SUCCESS' : 'FAIL') . "] The value is need to be 100 integer\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->null === null ? 'SUCCESS' : 'FAIL') . "] The null value is need to be null\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->null_between_quotes === 'null' ? 'SUCCESS' : 'FAIL') . "] The null value between quotes is need to be \"null\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->float_between_quotes === '102.1' ? 'SUCCESS' : 'FAIL') . "] The float value between quotes is need to be \"102.1\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->float === 300.34 ? 'SUCCESS' : 'FAIL') . "] The float value is need to be 300.34 float \n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->float_long === 1001.23145 ? 'SUCCESS' : 'FAIL') . "] The long float value is need to be 1001.23145 float\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->float_with_two_dots === '5000.400.400' ? 'SUCCESS' : 'FAIL') . "] The float value with two dots is need to be \"5000.400.400\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_doubled_quotes === 'test "value"' ? 'SUCCESS' : 'FAIL') . "] The value with quotes is need to be \"test \"value\"\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_doubled_quotes_twice === 'another" test " value with "two double quotes pairs" yeah' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"another\" test \" value with \"two double quotes pairs\" yeah\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_space_at_the_beginning === ' starts with space' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \" starts with space\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_escaped_comma === 'has, an escaped comma inside' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"has, an escaped comma inside\" string\n";
echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_unicode_symbols === 'unicode символы' ? 'SUCCESS' : 'FAIL') . "] The valueis need to be \"unicode символы\" string\n";

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebaboba;
// Files of the project
use mirzaev\ebaboba\database,
mirzaev\ebaboba\enumerations\type;
// Built-in libraries
use DomainException as exception_domain,
LogicException as exception_logic,
InvalidArgumentException as exceptiin_invalid_argument;
/**
* Column
*
* @package mirzaev\ebaboba
*
* @var string $name Name of the column
* @var type $type Type of the column values
* @var int $length Length of every binary value that will be written to the database file
*
* @method void __construct(string $name, type $type, array $parameters) Constructor
*
* @license http://www.wtfpl.net Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class column
{
/**
* Name
*
* @var string $name Name of the column
*/
public readonly protected(set) string $name;
/**
* Type
*
* @see https://www.php.net/manual/en/function.pack.php Pack (types are shown here)
* @see https://www.php.net/manual/en/function.unpack.php Unpack
*
* @var type $type Type of the column values
*/
public readonly protected(set) type $type;
/**
* Length
*
* Length of every binary value that will be written to the database file
*
* @throws exception_logic if the length property is already initialized
* @throws exception_logic if the type is not initialized
* @throws exception_domain if the type can not has length
*
* @var int $length Length of every binary values
*/
public protected(set) int $length {
// Write
set (int $value) {
if (isset($this->length)) {
// Already been initialized
// Exit (fail)
throw new exception_logic('The length property is already initialized');
} else if (!isset($this->type)) {
// The type is not initialized
// Exit (fail)
throw new exception_logic('The type of the column values is not initialized');
} else if (match ($this->type) {
type::string => true,
default => false
}) {
// The type has length
// Writing into the property
$this->length = $value;
} else {
// The type has no length
// Exit (fail)
throw new exception_domain('The "' . $this->type->name . '" type can not has length');
}
}
}
/**
* Constructor
*
* @param string $name Name of the column
* @param type $type Type of the column values
* @param array $parameters Parameters of the column
*
* @return void
*/
public function __construct(string $name, type $type, array $parameters = [])
{
// Writing into the property
$this->name = $name;
// Writing into the property
$this->type = $type;
foreach ($parameters as $name => $value) {
// Iterating over parameters
if (property_exists($this, $name)) {
// Found the property
// Writing into the property
$this->{$name} = $value;
} else {
// Not found the property
// Exit (fail)
throw new exceptiin_invalid_argument("Not found the property: $name");
}
}
}
}

View File

@ -0,0 +1,617 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebaboba;
// Files of the project
use mirzaev\ebaboba\enumerations\encoding,
mirzaev\ebaboba\enumerations\type;
// Built-in libraries
use LogicException as exception_logic,
InvalidArgumentException as exception_invalid_argument,
RuntimeException as exception_runtime;
/**
* Database
*
* @package mirzaev\ebaboba
*
* @var string $database Path to the database file
* @var string $backups Path to the backups files directory
* @var encoding $encoding Encoding of records in the database file
* @var array $columns The database columns
* @var int $length Binary size of every record in the database file
*
* @method self encoding(encoding $encoding) Write encoding into the database instance property (fluent interface)
* @method self columns(column ...$columns) Write columns into the database instance property (fluent interface)
* @method self connect(string $database) Initialize the database files (fluent interface)
* @method record|null record(...$values) Initialize the record by the database columns
* @method string pack(record $record) Pack the record values
* @method record unpack(array $binaries) Unpack binary values and implement them as a `record` instance
* @method bool write(record $record) Write the record into the database file
* @method array read(?callable $filter, ?callable $update, bool $delete, int $amount, int $offset) Read records from the database file
* @method bool backups() Initialize the backups files directory
* @method int|false save() Create an unique backup file from the database file
* @method bool load(int $identifier) Restore the database file from the backup file
*
* @license http://www.wtfpl.net Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class database
{
/**
* Database
*
* Path to the database file
*
* @var string $database Path to the database file
*/
public protected(set) string $database = __DIR__ . DIRECTORY_SEPARATOR . 'database.ba';
/**
* Backups
*
* Path to the backups files directory
*
* @var string $backups Path to the backups files directory
*/
public protected(set) string $backups = __DIR__ . DIRECTORY_SEPARATOR . 'backups';
/**
* Encoding
*
* @var encoding $encoding Encoding of records in the database file
*/
public protected(set) encoding $encoding;
/**
* Columns
*
* @var record[] $columns The database columns
*/
public protected(set) array $columns;
/**
* Length
*
* @var int $length Binary size of every record in the database file
*/
public protected(set) int $length;
/**
* Encoding
*
* Write encoding into the database instance property
*
* @see https://en.wikipedia.org/wiki/Fluent_interface#PHP Fluent Interface
*
* @param encoding $encoding The database file encoding
*
* @return self The database instance (fluent interface)
*/
public function encoding(encoding $encoding): self
{
// Writing into the database instance property
$this->encoding = $encoding;
// Exit (success)
return $this;
}
/**
* Columns
*
* Write columns into the database instance property
*
* @see https://en.wikipedia.org/wiki/Fluent_interface#PHP Fluent Interface
*
* @param column[] ...$columns The database columns
*
* @return self The database instance (fluent interface)
*/
public function columns(column ...$columns): self
{
// Writing into the database instance property
$this->columns = $columns;
// Initializing the database instance property
$this->length ??= 0;
foreach ($this->columns as $column) {
// Iterating over columns
if ($column->type === type::string) {
// String
// Adding the column string maximum length to the database instance property
$this->length += $column->length;
} else {
// Other types
// Adding the column type size to the database instance property
$this->length += $column->type->size();
}
}
// Exit (success)
return $this;
}
/**
* Connect
*
* Initialize the database files
*
* @see https://en.wikipedia.org/wiki/Fluent_interface#PHP Fluent Interface
*
* @param string $database Path to the database file
*
* @return self The database instance (fluent interface)
*/
public function connect(string $database): self
{
// Writing into the database instance property
$this->database = $database;
// Exit (success)
return $this;
}
/**
* Record
*
* Initialize the record by the database columns
*
* @param mixed[] $values Values of the record
*
* @throws exceptiin_invalid_argument if the balue type not matches the column values types
* @throws exception_logic if amount of columns not matches the amount of values
*
* @return record|null The record instance
*/
public function record(string|int|float ...$values): ?record
{
if (count($values) === count($this->columns)) {
// Amount of values matches amount of columns
// Declaring the buffer of combined values
$combined = [];
foreach ($this->columns as $index => $column) {
// Iterating over columns
if (gettype($values[$index]) === $column->type->type()) {
// The value type matches the column values type
// Writing named index value into the buffer of combined values
$combined[$column->name] = $values[$index];
} else {
// The value type not matches the column values type
// Exit (fail)
throw new exception_invalid_argument('The value type not matches the column values type');
}
}
// Initializing the record by the buffer of combined values
$record = new record(...$combined);
// Exit (success)
return $record;
} else {
// Amount of values not matches amount of columns
// Exit (fail)
throw new exception_logic('Amount of values not matches amount of columns');
}
// Exit (fail)
return null;
}
/**
* Pack
*
* Pack the record values
*
* @param record $record The record
*
* @return string Packed values
*/
public function pack(record $record): string
{
// Declaring buffer of packed values
$packed = '';
foreach ($this->columns as $column) {
// Iterating over columns
if ($column->type === type::string) {
// String
// Converting to the database encoding
$value = mb_convert_encoding($record->values()[$column->name], $this->encoding->value);
// Packung the value and writing into the buffer of packed values
$packed .= pack($column->type->value . $column->length, $value);
} else {
// Other types
// Packung the value and writing into the buffer of packed values
$packed .= pack($column->type->value, $record->values()[$column->name]);
}
}
// Exit (success)
return $packed;
}
/**
* Unpack
*
* Unpack binary values and implement them as a `record` instance
*
* @param array $binaries Binary values in the same order as the columns
*
* @return record The unpacked record from binary values
*/
public function unpack(array $binaries): record
{
if (count($binaries) === count($this->columns)) {
// Amount of binery values matches amount of columns
// Declaring the buffer of unpacked values
$unpacked = [];
foreach (array_combine($binaries, $this->columns) as $binary => $column) {
// Iterating over columns
if ($column->type === type::string) {
// String
// Unpacking the value
$value = unpack($column->type->value . $column->length, $binary)[1];
// Deleting NULL-characters
$unnulled = str_replace("\0", '', $value);
// Encoding the unpacked value
$encoded = mb_convert_encoding($unnulled, $this->encoding->value);
// Writing into the buffer of readed values
$unpacked[] = $encoded;
} else {
// Other types
// Writing into the buffer of readed values
$unpacked[] = unpack($column->type->value, $binary)[1];
}
}
// Implementing the record
$record = $this->record(...$unpacked);
// Exit (success)
return $record;
} else {
// Amount of binery values not matches amount of columns
// Exit (fail)
throw new exception_invalid_argument('Amount of binary values not matches amount of columns');
}
}
/**
* Write
*
* Write the record into the database file
*
* @param record $record The record
*
* @throws exception_runtime If failed to lock the file
* @throws exception_runtime If failed to unlock the file
*
* @return bool Is the record was writed into the end of the database file
*/
public function write(record $record): bool
{
try {
// Opening the database file
$file = fopen($this->database, 'ab');
if (flock($file, LOCK_EX)) {
// The file was locked
// Packing the record values
$packed = $this->pack($record);
// Writing the packed values to the database file
fwrite($file, $packed);
// Applying changes
fflush($file);
if (flock($file, LOCK_UN)) {
// The file was unlocked
// Exit (success)
return true;
} else {
// Failed to unlock the file
// Exit (fail)
throw new exception_runtime('Failed to unlock the file');
}
} else {
// Failed to lock the file
// Exit (fail)
throw new exception_runtime('Failed to lock the file');
}
} finally {
// Closing the database file
fclose($file);
}
// Exit (fail)
return false;
}
/**
* Read
*
* Read records from the database file
*
* Order: `$filter` -> `$offset` -> (`$delete` -> read deleted || `$update` -> read updated || read) -> `$amount`
*
* @param callable|null $filter Filtering records `function($record, $records): bool`
* @param callable|null $update Updating records `function(&$record): void`
* @param callable|null $delete Deleting records
* @param int $amount Amount iterator
* @param int $offset Offset iterator
*
* @return array|null Readed records
*/
public function read(?callable $filter = null, ?callable $update = null, bool $delete = false, int $amount = 1, int $offset = 0): ?array
{
// Opening the database file
$file = fopen($this->database, 'r+b');
if (flock($file, LOCK_EX)) {
// The file was locked
// Declaring the buffer of readed records
$records = [];
// Declaring the buffer of failed to reading records
/* $failed = []; */
while ($amount > 0) {
// Reading records
// Declaring the buffer of binary values
$binaries = [];
foreach ($this->columns as $column) {
// Iterating over columns
if ($column->type === type::string) {
// String
// Reading the binary value from the database
$binaries[] = fread($file, $column->length);
} else {
// Other types
// Reading the binary value from the database
$binaries[] = fread($file, $column->type->size());
}
}
// Terminate loop when end of file is reached
if (feof($file)) break;
try {
// Unpacking the record
$record = $this->unpack($binaries);
if (is_null($filter) || $filter($record, $records)) {
// Passed the filter
if ($offset-- <= 0) {
// Offsetted
if ($delete) {
// Requested deleting
// Moving to the beginning of the row
fseek($file, -$this->length, SEEK_CUR);
// Writing NUL-characters instead of the record to the database file
fwrite($file, str_repeat("\0", $this->length));
// Moving to the end of the row
fseek($file, $this->length, SEEK_CUR);
} else if ($update) {
// Requested updating
// Updating the record
$update($record);
// Packing the updated record
$packed = $this->pack($record);
// Moving to the beginning of the row
fseek($file, -$this->length, SEEK_CUR);
// Writing to the database file
fwrite($file, $packed);
// Moving to the end of the row
fseek($file, $this->length, SEEK_CUR);
}
// Writing into the buffer of records
$records[] = $record;
// Decreasing the amount iterator
--$amount;
}
}
} catch (exception_logic | exception_invalid_argument $exception) {
// Writing into the buffer of failed to reading records
/* $failed[] = $record; */
}
}
// Unlocking the file
flock($file, LOCK_UN);
// Closing the database file
fclose($file);
// Exit (success)
return $records;
}
// Exit (fail)
return null;
}
/**
* Backups
*
* Initialize the backups files directory
*
* @throws exception_runtime if failed to create the backups files directory
*
* @return bool Is the backups files directory created?
*/
public function backups(): bool
{
if (is_dir($this->backups) || is_writable($this->backups)) {
// The backups files directory exists
// Exit (success)
return true;
} else {
// The backups files directory is not exists
if (mkdir(directory: $this->backups, permissions: 0775, recursive: true)) {
// The backups files directory created
// Exit (success)
return true;
} else {
// The backups files directory is still not exists
// Exit (fail)
throw new exception_runtime('Failed to create the backups files directory: "' . $this->backups . '"');
}
}
// Exit (fail)
return false;
}
/**
* Save
*
* Create an unique backup file from the database file
*
* @throws exception_runtime if failed to copying the database file to the backup file
* @throws exception_runtime if failed to initialize the backups files directory
*
* @return int|false Unique identifier of the created backup file
*/
public function save(): int|false
{
if ($this->backups()) {
// Initialized the backups files directory
// Generation of unique identifier
generate:
// Generating unique identifier for the backup file
$identifier = uniqid();
// Initializing path to the backup file with generated identifier
$file = $this->backups . DIRECTORY_SEPARATOR . $identifier;
if (file_exists($file)) {
// File with this identifier is already exists
// Repeating generation (entering into recursion)
goto generate;
} else {
// Generated unique identifier for the backup file
if (copy($this->database, $file)) {
// Copied the database file to the backup file
// Exit (success)
return $identifier;
} else {
// Not copied the database file to the backup file
// Exit (fail)
throw new exception_runtime('Failed to copying the database file to the backup file');
}
}
} else {
// Not initialized the backups files directory
// Exit (fail)
throw new exception_runtime('Failed to initialize the backups files directory');
}
// Exit (fail)
return false;
}
/**
* Load
*
* Restore the database file from the backup file
*
* @throws exception_runtime if not found the backup file
* @throws exception_runtime if failed to initialize the backups files directory
*
* @return int|false Unique identifier of the created backup file
*/
public function load(int $identifier): bool
{
if ($this->backups()) {
// Initialized the backups files directory
// Initializing path to the backup file
$file = $this->backups . DIRECTORY_SEPARATOR . $identifier;
if (file_exists($file)) {
// Initialized the backup file
if (rename($file, $this->database)) {
// Loaded the database file from the backup file
// Exit (success)
return true;
}
} else {
// Not initialized the backup file
// Exit (fail)
throw new exception_runtime('Not found the backup file');
}
} else {
// Not initialized the backups files directory
// Exit (fail)
throw new exception_runtime('Failed to initialize the backups files directory');
}
// Exit (fail)
return false;
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebaboba\enumerations;
// Build-in libraries
use UnexpectedValueException as exception_unexpected_value;
/**
* Encoding
*
* The selected encoding significantly affects the file size and the speed of working with it
*
* ASCII English
* CP1250 Central Europe
* CP1251 Cyrillic
* CP1252 Western Europe
* CP1253 Greek
* CP1254 Turkish
* CP1255 Hebrew
* CP1256 Arabic
* CP1257 Baltic Rim
* CP1258 Vietnam
*
* UTF-8 Length 1-4 bytes, better for Europe, backwards compatible with ASCII (minimum size)
* UTF-16 Length 2-4 bytes, better for Asia (medium size with medium performance)
* UTF-32 Fixed length of 4 bytes (maximum performance)
*
* In the database, the length for all encodings is fixed at the maximum value
*
* @see https://www.php.net/manual/ru/mbstring.supported-encodings.php Encodings in PHP
* @see https://www.ascii-code.com/ About ASCII encodings
* @see https://home.unicode.org/technical-quick-start-guide/ About unicode
* @see https://www.unicode.org/main.html Abount unicode encodings
* @see https://www.unicode.org/faq/utf_bom.html About UTF
*
* @package mirzaev\ebaboba\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum encoding: string
{
case ascii = 'ASCII';
case cp1251 = 'CP1251'; // Windows 1251
case cp1252 = 'CP1252'; // Windows-1252
case cp1253 = 'CP1253'; // Windows-1253
case cp1254 = 'CP1254'; // Windows-1254
case cp1255 = 'CP1255'; // Windows-1255
case cp1256 = 'CP1256'; // Windows-1256
case cp1257 = 'CP1257'; // Windows-1257
case cp1258 = 'CP1258'; // Windows-1258
case utf8 = 'UTF-8';
case utf16 = 'UTF-16';
case utf32 = 'UTF-32';
/**
* Length
*
* @return int Number of bits for a symbol
*/
public function maximum(): int
{
// Exit (success)
return match ($this) {
encoding::ascii => 7,
encoding::cp1251, encoding::cp1252, encoding::cp1253, encoding::cp1254, encoding::cp1255, encoding::cp1256, encoding::cp1257, encoding::cp1258 => 8,
encoding::utf8 => 8,
encoding::utf16 => 16,
encoding::utf32 => 32,
default => throw new exception_unexpected_value('Not found the encoding')
};
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebaboba\enumerations;
// Build-in libraries
use UnexpectedValueException as exception_unexpected_value;
/**
* Type
*
* @see https://www.php.net/pack Types
*
* @package mirzaev\ebaboba\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum type: string
{
case string = 'a';
case char = 'c';
case char_unsigned = 'C';
case short = 's';
case short_unsigned = 'S';
case integer = 'i';
case integer_unsigned = 'I';
case long = 'l';
case long_unsigned = 'L';
case long_long = 'q';
case long_long_unsigned = 'Q';
case float = 'f';
case double = 'd';
case null = 'x';
/**
* Type
*
* @see https://www.php.net/manual/en/function.gettype.php (here is why "double" instead of "float" and "NULL" instead of "null")
*
* @return string Type
*/
public function type(): string
{
// Exit (success)
return match ($this) {
type::char, type::string, type::short => 'string',
type::char_unsigned, type::short_unsigned, type::integer, type::integer_unsigned, type::long, type::long_unsigned, type::long_long, type::long_long_unsigned => 'integer',
type::float, type::double => 'double',
type::null => 'NULL',
default => throw new exception_unexpected_value('Not found the type')
};
}
/**
* Size
*
* @return int Size in bytes
*/
public function size(): int
{
// Exit (success)
return strlen(pack($this->value, 0));
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace mirzaev\ebaboba;
/**
* Record
*
* @package mirzaev\ebaboba
*
* @var array $values The record values
*
* @method void __construct(string|int|float $values) Constructor
* @method array values() Read all values of the record
* @method void __set(string $name, mixed $value) Write the record value
* @method mized __get(string $name) Read the record value
* @method void __unset(string $name) Delete the record value
* @method bool __isset(string $name) Check for initializing of the record value
*
* @license http://www.wtfpl.net Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class record
{
/**
* Values
*
* @var array $values The record values
*/
protected array $values = [];
/**
* Constructor
*
* @param string[]|int[]|float[] $values Values of the record
*
* @return void
*/
public function __construct(string|int|float ...$values)
{
// Initializing values
if (!empty($values)) $this->values = $values;
}
/**
* Values
*
* Read all values of the record
*
* @return array All values of the record
*/
public function values(): array
{
return $this->values ?? [];
}
/**
* Write
*
* Write the value
*
* @param string $name Name of the parameter
* @param mixed $value Content of the parameter
*
* @return void
*/
public function __set(string $name, mixed $value = null): void
{
// Writing the value and exit
$this->values[$name] = $value;
}
/**
* Read
*
* Read the value
*
* @param string $name Name of the value
*
* @return mixed Content of the value
*/
public function __get(string $name): mixed
{
// Reading the value and exit (success)
return $this->values[$name] ?? null;
}
/**
* Delete
*
* Delete the value
*
* @param string $name Name of the value
*
* @return void
*/
public function __unset(string $name): void
{
// Deleting the value
unset($this->values[$name]);
}
/**
* Check for initializing
*
* Check for initializing the value
*
* @param string $name Name of the value
*
* @return bool Is the value initialized?
*/
public function __isset(string $name): bool
{
// Checking for initializing the value and exit (success)
return isset($this->values[$name]);
}
}

1
mirzaev/ebaboba/tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
temporary

View File

@ -0,0 +1,278 @@
<?php
use mirzaev\ebaboba\database,
mirzaev\ebaboba\record,
mirzaev\ebaboba\column,
mirzaev\ebaboba\enumerations\encoding,
mirzaev\ebaboba\enumerations\type;
// Importing files of thr project and dependencies
require(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
// Initializing path to the database file
$file = __DIR__ . DIRECTORY_SEPARATOR . 'temporary' . DIRECTORY_SEPARATOR . 'database.ba';
echo "Started testing\n\n\n";
// Initializing the counter of actions
$action = 0;
if (!file_exists($file) || unlink($file)) {
// Deleted deprecated database
echo '[' . ++$action . '] ' . "Deleted deprecated database\n";
} else {
// Not deleted deprecated database
echo '[' . ++$action . '][FAIL] ' . "Failed to delete deprecated database\n";
die;
}
// Initializing the test database
/* $database = new database() */
$database = (new database())
->encoding(encoding::utf8)
->columns(
new column('name', type::string, ['length' => 32]),
new column('second_name', type::string, ['length' => 64]),
new column('age', type::integer),
new column('height', type::float)
)
->connect(__DIR__ . DIRECTORY_SEPARATOR . 'temporary' . DIRECTORY_SEPARATOR . 'database.ba');
echo '[' . ++$action . "] Initialized the database\n";
// Initializing the record
$record = $database->record(
'Arsen',
'Mirzaev',
24,
165.5
);
echo '[' . ++$action . "] Initialized the record\n";
// Initializing the counter of tests
$test = 0;
echo '[' . ++$action . '][' . ++$test . '][' . ($record->name === 'Arsen' ? 'SUCCESS' : 'FAIL') . "][\"name\"] Expected: \"Arsen\" (string). Actual: \"$record->name\" (" . gettype($record->name) . ")\n";
echo '[' . $action . '][' . ++$test . '][' . ($record->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . "][\"second_name\"] Expected: \"Mirzaev\" (string). Actual: \"$record->second_name\" (" . gettype($record->second_name) . ")\n";
echo '[' . $action . '][' . ++$test . '][' . ($record->age === 24 ? 'SUCCESS' : 'FAIL') . "][\"age\"] Expected: \"24\" (integer). Actual: \"$record->age\" (" . gettype($record->age) . ")\n";
echo '[' . $action . '][' . ++$test . '][' . ($record->height === 165.5 ? 'SUCCESS' : 'FAIL') . "][\"height\"] Expected: \"165.5\" (double). Actual: \"$record->height\" (" . gettype($record->height) . ")\n";
echo '[' . $action . "] The record parameters checks have been completed\n";
// Reinitializing the counter of tests
$test = 0;
// Writing the record into the database
$database->write($record);
echo '[' . ++$action . "] Writed the record into the database\n";
// Initializing the second record
$record_ivan = $database->record(
'Ivan',
'Ivanov',
24,
(float) 210,
);
echo '[' . ++$action . "] Initialized the record\n";
// Writing the second record into the databasse
$database->write($record_ivan);
echo '[' . ++$action . "] Writed the record into the database\n";
// Initializing the second record
$record_ivan = $database->record(
'Margarita',
'Esenina',
19,
(float) 165,
);
echo '[' . ++$action . "] Initialized the record\n";
// Writing the second record into the databasse
$database->write($record_ivan);
echo '[' . ++$action . "] Writed the record into the database\n";
// Reading all records from the database
$records_readed_all = $database->read(amount: 99999);
echo '[' . ++$action . "] Readed all records from the database\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_all) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_all) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_all) === 3 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 3 records. Actual: ' . count($records_readed_all) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_all[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_all[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_all[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_all[0]::class . "\"\n";
echo '[' . $action . "] The readed all records checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The readed all records checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
// Reading the first record from the database
$record_readed_first = $database->read(amount: 1);
echo '[' . ++$action . "] Readed the first record from the database\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($record_readed_first) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($record_readed_first) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($record_readed_first) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($record_readed_first) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($record_readed_first[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($record_readed_first[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($record_readed_first[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $record_readed_first[0]::class . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($record_readed_first[0]->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Mirzaev" (string). Actual: "' . $record_readed_first[0]->second_name . '" (' . gettype($record_readed_first[0]->second_name) . ")\n";
echo '[' . $action . "] The readed first record checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The readed first record checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
// Reading the second record from the database
$record_readed_second = $database->read(amount: 1, offset: 1);
echo '[' . ++$action . "] Readed the second record from the database\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($record_readed_second) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($record_readed_second) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($record_readed_second) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($record_readed_second) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($record_readed_second[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($record_readed_second[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($record_readed_second[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $record_readed_second[0]::class . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($record_readed_second[0]->second_name === 'Ivanov' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Ivanov" (string). Actual: "' . $record_readed_second[0]->second_name . '" (' . gettype($record_readed_second[0]->second_name) . ")\n";
echo '[' . $action . "] The readed second record checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The readed second record checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
// Reading the record from the database by filter
$record_readed_filter = $database->read(filter: fn($record) => $record?->second_name === 'Ivanov', amount: 1);
echo '[' . ++$action . "] Readed the record from the database by filter\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($record_readed_filter) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($record_readed_filter) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($record_readed_filter) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($record_readed_filter) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($record_readed_filter[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($record_readed_filter[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($record_readed_filter[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $record_readed_filter[0]::class . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($record_readed_filter[0]->second_name === 'Ivanov' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Ivanov" (string). Actual: "' . $record_readed_filter[0]->second_name . '" (' . gettype($record_readed_filter[0]->second_name) . ")\n";
echo '[' . $action . "] The readed record by filter checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The readed record by filter checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
// Reading the record from the database by filter with amount limit
$records_readed_filter_amount = $database->read(filter: fn($record) => $record?->age === 24, amount: 1);
echo '[' . ++$action . "] Readed the record from the database by filter with amount limit\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_amount) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_amount) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($records_readed_filter_amount) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_amount[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_amount[0]::class . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount[0]->age === 24 ? 'SUCCESS' : 'FAIL') . ']["age"] Expected: "24" (integer). Actual: "' . $records_readed_filter_amount[0]->age . '" (' . gettype($records_readed_filter_amount[0]->age) . ")\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount[0]->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Mirzaev" (string). Actual: "' . $records_readed_filter_amount[0]->second_name . '" (' . gettype($records_readed_filter_amount[0]->second_name) . ")\n";
echo '[' . $action . "] The readed record by filter with amount limit checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The readed record by filter with amount limit checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
// Reading the record from the database by filter with amount limit and offset
$records_readed_filter_amount_offset = $database->read(filter: fn($record) => $record?->age === 24, amount: 1, offset: 1);
echo '[' . ++$action . "] Readed the record from the database by filter with amount limit and offset\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount_offset) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_amount_offset) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_amount_offset) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($records_readed_filter_amount_offset) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount_offset[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_amount_offset[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount_offset[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_amount_offset[0]::class . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount_offset[0]->age === 24 ? 'SUCCESS' : 'FAIL') . ']["age"] Expected: "24" (integer). Actual: "' . $records_readed_filter_amount_offset[0]->age . '" (' . gettype($records_readed_filter_amount_offset[0]->age) . ")\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount_offset[0]->second_name === 'Ivanov' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Ivanov" (string). Actual: "' . $records_readed_filter_amount_offset[0]->second_name . '" (' . gettype($records_readed_filter_amount_offset[0]->second_name) . ")\n";
echo '[' . $action . "] The readed record by filter with amount limit and offset checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The readed record by filter with amount limit and offset checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
// Deleting the record in the database by filter
$records_readed_filter_delete = $database->read(filter: fn($record) => $record?->name === 'Ivan', delete: true, amount: 1);
echo '[' . ++$action . "] Deleted the record from the database by filter\n";
// Reading records from the database after deleting
$records_readed_filter_delete_readed = $database->read(amount: 100);
echo '[' . ++$action . "] Readed records from the database after deleting the record\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_delete) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_delete) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_delete) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of deleted records] Expected: 1 records. Actual: ' . count($records_readed_filter_delete) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_delete[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_delete[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_delete[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_delete[0]::class . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_delete[0]->name === 'Ivan' ? 'SUCCESS' : 'FAIL') . ']["name"] Expected: "Ivan" (string). Actual: "' . $records_readed_filter_delete[0]->second_name . '" (' . gettype($records_readed_filter_delete[0]->second_name) . ")\n";
echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_delete_readed) === 2 ? 'SUCCESS' : 'FAIL') . '][amount of readed records after deleting] Expected: 2 records. Actual: ' . count($records_readed_filter_delete_readed) . " records\n";
echo '[' . $action . "] The deleted record by filter checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The deleted record by filter checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
// Updating the record in the database
$records_readed_filter_update = $database->read(filter: fn($record) => $record?->name === 'Margarita', update: function (&$record) { $record->height += 0.5; }, amount: 1);
echo '[' . ++$action . "] Updated the record in the database by filter\n";
// Reading records from the database after updating
$records_readed_filter_update_readed = $database->read(amount: 100);
echo '[' . ++$action . "] Readed records from the database after updating the record\n";
try {
echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_update) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_update) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_update) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of updated records] Expected: 1 records. Actual: ' . count($records_readed_filter_update) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_update[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_update[0]) . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_update[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_update[0]::class . "\"\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_update[0]->height === 165.5 ? 'SUCCESS' : 'FAIL') . ']["height"] Expected: "165.5" (double). Actual: "' . $records_readed_filter_update[0]->height . '" (' . gettype($records_readed_filter_update[0]->height) . ")\n";
echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_update_readed) === 2 ? 'SUCCESS' : 'FAIL') . '][amount of readed records after updating] Expected: 2 records. Actual: ' . count($records_readed_filter_update_readed) . " records\n";
echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_update_readed[1]->height === $records_readed_filter_update[0]->height ? 'SUCCESS' : 'FAIL') . "] Height from `update` process response matched height from the `read` preocess response\n";
echo '[' . $action . "] The updated record by filter checks have been completed\n";
} catch (exception $e) {
echo '[' . $action . "][WARNING] The updated record by filter checks have been completed with errors\n";
}
// Reinitializing the counter of tests
$test = 0;
echo "\n\nCompleted testing";