nulib/php/src_file/base/Stream.php

401 lines
12 KiB
PHP

<?php
namespace nulib\file\base;
use nulib\file\csv\csv_flavours;
use nulib\file\IReader;
use nulib\file\IWriter;
use nulib\NoMoreDataException;
use nulib\os\EOFException;
use nulib\os\IOException;
use nulib\php\iter\AbstractIterator;
use nulib\ref\os\csv\ref_csv;
use nulib\str;
use nulib\ValueException;
/**
* Class Stream: lecture/écriture générique dans un flux
*/
class Stream extends AbstractIterator implements IReader, IWriter {
use TStreamFilter;
/** @var bool les opérations de verrouillages sont-elle activées? */
const USE_LOCKING = false;
/** @var resource */
protected $fd;
/** @var bool */
protected $close;
/** @var bool */
protected $throwOnError;
/** @var bool */
protected $useLocking;
/** @var int nombre d'octets à ignorer au début du fichier lors d'un seek */
protected $seekOffset = 0;
/** @var int|null */
protected $serial;
/** @var array */
protected $stat;
function __construct($fd, bool $close=true, bool $throwOnError=true, ?bool $useLocking=null) {
if ($fd === null) throw ValueException::null("resource");
$this->fd = $fd;
$this->close = $close;
$this->throwOnError = $throwOnError;
if ($useLocking === null) $useLocking = static::USE_LOCKING;
$this->useLocking = $useLocking;
}
#############################################################################
# File
/**
* @return resource|null retourner la resource associée à ce fichier si cela
* a du sens
*/
function getResource() {
IOException::ensure_open($this->fd === null);
$this->_streamAppendFilters($this->fd);
return $this->fd;
}
protected function lock(int $operation, ?int &$wouldBlock=null): bool {
$locked = flock($this->getResource(), $operation, $wouldBlock);
if ($locked) return true;
if ($operation & LOCK_NB) return false;
else throw IOException::error();
}
protected function unlock(bool $close=false): void {
if ($this->fd !== null) {
flock($this->fd, LOCK_UN);
if ($close) $this->close();
}
}
function isatty(): bool {
return stream_isatty($this->getResource());
}
/** obtenir des informations sur le fichier */
function fstat(bool $reload=false): array {
if ($this->stat === null || $reload) {
$fd = $this->getResource();
$this->stat = IOException::ensure_valid(fstat($fd), $this->throwOnError);
}
return $this->stat;
}
function getSize(?int $seekOffset=null): int {
if ($seekOffset === null) $seekOffset = $this->seekOffset;
return $this->fstat()["size"] - $seekOffset;
}
/** @throws IOException */
function ftell(?int $seekOffset=null): int {
$fd = $this->getResource();
if ($seekOffset === null) $seekOffset = $this->seekOffset;
return IOException::ensure_valid(ftell($fd), $this->throwOnError) - $seekOffset;
}
/**
* @return int la position après avoir déplacé le pointeur
* @throws IOException
*/
function fseek(int $offset, int $whence=SEEK_SET, ?int $seekOffset=null): int {
$fd = $this->getResource();
if ($seekOffset === null) $seekOffset = $this->seekOffset;
if ($whence === SEEK_SET) $offset += $seekOffset;
IOException::ensure_valid(fseek($fd, $offset, $whence), $this->throwOnError, -1);
return $this->ftell($seekOffset);
}
function seek(int $offset, int $whence=SEEK_SET): self {
$this->fseek($offset, $whence);
return $this;
}
/** fermer le fichier si c'est nécessaire */
function close(bool $close=true, ?int $ifSerial=null): void {
AbstractIterator::rewind();
if ($this->fd !== null && $close && $this->close && ($ifSerial === null || $this->serial === $ifSerial)) {
fclose($this->fd);
$this->fd = null;
}
}
function copyTo(IWriter $dest, bool $closeWriter=false, bool $closeReader=true): void {
$srcr = $this->getResource();
$destr = $dest->getResource();
if ($srcr !== null && $destr !== null) {
while (!feof($srcr)) {
fwrite($destr, fread($srcr, 8192));
}
} else {
$dest->fwrite($this->getContents(false));
}
if ($closeWriter) $dest->close();
if ($closeReader) $this->close();
}
const DEFAULT_CSV_FLAVOUR = ref_csv::OO_FLAVOUR;
/** @var array paramètres pour la lecture et l'écriture de flux au format CSV */
protected $csvFlavour;
function setCsvFlavour(string $flavour): void {
$this->csvFlavour = csv_flavours::verifix($flavour);
}
protected function getCsvParams($fd): array {
$flavour = $this->csvFlavour;
if ($flavour === null) {
if ($fd === null) {
# utiliser la valeur par défaut
$flavour = static::DEFAULT_CSV_FLAVOUR;
} else {
# il faut déterminer le type de fichier CSV en lisant la première ligne
$pos = IOException::ensure_valid(ftell($fd));
$line = IOException::ensure_valid(fgets($fd));
$line = strpbrk($line, ",;\t");
if ($line === false) {
# aucun séparateur trouvé, prender la valeur par défaut
$flavour = static::DEFAULT_CSV_FLAVOUR;
} else {
$flavour = substr($line, 0, 1);
$flavour = csv_flavours::verifix($flavour);
}
IOException::ensure_valid(fseek($fd, $pos), true, -1);
}
$this->csvFlavour = $flavour;
}
return csv_flavours::get_params($flavour);
}
#############################################################################
# Reader
/** @throws IOException */
function fread(int $length): string {
$fd = $this->getResource();
return IOException::ensure_valid(fread($fd, $length), $this->throwOnError);
}
/**
* lire la prochaine ligne. la ligne est retournée avec le caractère de fin
* de ligne[\r]\n
*
* @throws EOFException si plus aucune ligne n'est disponible
* @throws IOException si une erreur se produit
*/
function fgets(?int $length=null): string {
$fd = $this->getResource();
if ($length === null) $r = fgets($fd);
else $r = fgets($fd, $length);
return EOFException::ensure_not_eof($r, $this->throwOnError);
}
/** @throws IOException */
function fpassthru(): int {
$fd = $this->getResource();
return IOException::ensure_valid(fpassthru($fd), $this->throwOnError);
}
/**
* retourner la prochaine ligne au format CSV ou null si le fichier est arrivé
* à sa fin
*/
function fgetcsv(): ?array {
$fd = $this->getResource();
$params = $this->getCsvParams($fd);
$row = fgetcsv($fd, 0, $params[0], $params[1], $params[2]);
if ($row === false && feof($fd)) return null;
return IOException::ensure_valid($row, $this->throwOnError);
}
/**
* lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin
* de ligne [\r]\n
*
* @throws EOFException si plus aucune ligne n'est disponible
* @throws IOException si une erreur se produit
*/
function readLine(): ?string {
return str::strip_nl($this->fgets());
}
/** lire et retourner toutes les lignes */
function readLines(): array {
return iterator_to_array($this);
}
/**
* essayer de verrouiller le fichier en lecture. retourner true si l'opération
* réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument
* true
*/
function canRead(): bool {
if ($this->useLocking) return $this->lock(LOCK_SH + LOCK_NB);
else return true;
}
/**
* verrouiller en mode partagé puis retourner un objet permettant de lire le
* fichier.
*/
function getReader(bool $lockedByCanRead=false): IReader {
if ($this->useLocking && !$lockedByCanRead) $this->lock(LOCK_SH);
return new class($this->fd, ++$this->serial, $this) extends Stream {
function __construct($fd, int $serial, Stream $parent) {
$this->parent = $parent;
parent::__construct($fd);
}
/** @var Stream */
private $parent;
function close(bool $close=true): void {
if ($this->parent !== null && $close) {
$this->parent->close(true, $this->serial);
$this->fd = null;
$this->parent = null;
}
}
};
}
/** retourner le contenu du fichier sous forme de chaine */
function getContents(bool $close=true, bool $lockedByCanRead=false): string {
$useLocking = $this->useLocking;
if ($useLocking && !$lockedByCanRead) $this->lock(LOCK_SH);
try {
return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError);
} finally {
if ($useLocking) $this->unlock($close);
elseif ($close) $this->close();
}
}
function unserialize(?array $options=null, bool $close=true, bool $lockedByCanRead=false) {
$args = [$this->getContents($lockedByCanRead)];
if ($options !== null) $args[] = $options;
return unserialize(...$args);
}
#############################################################################
# Iterator
protected function _setup(): void {
}
protected function _next(&$key) {
try {
return $this->fgets();
} catch (EOFException $e) {
throw new NoMoreDataException();
}
}
protected function _teardown(): void {
$md = stream_get_meta_data($this->fd);
if ($md["seekable"]) $this->fseek(0);
}
#############################################################################
# Writer
/** @throws IOException */
function fwrite(string $data, ?int $length=null): int {
$fd = $this->getResource();
if ($length === null) $r = fwrite($fd, $data);
else $r = fwrite($fd, $data, $length);
return IOException::ensure_valid($r, $this->throwOnError);
}
function fputcsv(array $row): void {
$fd = $this->getResource();
$params = $this->getCsvParams($fd);
IOException::ensure_valid(fputcsv($fd, $row, $params[0], $params[1], $params[2]));
}
/** @throws IOException */
function fflush(): self {
$fd = $this->getResource();
IOException::ensure_valid(fflush($fd), $this->throwOnError);
return $this;
}
/** @throws IOException */
function ftruncate(int $size): self {
$fd = $this->getResource();
IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError);
return $this;
}
function writeLines(?iterable $lines): IWriter {
if ($lines !== null) {
foreach ($lines as $line) {
$this->fwrite($line);
$this->fwrite("\n");
}
}
return $this;
}
/**
* essayer de verrouiller le fichier en écriture. retourner true si l'opération
* réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument
* true
*/
function canWrite(): bool {
if ($this->useLocking) return $this->lock(LOCK_EX + LOCK_NB);
else return true;
}
/**
* verrouiller en mode exclusif puis retourner un objet permettant d'écrire
* dans le fichier
*/
function getWriter(bool $lockedByCanWrite=false): IWriter {
if ($this->useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX);
return new class($this->fd, ++$this->serial, $this) extends Stream {
function __construct($fd, int $serial, Stream $parent) {
$this->parent = $parent;
$this->serial = $serial;
parent::__construct($fd);
}
/** @var Stream */
private $parent;
function close(bool $close=true): void {
if ($this->parent !== null && $close) {
$this->parent->close(true, $this->serial);
$this->fd = null;
$this->parent = null;
}
}
};
}
function putContents(string $contents, bool $close=true, bool $lockedByCanWrite=false): void {
$useLocking = $this->useLocking;
if ($useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX);
try {
$this->fwrite($contents);
} finally {
if ($useLocking) $this->unlock($close);
elseif ($close) $this->close();
}
}
function serialize($object, bool $close=true, bool $lockedByCanWrite=false): void {
$this->putContents(serialize($object), $lockedByCanWrite);
}
}