modifs.mineures sans commentaires

This commit is contained in:
Jephté Clain 2024-04-25 17:46:18 +04:00
parent 451b2768e6
commit 5460903a9f
64 changed files with 3537 additions and 48 deletions

View File

@ -13,6 +13,8 @@
},
"require-dev": {
"nulib/tests": "7.4",
"ext-posix": "*",
"ext-pcntl": "*",
"ext-curl": "*"
},
"autoload": {
@ -20,6 +22,9 @@
"nulib\\": "php/src_base",
"nulib\\ref\\": "php/src_ref",
"nulib\\php\\": "php/src_php",
"nulib\\os\\": "php/src_os",
"nulib\\file\\": "php/src_file",
"nulib\\values\\": "php/src_values",
"nulib\\output\\": "php/src_output",
"nulib\\web\\": "php/src_web"
}

16
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d0cd1ea89a82b87aa4a078dcea524293",
"content-hash": "0f0743123abf677caf20c5e71ece9616",
"packages": [],
"packages-dev": [
{
@ -665,16 +665,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.18",
"version": "9.6.19",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04"
"reference": "a1a54a473501ef4cdeaae4e06891674114d79db8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04",
"reference": "32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8",
"reference": "a1a54a473501ef4cdeaae4e06891674114d79db8",
"shasum": ""
},
"require": {
@ -748,7 +748,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.18"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19"
},
"funding": [
{
@ -764,7 +764,7 @@
"type": "tidelift"
}
],
"time": "2024-03-21T12:07:32+00:00"
"time": "2024-04-05T04:35:58+00:00"
},
{
"name": "sebastian/cli-parser",
@ -1789,6 +1789,8 @@
"php": ">=7.4"
},
"platform-dev": {
"ext-posix": "*",
"ext-pcntl": "*",
"ext-curl": "*"
},
"plugin-api-version": "2.2.0"

View File

@ -1,8 +1,6 @@
<?php
namespace nulib;
use Exception;
/**
* Class AccessException: indiquer que la resource ou l'objet auquel on veut
* accéder n'est pas accessible. il s'agit donc d'une erreur de l'utilisateur

View File

@ -0,0 +1,10 @@
<?php
namespace nulib;
use RuntimeException;
/**
* Class NoMoreDataException: indiquer que plus aucune donnée n'est disponible
*/
class NoMoreDataException extends RuntimeException {
}

View File

