importation nulib/phpss:php74

This commit is contained in:
Jephté Clain 2025-03-01 13:08:22 +04:00
parent bd9b0027f3
commit ea95c9a5e6
19 changed files with 3393 additions and 1 deletions

0
.dockerignore Normal file
View File

8
.gitignore vendored
View File

@ -1,2 +1,10 @@
.~lock*#
.*.swp
/vendor/
/.idea/shelf/
/.idea/workspace.xml
/.idea/httpRequests/
/.idea/dataSources/
/.idea/dataSources.local.xml
/.phpunit.result.cache

17
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State />
</expanded-state>
<selected-state>
<State>
<id>Angular</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/nulib-phpss.iml" filepath="$PROJECT_DIR$/.idea/nulib-phpss.iml" />
</modules>
</component>
</project>

12
.idea/nulib-phpss.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="nulib\ext\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="nulib\ext\" />
<excludeFolder url="file://$MODULE_DIR$/vendor" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

38
.idea/php-docker-settings.xml generated Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpDockerContainerSettings">
<list>
<map>
<entry key="125ffb9d-fd5f-4e71-8182-94191665795a">
<value>
<DockerContainerSettings>
<option name="version" value="1" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/opt/project" />
<option name="hostPath" value="$PROJECT_DIR$" />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
<entry key="c4cf2564-ed91-488c-a93d-fe2daeae80db">
<value>
<DockerContainerSettings>
<option name="version" value="1" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/opt/project" />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
</map>
</list>
</component>
</project>

66
.idea/php.xml generated Normal file
View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/nulib/php" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/ezyang/htmlpurifier" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/myclabs/php-enum" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4" />
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -9,4 +9,4 @@ TAG_PREFIX=
TAG_SUFFIX=p74
HOTFIX=hotf74-
DIST=
NOAUTO=
NOAUTO=1

8
.runphp.conf Normal file
View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
# Chemin vers runphp, e.g sbin/runphp
RUNPHP=
# Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies
DIST=d11
#REGISTRY=pubdocker.univ-reunion.fr/dist

5
TODO.md Normal file
View File

@ -0,0 +1,5 @@
# TODO
* [ ] mettre en cohérence avec l'API de nulib/spout (notamment wsname et spout)
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

45
composer.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "nulib/phpss",
"type": "library",
"description": "wrapper pour phpoffice/phpspreadsheet",
"repositories": [
{
"type": "path",
"url": "../nulib"
},
{
"type": "composer",
"url": "https://repos.univ-reunion.fr/composer"
}
],
"extra": {
"branch-alias": {
"dev-dev74": "7.4.x-dev",
"dev-dev82": "8.2.x-dev"
}
},
"require": {
"nulib/php": "^7.4-dev",
"phpoffice/phpspreadsheet": "^1.0",
"php": "^7.4"
},
"require-dev": {
"nulib/tests": "^7.4"
},
"autoload": {
"psr-4": {
"nulib\\ext\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"nulib\\ext\\": "tests"
}
},
"authors": [
{
"name": "Jephte Clain",
"email": "Jephte.Clain@univ-reunion.fr"
}
]
}

2857
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

