total rebuild + tests + more powerfull functions

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2024-11-23 23:42:59 +07:00
parent fb5738c4b0
commit ddf593e42d
7 changed files with 579 additions and 232 deletions

View File

@ -30,5 +30,11 @@
"psr-4": {
"mirzaev\\csv\\": "mirzaev/csv/system/"
}
},
"autoload-dev": {
"psr-4": {
"mirzaev\\csv\\tests\\": "mirzaev/csv/tests"
}
}
}

View File

@ -0,0 +1,240 @@
<?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;
}
}

View File

@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\csv\interfaces;
// Files of the project
use mirzaev\csv\traits\csv as csv_trait;
/**
* CSV
*
* Comma-Separated Values by RFC 4180
*
* @see https://tools.ietf.org/html/rfc4180
*
* @used-by csv_trait
* @package mirzaev\csv\interfaces
*
* @var string FILE Path to the database file
*
* @method void static write() Write to the database file
* @method array|null static read() Read from the database file
* @method string|false static serialize() Preparing data for writing to the database
* @method array|false static deserialize() Preparing data from the database to processing
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
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;
}

View File

@ -0,0 +1,237 @@
<?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]);
}
}

View File

@ -1,134 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\csv\traits;
// Files of the project
use mirzaev\csv\interfaces\csv as csv_interface;
// Built-in libraries
use exception;
/**
* CSV
*
* Comma-Separated Values by RFC 4180
*
* @see https://tools.ietf.org/html/rfc4180
*
* @uses csv_interface
* @package mirzaev\csv\traits
*
* @method static array|null read(int $rows, array &$errors) Read from the start of the database file
* @method static string|false serialize(array $parameters, bool $created) Preparing data for writing to the database
* @method static array|false deserialize(string $row) Preparing data from the database to processing
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
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];
}
}

View File

@ -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) {

View File

@ -0,0 +1,92 @@
<?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";