@ -11,8 +11,14 @@ class ValueException extends UserException {
} elseif (is_array($value)) {
$values = $value;
$parts = [];
foreach ($values as $value) {
$parts[] = self::value($value);
$index = 0;
foreach ($values as $key => $value) {
if ($key === $index) {
$index++;
$parts[] = self::value($value);
} else {
$parts[] = "$key=>".self::value($value);
}
}
return "[" . implode(", ", $parts) . "]";
} elseif (is_string($value)) {

View File

@ -141,6 +141,15 @@ class cl {
return $array !== null? array_keys($array): [];
}
/**
* retourner la première valeur de $array ou $default si le tableau est null
* ou vide
*/
static final function first($array, $default=null) {
if (is_array($array)) return $array[array_key_first($array)];
return $default;
}
#############################################################################
/**

View File

@ -79,6 +79,15 @@ class cv {
#############################################################################
/** échanger les deux valeurs */
static final function swap(&$a, &$b): void {
$tmp = $a;
$a = $b;
$b = $tmp;
}
#############################################################################
/** mettre à jour $dest avec $value si $cond($value) est vrai */
static final function set_if(&$dest, $value, callable $cond) {
if ($cond($value)) $dest = $value;

86
php/src_base/file.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace nulib;
use nulib\file\base\FileReader;
use nulib\file\base\FileWriter;
use nulib\file\base\MemoryStream;
use nulib\file\base\SharedFile;
use nulib\file\base\TempStream;
use nulib\file\base\TmpfileWriter;
/**
* Class file: outils pour gérer les fichiers
*/
class file {
static function reader($input, ?callable $func=null): FileReader {
$file = new FileReader($input);
if ($func !== null) {
try {
$func($file);
} finally {
$file->close();
}
}
return $file;
}
static function writer($output, ?string $mode=null, ?callable $func=null): FileWriter {
$file = new FileWriter($output, $mode);
if ($func !== null) {
try {
$func($file);
} finally {
$file->close();
}
}
return $file;
}
static function shared($file, ?callable $func=null): SharedFile {
$file = new SharedFile($file);
if ($func !== null) {
try {
$func($file);
} finally {
$file ->close();
}
}
return $file;
}
static function tmpwriter($destdir=null, ?callable $func=null): TmpfileWriter {
$file = new TmpfileWriter($destdir);
if ($func !== null) {
try {
$func($file);
} finally {
$file->close();
}
}
return $file;
}
static function memory(?callable $func=null): MemoryStream {
$file = new MemoryStream();
if ($func !== null) {
try {
$func($file);
} finally {
$file->close();
}
}
return $file;
}
static function temp(?callable $func=null): TempStream {
$file = new TempStream();
if ($func !== null) {
try {
$func($file);
} finally {
$file->close();
}
}
return $file;
}
}

52
php/src_file/IReader.php Normal file
View File

@ -0,0 +1,52 @@
<?php
namespace nulib\file;
use nulib\os\EOFException;
use nulib\os\IOException;
/**
* Interface IReader: un objet depuis lequel on peut lire des données
*/
interface IReader extends _IFile {
/** @throws IOException */
function fread(int $length): string;
/** @throws IOException */
function fgets(): string;
/** @throws IOException */
function fpassthru(): int;
/**
* 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 autre erreur se produit
*/
function readLine(): ?string;
/**
* 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;
/**
* verrouiller en mode partagé puis retourner un objet permettant de lire le
* fichier.
*/
function getReader(bool $lockedByCanRead=false): IReader;
/**
* lire tout le contenu du fichier en une seule fois, puis, si $close==true,
* le fermer
*
* @throws IOException si une erreur se produit
*/
function getContents(bool $close=true, bool $lockedByCanRead=false): string;
/** désérialiser le contenu du fichier, puis, si $close===true, le fermer */
function unserialize(?array $options=null, bool $close=true, bool $lockedByCanRead=false);
}

40
php/src_file/IWriter.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace nulib\file;
use nulib\os\IOException;
/**
* Interface IWriter: un objet dans lequel on peut écrire des données
*/
interface IWriter extends _IFile {
/** @throws IOException */
function fwrite(string $data, int $length=0): int;
/** @throws IOException */
function fflush(): self;
/** @throws IOException */
function ftruncate(int $size): self;
/** afficher les lignes */
function writeLines(?iterable $lines): self;
/**
* 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;
/**
* verrouiller en mode exclusif puis retourner un objet permettant d'écrire
* dans le fichier
*/
function getWriter(bool $lockedByCanWrite=false): IWriter;
/** écrire le contenu spécifié dans le fichier */
function putContents(string $contents, bool $close=true, bool $lockedByCanWrite=false): void;
/** sérialiser l'objet dans la destination */
function serialize($object, bool $close=true, bool $lockedByCanWrite=false): void;
}

56
php/src_file/_IFile.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace nulib\file;
use Iterator;
use nulib\os\IOException;
use nulib\php\ICloseable;
/**
* Interface _IFile: méthodes communes entre {@link IReader} et {@link IWriter}
*/
interface _IFile extends Iterator, ICloseable {
/**
* @return resource|null retourner la resource associée à ce fichier si cela
* a du sens
*/
function getResource();
/** si ce fichier est basé sur une resource, ajouter un filtre. */
function appendFilter(string $filterName, ?int $readWrite=null, $params=null): void;
/** si ce fichier est basé sur une resource, ajouter un filtre. */
function prependFilter(string $filterName, ?int $readWrite=null, $params=null): void;
/** si ce fichier est basé sur une resource, spécifier l'encoding. */
function setEncodingFilter(string $from, string $to): void;
/** vérifier si la resource est associée à un terminal */
function isatty(): bool;
/** obtenir des informations sur le fichier */
function fstat(): array;
/** retourner la taille du fichier */
function getSize(): int;
/**
* retourner la position actuelle de lecture/écriture
*
* @throws IOException
*/
function ftell(): int;
/**
* modifier la position actuelle de lecture/écriture
*
* @return int la position après avoir déplacé le pointeur
* @throws IOException
*/
function fseek(int $offset, int $whence=SEEK_SET): int;
/** comme {@link fseek()} mais retourne self */
function seek(int $offset, int $whence=SEEK_SET): self;
/** fermer le fichier si c'est nécessaire */
function close(bool $close=true): void;
}

View File

@ -0,0 +1,51 @@
<?php
namespace nulib\file\base;
/**
* Class FileReader: un fichier accédé en lecture
*/
class FileReader extends _File {
/** @var bool *à l'ouverture du fichier*, faut-il ignorer le BOM? */
const IGNORE_BOM = true;
const DEFAULT_MODE = "rb";
/** @var bool */
protected $ignoreBom;
function __construct($input, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null, ?bool $ignoreBom=null) {
if ($ignoreBom === null) $ignoreBom = static::IGNORE_BOM;
$this->ignoreBom = $ignoreBom;
if ($input === null) {
$fd = STDIN;
$close = false;
} elseif (is_resource($input)) {
$fd = $input;
$close = false;
} else {
$file = $input;
if ($mode === null) $mode = static::DEFAULT_MODE;
$this->file = $file;
$this->mode = $mode;
$fd = $this->open();
$close = true;
}
parent::__construct($fd, $close, $throwOnError, $allowLocking);
}
/** @return resource */
protected function open() {
$fd = parent::open();
$this->haveBom = false;
if ($this->ignoreBom) {
$bom = fread($fd, 3);
if ($bom === "\xEF\xBB\xBF") $this->seekOffset = 3;
else rewind($fd);
}
return $fd;
}
function haveBom(): bool {
return $this->seekOffset !== 0;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace nulib\file\base;
use nulib\os\IOException;
use nulib\os\sh;
/**
* Class FileWriter: un fichier accédé en lecture/écriture
*/
class FileWriter extends _File {
const DEFAULT_MODE = "a+b";
function __construct($output, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null) {
if ($output === null) {
$fd = STDOUT;
$close = false;
} elseif (is_resource($output)) {
$fd = $output;
$close = false;
} else {
$file = $output;
if ($mode === null) $mode = static::DEFAULT_MODE;
IOException::ensure_valid(sh::mkdirof($file));
$this->file = $file;
$this->mode = $mode;
$fd = $this->open();
$close = true;
}
parent::__construct($fd, $close, $throwOnError, $allowLocking);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace nulib\file\base;
/**
* Class MemoryStream: un flux qui peut être lu ou écrit, et qui reste
* uniquement en mémoire.
*/
class MemoryStream extends Stream {
protected static function memory_fd() {
return fopen("php://memory", "w+b");
}
function __construct(bool $throwOnError=true) {
parent::__construct(self::memory_fd(), true, $throwOnError);
}
function getResource() {
if ($this->fd === null) $this->fd = self::memory_fd();
return parent::getResource();
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace nulib\file\base;
use nulib\ValueException;
class SharedFile extends FileWriter {
const USE_LOCKING = true;
const DEFAULT_MODE = "c+b";
function __construct($file, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null) {
if ($file === null) throw ValueException::null("file");
parent::__construct($file, $mode, $throwOnError, $allowLocking);
}
}

View File

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

View File

@ -0,0 +1,50 @@
<?php
namespace nulib\file\base;
use nulib\file\_IFile;
use nulib\os\IOException;
trait TStreamFilter {
private $filters = null;
function appendFilter(string $filterName, ?int $readWrite=null, $params=null): void {
$this->filters[] = [$filterName, $readWrite, $params];
}
function prependFilter(string $filterName, ?int $readWrite=null, $params=null): void {
if ($this->filters === null) $this->filters = [];
array_unshift($this->filters, [$filterName, $readWrite, $params]);
}
function setEncodingFilter(string $from, string $to): void {
if ($to !== $from) {
$this->appendFilter("convert.iconv.$from.$to");
}
}
/**
* @param $fd resource
* @throws IOException
*/
protected function _streamAppendFilters($fd): void {
if ($this->filters !== null) {
foreach ($this->filters as [$filterName, $readWrite, $params]) {
if (stream_filter_append($fd, $filterName, $readWrite, $params) === false) {
throw new IOException("unable to add filter $filterName");
}
}
$this->filters = null;
}
}
/**
* @param $file _IFile
*/
protected function _appendFilters($file): void {
if ($this->filters !== null) {
foreach ($this->filters as [$filterName, $readWrite, $params]) {
$file->appendFilter($filterName, $readWrite, $params);
}
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace nulib\file\base;
/**
* Class TempStream: un flux qui peut être lu ou écrit, et qui reste en mémoire,
* jusqu'à ce que la taille des données atteint {@link self::MAX_MEMORY} et à
* ce moment- un fichier temporaire est automatiquement créé.
*/
class TempStream extends Stream {
const MAX_MEMORY = 2 * 1024 * 1024;
function __construct(?int $maxMemory=null, bool $throwOnError=true) {
if ($maxMemory === null) $maxMemory = static::MAX_MEMORY;
$this->maxMemory = $maxMemory;
parent::__construct($this->tempFd(), true, $throwOnError);
}
/** @var int */
protected $maxMemory;
protected function tempFd() {
return fopen("php://temp/maxmemory:$this->maxMemory", "w+b");
}
function getResource() {
if ($this->fd === null) $this->fd = $this->tempFd();
return parent::getResource();
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace nulib\file\base;
use nulib\os\IOException;
use nulib\os\path;
/**
* Class TmpfileWriter: un fichier temporaire accédé en lecture/écriture
*/
class TmpfileWriter extends FileWriter {
const DEFAULT_MODE = "w+b";
function __construct(?string $destdir=null, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null) {
$tmpDir = sys_get_temp_dir();
if ($destdir === null) $destdir = $tmpDir;
if (is_dir($destdir)) {
# si on spécifie un répertoire, créer un fichier temporaire dedans
$file = tempnam($destdir, "tmp_nulib_");
$this->delete = true;
} elseif (is_file($destdir)) {
# si on spécifie un fichier qui existe le prendre comme "fichier
# temporaire" mais ne pas le supprimer automatiquement
$file = $destdir;
if (!path::is_qualified($file)) $file = path::join($tmpDir, $file);
$this->delete = false;
} else {
# un chemin qui n'existe pas: ne le sélectionner que si le répertoire
# existe. dans ce cas, le fichier sera créé automatiquement, mais pas
# supprimé
if (!is_dir(dirname($destdir))) {
throw new IOException("$destdir: no such file or directory");
}
$file = $destdir;
$this->delete = false;
}
parent::__construct($file, $mode, $throwOnError, $allowLocking);
}
/** @var bool */
protected $delete;
function __destruct() {
$this->close();
if ($this->delete) $this->delete();
}
/** supprimer le fichier. NB: le flux **n'est pas** fermé au préalable */
function delete(): self {
if (file_exists($this->file)) unlink($this->file);
return $this;
}
/**
* renommer le fichier. le flux est fermé d'abord
*
* @param int|null $defaultMode mode par défaut si le fichier destination
* n'existe pas. sinon, changer le mode du fichier temporaire à la valeur du
* fichier destination après renommage
* @param bool $setOwner si le propriétaire et/ou le groupe du fichier
* temporaire ne sont pas les mêmes que le fichier destination, tenter de
* changer le propriétaire et le groupe du fichier temporaire à la valeur
* du fichier destination après le renommage (nécessite les droits de root)
* @throws IOException
*/
function rename(string $dest, ?int $defaultMode=0644, bool $setOwner=true): void {
$this->close();
$file = $this->file;
if (file_exists($dest)) {
$mode = fileperms($dest);
if ($setOwner) {
$tmpowner = fileowner($file);
$owner = fileowner($dest);
$tmpgroup = filegroup($file);
$group = filegroup($dest);
} else {
$owner = $group = null;
}
} else {
$mode = $defaultMode;
$owner = $group = null;
}
if (!rename($file, $dest)) {
throw new IOException("$file: unable to rename to $dest");
}
$this->file = $dest;
if ($mode !== null) chmod($dest, $mode);
if ($owner !== null) {
if ($owner !== $tmpowner) chown($dest, $owner);
if ($group !== $tmpgroup) chgrp($dest, $group);
}
if ($mode !== null || $owner !== null) clearstatcache(true, $file);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace nulib\file\base;
use nulib\os\IOException;
use nulib\web\http;
abstract class _File extends Stream {
function __construct($fd, bool $close, bool $throwOnError=true, ?bool $allowLocking=null) {
parent::__construct($fd, $close, $throwOnError, $allowLocking);
}
/** @var string */
protected $file;
/** @var string */
protected $mode;
/** @return resource */
protected function open() {
return IOException::ensure_valid(@fopen($this->file, $this->mode));
}
/** @return resource */
function getResource() {
if ($this->fd === null && $this->file !== null) $this->fd = $this->open();
return parent::getResource();
}
/** streamer le contenu du fichier en sortie */
function readfile(?string $contentType=null, ?string $charset=null, ?string $filename=null, string $disposition=null): bool {
if ($contentType !== null) http::content_type($contentType, $charset);
if ($filename !== null) http::download_as($filename, $disposition);
return readfile($this->file) !== false;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace nulib\file\csv;
use nulib\cl;
use nulib\ref\os\csv\ref_csv;
class csv_flavours {
const MAP = [
"oo" => ref_csv::OO_FLAVOUR,
"ooffice" => ref_csv::OO_FLAVOUR,
ref_csv::OO_NAME => ref_csv::OO_FLAVOUR,
"xl" => ref_csv::XL_FLAVOUR,
"excel" => ref_csv::XL_FLAVOUR,
ref_csv::XL_NAME => ref_csv::XL_FLAVOUR,
];
const ENCODINGS = [
ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING,
ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING,
];
static final function verifix(string $flavour): string {
$lflavour = strtolower($flavour);
if (array_key_exists($lflavour, self::MAP)) {
$flavour = self::MAP[$lflavour];
}
if (strlen($flavour) < 1) $flavour .= ",";
if (strlen($flavour) < 2) $flavour .= "\"";
if (strlen($flavour) < 3) $flavour .= "\\";
return $flavour;
}
static final function get_name(string $flavour): string {
if ($flavour == ref_csv::OO_FLAVOUR) return ref_csv::OO_NAME;
elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::XL_NAME;
else return $flavour;
}
static final function get_params(string $flavour): array {
return [$flavour[0], $flavour[1], $flavour[2]];
}
static final function get_encoding(string $flavour): ?string {
return cl::get(self::ENCODINGS, $flavour);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\os;
/**
* Class EOFException: indiquer que plus aucune donnée n'est disponible sur un
* flux
*/
class EOFException extends IOException {
static final function ensure_not_eof($data, bool $throw=true, $eof=false) {
if ($data !== $eof) return $data;
elseif (!$throw) return null;
else throw new self();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace nulib\os;
use RuntimeException;
/**
* Class IOException: erreur sur un flux ou un système de fichiers
*/
class IOException extends RuntimeException {
static final function last_error(?string $prefix=null): ?self {
$error = error_get_last();
if ($error === null) return null;
$message = $error["message"];
if ($prefix) $message = "$prefix: $message";
return new static($message);
}
static final function generic_error(?string $message=null): self {
if ($message === null) $message = "generic error";
return new static($message);
}
static final function error(?string $prefix=null): self {
$exception = self::last_error($prefix);
if ($exception !== null) throw $exception;
else throw self::generic_error($prefix);
}
static final function ensure_valid($value, bool $throw=true, $invalid=false) {
if ($value !== $invalid) return $value;
elseif (!$throw) return null;
else throw self::error();
}
static final function already_closed(): self {
return new static("already closed");
}
static final function ensure_open(bool $closed): void {
if ($closed) throw self::already_closed();
}
}

7
php/src_os/README.md Normal file
View File

@ -0,0 +1,7 @@
# nulib\os
Ce package contient des services permettant d'interagir avec le système d'exploitation
exemple: args, etc.
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

8
php/src_os/TODO.md Normal file
View File

@ -0,0 +1,8 @@
# TODO
* [ ] pour les classes Reader, options pour ignorer automatiquement le BOM
important notamment pour un futur CsvReader.
les méthodes size(), seek(), etc. tiennent compte de la taille du BOM et
corrigent les valeurs en conséquence...
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

314
php/src_os/path.php Normal file
View File

@ -0,0 +1,314 @@
<?php # -*- coding: utf-8 mode: php -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
namespace nulib\os;
use nulib\str;
/**
* Class path: manipulation de chemins du système de fichiers
*/
class path {
/**
* Normaliser le chemin spécifié:
* - supprimer si possible les ocurrences de ./ et ../
* - supprimer les slash multiples, sauf s'il y en a exactement 2 au début de
* la chaine
* - "~/path" est transformé en "$HOME/path"
*
* cas particuliers:
* - retourner la valeur null inchangée
* - retourner '.' pour un chemin vide
*/
static final function normalize($path): ?string {
if ($path === null || $path === false) return null;
$path = strval($path);
if ($path !== "") {
if (substr($path, 0, 2) == "~/") {
$path = sh::homedir().substr($path, 1);
}
$initial_slashes = strpos($path, "/") === 0;
if ($initial_slashes &&
strpos($path, "//") === 0 &&
strpos($path, "///") === false) {
$initial_slashes = 2;
}
$initial_slashes = intval($initial_slashes);
$comps = explode("/", $path);
$new_comps = array();
foreach ($comps as $comp) {
if ($comp === "" || $comp === ".") continue;
if ($comp != ".." ||
(!$initial_slashes && !$new_comps) ||
($new_comps && end($new_comps) == "..")) {
array_push($new_comps, $comp);
} elseif ($new_comps) {
array_pop($new_comps);
}
}
$comps = $new_comps;
$path = implode("/", $comps);
if ($initial_slashes) $path = str_repeat("/", $initial_slashes) . $path;
}
return $path !== ""? $path: ".";
}
/** obtenir le chemin absolu normalisé correspondant à $path */
static final function abspath($path, ?string $cwd=null): ?string {
if ($path === null || $path === false) return null;
$path = strval($path);
if (substr($path, 0, 1) !== "/") {
if ($cwd === null) $cwd = getcwd();
$path = "$cwd/$path";
}
return self::normalize($path);
}
/**
* obtenir le chemin $path exprimé relativement à $basedir.
* si $basedir est relatif, il est exprimé par rapport à $cwd qui vaut par
* défaut le chemin courant.
*/
static final function relpath($path, string $basedir="", ?string $cwd=null): string {
$path = self::abspath($path, $cwd);
$basedir = self::abspath($basedir, $cwd);
if ($path === $basedir) return "";
$initial_slashes = strpos($path, "//") === 0? 2: 1;
$path = substr($path, $initial_slashes);
$initial_slashes = strpos($basedir, "//") === 0? 2: 1;
$basedir = substr($basedir, $initial_slashes);
if ($basedir === "") return $path;
$pparts = $path? explode("/", $path): [];
$pcount = count($pparts);
$bparts = explode("/", $basedir);
$bcount = count($bparts);
$i = 0;
while ($i < $pcount && $i < $bcount && $pparts[$i] === $bparts[$i]) {
$i++;
}
$ups = array_fill(0, $bcount - $i, "..");
$relparts = array_merge($ups, array_slice($pparts, $i));
return implode("/", $relparts);
}
/**
* obtenir un chemin visuellement agréable:
* - $HOME est remplacé par "~"
* - $CWD/ est remplacé par ""
*/
static final function ppath($path, ?string $cwd=null): string {
$path = self::abspath($path, $cwd);
$homedir = sh::homedir();
$cwd = getcwd()."/";
if (str::del_prefix($path, $cwd)) {
return $path;
}
if (str::starts_with($homedir, $path)) {
str::del_prefix($path, $homedir);
$path ="~$path";
}
return $path;
}
/**
* obtenir le chemin absolu canonique correspondant à $path
*
* @throws IOException si le chemin n'existe pas ou si une autre erreur se
* produit
*/
static final function realpath($path): ?string {
if ($path === null || $path === false) return null;
$path = strval($path);
return IOException::ensure_valid(realpath($path));
}
/**
* obtenir le chemin parent de $path
*
* cas particuliers:
* dirname("/") === "/";
* dirname("filename") === ".";
*/
static final function dirname($path): ?string {
if ($path === null || $path === false) return null;
else return dirname(strval($path));
}
/** obtenir le nom du fichier sans son chemin */
static final function filename($path): ?string {
if ($path === null || $path === false) return null;
$path = strval($path);
$index = strrpos($path, "/");
if ($index !== false) $path = substr($path, $index + 1);
return $path;
}
/** obtenir le nom de base du fichier (sans son chemin et sans l'extension) */
static final function basename($path): ?string {
if ($path === null || $path === false) return null;
$basename = self::filename($path);
$index = strrpos($basename, ".");
if ($index !== false) $basename = substr($basename, 0, $index);
return $basename;
}
/** obtenir l'extension du fichier. l'extension est retournée avec le '.' */
static final function ext($path): ?string {
if ($path === null || $path === false) return null;
$ext = self::filename($path);
$index = strrpos($ext, ".");
if ($index === false) $ext = "";
else $ext = substr($ext, $index);
return $ext;
}
/** découper le chemin entre sa partie "répertoire" et "fichier" */
static final function split($path): array {
if ($path === null || $path === false) return [null, null];
elseif ($path === "") return ["", ""];
$path = strval($path);
$index = strrpos($path, "/");
if ($index !== false) {
if ($index == 0) $dir = "/";
else $dir = substr($path, 0, $index);
$file = substr($path, $index + 1);
} else {
$dir = "";
$file = $path;
}
return [$dir, $file];
}
/**
* joindre les composantes pour faire un seul chemin normalisé. les composantes
* sont joints inconditionnellements
*/
static final function join(...$parts): ?string {
$path = null;
foreach ($parts as $part) {
if ($part === null || $part === false) continue;
if ($path && substr($path, -1) !== "/"
&& substr($part, 0, 1) !== "/") $path .= "/";
if ($path === null) $path = "";
$path .= $part;
}
return self::normalize($path);
}
/**
* joindre les composantes pour faire un seul chemin normalisé. chaque
* composante relative s'ajoute au chemin. chaque composante absolue relance
* le calcul de puis le début
* e.g reljoin("a", "b", "../c", "d/e") retourne "a/c/d/e"
* alors que reljoin("a", "b", "/c", "d/e") retourne "/c/d/e"
*/
static final function reljoin(...$parts): ?string {
$path = null;
foreach ($parts as $part) {
if ($part === null || $part === false) continue;
if (substr($part, 0, 1) == "/") {
$path = $part;
continue;
}
if ($path && substr($path, -1) !== "/") $path .= "/";
elseif ($path === null) $path = "";
$path .= $part;
}
return self::normalize($path);
}
/**
* tester si $dir est situé dans $basedir.
*
* par défaut, on considère qu'un chemin $dir est situé dans lui-même sauf si
* $strict == true, auquel cas "$dir est dans $basedir" implique $dir !== $basedir
*/
static final function is_within(string $dir, string $basedir, bool $strict=false): bool {
$dir = self::abspath($dir);
$basedir = self::abspath($basedir);
if ($dir === $basedir) return !$strict;
$prefix = $basedir;
if ($prefix !== "/") $prefix .= "/";
return substr($dir, 0, strlen($prefix)) === $prefix;
}
/**
* tester si $file a un répertoire, c'est à dire s'il contient le caractère '/'
* e.g have_dir('dir/name') est vrai alors que have_dir('name.ext') est faux
*/
static final function have_dir(string $file): bool {
return strpos($file, "/") !== false;
}
/**
* tester si le chemin est qualifié, c'est à dire s'il est absolu ou s'il est
* relatif à '.' ou '..'
* e.g is_qualified('./a/b') est vrai alors que is_qualified('a/b') est faux
*/
static final function is_qualified(string $file): bool {
if (strpos($file, "/") === false) return false;
if (substr($file, 0, 1) == "/") return true;
if (substr($file, 0, 2) == "./") return true;
if (substr($file, 0, 3) == "../") return true;
return false;
}
/** tester si $file a une extension */
static final function have_ext(string $file): bool {
$pos = strrpos($file, "/");
if ($pos === false) $pos = 0;
return strpos($file, ".", $pos) !== false;
}
/**
* s'assurer que $path a l'extension $new_ext. si $path a l'une des extensions
* de $replace_ext, l'enlever avant de rajouter $new_ext.
*
* si $strict === false alors $new_ext est ajouté à la liste $replace_ext
* c'est à dire que si $path a déjà l'extension $new_ext, elle n'est pas
* rajoutée de nouveau.
*
* si $strict === true alors seules les extensions de $replace_ext sont
* enlevées le cas échéant, et $new_ext est systématiquement rajouté
*
* @param string $path
* @param string $new_ext
* @param string|array|null $replace_ext
* @return string
*/
static final function ensure_ext(string $path, string $new_ext, $replace_ext=null, bool $strict=false): string {
[$dir, $filename] = self::split($path);
$ext = self::ext($filename);
if (is_string($replace_ext)) $replace_ext = [$replace_ext];
if (!$strict) $replace_ext[] = $new_ext;
if ($ext !== null && $replace_ext !== null) {
foreach ($replace_ext as $old_ext) {
if ($ext === $old_ext) {
$filename = self::basename($filename);
break;
}
}
}
$filename .= $new_ext;
return self::join($dir, $filename);
}
/** tester si le fichier, le répertoire ou le lien symbolique existe */
static final function exists(string $file): bool {
return is_link($file) || file_exists($file);
}
/** tester si $dir est un répertoire (mais pas un lien symbolique) */
static final function is_dir(string $dir, bool $allow_link=false): bool {
return is_dir($dir) && ($allow_link || !is_link($dir));
}
/** tester si $dir est un fichier (mais pas un lien symbolique) */
static final function is_file(string $file, bool $allow_link=false): bool {
return is_file($file) && ($allow_link || !is_link($file));
}
}

310
php/src_os/sh.php Normal file
View File

@ -0,0 +1,310 @@
<?php
namespace nulib\os;
use nulib\cl;
use RuntimeException;
class sh {
static final function _quote(string $value): string {
if (preg_match('/^[a-zA-Z0-9_.@:,\/+-]+$/', $value)) return $value;
return escapeshellarg($value);
}
/**
* obtenir la valeur $value quotée pour le shell
*
* si c'est une valeur scalaire, elle sera quotée sous la forme 'value'
* si c'est un tableau séquentiel, elle sera quoté sous la forme ('value'...)
* sinon, elle sera quotée sour la forme ([key]=value...)
*/
static final function quote($value): string {
if (is_array($value)) {
$parts = [];
if (cl::is_list($value)) {
foreach ($value as $part) {
$parts[] = self::_quote(strval($part));
}
} else {
foreach ($value as $key => $part) {
$key = self::_quote($key);
$val = self::_quote($part);
$parts[] = "[$key]=$val";
}
}
return "(".implode(" ", $parts).")";
} else {
return self::_quote(strval($value));
}
}
/**
* obtenir une commande shell à partir du tableau des arguments.
* à utiliser avec exec()
*/
static final function join(array $parts): string {
$count = count($parts);
for($i = 0; $i < $count; $i++) {
$parts[$i] = self::_quote(strval($parts[$i]));
}
return implode(" ", $parts);
}
private static final function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void {
if ($redir !== null) {
switch ($redir) {
case "outonly":
case "noerr":
$redir = "2>/dev/null";
break;
case "erronly":
case "noout":
$redir = "2>&1 >/dev/null";
break;
case "both":
case "err2out":
$redir = "2>&1";
break;
case "none":
case "null":
$redir = ">/dev/null 2>&1";
break;
case "default":
$redir = null;
break;
}
}
if ($input !== null) {
$redir = $redir !== null? "$redir ": "";
$redir .= "<".escapeshellarg($input);
}
if ($output !== null) {
$redir = $redir !== null? "$redir ": "";
$redir .= ">".escapeshellarg($output);
}
if ($redir !== null) $cmd .= " $redir";
}
/**
* Corriger la commande $cmd:
* - si c'est tableau, joindre les arguments avec {@link join()}
* - sinon, mettre les caractères en échappement avec {@link escapeshellarg()}
*/
static final function verifix_cmd(&$cmd, ?string $redir=null, ?string $input=null, ?string $output=null): void {
if (is_array($cmd)) $cmd = self::join($cmd);
else $cmd = escapeshellcmd(strval($cmd));
self::add_redir($cmd, $redir, $input, $output);
}
/**
* Lancer la commande spécifiée avec passthru() et retourner le code de retour
* dans la variable $retcode. $cmd doit déjà être formaté comme il convient
*
* voici la différence entre system(), passthru() et exec()
* +----------------+-----------------+----------------+----------------+
* | Command | Displays Output | Can Get Output | Gets Exit Code |
* +----------------+-----------------+----------------+----------------+
* | passthru() | Yes (raw) | No | Yes |
* | system() | Yes (as text) | Last line only | Yes |
* | exec() | No | Yes (array) | Yes |
* +----------------+-----------------+----------------+----------------+
*
* @return bool true si la commande s'est lancée sans erreur, false sinon
*/
static final function _passthru(string $cmd, int &$retcode=null): bool {
passthru($cmd, $retcode);
return $retcode == 0;
}
/**
* Comme {@link _passthru()} mais lancer la commande spécifiée avec system().
* cf la doc de {@link _passthru()} pour les autres détails
*/
static final function _system(string $cmd, string &$output=null, int &$retcode=null): bool {
$last_line = system($cmd, $retcode);
if ($last_line !== false) $output = $last_line;
return $retcode == 0;
}
/**
* Comme {@link _passthru()} mais lancer la commande spécifiée avec exec().
* cf la doc de {@link _passthru()} pour les autres détails
*/
static final function _exec(string $cmd, array &$output=null, int &$retcode=null): bool {
exec($cmd, $output, $retcode);
return $retcode == 0;
}
/**
* Lancer la commande $cmd dans un processus fils via un shell et attendre la
* fin de son exécution.
*
* $cmd doit déjà être formaté comme il convient
*/
static final function _fork_exec(string $cmd, int &$retcode=null): bool {
$pid = pcntl_fork();
if ($pid == -1) {
// parent, impossible de forker
throw new RuntimeException("unable to fork");
} elseif ($pid) {
// parent, fork ok
pcntl_waitpid($pid, $status);
if (pcntl_wifexited($status)) {
$retcode = pcntl_wexitstatus($status);
} else {
$retcode = 127;
}
return $retcode == 0;
}
// child, fork ok
pcntl_exec("/bin/sh", ["-c", $cmd]);
return false;
}
/**
* Corriger la commande spécifiée avec {@link verifix_cmd()} puis la lancer
* avec passthru() et retourner le code de retour dans la variable $retcode
*
* $redir spécifie le type de redirection demandée:
* - "default" | null: $output reçoit STDOUT et STDERR n'est pas redirigé
* - "outonly" | "noerr": $output ne reçoit que STDOUT et STDERR est perdu
* - "erronly" | "noout": $output ne reçoit que STDERR et STDOUT est perdu
* - "both" | "err2out": $output reçoit STDOUT et STDERR
* - "none" | "null": STDOUT et STDERR sont perdus
* - sinon c'est une redirection spécifique, et la valeur est rajoutée telle
* quelle à la ligne de commande
*
* @return bool true si la commande s'est lancée sans erreur, false sinon
*/
static final function passthru($cmd, int &$retcode=null, ?string $redir=null): bool {
self::verifix_cmd($cmd, $redir);
return self::_passthru($cmd, $retcode);
}
/**
* Comme {@link passthru()} mais lancer la commande spécifiée avec system().
* Cf la doc de {@link passthru()} pour les autres détails
*/
static final function system($cmd, string &$output=null, int &$retcode=null, ?string $redir=null): bool {
self::verifix_cmd($cmd, $redir);
return self::_system($cmd, $output, $retcode);
}
/**
* Comme {@link passthru()} mais lancer la commande spécifiée avec exec().
* Cf la doc de {@link passthru()} pour les autres détails
*/
static final function exec($cmd, array &$output=null, int &$retcode=null, ?string $redir=null): bool {
self::verifix_cmd($cmd, $redir);
return self::_exec($cmd, $output, $retcode);
}
/**
* Corriger la commande spécifiée avec {@link verifix_cmd()}, la préfixer de
* "exec" puis la lancer avec {@link _fork_exec()}
*/
static final function fork_exec($cmd, int &$retcode=null, ?string $redir=null): bool {
self::verifix_cmd($cmd, $redir);
return self::_fork_exec("exec $cmd", $retcode);
}
#############################################################################
/** retourner le répertoire $HOME */
static final function homedir(): string {
$homedir = getenv("HOME");
if ($homedir === false) {
$homedir = posix_getpwuid(posix_getuid())["dir"];
}
return path::abspath($homedir);
}
/** s'assurer que le répertoire $dir existe */
static final function mkdirp(string $dir): bool {
if (is_dir($dir)) return true;
return mkdir($dir, 0777, true);
}
/** créer le répertoire qui va contenir le fichier $file */
static final function mkdirof(string $file): bool {
if (file_exists($file)) return true;
$dir = path::dirname($file);
if (file_exists($dir)) return true;
return mkdir($dir, 0777, true);
}
/**
* créer un répertoire avec un nom unique. ce répertoire doit être supprimé
* manuellement quand il n'est plus utilisé.
*
* @return string le chemin du répertoire
* @throws IOException si une erreur se produit (impossible de créer un
* répertoire unique après 2560 essais)
*/
static final function mktempdir(?string $prefix=null, ?string $basedir=null): string {
if ($basedir === null) $basedir = sys_get_temp_dir();
if ($prefix !== null) $prefix .= "-";
$max = 2560;
do {
$dir = "$basedir/$prefix".uniqid();
$r = @mkdir($dir);
$max--;
} while ($r === false && $max > 0);
if ($r === false) {
throw IOException::last_error("$dir: unable to create directory");
}
return $dir;
}
/**
* Supprimer un répertoire créé avec mktempdir
*
* un minimum de vérification est effectué qu'il s'agit bien d'un répertoire
* généré par mktempdir
*/
static final function rmtempdir(string $tmpdir, ?string $prefix=null, ?string $basedir=null): void {
if ($basedir === null) $basedir = sys_get_temp_dir();
if ($prefix !== null) $prefix .= "-";
// 13 '?' parce que c'est la taille d'une chaine générée par uniqid()
if (fnmatch("$basedir/$prefix?????????????", $tmpdir)) {
self::exec(["rm", "-rf", $tmpdir]);
} else {
throw new IOException("$tmpdir: n'est pas un répertoire temporaire");
}
}
/**
* supprimer tous les répertoires temporaires qui ont été créés avec le
* suffixe spécifié dans le répertoire $basedir
*/
static final function cleantempdirs(string $prefix, ?string $basedir=null): void {
if ($basedir === null) $basedir = sys_get_temp_dir();
$prefix .= "-";
// 13 '?' parce que c'est la taille d'une chaine générée par uniqid()
$tmpdirs = glob("$basedir/$prefix?????????????", GLOB_ONLYDIR);
if ($tmpdirs) {
self::exec(["rm", "-rf", ...$tmpdirs]);
}
}
static final function is_diff_file(string $f, string $g): bool {
if (!is_file($f) || !is_file($g)) return true;
self::exec(array("diff", "-q", $f, $g), $output, $retcode);
return $retcode !== 0;
}
static final function is_same_file(string $f, string $g): bool {
if (!is_file($f) || !is_file($g)) return false;
self::exec(array("diff", "-q", $f, $g), $output, $retcode);
return $retcode === 0;
}
static final function is_diff_link(string $f, string $g): bool {
if (!is_link($f) || !is_link($g)) return true;
return @readlink($f) !== @readlink($g);
}
static final function is_same_link(string $f, string $g): bool {
if (!is_link($f) || !is_link($g)) return false;
return @readlink($f) === @readlink($g);
}
}

View File

@ -4,5 +4,8 @@
rotation des logs
* [ ] lors de la rotation, si l'ouverture du nouveau fichier échoue, continuer
à écrire dans l'ancien fichier
* [ ] dans `StdMessenger::resetParams()`, `[output]` peut être une instance de
StdOutput pour mettre à jour $out ET $err, ou un tableau de deux éléments pour
mettre à jour séparément $out et $err
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

View File

@ -6,6 +6,9 @@ use nulib\output\std\ProxyMessenger;
/**
* Class log: inscrire un message dans les logs uniquement
*
* Cette classe (ou la classe parallèle {@link msg} DOIT être initialisée avant
* d'être utilisée
*/
class log extends _messenger {
static function set_messenger(IMessenger $log=null) {

View File

@ -6,6 +6,10 @@ use nulib\output\std\ProxyMessenger;
/**
* Class msg: inscrire un message dans les logs ET l'afficher sur la console
*
* Cette classe DOIT être initialisée avec {@link set_messenger()} ou
* {@link set_messenger_class()} avant d'être utilisée. Une fois initialisée,
* les classes {@link say} et {@link log} sont utilisables aussi
*/
class msg extends _messenger {
static function set_messenger(IMessenger $say, ?IMessenger $log=null) {

34
php/src_output/out.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace nulib\output;
use nulib\output\std\StdOutput;
/**
* Class out: affichage sur la sortie standard
*/
class out {
/** @var StdOutput */
private static $out;
static function get(): StdOutput {
return self::$out;
}
protected static function set(StdOutput $out): StdOutput {
return self::$out = $out;
}
/** reparamétrer l'instance */
static function reset($output=null, ?array $params=null): StdOutput {
if (self::$out === null) return self::set(new StdOutput($output, $params));
if ($output !== null) $params["output"] = $output;
self::$out->resetParams($params);
return self::$out;
}
static function write(...$values): void { self::$out->write(...$values); }
static function print(...$values): void { self::$out->print(...$values); }
static function iwrite(int $indentLevel, ...$values): void { self::$out->iwrite($indentLevel, ...$values); }
static function iprint(int $indentLevel, ...$values): void { self::$out->iprint($indentLevel, ...$values); }
}
out::reset();

View File

@ -6,6 +6,9 @@ use nulib\output\std\ProxyMessenger;
/**
* Class say: afficher un message sur la console uniquement
*
* Cette classe (ou la classe parallèle {@link msg} DOIT être initialisée avant
* d'être utilisée
*/
class say extends _messenger {
static function set_messenger(IMessenger $say) {

View File

@ -150,9 +150,14 @@ class StdMessenger implements _IMessenger {
"color" => $color,
"indent" => $indent,
];
if ($output !== null) {
if ($this->out === $this->err) {
$this->out->resetParams($params);
} else {
# NB: si initialement [output] était null, et qu'on spécifie une valeur
# [output], alors les deux instances $out et $err sont mis à jour
# séparément avec la même valeur de output
# de plus, on ne peut plus revenir à la situation initiale avec une
# destination différente pour $out et $err
$this->out->resetParams($params);
$this->err->resetParams($params);
}

View File

@ -3,8 +3,10 @@ namespace nulib\output\std;
use Exception;
use nulib\cl;
use nulib\output\IContent;
use nulib\output\IPrintable;
use nulib\os\file\Stream;
use nulib\php\content\content;
use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
/**
* Class StdOutput: affichage sur STDOUT, STDERR ou dans un fichier quelconque
@ -86,6 +88,7 @@ class StdOutput {
$indent = cl::get($params, "indent");
$flush = cl::get($params, "flush");
if ($output instanceof Stream) $output = $output->getResource();
if ($output !== null) {
if ($output === "php://stdout") {
$outf = STDOUT;
@ -181,37 +184,12 @@ class StdOutput {
return preg_replace('/\x1B\[.*?m/', "", $text);
}
static function flatten($values, ?array &$dest=null): array {
if ($dest === null) $dest = [];
if ($values === null) return $dest;
if (is_string($values)) {
$dest[] = $values;
return $dest;
} elseif (!is_array($values)) {
if ($values instanceof IContent) {
$values = $values->getContent();
} elseif ($values instanceof IPrintable) {
ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE);
$values->print();
$dest[] = ob_get_clean();
return $dest;
} elseif (!is_iterable($values)) {
$dest[] = strval($values);
return $dest;
}
}
foreach ($values as $value) {
self::flatten($value, $dest);
}
return $dest;
}
function getIndent(int $indentLevel): string {
return str_repeat($this->indent, $indentLevel);
}
function getLines(bool $withNl, ...$values): array {
$values = self::flatten($values);
$values = content::flatten($values);
if (!$values) return [];
$text = implode("", $values);
if ($text === "") return [""];

View File

@ -0,0 +1,44 @@
<?php
namespace nulib\php\coll;
use nulib\cl;
/**
* Class AutoArray: un tableau dont les éléments peuvent être accédés comme des
* propriétés
*/
class AutoArray extends BaseArray {
const _AUTO_PROPERTIES = null;
static function _AUTO_PROPERTIES(): ?array { return static::_AUTO_PROPERTIES; }
function __isset($name) {
if ($this->has($name)) return true;
$properties = self::_AUTO_PROPERTIES();
if ($properties === null) return false;
return array_key_exists($name, $properties);
}
function __get($name) {
$properties = self::_AUTO_PROPERTIES();
if ($this->has($name)) return $this->get($name);
$pkey = cl::get($properties, $name, $name);
return cl::pget($this->data, $pkey);
}
function __set($name, $value) {
$properties = self::_AUTO_PROPERTIES();
if ($this->has($name)) {
$this->set($name, $value);
} else {
$pkey = cl::get($properties, $name, $name);
cl::pset($this->data, $pkey, $value);
}
}
function __unset($name) {
$properties = self::_AUTO_PROPERTIES();
if ($this->has($name)) {
$this->del($name);
} else {
$pkey = cl::get($properties, $name, $name);
cl::pdel($this->data, $pkey);
}
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace nulib\php\coll;
use ArrayAccess;
use Countable;
use Iterator;
use nulib\cl;
/**
* Class BaseArray: implémentation de base d'un objet array-like, qui peut aussi
* servir comme front-end pour un array
*/
class BaseArray implements ArrayAccess, Countable, Iterator {
function __construct(?array &$data=null) {
$this->reset($data);
}
/** @var array */
protected $data;
function __toString(): string { return var_export($this->data, true); }
#function __debugInfo() { return $this->data; }
function reset(?array &$data): void { $this->data =& $data; }
function &array(): ?array { return $this->data; }
function count(): int { return $this->data !== null? count($this->data): 0; }
function keys(): array { return $this->data !== null? array_keys($this->data): []; }
#############################################################################
# base
function has($key): bool {
return $this->data !== null && array_key_exists($key, $this->data);
}
function &get($key, $default=null) {
if ($this->data !== null && array_key_exists($key, $this->data)) {
return $this->data[$key];
} else return $default;
}
function set($key, $value): void {
if ($key === null) $this->data[] = $value;
else $this->data[$key] = $value;
}
function del($key): void {
unset($this->data[$key]);
}
function offsetExists($offset): bool { return $this->has($offset); }
function &offsetGet($offset) { return $this->get($offset); }
function offsetSet($offset, $value) { $this->set($offset, $value); }
function offsetUnset($offset) { $this->del($offset); }
function __isset($name) { return $this->has($name); }
function &__get($name) { return $this->get($name); }
function __set($name, $value) { $this->set($name, $value); }
function __unset($name) { $this->del($name); }
#############################################################################
# iterator
/** @var bool */
private $valid = false;
function rewind() {
if ($this->data !== null) {
$first = reset($this->data);
$this->valid = $first !== false || key($this->data) !== null;
} else {
$this->valid = false;
}
}
function valid(): bool { return $this->valid; }
function key() { return key($this->data); }
function current() { return current($this->data); }
function next() {
$next = next($this->data);
$this->valid = $next !== false || key($this->data) !== null;
}
#############################################################################
# divers
function phas($pkey): bool { return cl::phas($this->data, $pkey); }
function pget($pkey, $default=null): bool { return cl::pget($this->data, $pkey, $default); }
function pset($pkey, $value): void { cl::pset($this->data, $pkey, $value); }
function pdel($pkey): void { cl::pdel($this->data, $pkey); }
function contains($value, bool $strict=false): bool {
if ($value === null || $this->data === null) return false;
return in_array($value, $this->data, $strict);
}
function add($value, bool $unique=true, bool $strict=false): bool {
if ($unique && $this->contains($value, $strict)) return false;
$this->set(null, $value);
return true;
}
function addAll(?array $values, bool $unique=true, bool $strict=false): void {
if ($values === null) return;
$index = 0;
foreach ($values as $key => $value) {
if ($key === $index) {
$this->add($value, $unique, $strict);
$index++;
} else {
$this->set($key, $value);
}
}
}
function resetAll(?array $values): void {
$this->data = null;
$this->addAll($values);
}
}

View File

@ -3,7 +3,7 @@ namespace nulib\php\content;
/**
* Interface IContent: un objet capable de produire du contenu à afficher. le
* contenu retourné doit être pris tel quel,sans plus d'analyse
* contenu retourné doit être pris tel quel, sans plus d'analyse
*/
interface IContent {
/** retourner le contenu à afficher */

View File

@ -4,7 +4,7 @@ namespace nulib\php;
use Closure;
use nulib\cl;
use nulib\ValueException;
use nulib\ref\sys\ref_func;
use nulib\ref\php\ref_func;
use nulib\schema\Schema;
use ReflectionClass;
use ReflectionFunction;
@ -300,12 +300,16 @@ class func {
return true;
}
/** @var Schema */
private static $call_all_params_schema;
/**
* retourner la liste des méthodes de $class_or_object qui correspondent au
* filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_PARAMS_SCHEMA}
*/
static function get_all($class_or_object, $params=null): array {
Schema::nv($paramsv, $params, null, $schema, ref_func::CALL_ALL_PARAMS_SCHEMA);
Schema::nv($paramsv, $params, null
, self::$call_all_params_schema, ref_func::CALL_ALL_PARAMS_SCHEMA);
if (is_callable($class_or_object, true) && is_array($class_or_object)) {
# callable sous forme de tableau
$class_or_object = $class_or_object[0];

View File

@ -3,7 +3,7 @@ namespace nulib\php\iter;
use Exception;
use Iterator;
use nulib\DataException;
use nulib\NoMoreDataException;
use nulib\php\ICloseable;
/**
@ -29,12 +29,13 @@ abstract class AbstractIterator implements Iterator, ICloseable {
protected function beforeIter() {}
/**
* retourner le prochain élément. lancer l'exception {@link DataException}
* pour indiquer que plus aucun élément n'est disponible
* retourner le prochain élément.
* lancer l'exception {@link NoMoreDataException} pour indiquer que plus aucun
* élément n'est disponible
*
* le cas échéant, initialiser $key
*
* @throws DataException
* @throws NoMoreDataException
*/
abstract protected function _next(&$key);
@ -94,7 +95,7 @@ abstract class AbstractIterator implements Iterator, ICloseable {
$this->valid = false;
try {
$item = $this->_next($key);
} catch (DataException $e) {
} catch (NoMoreDataException $e) {
$this->beforeClose();
try {
$this->_teardown();

20
php/src_php/time/Date.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace nulib\php\time;
use DateTimeZone;
/**
* Class Date: une date
*/
class Date extends DateTime {
const DEFAULT_FORMAT = "d/m/Y";
function __construct($datetime="now", DateTimeZone $timezone=null) {
parent::__construct($datetime, $timezone);
$this->setTime(0, 0, 0, 0);
}
function format($format=self::DEFAULT_FORMAT): string {
return \DateTime::format($format);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace nulib\php\time;
use InvalidArgumentException;
class DateInterval extends \DateInterval {
protected static function to_string(DateInterval $interval) {
$string = "P";
$y = $interval->y;
$m = $interval->m;
$d = $interval->d;
if ($y > 0) $string .= "${y}Y";
if ($m > 0) $string .= "${m}M";
if ($d > 0) $string .= "${d}D";
$string .= "T";
$h = $interval->h;
$i = $interval->i;
$s = $interval->s;
if ($h > 0) $string .= "${h}H";
if ($i > 0) $string .= "${i}M";
if ($s > 0 || $string == "PT") $string .= "${s}S";
if ($interval->invert == 1) $string = "-$string";
return $string;
}
function __construct($duration) {
if ($duration instanceof \DateInterval) {
$this->y = $duration->y;
$this->m = $duration->m;
$this->d = $duration->d;
$this->h = $duration->h;
$this->i = $duration->i;
$this->s = $duration->s;
$this->invert = $duration->invert;
$this->days = $duration->days;
} elseif (!is_string($duration)) {
throw new InvalidArgumentException("duration must be a string");
} else {
if (substr($duration, 0, 1) == "-") {
$duration = substr($duration, 1);
$invert = true;
} else {
$invert = false;
}
parent::__construct($duration);
if ($invert) $this->invert = 1;
}
}
function __toString(): string {
return self::to_string($this);
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace nulib\php\time;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
/**
* Class DateTime: une date et une heure
*
* @property-read int $year
* @property-read int $month
* @property-read int $day
* @property-read int $hour
* @property-read int $minute
* @property-read int $second
* @property-read int $wday
* @property-read int $wnum
* @property-read string $timezone
* @property-read string $datetime
* @property-read string $date
* @property-read string $Ymd
* @property-read string $YmdHMS
* @property-read string $YmdHMSZ
*/
class DateTime extends \DateTime {
const DMY_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))?$/';
const YMD_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})$/';
const DMYHIS_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))? +(\d+)[h:.](\d+)(?:[:.](\d+))?$/';
const YMDHISZ_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})[tT](\d{2})(\d{2})(\d{2})?([zZ])?$/';
static function _YmdHMSZ_format(\DateTime $datetime): string {
$YmdHMS = $datetime->format("Ymd\\THis");
$Z = $datetime->format("P");
if ($Z === "+00:00") $Z = "Z";
return "$YmdHMS$Z";
}
const DEFAULT_FORMAT = "d/m/Y H:i:s";
const INT_FORMATS = [
"year" => "Y",
"month" => "m",
"day" => "d",
"hour" => "H",
"minute" => "i",
"second" => "s",
"wday" => "N",
"wnum" => "W",
];
const STRING_FORMATS = [
"timezone" => "P",
"datetime" => "d/m/Y H:i:s",
"date" => "d/m/Y",
"Ymd" => "Ymd",
"YmdHMS" => "Ymd\\THis",
"YmdHMSZ" => [self::class, "_YmdHMSZ_format"],
];
static function clone(DateTimeInterface $dateTime): self {
if ($dateTime instanceof static) return clone $dateTime;
$clone = new static();
$clone->setTimestamp($dateTime->getTimestamp());
$clone->setTimezone($dateTime->getTimezone());
return $clone;
}
/**
* corriger une année à deux chiffres qui est située dans le passé et
* retourner l'année à 4 chiffres.
*
* par exemple, si l'année courante est 2019, alors:
* - fix_past_year('18') === '2018'
* - fix_past_year('19') === '1919'
* - fix_past_year('20') === '1920'
*/
static function fix_past_year(int $year): int {
if ($year < 100) {
$y = getdate(); $y = $y["year"];
$r = $y % 100;
$c = $y - $r;
if ($year >= $r) $year += $c - 100;
else $year += $c;
}
return $year;
}
/**
* corriger une année à deux chiffres et retourner l'année à 4 chiffres.
* l'année charnière entre année passée et année future est 70
*
* par exemple, si l'année courante est 2019, alors:
* - fix_past_year('18') === '2018'
* - fix_past_year('19') === '2019'
* - fix_past_year('20') === '2020'
* - fix_past_year('69') === '2069'
* - fix_past_year('70') === '1970'
* - fix_past_year('71') === '1971'
*/
static function fix_any_year(int $year): int {
if ($year < 100) {
$y = intval(date("Y"));
$r = $y % 100;
$c = $y - $r;
if ($year >= 70) $year += $c - 100;
else $year += $c;
}
return $year;
}
function __construct($datetime="now", DateTimeZone $timezone=null) {
if ($datetime instanceof \DateTimeInterface) {
if ($timezone === null) $timezone = $datetime->getTimezone();
parent::__construct();
$this->setTimestamp($datetime->getTimestamp());
$this->setTimezone($timezone);
} elseif (is_int($datetime)) {
parent::__construct("now", $timezone);
$this->setTimestamp($datetime);
} elseif (!is_string($datetime)) {
throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface");
} else {
$Y = $H = $Z = null;
if (preg_match(self::DMY_PATTERN, $datetime, $ms)) {
$Y = $ms[3] ?? null;
if ($Y !== null) $Y = self::fix_any_year(intval($Y));
else $Y = intval(date("Y"));
$m = intval($ms[2]);
$d = intval($ms[1]);
} elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) {
$Y = $ms[1];
if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
else $Y = intval($Y);
$m = intval($ms[2]);
$d = intval($ms[3]);
} elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) {
$Y = $ms[3];
if ($Y !== null) $Y = self::fix_any_year(intval($Y));
else $Y = intval(date("Y"));
$m = intval($ms[2]);
$d = intval($ms[1]);
$H = intval($ms[4]);
$M = intval($ms[5]);
$S = intval($ms[6] ?? 0);
} elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) {
$Y = $ms[1];
if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
else $Y = intval($Y);
$m = intval($ms[2]);
$d = intval($ms[3]);
$H = intval($ms[4]);
$M = intval($ms[5]);
$S = intval($ms[6] ?? 0);
$Z = $ms[7] ?? null;
}
if ($Y !== null) {
if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S);
if ($Z !== null) $timezone = new DateTimeZone("UTC");
}
parent::__construct($datetime, $timezone);
}
}
function diff($target, $absolute=false): DateInterval {
return new DateInterval(parent::diff($target, $absolute));
}
function format($format=self::DEFAULT_FORMAT): string {
return \DateTime::format($format);
}
function __toString(): string {
return $this->format();
}
function __get($name) {
if (array_key_exists($name, self::INT_FORMATS)) {
$format = self::INT_FORMATS[$name];
if (is_callable($format)) return $format($this);
else return intval($this->format($format));
} elseif (array_key_exists($name, self::STRING_FORMATS)) {
$format = self::STRING_FORMATS[$name];
if (is_callable($format)) return $format($this);
else return $this->format($format);
}
throw new InvalidArgumentException("Unknown property $name");
}
}

139
php/src_php/time/Delay.php Normal file
View File

@ -0,0 +1,139 @@
<?php
namespace nulib\php\time;
use DateTimeInterface;
use InvalidArgumentException;
/**
* Class Delay: une durée jusqu'à un moment destination. le moment destination
* est calculé à la création de l'objet en fonction du moment courant.
*
* La durée peut-être:
* - un nombre, qui est le nombre de secondes jusqu'au moment destination
* - une chaine de la forme "x[WDHMS]y" x et y sont des nombres et la lettre
* est l'unité de temps: W représente une semaine, D une journée, H une heure,
* M une minute et S une seconde.
*
* Dans cette dernière forme, le timestamp destination est calculé en ajoutant x
* unités. puis l'unité inférieure est ramenée à (0 + y)
* par exemple,
* - "1D5" signifie le lendemain à 5h (+1 jour, puis ramené à 5h)
* - "2H0" signifie au plus deux heures plus tard
* - "0W5" signifie à 5 le dimanche de la semaine en cours
*
* NB: la valeur y pour l'unité S est ignorée
*/
class Delay {
/** valeurs par défaut de x et y pour les unités supportées */
const DEFAULTS = [
"w" => [0, 5],
"d" => [1, 5],
"h" => [1, 0],
"m" => [1, 0],
"s" => [1, 0],
];
static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array {
$dest = DateTime::clone($from);
switch ($u) {
case "w":
if ($x > 0) {
$x *= 7;
$dest->add(new \DateInterval("P${x}D"));
}
$w = 7 - intval($dest->format("w"));
$dest->add(new \DateInterval("P${w}D"));
$u = "h";
break;
case "d":
$dest->add(new \DateInterval("P${x}D"));
$u = "h";
break;
case "h":
$dest->add(new \DateInterval("PT${x}H"));
$u = "m";
break;
case "m":
$dest->add(new \DateInterval("PT${x}M"));
$u = "s";
break;
case "s":
$dest->add(new \DateInterval("PT${x}S"));
$u = null;
break;
}
if ($y !== null && $u !== null) {
$h = intval($dest->format("H"));
$m = intval($dest->format("i"));
switch ($u) {
case "h":
$dest->setTime($y, 0, 0, 0);
break;
case "m":
$dest->setTime($h, $y, 0, 0);
break;
case "s":
$dest->setTime($h, $m, $y, 0);
break;
}
}
$repr = $y !== null? "$x$y$y": "$x";
return [$dest, $repr];
}
function __construct($delay, ?DateTimeInterface $from=null) {
if ($from === null) $from = new DateTime();
if (is_int($delay)) {
[$dest, $repr] = self::compute_dest($delay, "s", null, $from);
} elseif (is_string($delay) && preg_match('/^\d+$/', $delay)) {
$x = intval($delay);
[$dest, $repr] = self::compute_dest($x, "s", null, $from);
} elseif (is_string($delay) && preg_match('/^(\d*)([wdhms])(\d*)$/i', $delay, $ms)) {
[$x, $u, $y] = [$ms[1], $ms[2], $ms[3]];
$u = strtolower($u);
$default = self::DEFAULTS[$u];
if ($x === "") $x = $default[0];
else $x = intval($x);
if ($y === "") $y = $default[1];
else $y = intval($y);
[$dest, $repr] = self::compute_dest($x, $u, $y, $from);
} else {
throw new InvalidArgumentException("invalid delay");
}
$this->dest = $dest;
$this->repr = $repr;
}
/** @var DateTime */
protected $dest;
function getDest(): DateTime {
return $this->dest;
}
/** @var string */
protected $repr;
function __toString(): string {
return $this->repr;
}
protected function _getDiff(?DateTimeInterface $now=null): \DateInterval {
if ($now === null) $now = new DateTime();
return $this->dest->diff($now);
}
/** retourner true si le délai imparti est écoulé */
function isElapsed(?DateTimeInterface $now=null): bool {
return $this->_getDiff($now)->invert == 0;
}
/**
* retourner l'intervalle entre le moment courant et la destination.
*
* l'intervalle est négatif si le délai n'est pas écoulé, positif sinon
*/
function getDiff(?DateTimeInterface $now=null): DateInterval {
return new DateInterval($this->_getDiff($now));
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace nulib\ref\cli;
/**
* Class ref_args: référence du format des arguments pour une application
*/
class ref_args {
const DEFS_SCHEMA = [
"set_defaults" => [null, null, "tableau contenant des paramètres et des options par défaut"],
"merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci avant de calculer la liste effective des options"],
"merge" => [null, null, "tableau à merger à celui-ci avant de calculer la liste effective des options",
# si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays"
],
"prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"],
"name" => [null, null, "nom du programme, utilisé pour l'affichage de l'aide"],
"purpose" => [null, null, "courte description de l'objet de ce programme"],
"usage" => [null, null, "exposé textuel des arguments valides du programme",
# ce peut être une chaine e.g '[options] SRC DESC'
# ou un tableau auquel cas autant de lignes que nécessaire sont affichées
],
"description" => [null, null, "description longue de l'objet du programme, affiché après usage"],
"suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"],
"dynamic_command" => [null, null, "fonction indiquant si une commande est valide",
# la signature de la fonction est function(string $command):?array
# elle doit retourner un tableau au format DEFS_SCHEMA qui définit la
# commande spécifiée, ou null si ce n'est pas une commande valide
],
"sections" => [null, null, "liste de sections permettant de grouper les arguments"],
"commandname" => [null, null, "propriété ou clé qui obtient la commande courante",
# la valeur par défaut est "command" si ni commandproperty ni commandkey ne sont définis
],
"commandproperty" => [null, null, "comme commandname mais force l'utilisation d'une propriété"],
"commandkey" => [null, null, "comme commandname mais force l'utilisation d'une clé"],
"argsname" => [null, null, "propriété ou clé qui obtient les arguments restants",
# la valeur par défaut est "args" si ni argsproperty ni argskey ne sont définis
],
"argsproperty" => [null, null, "comme argsname mais force l'utilisation d'une propriété"],
"argskey" => [null, null, "comme argsname mais force l'utilisation d'une clé"],
"autohelp" => ["?bool", null, "faut-il ajouter automatiquement le support de l'option --help"],
"autoremains" => ["?bool", null, "faut-il ajouter automatiquement la prise en compte des arguments restants"],
];
const SECTION_SCHEMA = [
"show" => ["bool", true, "faut-il afficher cette section?"],
"title" => [null, null, "titre de la section"],
"prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"],
"suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"],
# ces valeurs sont calculées
"defs" => [null, null, "(interne) liste des définitions de cette section"],
];
const DEF_SCHEMA = [
"set_defaults" => [null, null, "tableau contenant des paramètres par défaut"],
"merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci"],
"merge" => [null, null, "tableau à merger à celui-ci",
# si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays"
],
"kind" => [null, null, "type de définition: 'option' ou 'command'"],
"arg" => [null, null, "type de l'argument attendu par l'option"],
"args" => [null, null, "type des arguments attendus par l'option",
# si args est spécifié, arg est ignoré
],
"argsdesc" => [null, null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"],
"type" => [null, null, "types dans lesquels convertir les arguments avant de les fournir à l'utilisateur"],
"action" => [null, null, "fonction à appeler quand cette option est utilisée",
# la signature de la fonction est ($value, $name, $arg, $dest, $def)
],
"name" => [null, null, "propriété ou clé à initialiser en réponse à l'utilisation de cette option",
# le nom à spécifier est au format under_score, qui est transformée en camelCase si la destination est un objet
],
"property" => [null, null, "comme name mais force l'utilisation d'une propriété"],
"key" => [null, null, "comme name mais force l'utilisation d'une clé"],
"inverse" => ["bool", false, "décrémenter la destination au lieu de l'incrémenter pour une option sans argument"],
"value" => ["mixed", null, "valeur à forcer au lieu d'incrémenter la destination"],
"ensure_array" => [null, null, "forcer la destination à être un tableau"],
"help" => [null, null, "description de cette option, utilisé pour l'affichage de l'aide"],
"cmd_args" => [null, null, "définition des sous-options pour une commande"],
# ces valeurs sont calculées
"cmd_defs" => [null, null, "(interne) liste des définitions correspondant au paramètre options"],
];
const ARGS_ALLOWED_VALUES = ["value", "path", "dir", "file", "host"];
}

View File

@ -0,0 +1,20 @@
<?php
namespace nulib\ref\os\csv;
/**
* Class ref_csv: références des valeurs normalisées pour les fichiers CSV à
* destination de Microsoft Excel et de LibreOffice Calc
*/
class ref_csv {
const UTF8 = "utf-8";
const CP1252 = "cp1252";
const LATIN1 = "iso-8859-1";
const OO_NAME = "oocalc";
const OO_FLAVOUR = ",\"\\";
const OO_ENCODING = self::UTF8;
const XL_NAME = "msexcel";
const XL_FLAVOUR = ";\"\\";
const XL_ENCODING = self::CP1252;
}

View File

@ -0,0 +1,25 @@
<?php
namespace nulib\ref\schema;
class ref_analyze {
/** @var int résultat de l'analyse: valeur inexistante */
const MISSING = 0;
/** @var int résultat de l'analyse: valeur non disponible */
const UNAVAILABLE = 1;
/** @var int résultat de l'analyse: valeur nulle */
const NULL = 2;
/** @var int résultat de l'analyse: valeur chaine à parser */
const STRING = 3;
/** @var int résultat de l'analyse: valeur invalide */
const INVALID = 4;
/** @var int résultat de l'analyse: valeur valide mais pas normalisée */
const VALID = 5;
/** @var int résultat de l'analyse: valeur valide normalisée */
const NORMALIZED = 6;
}

View File

@ -0,0 +1,58 @@
<?php
namespace nulib\ref\schema;
class ref_schema {
/** @var array schéma des natures de schéma */
const NATURE_METASCHEMA = [
"nature" => ["string", null, "nature du schéma",
"pkey" => 0,
"allowed_values" => ["assoc", "list", "scalar"],
],
"title" => ["?string", null, "libellé de la valeur"],
"required" => ["bool", false, "la valeur est-elle requise?"],
"nullable" => ["?bool", null, "la valeur peut-elle être nulle?"],
"desc" => ["?content", null, "description de la valeur"],
"name" => ["?key", null, "identifiant de la valeur"],
"schema" => ["?array", null, "définition du schéma"],
];
/** @var array meta-schema d'un schéma de nature scalaire */
const SCALAR_METASCHEMA = [
"type" => ["array", null, "types possibles de la valeur", "required" => true],
"default" => [null, null, "valeur par défaut si la valeur n'existe pas"],
"title" => ["?string", null, "libellé de la valeur"],
"required" => ["bool", false, "la valeur est-elle requise?"],
"nullable" => ["?bool", null, "la valeur peut-elle être nulle?"],
"desc" => ["?content", null, "description de la valeur"],
"analyzer_func" => ["?callable", null, "fonction qui analyse une valeur entrante et indique comment la traiter"],
"extractor_func" => ["?callable", null, "fonction qui extrait la valeur à analyser dans une chaine de caractère"],
"parser_func" => ["?callable", null, "fonction qui analyse une chaine de caractères pour produire la valeur"],
"normalizer_func" => ["?callable", null, "fonction qui normalise la valeur"],
"messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"],
"formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"],
"format" => [null, null, "format à utiliser pour l'affichage"],
"" => ["array", "scalar", "nature du schéma",
"" => ["assoc", "schema" => self::NATURE_METASCHEMA],
],
"name" => ["?string", null, "identifiant de la valeur"],
"pkey" => ["?pkey", null, "chemin de clé de la valeur dans un tableau associatif"],
"header" => ["?string", null, "nom de l'en-tête s'il faut présenter cette donnée dans un tableau"],
"composite" => ["?bool", null, "ce champ fait-il partie d'une valeur composite?"],
];
const MESSAGES = [
"missing" => "{key}: Vous devez spécifier cette valeur",
"unavailable" => "{key}: Vous devez spécifier cette valeur",
"null" => "{key}: cette valeur ne doit pas être nulle",
"empty" => "{key}: cette valeur ne doit pas être vide",
"invalid" => "{key}: {orig}: cette valeur est invalide",
];
/** @var array meta-schema d'un schéma de nature associative */
const ASSOC_METASCHEMA = [
];
/** @var array meta-schema d'un schéma de nature liste */
const LIST_METASCHEMA = [
];
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\ref\schema;
class ref_types {
const ALIASES = [
"boolean" => "bool",
"integer" => "int",
"flt" => "float", "double" => "float", "dbl" => "float",
];
}

98
php/src_values/akey.php Normal file
View File

@ -0,0 +1,98 @@
<?php
namespace nulib\values;
use ArrayAccess;
use nulib\cl;
/**
* Class akey: accéder aux valeurs d'un tableau
*
* @see valx
*/
class akey {
/** obtenir la valeur d'une clé */
static final function get($array, $key, $default=null) {
if ($array instanceof ArrayAccess) {
if ($array->offsetExists($key)) return $array->offsetGet($key);
else return $default;
} else {
if (!is_array($array)) $array = cl::with($array);
return cl::get($array, $key, $default);
}
}
/** spécifier la valeur d'une clé */
static final function set(&$array, $key, $value) {
if ($array instanceof ArrayAccess) {
$array->offsetSet($key, $value);
} else {
cl::set($array, $key, $value);
}
return $value;
}
/** initialiser $dest avec les valeurs de $values */
static final function set_values(&$array, ?array $values): void {
if ($values === null) return;
foreach ($values as $key => $value) {
self::set($array, $key, $value);
}
}
/** incrémenter la valeur de la clé */
static final function inc(&$array, $key): int {
if ($array instanceof ArrayAccess) {
$value = (int)$array->offsetGet($key);
$array->offsetSet($key, ++$value);
return $value;
} else {
cl::ensure_array($array);
$value = (int)cl::get($array, $key);
return $array[$key] = ++$value;
}
}
/** décrémenter la valeur de la clé */
static final function dec(&$array, $key, bool $allow_negative=false): int {
if ($array instanceof ArrayAccess) {
$value = (int)$array->offsetGet($key);
if ($allow_negative || $value > 0) $array->offsetSet($key, --$value);
return $value;
} else {
cl::ensure_array($array);
$value = (int)cl::get($array, $key);
if ($allow_negative || $value > 0) $array[$key] = --$value;
return $value;
}
}
/**
* fusionner $merge dans la valeur de la clé, qui est d'abord transformé en
* tableau si nécessaire
*/
static final function merge(&$array, $key, $merge): void {
if ($array instanceof ArrayAccess) {
$value = $array->offsetGet($key);
$value = cl::merge($value, $merge);
$array->offsetSet($key, $value);
} else {
cl::ensure_array($array);
$array[$key] = cl::merge($array[$key], $merge);
}
}
/**
* ajouter $value à la valeur de la clé, qui est d'abord transformé en
* tableau si nécessaire
*/
static final function append(&$array, $key, $value): void {
if ($array instanceof ArrayAccess) {
$value = $array->offsetGet($key);
cl::set($value, null, $value);
$array->offsetSet($key, $value);
} else {
cl::ensure_array($array);
cl::set($array[$key], null, $value);
}
}
}

123
php/src_values/mprop.php Normal file
View File

@ -0,0 +1,123 @@
<?php
namespace nulib\values;
use nulib\cl;
use nulib\str;
use nulib\php\func;
use ReflectionClass;
use ReflectionException;
/**
* Class mprop: accéder aux propriétés d'un objet. la différence avec
* {@link oprop} est qu'une tentative est effectuée pour accéder d'abord à la
* propriété via une méthode normalisée
*
* @see valx
*/
class mprop {
static function split_prefix_name(string $name): array {
preg_match('/^(_*)(.*)/', $name, $ms);
return [$ms[1], $ms[2]];
}
static function get_getter_name(string $property, bool $bool=false): string {
[$prefix, $name] = self::split_prefix_name($property);
$get = $bool? "is": "get";
return $prefix.$get.str::upper1(str::us2camel($name));
}
static function get_setter_name(string $property): string {
[$prefix, $name] = self::split_prefix_name($property);
return $prefix."set".str::upper1(str::us2camel($name));
}
static function get_deletter_name(string $property): string {
[$prefix, $name] = self::split_prefix_name($property);
return $prefix."del".str::upper1(str::us2camel($name));
}
/** obtenir la valeur d'une propriété */
static final function get(object $object, string $property, $default=null, ?string $method=null) {
if ($method === null) $method = self::get_getter_name($property);
$c = new ReflectionClass($object);
try {
$m = $c->getMethod($method);
} catch (ReflectionException $e) {
return oprop::get($object, $property, $default);
}
return func::call([$object, $m], $default);
}
/** spécifier la valeur d'une propriété */
static final function set(object $object, string $property, $value, ?string $method=null) {
$c = new ReflectionClass($object);
return self::_set($c, $object, $property, $value, $method);
}
private static final function _set(ReflectionClass $c, object $object, string $property, $value, ?string $method) {
if ($method === null) $method = self::get_setter_name($property);
try {
$m = $c->getMethod($method);
} catch (ReflectionException $e) {
return oprop::_set($c, $object, $property, $value);
}
func::call([$object, $m], $value);
return $value;
}
/**
* initialiser $dest avec les valeurs de $values
*
* les noms des clés de $values sont transformées en camelCase pour avoir les
* noms des propriétés correspondantes
*/
static final function set_values(object $object, ?array $values, ?array $keys=null): void {
if ($values === null) return;
if ($keys === null) $keys = array_keys($values);
$c = new ReflectionClass($object);
foreach ($keys as $key) {
if (array_key_exists($key, $values)) {
$property = str::us2camel($key);
self::_set($c, $object, $property, $values[$key], null);
}
}
}
/** incrémenter la valeur d'une propriété */
static final function inc(object $object, string $property): int {
$value = intval(self::get($object, $property, 0));
$value++;
self::set($object, $property, $value);
return $value;
}
/** décrémenter la valeur d'une propriété */
static final function dec(object $object, string $property, bool $allow_negative=false): int {
$value = intval(self::get($object, $property, 0));
if ($allow_negative || $value > 0) {
$value--;
self::set($object, $property, $value);
}
return $value;
}
/**
* Fusionner la valeur à la propriété qui est transformée en tableau si
* nécessaire
*/
static final function merge(object $object, string $property, $array): void {
$values = cl::with(self::get($object, $property));
$values = cl::merge($values, cl::with($array));
self::set($object, $property, $values);
}
/**
* Ajouter la valeur à la propriété qui est transformée en tableau si
* nécessaire
*/
static final function append(object $object, string $property, $value): void {
$values = cl::with(self::get($object, $property));
$values[] = $value;
self::set($object, $property, $values);
}
}

151
php/src_values/oprop.php Normal file
View File

@ -0,0 +1,151 @@
<?php
namespace nulib\values;
use nulib\cl;
use ReflectionClass;
use ReflectionException;
/**
* Class oprop: accéder aux propriétés d'un objet
*
* @see valx
*/
class oprop {
/** obtenir la valeur d'une propriété */
static final function get(object $object, string $property, $default=null) {
$c = new ReflectionClass($object);
try {
$p = $c->getProperty($property);
$p->setAccessible(true);
return $p->getValue($object);
} catch (ReflectionException $e) {
if (property_exists($object, $property)) return $object->$property;
else return $default;
}
}
static final function _set(ReflectionClass $c, object $object, string $property, $value) {
try {
$p = $c->getProperty($property);
$p->setAccessible(true);
$p->setValue($object, $value);
} catch (ReflectionException $e) {
$object->$property = $value;
}
return $value;
}
/** spécifier la valeur d'une propriété */
static final function set(object $object, string $property, $value) {
$c = new ReflectionClass($object);
return self::_set($c, $object, $property, $value);
}
/**
* initialiser $dest avec les valeurs de $values
*
* les noms des clés de $values sont transformées en camelCase pour avoir les
* noms des propriétés correspondantes
*/
static final function set_values(object $object, ?array $values, ?array $keys=null): void {
if ($values === null) return;
if ($keys === null) $keys = array_keys($values);
$c = new ReflectionClass($object);
foreach ($keys as $key) {
if (array_key_exists($key, $values)) {
$property = str::us2camel($key);
self::_set($c, $object, $property, $values[$key]);
}
}
}
/** incrémenter la valeur d'une propriété */
static final function inc(object $object, string $property): int {
$c = new ReflectionClass($object);
try {
$p = $c->getProperty($property);
$p->setAccessible(true);
$value = (int)$p->getValue($object);
$value++;
$p->setValue($object, $value);
} catch (ReflectionException $e) {
if (property_exists($object, $property)) {
$value = (int)$object->$property;
$value++;
} else {
$value = 1;
}
$object->$property = $value;
}
return $value;
}
/** décrémenter la valeur d'une propriété */
static final function dec(object $object, string $property, bool $allow_negative=false): int {
$c = new ReflectionClass($object);
try {
$p = $c->getProperty($property);
$p->setAccessible(true);
$value = (int)$p->getValue($object);
if ($allow_negative || $value > 0) {
$value --;
$p->setValue($object, $value);
}
} catch (ReflectionException $e) {
if (property_exists($object, $property)) {
$value = (int)$object->$property;
} else {
$value = 0;
}
if ($allow_negative || $value > 0) $value--;
$object->$property = $value;
}
return $value;
}
/**
* Fusionner la valeur à la propriété qui est transformée en tableau si
* nécessaire
*/
static final function merge(object $object, string $property, $array): void {
$c = new ReflectionClass($object);
try {
$p = $c->getProperty($property);
$p->setAccessible(true);
$values = cl::with($p->getValue($object));
$values = cl::merge($values, cl::with($array));
$p->setValue($object, $values);
} catch (ReflectionException $e) {
if (property_exists($object, $property)) {
$values = cl::with($object->$property);
} else {
$values = [];
}
$values = cl::merge($values, cl::with($array));
$object->$property = $values;
}
}
/**
* Ajouter la valeur à la propriété qui est transformée en tableau si
* nécessaire
*/
static final function append(object $object, string $property, $value): void {
$c = new ReflectionClass($object);
try {
$p = $c->getProperty($property);
$p->setAccessible(true);
$values = cl::with($p->getValue($object));
$values[] = $value;
$p->setValue($object, $values);
} catch (ReflectionException $e) {
if (property_exists($object, $property)) {
$values = cl::with($object->$property);
} else {
$values = [];
}
$values[] = $value;
$object->$property = $values;
}
}
}

84
php/src_values/valm.php Normal file
View File

@ -0,0 +1,84 @@
<?php
namespace nulib\values;
use ArrayAccess;
use nulib\str;
/**
* Class valm: frontend pour accéder à une propriété d'un objet (par une méthode
* normalisée ou en direct) ou à la clé d'un tableau. cette classe utilise
* {@link mprop} ou {@link akey} en fonction de la nature de la cible
*
* si la cible est un objet, les noms des clés sont transformés en camelCase
* pour avoir les propriétés correspondantes
*/
class valm {
static final function get($src, $name, $default=null) {
if (is_object($src) && !($src instanceof ArrayAccess)) {
$name = str::us2camel($name);
return mprop::get($src, $name, $default);
} else {
return akey::get($src, $name, $default);
}
}
static final function set(&$dest, $name, $value) {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
return mprop::set($dest, $name, $value);
} else {
return akey::set($dest, $name, $value);
}
}
/**
* initialiser $dest avec les valeurs de $values
*
* si $dest est un objet, les noms des clés sont transformées en camelCase
* pour avoir les propriétés correspondantes
*/
static final function set_values(&$dest, ?array $values): void {
if ($values === null) return;
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
mprop::set_values($dest, $values);
} else {
akey::set_values($dest, $values);
}
}
static final function inc(&$dest, $name): int {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
return mprop::inc($dest, $name);
} else {
return akey::inc($dest, $name);
}
}
static final function dec(&$dest, $name): int {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
return mprop::dec($dest, $name);
} else {
return akey::dec($dest, $name);
}
}
static final function merge(&$dest, $name, $merge): void {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
mprop::merge($dest, $name, $merge);
} else {
akey::merge($dest, $name, $merge);
}
}
static final function append(&$dest, $name, $value): void {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
mprop::append($dest, $name, $value);
} else {
akey::append($dest, $name, $value);
}
}
}

84
php/src_values/valx.php Normal file
View File

@ -0,0 +1,84 @@
<?php
namespace nulib\values;
use ArrayAccess;
use nulib\str;
/**
* Class valx: frontend pour accéder à une propriété d'un objet ou à la clé
* d'un tableau. cette classe utilise {@link oprop} ou {@link akey} en fonction
* de la nature de la cible
*
* si la cible est un objet, les noms des clés sont transformés en camelCase
* pour avoir les propriétés correspondantes
*/
class valx {
static final function get($src, $name, $default=null) {
if (is_object($src) && !($src instanceof ArrayAccess)) {
$name = str::us2camel($name);
return oprop::get($src, $name, $default);
} else {
return akey::get($src, $name, $default);
}
}
static final function set(&$dest, $name, $value) {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
return oprop::set($dest, $name, $value);
} else {
return akey::set($dest, $name, $value);
}
}
/**
* initialiser $dest avec les valeurs de $values
*
* si $dest est un objet, les noms des clés sont transformées en camelCase
* pour avoir les propriétés correspondantes
*/
static final function set_values(&$dest, ?array $values): void {
if ($values === null) return;
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
oprop::set_values($dest, $values);
} else {
akey::set_values($dest, $values);
}
}
static final function inc(&$dest, $name): int {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
return oprop::inc($dest, $name);
} else {
return akey::inc($dest, $name);
}
}
static final function dec(&$dest, $name): int {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
return oprop::dec($dest, $name);
} else {
return akey::dec($dest, $name);
}
}
static final function merge(&$dest, $name, $merge): void {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
oprop::merge($dest, $name, $merge);
} else {
akey::merge($dest, $name, $merge);
}
}
static final function append(&$dest, $name, $value): void {
if (is_object($dest) && !($dest instanceof ArrayAccess)) {
$name = str::us2camel($name);
oprop::append($dest, $name, $value);
} else {
akey::append($dest, $name, $value);
}
}
}

144
php/src_web/http.php Normal file
View File

@ -0,0 +1,144 @@
<?php
namespace nulib\web;
use nulib\cv;
/**
* Class http: des outils pour parler le protocole HTTP dans le cadre de la
* génération d'une page
*/
class http {
static final function get_url(): string {
$proto = $_SERVER["HTTP_X_FORWARDED_PROTO"];
$host = $_SERVER["HTTP_X_FORWARDED_HOST"];
$uri = $_SERVER["SCRIPT_NAME"];
return "$proto://$host$uri";
}
static final function get_baseurl(): string {
$proto = $_SERVER["HTTP_X_FORWARDED_PROTO"];
$host = $_SERVER["HTTP_X_FORWARDED_HOST"];
$baseuri = dirname($_SERVER["SCRIPT_NAME"]);
return "$proto://$host$baseuri";
}
/**
* ne pas mettre en cache la réponse.
*
* appeler cette méthode est nécessaire si une page est dynamique
* et que les sessions ne sont pas utilisées.
*
* si un session est déjà démarrée, cette méthode est un NOP, sauf si
* $force==true
*/
static function no_cache(bool $force=false): void {
if (session_status() == PHP_SESSION_ACTIVE && !$force) return;
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-cache, max-age=0, must-revalidate");
header("Pragma: public");
}
/** spécifier le type de contenu de la réponse */
static function content_type(string $content_type=null, string $charset=null): void {
if ($content_type === null) $content_type = "application/octet-stream";
if (substr($content_type, 0, 5) == "text/") {
if ($charset === null) $charset = "utf-8";
if ($charset) $content_type .= "; charset=$charset";
}
header("Content-Type: $content_type");
}
static final function content_type_text(): void {
self::content_type("text/plain");
}
/** indiquer que la réponse doit être téléchargée */
static function download_as($filename, string $disposition=null): void {
if (cv::nz($filename)) {
$filename = basename($filename);
if ($disposition === null) $disposition = "attachment";
header("Content-Disposition: $disposition; filename=\"$filename\"");
}
}
/** rediriger vers l'url spécifiée et arrêter le script immédiatement */
static function redirect(string $url, bool $exit_now=true): void {
header("Location: $url");
if ($exit_now) exit;
}
/** rafraichir vers l'url spécifiée et arrêter le script immédiatement */
static function refresh(string $url, int $delay=1, bool $exit_now=true): void {
header("Refresh: $delay; url=$url");
if ($exit_now) exit;
}
/** envoyer le status "pas de contenu" et arrêter le script immédiatement */
static function send_no_content(bool $exit_now=true): void {
header("HTTP/1.1 204 No Content");
if ($exit_now) exit;
}
/**
* Envoyer le fichier spécifié pour téléchargement et arrêter le script
* immédiatement. On assume que les méthodes statiques {@link content_type()}
* et {@link download_as()} ont déjà été appelées.
*
* - si $delete==true, supprimer le fichier après téléchargement.
* - si $add_content_length==true, ajouter une en-tête qui précise la taille
* du fichier à télécharger. attention: la taille du fichier ne devrait pas
* changer
* - si $close_session==true, la session est fermée avant de lancer le
* téléchargement du fichier pour éviter de garder inutilement le verrou.
* NB: si le fichier à retourner est d'une certaine taille, il est avisé
* de fermer la session pour ne pas bloquer la navigation.
*
* si une erreur s'est produite, retourner false, même si $exit_now==true
*/
static function send_file(string $file, bool $delete=false, bool $close_session=true, bool $add_content_length=true, bool $exit_now=true): bool {
$fd = fopen($file, "r");
if ($fd === false) return false;
if ($close_session && session_status() == PHP_SESSION_ACTIVE) session_write_close();
if ($add_content_length) {
$size = filesize($file);
if ($size !== false) header("Content-Length: $size");
}
if ($delete) unlink($file);
$written = fpassthru($fd);
# XXX si $written !== $size, il faudrait agir en conséquence...
fclose($fd);
if ($written === false) return false;
if ($exit_now) exit;
return true;
}
static final function error(string $status, bool $exit=true): void {
header("HTTP/1.1 $status");
if ($exit) exit();
}
static final function error400(?string $message=null, ?string $body=null, bool $exit=true): void {
if ($message === null) $message = "Bad Request";
elseif ($body === null) $body = $message;
self::error("400 $message", false);
if ($body !== null) echo $body;
if ($exit) exit();
}
static final function error404(?string $message=null, ?string $body=null, bool $exit=true): void {
if ($message === null) $message = "Not Found";
elseif ($body === null) $body = $message;
self::error("404 $message", false);
if ($body !== null) echo $body;
if ($exit) exit();
}
static final function error500(?string $message=null, ?string $body=null, bool $exit=true): void {
if ($message === null) $message = "Internal Server Error";
elseif ($body === null) $body = $message;
self::error("500 $message", false);
if ($body !== null) echo $body;
if ($exit) exit();
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace nulib\file\base;
use PHPUnit\Framework\TestCase;
class FileReaderTest extends TestCase {
function testIgnoreBom() {
# la lecture avec et sans BOM doit être identique
## sans BOM
$reader = new FileReader(__DIR__ . '/impl/sans_bom.txt');
self::assertSame("0123456789", $reader->fread(10));
self::assertSame(10, $reader->ftell());
$reader->seek(30);
self::assertSame("abcdefghij", $reader->fread(10));
self::assertSame(40, $reader->ftell());
$reader->seek(10);
self::assertSame("ABCDEFGHIJ", $reader->fread(10));
self::assertSame(20, $reader->ftell());
$reader->seek(40);
self::assertSame("0123456789\n", $reader->getContents());
$reader->close();
## avec BOM
$reader = new FileReader(__DIR__ . '/impl/avec_bom.txt');
self::assertSame("0123456789", $reader->fread(10));
self::assertSame(10, $reader->ftell());
$reader->seek(30);
self::assertSame("abcdefghij", $reader->fread(10));
self::assertSame(40, $reader->ftell());
$reader->seek(10);
self::assertSame("ABCDEFGHIJ", $reader->fread(10));
self::assertSame(20, $reader->ftell());
$reader->seek(40);
self::assertSame("0123456789\n", $reader->getContents());
$reader->close();
}
function testCsvAutoParams() {
$reader = new FileReader(__DIR__ . '/impl/msexcel.csv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
$reader = new FileReader(__DIR__ . '/impl/ooffice.csv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
$reader = new FileReader(__DIR__ . '/impl/weird.tsv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
$reader = new FileReader(__DIR__ . '/impl/avec_bom.csv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
}
}

View File

@ -0,0 +1,2 @@
nom,prenom,age
clain,jephte,50
1 nom prenom age
2 clain jephte 50

View File

@ -0,0 +1 @@
0123456789ABCDEFGHIJ0123456789abcdefghij0123456789

View File

@ -0,0 +1,2 @@
nom;prenom;age
clain;jephte;50
1 nom prenom age
2 clain jephte 50

View File

@ -0,0 +1,2 @@
nom,prenom,age
clain,jephte,50
1 nom prenom age
2 clain jephte 50

View File

@ -0,0 +1 @@
0123456789ABCDEFGHIJ0123456789abcdefghij0123456789

View File

@ -0,0 +1,2 @@
nom prenom age
clain jephte 50
1 nom prenom age
2 clain jephte 50

View File

@ -0,0 +1,55 @@
<?php
namespace nulib\php\time;
use DateTimeZone;
use nulib\tests\TestCase;
class DateTest extends TestCase {
protected static function dt(string $datetime): Date {
return new Date($datetime, new DateTimeZone("Indian/Reunion"));
}
function testDate() {
$date = self::dt("2024-04-05 09:15:23");
self::assertSame("05/04/2024", $date->format());
self::assertSame("05/04/2024", strval($date));
self::assertSame(2024, $date->year);
self::assertSame(4, $date->month);
self::assertSame(5, $date->day);
self::assertSame(0, $date->hour);
self::assertSame(0, $date->minute);
self::assertSame(0, $date->second);
self::assertSame(5, $date->wday);
self::assertSame(14, $date->wnum);
self::assertSame("+04:00", $date->timezone);
self::assertSame("05/04/2024 00:00:00", $date->datetime);
self::assertSame("05/04/2024", $date->date);
}
function testClone() {
$date = self::dt("now");
$clone = Date::clone($date);
self::assertInstanceOf(DateTime::class, $clone);
}
function testConstruct() {
$y = date("Y");
self::assertSame("05/04/$y", strval(new Date("5/4")));
self::assertSame("05/04/2024", strval(new Date("5/4/24")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024")));
self::assertSame("05/04/2024", strval(new Date("05/04/2024")));
self::assertSame("05/04/2024", strval(new Date("20240405")));
self::assertSame("05/04/2024", strval(new Date("240405")));
self::assertSame("05/04/2024", strval(new Date("20240405T091523")));
self::assertSame("05/04/2024", strval(new Date("20240405T091523Z")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15:23")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15.23")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 9h15")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15:23")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15")));
self::assertSame("05/04/2024", strval(new Date("5/4/2024 09h15")));
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace nulib\php\time;
use DateTimeZone;
use nulib\tests\TestCase;
class DateTimeTest extends TestCase {
protected static function dt(string $datetime): DateTime {
return new DateTime($datetime, new DateTimeZone("Indian/Reunion"));
}
function testDateTime() {
$date = self::dt("2024-04-05 09:15:23");
self::assertEquals("05/04/2024 09:15:23", $date->format());
self::assertEquals("05/04/2024 09:15:23", strval($date));
self::assertSame(2024, $date->year);
self::assertSame(4, $date->month);
self::assertSame(5, $date->day);
self::assertSame(9, $date->hour);
self::assertSame(15, $date->minute);
self::assertSame(23, $date->second);
self::assertSame(5, $date->wday);
self::assertSame(14, $date->wnum);
self::assertEquals("+04:00", $date->timezone);
self::assertSame("05/04/2024 09:15:23", $date->datetime);
self::assertSame("05/04/2024", $date->date);
self::assertSame("20240405", $date->Ymd);
self::assertSame("20240405T091523", $date->YmdHMS);
self::assertSame("20240405T091523+04:00", $date->YmdHMSZ);
}
function testDateTimeZ() {
$date = new DateTime("20240405T091523Z");
self::assertSame("20240405T091523", $date->YmdHMS);
self::assertSame("20240405T091523Z", $date->YmdHMSZ);
}
function testClone() {
$date = self::dt("now");
$clone = DateTime::clone($date);
self::assertInstanceOf(DateTime::class, $clone);
}
function testConstruct() {
$y = date("Y");
self::assertSame("05/04/$y 00:00:00", strval(new DateTime("5/4")));
self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/24")));
self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/2024")));
self::assertSame("05/04/2024 00:00:00", strval(new DateTime("05/04/2024")));
self::assertSame("05/04/2024 00:00:00", strval(new DateTime("20240405")));
self::assertSame("05/04/2024 00:00:00", strval(new DateTime("240405")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523Z")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9:15:23")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9.15.23")));
self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9:15")));
self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9.15")));
self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9h15")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 09:15:23")));
self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09:15")));
self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09h15")));
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace nulib\php\time;
use DateTimeZone;
use nulib\tests\TestCase;
class DelayTest extends TestCase {
protected static function dt(string $datetime): DateTime {
return new DateTime($datetime, new DateTimeZone("Indian/Reunion"));
}
function testDelay() {
$from = self::dt("2024-04-05 09:15:23");
$delay = new Delay(10, $from);
self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest());
$delay = new Delay("10", $from);
self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest());
$delay = new Delay("10s", $from);
self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest());
$delay = new Delay("s", $from);
self::assertEquals(self::dt("2024-04-05 09:15:24"), $delay->getDest());
$delay = new Delay("5m", $from);
self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest());
$delay = new Delay("5m0", $from);
self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest());
$delay = new Delay("5m2", $from);
self::assertEquals(self::dt("2024-04-05 09:20:02"), $delay->getDest());
$delay = new Delay("m", $from);
self::assertEquals(self::dt("2024-04-05 09:16:00"), $delay->getDest());
$delay = new Delay("5h", $from);
self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest());
$delay = new Delay("5h0", $from);
self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest());
$delay = new Delay("5h2", $from);
self::assertEquals(self::dt("2024-04-05 14:02:00"), $delay->getDest());
$delay = new Delay("h", $from);
self::assertEquals(self::dt("2024-04-05 10:00:00"), $delay->getDest());
$delay = new Delay("5d", $from);
self::assertEquals(self::dt("2024-04-10 05:00:00"), $delay->getDest());
$delay = new Delay("5d2", $from);
self::assertEquals(self::dt("2024-04-10 02:00:00"), $delay->getDest());
$delay = new Delay("5d0", $from);
self::assertEquals(self::dt("2024-04-10 00:00:00"), $delay->getDest());
$delay = new Delay("d", $from);
self::assertEquals(self::dt("2024-04-06 05:00:00"), $delay->getDest());
$delay = new Delay("2w", $from);
self::assertEquals(self::dt("2024-04-21 05:00:00"), $delay->getDest());
$delay = new Delay("2w2", $from);
self::assertEquals(self::dt("2024-04-21 02:00:00"), $delay->getDest());
$delay = new Delay("2w0", $from);
self::assertEquals(self::dt("2024-04-21 00:00:00"), $delay->getDest());
$delay = new Delay("w", $from);
self::assertEquals(self::dt("2024-04-07 05:00:00"), $delay->getDest());
}
function testElapsed() {
$delay = new Delay(5);
sleep(2);
self::assertFalse($delay->isElapsed());
sleep(5);
self::assertTrue($delay->isElapsed());
}
}