diff --git a/Filesystem/AbstractCommonResource.php b/Filesystem/AbstractCommonResource.php new file mode 100644 index 00000000..4a5f5698 --- /dev/null +++ b/Filesystem/AbstractCommonResource.php @@ -0,0 +1,191 @@ +handler = $resource; + } + + /** + * @return resource + */ + public function getHandler() + { + return $this->handler; + } + + /** + * Warning! This method will rewind the file to the beginning before and after counting the lines! + * + * @throws \RuntimeException + * + * @return int + */ + public function getLineCount(): int + { + if (null === $this->lineCount) { + $this->rewind(); + $line = 0; + while (!$this->isEndOfFile()) { + ++$line; + $this->readRaw(); + } + $this->rewind(); + + $this->lineCount = $line; + } + + return $this->lineCount; + } + + /** + * {@inheritDoc} + */ + public function getLineNumber(): int + { + if ($this->seekCalled) { + throw new \LogicException('Cannot get current line number after calling "seek": the line number is lost'); + } + + return $this->lineNumber; + } + + /** + * @throws \RuntimeException + * + * @return bool + */ + public function isEndOfFile(): bool + { + $this->assertOpened(); + + return feof($this->handler); + } + + /** + * Warning, this function will return exactly the same value as the internal function used by the subsystem. + * + * @param null|int $length + * + * @throws \RuntimeException + * + * @return mixed + */ + abstract public function readRaw($length = null); + + /** + * This methods rewinds the file to the first line of data, skipping the headers. + * + * @throws \RuntimeException + */ + public function rewind(): void + { + $this->assertOpened(); + if (!rewind($this->handler)) { + throw new \RuntimeException("Unable to rewind '{$this->getResourceName()}'"); + } + $this->lineNumber = 1; + } + + /** + * @throws \RuntimeException + * + * @return int + */ + public function tell(): int + { + $this->assertOpened(); + + return ftell($this->handler); + } + + /** + * @param int $offset + * + * @throws \RuntimeException + * + * @return int + */ + public function seek($offset): int + { + $this->assertOpened(); + $this->seekCalled = true; + + return fseek($this->handler, $offset); + } + + /** + * @return bool + */ + public function close(): bool + { + if ($this->closed) { + return true; + } + + $this->closed = fclose($this->handler); + + return $this->closed; + } + + /** + * @return bool + */ + public function isClosed(): bool + { + return $this->closed; + } + + /** + * Closes the resource when the object is destroyed. + */ + public function __destruct() + { + $this->close(); + } + + /** + * @throws \RuntimeException + */ + protected function assertOpened(): void + { + if ($this->closed) { + throw new \RuntimeException("{$this->getResourceName()} was closed earlier"); + } + } + + /** + * @return string + */ + abstract protected function getResourceName(): string; +} diff --git a/Filesystem/CsvFile.php b/Filesystem/CsvFile.php index 360bfff3..47979a48 100644 --- a/Filesystem/CsvFile.php +++ b/Filesystem/CsvFile.php @@ -18,6 +18,8 @@ */ class CsvFile extends CsvResource { + use FileHelperTrait; + /** @var string */ protected $filePath; @@ -41,18 +43,8 @@ public function __construct( $mode = 'rb' ) { $this->filePath = $filePath; + $resource = $this->openResource($filePath, $mode); - if (!\in_array($filePath, ['php://stdin', 'php://stdout', 'php://stderr'])) { - $dirname = \dirname($this->filePath); - if (!@mkdir($dirname, 0755, true) && !is_dir($dirname)) { - throw new \RuntimeException(sprintf('Directory "%s" was not created', $dirname)); - } - } - - $resource = fopen($filePath, $mode); - if (false === $resource) { - throw new \UnexpectedValueException("Unable to open file: '{$filePath}' in {$mode} mode"); - } // All modes allowing file reading, binary safe modes are handled by stripping out the b during test $readAllowedModes = ['r', 'r+', 'w+', 'a+', 'x+', 'c+']; if (null === $headers && !\in_array(str_replace('b', '', $mode), $readAllowedModes, true)) { diff --git a/Filesystem/CsvResource.php b/Filesystem/CsvResource.php index 143a62e0..44d328ae 100644 --- a/Filesystem/CsvResource.php +++ b/Filesystem/CsvResource.php @@ -16,7 +16,7 @@ * @author Valentin Clavreul * @author Vincent Chalnot */ -class CsvResource implements WritableStructuredFileInterface, SeekableFileInterface +class CsvResource extends AbstractCommonResource implements WritableStructuredFileInterface, SeekableFileInterface { /** @var string */ protected $delimiter; @@ -27,12 +27,6 @@ class CsvResource implements WritableStructuredFileInterface, SeekableFileInterf /** @var string */ protected $escape; - /** @var resource */ - protected $handler; - - /** @var int|null */ - protected $lineCount; - /** @var array */ protected $headers; @@ -42,15 +36,6 @@ class CsvResource implements WritableStructuredFileInterface, SeekableFileInterf /** @var int */ protected $headerCount; - /** @var int */ - protected $lineNumber = 1; - - /** @var bool */ - protected $closed; - - /** @var bool */ - protected $seekCalled = false; - /** * @param resource $resource * @param string $delimiter CSV delimiter @@ -67,15 +52,12 @@ public function __construct( $escape = '\\', array $headers = null ) { - if (!\is_resource($resource)) { - $type = \gettype($resource); - throw new \UnexpectedValueException("Resource argument must be a resource, '{$type}' given"); - } + parent::__construct($resource); + $this->delimiter = $delimiter; $this->enclosure = $enclosure; $this->escape = $escape; - $this->handler = $resource; $this->headers = $this->parseHeaders($headers); $this->headerCount = \count($this->headers); } @@ -104,38 +86,6 @@ public function getEscape(): string return $this->escape; } - /** - * @return resource - */ - public function getHandler() - { - return $this->handler; - } - - /** - * Warning! This method will rewind the file to the beginning before and after counting the lines! - * - * @throws \RuntimeException - * - * @return int - */ - public function getLineCount(): int - { - if (null === $this->lineCount) { - $this->rewind(); - $line = 0; - while (!$this->isEndOfFile()) { - ++$line; - $this->readRaw(); - } - $this->rewind(); - - $this->lineCount = $line; - } - - return $this->lineCount; - } - /** * @return array */ @@ -162,30 +112,6 @@ public function writeHeaders(): void $this->writeRaw($this->headers); } - /** - * {@inheritDoc} - */ - public function getLineNumber(): int - { - if ($this->seekCalled) { - throw new \LogicException('Cannot get current line number after calling "seek": the line number is lost'); - } - - return $this->lineNumber; - } - - /** - * @throws \RuntimeException - * - * @return bool - */ - public function isEndOfFile(): bool - { - $this->assertOpened(); - - return feof($this->handler); - } - /** * Warning, this function will return exactly the same value as the fgetcsv() function. * @@ -278,7 +204,7 @@ public function writeLine(array $fields): int $parsedFields = []; foreach ($this->headers as $column) { - if (!array_key_exists($column, $fields)) { + if (!\array_key_exists($column, $fields)) { $message = "Missing column {$column} in given fields for {$this->getResourceName()}"; throw new \UnexpectedValueException($message); } @@ -300,57 +226,12 @@ public function writeLine(array $fields): int */ public function rewind(): void { - $this->assertOpened(); - if (!rewind($this->handler)) { - throw new \RuntimeException("Unable to rewind '{$this->getResourceName()}'"); - } - $this->lineNumber = 1; + parent::rewind(); if (!$this->manualHeaders) { $this->readRaw(); // skip headers if not manual headers } } - /** - * @throws \RuntimeException - * - * @return int - */ - public function tell(): int - { - $this->assertOpened(); - - return ftell($this->handler); - } - - /** - * @param int $offset - * - * @throws \RuntimeException - * - * @return int - */ - public function seek($offset): int - { - $this->assertOpened(); - $this->seekCalled = true; - - return fseek($this->handler, $offset); - } - - /** - * @return bool - */ - public function close(): bool - { - if ($this->closed) { - return true; - } - - $this->closed = fclose($this->handler); - - return $this->closed; - } - /** * @return bool */ @@ -359,32 +240,6 @@ public function isManualHeaders(): bool return $this->manualHeaders; } - /** - * @return bool - */ - public function isClosed(): bool - { - return $this->closed; - } - - /** - * Closes the resource when the object is destroyed. - */ - public function __destruct() - { - $this->close(); - } - - /** - * @throws \RuntimeException - */ - protected function assertOpened(): void - { - if ($this->closed) { - throw new \RuntimeException("{$this->getResourceName()} was closed earlier"); - } - } - /** * @param array $headers * diff --git a/Filesystem/File.php b/Filesystem/File.php new file mode 100644 index 00000000..266b79d9 --- /dev/null +++ b/Filesystem/File.php @@ -0,0 +1,60 @@ + + * @author Vincent Chalnot + */ +class File extends FileResource +{ + use FileHelperTrait; + + /** @var string */ + protected $filePath; + + /** + * @param string $filePath Also accept a resource + * @param string $mode Same parameter as the mode in the fopen function (r, w, a, etc.) + * + * @throws \RuntimeException + * @throws \UnexpectedValueException + */ + public function __construct( + $filePath, + $mode = 'rb' + ) { + $this->filePath = $filePath; + $resource = $this->openResource($filePath, $mode); + + parent::__construct($resource); + } + + /** + * Will return a resource if the file was created using a resource + * + * @return string|resource + */ + public function getFilePath(): string + { + return $this->filePath; + } + + /** + * @return string + */ + protected function getResourceName(): string + { + return "file '{$this->filePath}'"; + } +} diff --git a/Filesystem/FileHelperTrait.php b/Filesystem/FileHelperTrait.php new file mode 100644 index 00000000..bd38c448 --- /dev/null +++ b/Filesystem/FileHelperTrait.php @@ -0,0 +1,43 @@ + + * @author Vincent Chalnot + */ +trait FileHelperTrait +{ + /** + * @param string $filePath + * @param string $mode + * + * @return resource + */ + protected function openResource(string $filePath, string $mode) + { + if (!\in_array($filePath, ['php://stdin', 'php://stdout', 'php://stderr'])) { + $dirname = \dirname($filePath); + if (!@mkdir($dirname, 0755, true) && !is_dir($dirname)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $dirname)); + } + } + + $resource = fopen($filePath, $mode); + if (false === $resource) { + throw new \UnexpectedValueException("Unable to open file: '{$filePath}' in {$mode} mode"); + } + + return $resource; + } +} diff --git a/Filesystem/FileInterface.php b/Filesystem/FileInterface.php new file mode 100644 index 00000000..a0ed12d0 --- /dev/null +++ b/Filesystem/FileInterface.php @@ -0,0 +1,24 @@ + + * @author Vincent Chalnot + */ +class FileResource extends AbstractCommonResource implements FileInterface, WritableFileInterface, SeekableFileInterface +{ + /** + * Warning, this function will return exactly the same value as the fgets() function. + * + * @param null|int $length + * + * @throws \RuntimeException + * + * @return string|false + */ + public function readRaw($length = null) + { + $this->assertOpened(); + ++$this->lineNumber; + + return fgets($this->handler, $length); + } + + /** + * @param int|null $length + * + * @throws \UnexpectedValueException + * @throws \RuntimeException + * + * @return string|null + */ + public function readLine($length = null): ?string + { + if ($this->seekCalled) { + $filePosition = "at position {$this->tell()}"; + } else { + $filePosition = "on line {$this->getLineNumber()}"; + } + $line = $this->readRaw($length); + + if (false === $line) { + if ($this->isEndOfFile()) { + return null; + } + $message = "Unable to read line {$filePosition} for {$this->getResourceName()}"; + throw new \UnexpectedValueException($message); + } + + return $line; + } + + /** + * Warning, this function will return exactly the same value as the fwrite() function. + * + * @param string $line + * + * @throws \RuntimeException + * + * @return int + */ + public function writeRaw(string $line): int + { + $this->assertOpened(); + ++$this->lineNumber; + + return fwrite($this->handler, $line); + } + + /** + * @param string $line + * + * @throws \RuntimeException + * + * @return int + */ + public function writeLine(string $line): int + { + $length = $this->writeRaw($line); + if (false === $length) { + throw new \RuntimeException("Unable to write data to {$this->getResourceName()}"); + } + + return $length; + } + + /** + * @return string + */ + protected function getResourceName(): string + { + return "File resource '{$this->handler}'"; + } +} diff --git a/Filesystem/FileStreamInterface.php b/Filesystem/FileStreamInterface.php index 3ab9d50c..2aa00825 100644 --- a/Filesystem/FileStreamInterface.php +++ b/Filesystem/FileStreamInterface.php @@ -32,13 +32,6 @@ public function getLineNumber(): int; */ public function isEndOfFile(): bool; - /** - * @param int|null $length - * - * @return array|null - */ - public function readLine($length = null): ?array; - /** * This methods rewinds the file to the first line of data, skipping the headers. */ diff --git a/Filesystem/JsonStreamFile.php b/Filesystem/JsonStreamFile.php index f971fc0c..401a1122 100644 --- a/Filesystem/JsonStreamFile.php +++ b/Filesystem/JsonStreamFile.php @@ -13,7 +13,7 @@ /** * Wrapper around JSON files to read them in a stream */ -class JsonStreamFile implements FileStreamInterface, WritableFileInterface +class JsonStreamFile implements FileStreamInterface { /** @var \SplFileObject */ protected $file; diff --git a/Filesystem/StructuredFileInterface.php b/Filesystem/StructuredFileInterface.php index 14732a6c..2d72572e 100644 --- a/Filesystem/StructuredFileInterface.php +++ b/Filesystem/StructuredFileInterface.php @@ -15,6 +15,13 @@ */ interface StructuredFileInterface extends FileStreamInterface { + /** + * @param int|null $length + * + * @return array|null + */ + public function readLine($length = null): ?array; + /** * @return array */ diff --git a/Filesystem/WritableFileInterface.php b/Filesystem/WritableFileInterface.php index 0ad94cbd..55a6a37b 100644 --- a/Filesystem/WritableFileInterface.php +++ b/Filesystem/WritableFileInterface.php @@ -11,14 +11,14 @@ namespace CleverAge\ProcessBundle\Filesystem; /** - * Define a common interface for all file reading systems + * Defines the interface for basic file writing systems (not structured) */ interface WritableFileInterface extends FileStreamInterface { /** - * @param array $fields + * @param string $string * * @return int */ - public function writeLine(array $fields): int; + public function writeLine(string $string): int; } diff --git a/Filesystem/WritableStructuredFileInterface.php b/Filesystem/WritableStructuredFileInterface.php index 710a8aa9..ca79f86f 100644 --- a/Filesystem/WritableStructuredFileInterface.php +++ b/Filesystem/WritableStructuredFileInterface.php @@ -13,8 +13,15 @@ /** * Define a common interface for all file with headers */ -interface WritableStructuredFileInterface extends StructuredFileInterface, WritableFileInterface +interface WritableStructuredFileInterface extends StructuredFileInterface { + /** + * @param array $fields + * + * @return int + */ + public function writeLine(array $fields): int; + /** * Write headers to the file *