From ddf593e42df13c39c0b2fc9d7eabb794b8cf0f85 Mon Sep 17 00:00:00 2001 From: mirzaev Date: Sat, 23 Nov 2024 23:42:59 +0700 Subject: [PATCH] total rebuild + tests + more powerfull functions --- composer.json | 8 +- mirzaev/csv/system/database.php | 240 ++++++++++++++++++++++++++ mirzaev/csv/system/interfaces/csv.php | 94 ---------- mirzaev/csv/system/record.php | 237 +++++++++++++++++++++++++ mirzaev/csv/system/traits/csv.php | 134 -------------- mirzaev/csv/system/traits/file.php | 6 +- mirzaev/csv/tests/record.php | 92 ++++++++++ 7 files changed, 579 insertions(+), 232 deletions(-) create mode 100644 mirzaev/csv/system/database.php delete mode 100755 mirzaev/csv/system/interfaces/csv.php create mode 100644 mirzaev/csv/system/record.php delete mode 100755 mirzaev/csv/system/traits/csv.php create mode 100644 mirzaev/csv/tests/record.php diff --git a/composer.json b/composer.json index 1bf3031..e11a811 100755 --- a/composer.json +++ b/composer.json @@ -30,5 +30,11 @@ "psr-4": { "mirzaev\\csv\\": "mirzaev/csv/system/" } - } + }, + "autoload-dev": { + "psr-4": { + "mirzaev\\csv\\tests\\": "mirzaev/csv/tests" + } + } + } diff --git a/mirzaev/csv/system/database.php b/mirzaev/csv/system/database.php new file mode 100644 index 0000000..215ea94 --- /dev/null +++ b/mirzaev/csv/system/database.php @@ -0,0 +1,240 @@ + + */ + +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; + } +} diff --git a/mirzaev/csv/system/interfaces/csv.php b/mirzaev/csv/system/interfaces/csv.php deleted file mode 100755 index c225e07..0000000 --- a/mirzaev/csv/system/interfaces/csv.php +++ /dev/null @@ -1,94 +0,0 @@ - - */ -interface csv -{ - /** - * 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'; - - /** - * Write - * - * Write to the database file - * - * @return void - */ - public static function write(): void; - - /** - * Read - * - * Read from the start of the database file - * - * @param int $rows Amount of rows for reading - * - * @return array|null Readed records - */ - public static function read(int $rows = 1): ?array; - - /** - * Last - * - * Read from the end of the database file - * - * @param int $rows Amount of rows for reading - * - * @return array|null Readed records - */ - public static function last(int $rows = 1): ?array; - - /** - * Serialize - * - * Preparing data for writing to the database - * - * @param array $parameters Values for serializing - * - * @return string|false Serialized data - */ - public static function serialize(array $parameters): string|false; - - /** - * Deserialize - * - * Preparing data from the database to processing - * - * @param string $row Record for deserializing - * - * @return array|false Serialized data - */ - public static function deserialize(string $row): array|false; -} diff --git a/mirzaev/csv/system/record.php b/mirzaev/csv/system/record.php new file mode 100644 index 0000000..9a3ea43 --- /dev/null +++ b/mirzaev/csv/system/record.php @@ -0,0 +1,237 @@ + + */ +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('/(.*)(?>(?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]); + } + +} diff --git a/mirzaev/csv/system/traits/csv.php b/mirzaev/csv/system/traits/csv.php deleted file mode 100755 index 84542eb..0000000 --- a/mirzaev/csv/system/traits/csv.php +++ /dev/null @@ -1,134 +0,0 @@ - - */ -trait csv -{ - /** - * Read - * - * Read from the start of the database file - * - * @param int $rows Amount of rows for reading - * @param array &$errors Buffer of errors - * - * @return array|null Readed records - */ - public static function read(int $rows = 0, &$errors = []): ?array - { - try { - // Initializing the buffer of readed records - $records = []; - - // Opening the file with views records - $file = fopen(static::FILE, 'c+'); - - while (--$rows >= 0 && ($row = fgets($file, 4096)) !== false) { - // Iterating over rows (records) - - // Deserealizing record - $deserialized = static::deserialize($row); - - if ($deserialized) { - // Deserialized record - - // Writing to the buffer of readed records - $records[] = $deserialized; - } - } - - // Closing file with views records - 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; - } - - /** - * Serialize - * - * Preparing data for writing to the database - * - * @param array $parameters Values for serializing - * @param bool $created Add date of creating at the end? - * - * @return string|false Serialized data - */ - public static function serialize(array $parameters, bool $created = false): string|false - { - // Declaring the buffer of serialized values - $serialized = ''; - - // Sanitizing values - foreach ($parameters as $value) $serialized .= ',' . preg_replace('/(?<=[^^])"(?=[^$])/', '""', preg_replace('/(?<=[^^]),(?=[^$])/', '\,', $value ?? '')); - - // Writing date of creating to the buffer of serialized values - if ($created) $serialized .= ',' . time(); - - // Trimming excess first comma in the buffer of serialized values - $serialized = mb_substr($serialized, 1, mb_strlen($serialized)); - - // Exit (success/fail) - return empty($serialized) ? false : $serialized; - } - - /** - * Deserialize - * - * Preparing data from the database to processing - * - * @param string $row Record for deserializing - * - * @return array|false Deserialized data - */ - public static function deserialize(string $row): array|false - { - // Separating row by commas - preg_match_all('/(?:^|,)(?=[^"]|(")?)"?((?(1)[^"]*|[^,"]*))"?(?=,|$)/', $row, $matches); - - // Converting double quotes to single quotes - foreach ($matches[2] as &$match) - if (empty($match = preg_replace('/[\n\r]/', '', preg_replace('/""/', '"', preg_replace('/\\\,/', ',', trim((string) $match, '"')))))) - $match = null; - - // Exit (success/fail) - return empty($matches[2]) ? false : $matches[2]; - } -} diff --git a/mirzaev/csv/system/traits/file.php b/mirzaev/csv/system/traits/file.php index 06c5ef0..31b4a2b 100755 --- a/mirzaev/csv/system/traits/file.php +++ b/mirzaev/csv/system/traits/file.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace mirzaev\site\repression\models\traits; +namespace mirzaev\csv\traits; // Built-in libraries use Exception as exception, @@ -26,15 +26,15 @@ trait file * Read the file * * @param resource $file Pointer to the file (fopen()) - * @param int $offset Offset of rows for start reading * @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 $offset = 0, int $rows = 10, int $position = 0, int $step = 1, array &$errors = []): 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) { diff --git a/mirzaev/csv/tests/record.php b/mirzaev/csv/tests/record.php new file mode 100644 index 0000000..7706f59 --- /dev/null +++ b/mirzaev/csv/tests/record.php @@ -0,0 +1,92 @@ +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";