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); } }