429 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			429 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
namespace nur\sery\file;
 | 
						|
 | 
						|
use nur\sery\file\csv\csv_flavours;
 | 
						|
use nur\sery\NoMoreDataException;
 | 
						|
use nur\sery\os\EOFException;
 | 
						|
use nur\sery\os\IOException;
 | 
						|
use nur\sery\php\iter\AbstractIterator;
 | 
						|
use nur\sery\ref\file\csv\ref_csv;
 | 
						|
use nur\sery\str;
 | 
						|
use nur\sery\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 = fgets($fd);
 | 
						|
        if ($line !== false) $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);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * verrouiller le fichier en lecture de façon inconditionelle (ignorer la
 | 
						|
   * valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible
 | 
						|
   */
 | 
						|
  function lockRead(): void {
 | 
						|
    $this->lock(LOCK_SH);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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 $alreadyLocked=false): IReader {
 | 
						|
    if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_SH);
 | 
						|
    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, ?int $ifSerial=null): 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 $alreadyLocked=false): string {
 | 
						|
    $useLocking = $this->useLocking;
 | 
						|
    if ($useLocking && !$alreadyLocked) $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 $alreadyLocked=false) {
 | 
						|
    $args = [$this->getContents($close, $alreadyLocked)];
 | 
						|
    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();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  private function _rewindFd(): void {
 | 
						|
    $md = stream_get_meta_data($this->fd);
 | 
						|
    if ($md["seekable"]) $this->fseek(0);
 | 
						|
  }
 | 
						|
 | 
						|
  protected function _teardown(): void {
 | 
						|
    $this->_rewindFd();
 | 
						|
  }
 | 
						|
 | 
						|
  function rewind(): void {
 | 
						|
    # il faut toujours faire un rewind sur la resource, que l'itérateur aie été
 | 
						|
    # initialisé ou non
 | 
						|
    if ($this->_hasIteratorBeenSetup()) parent::rewind();
 | 
						|
    else $this->_rewindFd();
 | 
						|
  }
 | 
						|
 | 
						|
  #############################################################################
 | 
						|
  # Writer
 | 
						|
 | 
						|
  /** @throws IOException */
 | 
						|
  function ftruncate(int $size=0, bool $rewind=true): self {
 | 
						|
    $fd = $this->getResource();
 | 
						|
    IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError);
 | 
						|
    if ($rewind) rewind($fd);
 | 
						|
    return $this;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @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);
 | 
						|
  }
 | 
						|
 | 
						|
  /** @throws IOException */
 | 
						|
  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;
 | 
						|
  }
 | 
						|
 | 
						|
  function writeLines(?iterable $lines): IWriter {
 | 
						|
    if ($lines !== null) {
 | 
						|
      foreach ($lines as $line) {
 | 
						|
        $this->fwrite($line);
 | 
						|
        $this->fwrite("\n");
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return $this;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * verrouiller le fichier en écriture de façon inconditionelle (ignorer la
 | 
						|
   * valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible
 | 
						|
   */
 | 
						|
  function lockWrite(): void {
 | 
						|
    $this->lock(LOCK_EX);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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 $alreadyLocked=false): IWriter {
 | 
						|
    if ($this->useLocking && !$alreadyLocked) $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, ?int $ifSerial=null): 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 $alreadyLocked=false): void {
 | 
						|
    $useLocking = $this->useLocking;
 | 
						|
    if ($useLocking && !$alreadyLocked) $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 $alreadyLocked=false): void {
 | 
						|
    $this->putContents(serialize($object), $close, $alreadyLocked);
 | 
						|
  }
 | 
						|
}
 |