diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..1bf3031 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "mirzaev/csv", + "description": "Lightweight library for creating CSV databases", + "homepage": "https://git.mirzaev.sexy/mirzaev/csv", + "type": "library", + "keywords": [ + "csv", + "database" + ], + "readme": "README.md", + "license": "WTFPL", + "authors": [ + { + "name": "Arsen Mirzaev Tatyano-Muradovich", + "email": "arsen@mirzaev.sexy", + "homepage": "https://mirzaev.sexy", + "role": "Programmer" + } + ], + "support": { + "email": "arsen@mirzaev.sexy", + "wiki": "https://git.mirzaev.sexy/mirzaev/csv/wiki", + "issues": "https://git.mirzaev.sexy/mirzaev/csv/issues" + }, + "minimum-stability": "stable", + "require": { + "php": "^8.4" + }, + "autoload": { + "psr-4": { + "mirzaev\\csv\\": "mirzaev/csv/system/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100755 index 0000000..fde7627 --- /dev/null +++ b/composer.lock @@ -0,0 +1,20 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "70ef8045ba581d96d3a68483b6031a33", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/mirzaev/csv/system/interfaces/csv.php b/mirzaev/csv/system/interfaces/csv.php new file mode 100755 index 0000000..c225e07 --- /dev/null +++ b/mirzaev/csv/system/interfaces/csv.php @@ -0,0 +1,94 @@ + + */ +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/traits/csv.php b/mirzaev/csv/system/traits/csv.php new file mode 100755 index 0000000..84542eb --- /dev/null +++ b/mirzaev/csv/system/traits/csv.php @@ -0,0 +1,134 @@ + + */ +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 new file mode 100755 index 0000000..06c5ef0 --- /dev/null +++ b/mirzaev/csv/system/traits/file.php @@ -0,0 +1,103 @@ + + */ +trait file +{ + /** + * Read + * + * 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 $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 + { + 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; + } +}