Compare commits
23 Commits
|
@ -0,0 +1,6 @@
|
|||
root = true
|
||||
|
||||
[README.md]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
tab_width = 2
|
69
README.md
69
README.md
|
@ -1,2 +1,69 @@
|
|||
# csv
|
||||
# Baza
|
||||
Lightweight binary database by pure PHP<br>
|
||||
|
||||
## Dependencies
|
||||
1. [PHP 8.4](https://www.php.net/releases/8.4/en.php)
|
||||
2. [Composer](https://getcomposer.org/) (php package manager)
|
||||
|
||||
## Installation
|
||||
`composer require mirzaev/baza`
|
||||
|
||||
## Example
|
||||
```php
|
||||
<?php
|
||||
|
||||
use mirzaev\baza\database,
|
||||
mirzaev\baza\column,
|
||||
mirzaev\baza\record,
|
||||
mirzaev\baza\enumerations\encoding,
|
||||
mirzaev\baza\enumerations\type;
|
||||
|
||||
// Initializing the 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 . 'database.ba');
|
||||
|
||||
// Initializing the record
|
||||
$record = $database->record(
|
||||
'Arsen',
|
||||
'Mirzaev',
|
||||
23,
|
||||
(float) 165
|
||||
);
|
||||
|
||||
if ($database->write($record)) {
|
||||
// Writed the record into the database
|
||||
|
||||
// Updating the record in the database
|
||||
$updated = $database->read(
|
||||
filter: fn($record) => $record->name === 'Arsen',
|
||||
update: fn(&$record) => $record->age = 24,
|
||||
amount: 1
|
||||
);
|
||||
|
||||
// Reading the record from the database
|
||||
$readed = $database->read(
|
||||
filter: fn($record) => $record->name === 'Arsen' && $record->age === 24,
|
||||
amount: 1
|
||||
);
|
||||
|
||||
// Deleting the record from the database
|
||||
$deleted = $database->read(
|
||||
filter: fn($record) => $record->age < 25,
|
||||
delete: true,
|
||||
amount: 1000
|
||||
);
|
||||
}
|
||||
?>
|
||||
```
|
||||
|
||||
## Used by
|
||||
- My site-article about how i was kidnapped by PMC Wagner operatives [mirzaev/repression](https://git.svoboda.works/mirzaev/repression)
|
||||
- My decentralized P2P blockchain chats project [mirzaev/notchat](https://git.svoboda.works/mirzaev/notchat)
|
||||
- Svoboda Telegram chat-robot negotiator [svoboda/negotiator](https://git.svoboda.works/svoboda/negotiator)
|
||||
|
|
|
@ -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/baza",
|
||||
"description": "Lightweight binary database by pure PHP",
|
||||
"homepage": "https://git.svoboda.works/mirzaev/baza",
|
||||
"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/baza/wiki",
|
||||
"issues": "https://git.svoboda.works/mirzaev/baza/issues"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"require": {
|
||||
|
@ -28,12 +29,12 @@
|
|||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"mirzaev\\csv\\": "mirzaev/csv/system/"
|
||||
"mirzaev\\baza\\": "mirzaev/baza/system/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"mirzaev\\csv\\tests\\": "mirzaev/csv/tests"
|
||||
"mirzaev\\baza\\tests\\": "mirzaev/baza/tests"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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": "a83cda6b5d1de267690c8b7e2a2ddf86",
|
||||
"packages": [],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\baza;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\baza\database,
|
||||
mirzaev\baza\enumerations\type;
|
||||
|
||||
// Built-in libraries
|
||||
use DomainException as exception_domain,
|
||||
LogicException as exception_logic,
|
||||
InvalidArgumentException as exceptiin_invalid_argument;
|
||||
|
||||
/**
|
||||
* Column
|
||||
*
|
||||
* @package mirzaev\baza
|
||||
*
|
||||
* @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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,646 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\baza;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\baza\enumerations\encoding,
|
||||
mirzaev\baza\enumerations\type;
|
||||
|
||||
// Built-in libraries
|
||||
use LogicException as exception_logic,
|
||||
InvalidArgumentException as exception_invalid_argument,
|
||||
RuntimeException as exception_runtime;
|
||||
|
||||
/**
|
||||
* Database
|
||||
*
|
||||
* @package mirzaev\baza
|
||||
*
|
||||
* @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.baza';
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// Declaring the buffer of unpacked values
|
||||
$unpacked = [];
|
||||
|
||||
foreach ($this->columns as $index => $column) {
|
||||
// Iterating over columns
|
||||
|
||||
// Initializing link to the binary value
|
||||
$binary = $binaries[$index] ?? null;
|
||||
|
||||
if ($column->type === type::string) {
|
||||
// String
|
||||
|
||||
// Unpacking the value
|
||||
$value = unpack($column->type->value . $column->length, $binary ?? str_repeat("\0", $column->length))[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 ?? "\0")[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Implementing the record
|
||||
$record = $this->record(...$unpacked);
|
||||
|
||||
// Exit (success)
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, 'c+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 2; */
|
||||
}
|
||||
|
||||
// Terminate loop when end of file is reached
|
||||
if (feof($file)) break;
|
||||
|
||||
try {
|
||||
// Unpacking the record
|
||||
$record = $this->unpack($binaries);
|
||||
|
||||
if ((bool) array_filter($record->values())) {
|
||||
// The record contains at least one non-empty value
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The record contains only empty values
|
||||
}
|
||||
} catch (exception_logic | exception_invalid_argument | exception_domain $exception) {
|
||||
// Writing into the buffer of failed to reading records
|
||||
|
||||
// Exit (fail)
|
||||
throw new exception_runtime('Failed to processing the record', previous: $exception);
|
||||
}
|
||||
}
|
||||
|
||||
// Unlocking the file
|
||||
flock($file, LOCK_UN);
|
||||
|
||||
// Closing the database file
|
||||
fclose($file);
|
||||
|
||||
// Exit (success)
|
||||
return $records;
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count
|
||||
*
|
||||
* @throws exception_runtime If the database is corrupted (counting result is float)
|
||||
*
|
||||
* @return int Amount of records
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
// Deleting the database file cache
|
||||
clearstatcache(true, $this->database);
|
||||
|
||||
// Counting
|
||||
$amount = $this->length > 0 && file_exists($this->database) ? filesize($this->database) / $this->length : 0;
|
||||
|
||||
// Exit (success/fail)
|
||||
return is_int($amount) ? $amount : throw new exception_runtime('The database is corrupted');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\baza\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\baza\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')
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\baza\enumerations;
|
||||
|
||||
// Build-in libraries
|
||||
use UnexpectedValueException as exception_unexpected_value;
|
||||
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* @see https://www.php.net/pack Types
|
||||
*
|
||||
* @package mirzaev\baza\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::string, type::short => 'string',
|
||||
type::char, 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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\baza;
|
||||
|
||||
// Built-in libraries
|
||||
use DomainException as exception_domain;
|
||||
|
||||
/**
|
||||
* Record
|
||||
*
|
||||
* @package mirzaev\baza
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @throws exception_domain if not found the parameter
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
if (isset($this->values[$name])) {
|
||||
// Initialized the parameter
|
||||
|
||||
// Writing the value and exit
|
||||
$this->values[$name] = $value;
|
||||
} else {
|
||||
// Not initialized the parameter
|
||||
|
||||
// Exit (fail)
|
||||
throw new exception_domain("Not found the parameter: $name");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
temporary/*
|
|
@ -0,0 +1,305 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use mirzaev\baza\database,
|
||||
mirzaev\baza\record,
|
||||
mirzaev\baza\column,
|
||||
mirzaev\baza\enumerations\encoding,
|
||||
mirzaev\baza\enumerations\type;
|
||||
|
||||
// Initializing path to the composer loader file (main project)
|
||||
$autoload =
|
||||
__DIR__ . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'vendor' . DIRECTORY_SEPARATOR .
|
||||
'autoload.php';
|
||||
|
||||
// Reinitializing path to the composer loaded file (depencendy project)
|
||||
if (!file_exists($autoload))
|
||||
$autoload =
|
||||
__DIR__ . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'..' . DIRECTORY_SEPARATOR .
|
||||
'autoload.php';
|
||||
|
||||
// Importing files of thr project and dependencies
|
||||
require($autoload);
|
||||
|
||||
// Initializing path to the database file
|
||||
$file = __DIR__ . DIRECTORY_SEPARATOR . 'temporary' . DIRECTORY_SEPARATOR . 'database.baza';
|
||||
|
||||
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),
|
||||
new column('active', type::char)
|
||||
)
|
||||
->connect($file);
|
||||
|
||||
echo '[' . ++$action . "] Initialized the database\n";
|
||||
|
||||
// Initializing the record
|
||||
$record = $database->record(
|
||||
'Arsen',
|
||||
'Mirzaev',
|
||||
24,
|
||||
165.5,
|
||||
1
|
||||
);
|
||||
|
||||
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 . '][' . ++$test . '][' . ($record->active === 1 ? 'SUCCESS' : 'FAIL') . "][\"active\"] Expected: \"1\" (integer). Actual: \"$record->active\" (" . gettype($record->active) . ")\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,
|
||||
0
|
||||
);
|
||||
|
||||
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,
|
||||
1
|
||||
);
|
||||
|
||||
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: fn(&$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";
|
|
@ -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 array|null $columns Columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(?array $columns = null)
|
||||
{
|
||||
// Initializing columns
|
||||
if (isset($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;
|
||||
}
|
||||
}
|
|
@ -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 string|null $row Row for converting to record instance parameters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(?string $row = null)
|
||||
{
|
||||
// Initializing parameters
|
||||
if (isset($row)) $this->parameters = static::deserialize($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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]);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(',"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";
|
Loading…
Reference in New Issue