0
src/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,112 @@
<?php
namespace nulib\ext\phpss;
use nulib\file\tab\AbstractBuilder;
use nulib\file\tab\TAbstractBuilder;
use nulib\os\path;
use nulib\web\http;
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
class PhpSpreadsheetBuilder extends AbstractBuilder {
use TAbstractBuilder;
/** @var string|int|null nom de la feuille dans laquelle écrire */
const WSNAME = null;
function __construct(?string $output, ?array $params=null) {
parent::__construct($output, $params);
$this->ss = new Spreadsheet();
$this->valueBinder = new StringValueBinder();
$this->setWsname($params["wsname"] ?? static::WSNAME);
}
protected Spreadsheet $ss;
protected IValueBinder $valueBinder;
protected ?Worksheet $ws;
protected int $nrow;
const STYLE_ROW = 0, STYLE_HEADER = 1;
protected int $rowStyle;
/**
* @param string|int|null $wsname
*/
function setWsname($wsname): self {
$ss = $this->ss;
$this->ws = null;
$this->nrow = 0;
$this->rowStyle = self::STYLE_ROW;
$ws = wsutils::get_ws($wsname, $ss);
if ($ws === null) {
$ws = $ss->createSheet()->setTitle($wsname);
$this->wroteHeaders = false;
} else {
$maxRow = wsutils::compute_max_coords($ws)[1];
$this->nrow = $maxRow - 1;
$this->wroteHeaders = $maxRow > 1;
}
$this->ws = $ws;
return $this;
}
function _write(array $row): void {
$ws = $this->ws;
$styleHeader = $this->rowStyle === self::STYLE_HEADER;
$nrow = ++$this->nrow;
$ncol = 1;
foreach ($row as $col) {
$ws->getCellByColumnAndRow($ncol++, $nrow)->setValue($col, $this->valueBinder);
}
if ($styleHeader) {
$ws->getStyle("$nrow:$nrow")->getFont()->setBold(true);
$maxcol = count($row);
for ($ncol = 1; $ncol <= $maxcol; $ncol++) {
$ws->getColumnDimensionByColumn($ncol)->setAutoSize(true);
}
}
}
function writeHeaders(?array $headers=null): void {
$this->rowStyle = self::STYLE_HEADER;
parent::writeHeaders($headers);
$this->rowStyle = self::STYLE_ROW;
}
function _sendContentType(): void {
switch (path::ext($this->output)) {
case ".ods":
$contentType = "application/vnd.oasis.opendocument.spreadsheet";
break;
case ".xlsx":
default:
$contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
break;
}
http::content_type($contentType);
}
protected function _checkOk(): bool {
switch (path::ext($this->output)) {
case ".ods":
$writer = new Ods($this->ss);
break;
case ".xlsx":
default:
$writer = new Xlsx($this->ss);
break;
}
$writer->save($this->getResource());
$this->rewind();
return true;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace nulib\ext\phpss;
use nulib\cl;
use nulib\file\tab\AbstractReader;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
class PhpSpreadsheetReader extends AbstractReader {
const DATETIME_FORMAT = 'dd/mm/yyyy hh:mm:ss';
const DATE_FORMAT = 'dd/mm/yyyy';
const TIME_FORMAT = 'hh:mm:ss';
const FORMAT_MAPPINGS = [
'mm/dd hh' => self::DATETIME_FORMAT,
'dd/mm hh' => self::DATETIME_FORMAT,
'mm/dd hh:mm' => self::DATETIME_FORMAT,
'dd/mm hh:mm' => self::DATETIME_FORMAT,
'mm/dd hh:mm:ss' => self::DATETIME_FORMAT,
'dd/mm hh:mm:ss' => self::DATETIME_FORMAT,
'mm/dd/yyyy hh' => self::DATETIME_FORMAT,
'dd/mm/yyyy hh' => self::DATETIME_FORMAT,
'mm/dd/yyyy hh:mm' => self::DATETIME_FORMAT,
'dd/mm/yyyy hh:mm' => self::DATETIME_FORMAT,
'mm/dd/yyyy hh:mm:ss' => self::DATETIME_FORMAT,
'dd/mm/yyyy hh:mm:ss' => self::DATETIME_FORMAT,
'yyyy/mm/dd hh' => self::DATETIME_FORMAT,
'yyyy/mm/dd hh:mm' => self::DATETIME_FORMAT,
'yyyy/mm/dd hh:mm:ss' => self::DATETIME_FORMAT,
'mm/dd' => self::DATE_FORMAT,
'dd/mm' => self::DATE_FORMAT,
'mm/dd/yyyy' => self::DATE_FORMAT,
'dd/mm/yyyy' => self::DATE_FORMAT,
'yyyy/mm/dd' => self::DATE_FORMAT,
'mm/yyyy' => self::DATE_FORMAT,
'hh AM/PM' => self::TIME_FORMAT,
'hh:mm AM/PM' => self::TIME_FORMAT,
'hh:mm:ss AM/PM' => self::TIME_FORMAT,
'hh' => self::TIME_FORMAT,
'hh:mm' => self::TIME_FORMAT,
'hh:mm:ss' => self::TIME_FORMAT,
'[hh]:mm:ss' => self::TIME_FORMAT,
'mm:ss' => self::TIME_FORMAT,
];
/** @var string|int|null nom de la feuille depuis laquelle lire */
const WSNAME = null;
function __construct($input, ?array $params=null) {
parent::__construct($input, $params);
$this->wsname = $params["wsname"] ?? static::WSNAME;
}
protected $wsname;
/**
* @param string|int|null $wsname
*/
function setWsname($wsname): self {
$this->wsname = $wsname;
return $this;
}
function getIterator() {
$ss = IOFactory::load($this->input);
$ws = wsutils::get_ws($this->wsname, $ss);
[$nbCols, $nbRows] = wsutils::compute_max_coords($ws);
$this->isrc = $this->idest = 0;
for ($nrow = 1; $nrow <= $nbRows; $nrow++) {
$row = [];
for ($ncol = 1; $ncol <= $nbCols; $ncol++) {
if ($ws->cellExistsByColumnAndRow($ncol, $nrow)) {
$cell = $ws->getCellByColumnAndRow($ncol, $nrow);
$col = $cell->getValue();
if ($col instanceof RichText) {
$col = $col->getPlainText();
} else {
$dataType = $cell->getDataType();
if ($dataType == DataType::TYPE_NUMERIC || $dataType == DataType::TYPE_FORMULA) {
# si c'est un format date, le forcer à une valeur standard
$origFormatCode = $cell->getStyle()->getNumberFormat()->getFormatCode();
if (strpbrk($origFormatCode, "ymdhs") !== false) {
$formatCode = $origFormatCode;
$formatCode = preg_replace('/y+/', "yyyy", $formatCode);
$formatCode = preg_replace('/m+/', "mm", $formatCode);
$formatCode = preg_replace('/d+/', "dd", $formatCode);
$formatCode = preg_replace('/h+/', "hh", $formatCode);
$formatCode = preg_replace('/s+/', "ss", $formatCode);
$formatCode = preg_replace('/-+/', "/", $formatCode);
$formatCode = preg_replace('/\\\\ /', " ", $formatCode);
$formatCode = preg_replace('/;@$/', "", $formatCode);
$formatCode = cl::get(self::FORMAT_MAPPINGS, $formatCode, $formatCode);
if ($formatCode !== $origFormatCode) {
$cell->getStyle()->getNumberFormat()->setFormatCode($formatCode);
}
}
}
$col = $cell->getFormattedValue();
$this->verifixCol($col);
}
} else {
$col = null;
}
$row[] = $col;
}
if ($this->cookRow($row)) {
yield $row;
$this->idest++;
}
$this->isrc++;
}
}
}

14
src/phpss/ssutils.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace nulib\ext\phpss;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class ssutils {
static function each_compute_max_coords(Spreadsheet $ss): array {
$max_coords = [];
foreach ($ss->getAllSheets() as $ws) {
$max_coords[$ws->getTitle()] = wsutils::compute_max_coords($ws);
}
return $max_coords;
}
}

80
src/phpss/wsutils.php Normal file
View File

@ -0,0 +1,80 @@
<?php
namespace nulib\ext\phpss;
use nulib\ValueException;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class wsutils {
static function get_ws(?string $wsname, Spreadsheet $ss, bool $create=false): ?Worksheet {
if ($wsname == null) {
$ws = $ss->getActiveSheet();
} elseif (is_numeric($wsname)) {
$sheetCount = $ss->getSheetCount();
if ($wsname < 1 || $wsname > $sheetCount) {
throw ValueException::invalid_value($wsname, "sheet index");
}
$ws = $ss->getSheet($wsname - 1);
} else {
$ws = $ss->getSheetByName($wsname);
if ($ws === null) {
if ($create) $ws = $ss->createSheet()->setTitle($wsname);
else throw ValueException::invalid_value($wsname, "sheet name");
}
}
return $ws;
}
static function get_highest_coords(Worksheet $ws): array {
$highestColumnA = $ws->getHighestColumn();
$highestCol = Coordinate::columnIndexFromString($highestColumnA);
$highestRow = $ws->getHighestRow();
return [$highestCol, $highestRow];
}
/**
* @var int nombre de colonnes/lignes au bout desquels on arrête de chercher
* si on n'a trouvé que des cellules vides.
*
* c'est nécessaire à cause de certains fichiers provenant d'Excel que j'ai
* reçus qui ont jusqu'à 10000 colonne vides et/ou 1048576 lignes vides. un
* algorithme "bête" perd énormément de temps à chercher dans le vide, donnant
* l'impression que le processus a planté.
*/
const MAX_EMPTY_THRESHOLD = 150;
static function compute_max_coords(Worksheet $ws): array {
[$highestCol, $highestRow] = self::get_highest_coords($ws);
$maxCol = 1;
$maxRow = 1;
$maxEmptyRows = self::MAX_EMPTY_THRESHOLD;
for ($row = 1; $row <= $highestRow; $row++) {
$emptyRow = true;
$maxEmptyCols = self::MAX_EMPTY_THRESHOLD;
for ($col = 1; $col <= $highestCol; $col++) {
$value = null;
if ($ws->cellExistsByColumnAndRow($col, $row)) {
$value = $ws->getCellByColumnAndRow($col, $row)->getValue();
}
if ($value === null) {
$maxEmptyCols--;
if ($maxEmptyCols == 0) break;
} else {
$maxEmptyCols = self::MAX_EMPTY_THRESHOLD;
if ($row > $maxRow) $maxRow = $row;
if ($col > $maxCol) $maxCol = $col;
$emptyRow = false;
}
}
if ($emptyRow) {
$maxEmptyRows--;
if ($maxEmptyRows == 0) break;
} else {
$maxEmptyRows = self::MAX_EMPTY_THRESHOLD;
}
}
return [$maxCol, $maxRow];
}
}

0
tests/.gitignore vendored Normal file
View File