Compare commits

...

No commits in common. "upstream-4.x" and "php74" have entirely different histories.

271 changed files with 15445 additions and 7721 deletions

6
.composer.yaml Normal file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
require:
nulib/php: ^7.4-dev
branch:
develop:
master:

0
.dockerignore Normal file
View File

10
.gitignore vendored Normal file
View File

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

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

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-spout.iml" filepath="$PROJECT_DIR$/.idea/nulib-spout.iml" />
</modules>
</component>
</project>

13
.idea/nulib-spout.iml generated Normal file
View File

@ -0,0 +1,13 @@
<?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\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="nulib\ext\" />
<sourceFolder url="file://$MODULE_DIR$/upstream-3.x/src" isTestSource="false" packagePrefix="OpenSpout\" />
<excludeFolder url="file://$MODULE_DIR$/vendor" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

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

@ -0,0 +1,23 @@
<?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" />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
</map>
</list>
</component>
</project>

14
.idea/php-test-framework.xml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpTestFrameworkVersionCache">
<tools_cache>
<tool tool_name="PHPUnit">
<cache>
<versions>
<info id="Local/vendor/autoload.php" version="9.6.21" />
</versions>
</cache>
</tool>
</tools_cache>
</component>
</project>

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

@ -0,0 +1,107 @@
<?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="PhpCSFixer">
<phpcsfixer_settings>
<PhpCSFixerConfiguration tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
</phpcsfixer_settings>
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/react/promise" />
<path value="$PROJECT_DIR$/vendor/react/cache" />
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
<path value="$PROJECT_DIR$/vendor/react/dns" />
<path value="$PROJECT_DIR$/vendor/react/child-process" />
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
<path value="$PROJECT_DIR$/vendor/react/socket" />
<path value="$PROJECT_DIR$/vendor/react/stream" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php81" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/vendor/nulib/php" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PhpStan">
<PhpStan_settings>
<PhpStanConfiguration tool_path="$PROJECT_DIR$/vendor/bin/phpstan" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" phpunit_phar_path="" />
</phpunit_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

10
.idea/phpunit.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PHPUnit">
<option name="directories">
<list>
<option value="$PROJECT_DIR$/tests" />
</list>
</option>
</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>

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

25
_merge2php82 Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
mydir="$(cd "$(dirname -- "$0")"; pwd)"
myself="$mydir/$(basename -- "$0")"
cwd="$(pwd)"
if [ "$1" != --stage2 -o -z "$2" ]; then
cp "$myself" /tmp/merge2php82.sh
exec /tmp/merge2php82.sh --stage2 "$mydir"
fi
cd "$2" || die
if [ -f vendor/nulib/php/load.sh ]; then
source ./vendor/nulib/php/load.sh || exit 1
else
source /etc/nulib.sh || exit 1
fi
git checkout php82
git rebase php74 ||
die "Le rebase automatique a échoué. Après avoir résolu les conflits, faire
git checkout php74
pp -af
"
git checkout php74
pp -af

62
composer.json Normal file
View File

@ -0,0 +1,62 @@
{
"name": "nulib/spout",
"type": "library",
"description": "wrapper pour openspout/openspout",
"repositories": [
{
"type": "path",
"url": "../nulib"
},
{
"type": "composer",
"url": "https://repos.univ-reunion.fr/composer"
}
],
"extra": {
"branch-alias": {
"dev-php74": "7.4.x-dev",
"dev-php82": "8.2.x-dev"
}
},
"replace": {
"openspout/openspout": "v3.7.4"
},
"require": {
"nulib/php": "^7.4-dev",
"ext-dom": "*",
"ext-filter": "*",
"ext-libxml": "*",
"ext-xmlreader": "*",
"ext-zip": "*",
"php": "^7.4"
},
"require-dev": {
"nulib/tests": "^7.4",
"friendsofphp/php-cs-fixer": "^3.4",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1.0",
"ext-zlib": "*"
},
"autoload": {
"psr-4": {
"nulib\\": "src",
"OpenSpout\\": "upstream-3.x/src"
}
},
"autoload-dev": {
"psr-4": {
"nulib\\ext\\": "tests"
}
},
"authors": [
{
"name": "Jephte Clain",
"email": "Jephte.Clain@univ-reunion.fr"
}
],
"config": {
"allow-plugins": {
"infection/extension-installer": false
}
}
}

4583
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

77
patches/v3.7.4.patch Normal file
View File

@ -0,0 +1,77 @@
diff --git a/src/Writer/WriterAbstract.php b/src/Writer/WriterAbstract.php
index fd7c472..a2a5e37 100644
--- a/src/Writer/WriterAbstract.php
+++ b/src/Writer/WriterAbstract.php
@@ -22,6 +22,9 @@ abstract class WriterAbstract implements WriterInterface
/** @var resource Pointer to the file/stream we will write to */
protected $filePointer;
+ /** @var bool faut-il garder ouvert le flux quand close() est appelé? */
+ protected $dontCloseFilePointer = false;
+
/** @var bool Indicates whether the writer has been opened or not */
protected $isWriterOpened = false;
@@ -57,6 +60,20 @@ abstract class WriterAbstract implements WriterInterface
return $this;
}
+ public function writeToStream($filePointer)
+ {
+ $this->outputFilePath = null;
+
+ $this->filePointer = $filePointer;
+ $this->dontCloseFilePointer = true;
+ $this->throwIfFilePointerIsNotAvailable();
+
+ $this->openWriter();
+ $this->isWriterOpened = true;
+
+ return $this;
+ }
+
/**
* {@inheritdoc}
*/
@@ -177,7 +194,7 @@ abstract class WriterAbstract implements WriterInterface
$this->closeWriter();
- if (\is_resource($this->filePointer)) {
+ if (!$this->dontCloseFilePointer && \is_resource($this->filePointer)) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
diff --git a/src/Reader/XLSX/Helper/CellValueFormatter.php b/src/Reader/XLSX/Helper/CellValueFormatter.php
index 1734fb5..08e5282 100644
--- a/src/Reader/XLSX/Helper/CellValueFormatter.php
+++ b/src/Reader/XLSX/Helper/CellValueFormatter.php
@@ -268,9 +268,13 @@ class CellValueFormatter
$dateObj->modify('+'.$secondsRemainder.'seconds');
if ($this->shouldFormatDates) {
- $styleNumberFormatCode = $this->styleManager->getNumberFormatCode($cellStyleId);
- $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
+ //$styleNumberFormatCode = $this->styleManager->getNumberFormatCode($cellStyleId);
+ //$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
+ // Toujours utiliser le format français complet
+ $phpDateFormat = "d/m/Y H:i:s";
$cellValue = $dateObj->format($phpDateFormat);
+ // Enlever la composante heure si elle n'existe pas
+ $cellValue = preg_replace('/ 00:00:00$/', "", $cellValue);
} else {
$cellValue = $dateObj;
}
diff --git a/src/Reader/XLSX/Manager/OptionsManager.php b/src/Reader/XLSX/Manager/OptionsManager.php
index b04b92c..5749f65 100644
--- a/src/Reader/XLSX/Manager/OptionsManager.php
+++ b/src/Reader/XLSX/Manager/OptionsManager.php
@@ -29,7 +29,7 @@ class OptionsManager extends OptionsManagerAbstract
protected function setDefaultOptions()
{
$this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir());
- $this->setOption(Options::SHOULD_FORMAT_DATES, false);
+ $this->setOption(Options::SHOULD_FORMAT_DATES, true);
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
$this->setOption(Options::SHOULD_USE_1904_DATES, false);
}

0
src/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,465 @@
<?php
namespace nulib\ext\spout;
use nulib\cl;
use nulib\file\tab\AbstractBuilder;
use nulib\file\tab\TAbstractBuilder;
use nulib\os\path;
use nulib\php\func;
use nulib\php\nur_func;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
use nulib\ref\ext\spout\ref_builder;
use nulib\ref\ext\spout\ref_builder_ods;
use nulib\ref\ext\spout\ref_builder_xlsx;
use nulib\str;
use nulib\web\http;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Style\Border;
use OpenSpout\Common\Entity\Style\BorderPart;
use OpenSpout\Common\Entity\Style\Style;
use OpenSpout\Common\Helper\CellTypeHelper;
use OpenSpout\Writer\Common\Creator\WriterEntityFactory;
use OpenSpout\Writer\WriterMultiSheetsAbstract;
use OpenSpout\Writer\XLSX\Entity\SheetView;
class SpoutBuilder extends AbstractBuilder {
use TAbstractBuilder;
protected static function apply_params($object, ?array $params, array $refParams) {
foreach (array_keys($refParams) as $method) {
if (!str::starts_with("->", $method)) continue;
$func = func::with([$object, $method]);
if (($args = $params[$method] ?? null) !== null) {
$func->invoke(cl::with($args));
}
if (($argss = $params["$method*"] ?? null) !== null) {
foreach ($argss as $args) {
$func->invoke(cl::with($args));
}
}
}
return $object;
}
protected static function add_border_part(?Border &$border, string $name, ?array $params): void {
if ($params === null) return;
if ($border === null) $border = new Border();
$part = new BorderPart($name);
if (($color = $params["color"] ?? null) !== null) {
$part->setColor(cl::get(ref_builder::COLORS, $color, $color));
}
if (($width = $params["width"] ?? null) !== null) $part->setWidth($width);
if (($style = $params["style"] ?? null) !== null) $part->setStyle($style);
$border->addPart($part);
}
protected static function set_defaults(?array &$params, string $key, array $defaults): void {
if ($params !== null && array_key_exists($key, $params)) {
if ($params[$key] === false) $params[$key] = null;
else $params[$key] ??= $defaults;
} else {
$params[$key] ??= $defaults;
}
}
protected static function ensure_style(&$style): ?Style {
if ($style === null) return null;
if ($style instanceof Style) return $style;
$cell = $style;
$style = new Style();
$font = $cell["font"] ?? null;
if ($font["bold"] ?? null) $style->setFontBold();
if ($font["italic"] ?? null) $style->setFontItalic();
if ($font["underline"] ?? null) $style->setFontUnderline();
if ($font["strikethrough"] ?? null) $style->setFontStrikethrough();
if (($name = $font["name"] ?? null) !== null) $style->setFontName($name);
if (($size = $font["size"] ?? null) !== null) $style->setFontSize($size);
if (($color = $font["color"] ?? null) !== null) {
$style->setFontColor(cl::get(ref_builder::COLORS, $color, $color));
}
if (($color = $cell["bg_color"] ?? null) !== null) {
$style->setBackgroundColor(cl::get(ref_builder::COLORS, $color, $color));
}
if (($align = $cell["align"] ?? null) !== null) $style->setCellAlignment($align);
//if (($align = $cell["valign"] ?? null) !== null) $style->setCellVerticalAlignment($align);
if (($wrap = $cell["wrap"] ?? null) !== null) $style->setShouldWrapText($wrap);
if (($format = $cell["format"] ?? null) !== null) $style->setFormat($format);
if (($border = $cell["border"] ?? null) !== null) {
if (is_string($border)) {
$parts = explode(" ", $border);
$border = [];
$styleAll = null;
$widthAll = null;
$colorAll = null;
foreach ($parts as $part) {
if ($part === "all") {
$border["left"] = [];
$border["top"] = [];
$border["right"] = [];
$border["bottom"] = [];
} elseif (preg_match('/^(left|top|right|bottom)$/', $part)) {
$border[$part] = [];
} elseif (preg_match('/^(none|solid|dashed|dotted|double)$/', $part)) {
$styleAll = $part;
} elseif (preg_match('/^(thin|medium|thick)$/', $part)) {
$widthAll = $part;
} else {
$colorAll = $part;
}
}
foreach ($border as &$part) {
if ($styleAll !== null) $part["style"] = $styleAll;
if ($widthAll !== null) $part["width"] = $widthAll;
if ($colorAll !== null) $part["color"] = $colorAll;
}; unset($part);
}
$top = $border["top"] ?? null;
$right = $border["right"] ?? null;
$bottom = $border["bottom"] ?? null;
$left = $border["left"] ?? null;
$border = null;
self::add_border_part($border, "top", $top);
self::add_border_part($border, "right", $right);
self::add_border_part($border, "bottom", $bottom);
self::add_border_part($border, "left", $left);
if ($border !== null) $style->setBorder($border);
}
return $style;
}
const DATE_FORMAT = "dd/mm/yyyy";
const DATETIME_FORMAT = "dd/mm/yyyy hh:mm:ss";
/** @var bool faut-il choisir le type numérique pour une chaine numérique? */
const TYPE_NUMERIC = true;
/** @var bool faut-il choisir le type date pour une chaine au bon format? */
const TYPE_DATE = true;
/** @var array configuration du writer */
const SPOUT_PARAMS = null;
/** @var array configuration de la première feuille */
const SHEET_PARAMS = null;
/** @var string nom de la première feuille */
const SHEET_NAME = null;
/** @var array configuration de la vue de la première feuille */
const SHEET_VIEW_PARAMS = null;
function __construct(?string $output, ?array $params=null) {
parent::__construct($output, $params);
$ssType = $params["ss_type"] ?? null;
if ($ssType === null) {
switch (path::ext($this->output)) {
case ".ods":
$ssType = self::SS_TYPE_ODS;
break;
case ".xlsx":
default:
$ssType = self::SS_TYPE_XLSX;
break;
}
}
$spoutParams = $params["spout"] ?? static::SPOUT_PARAMS;
$spoutParams["default_column_width"] ??= 10.5;
self::ensure_style($spoutParams["default_row_style"]);
switch ($ssType) {
case "ods":
case self::SS_TYPE_ODS:
$ssType = self::SS_TYPE_ODS;
$ssWriter = WriterEntityFactory::createODSWriter();
self::apply_params($ssWriter, $spoutParams, ref_builder_ods::PARAMS_SPOUT);
break;
case "xlsx":
case self::SS_TYPE_XLSX:
default:
$ssType = self::SS_TYPE_XLSX;
$ssWriter = WriterEntityFactory::createXLSXWriter();
self::apply_params($ssWriter, $spoutParams, ref_builder_xlsx::PARAMS_SPOUT);
break;
}
$defaultColumnWidth = $spoutParams["default_column_width"] ?? null;
if ($defaultColumnWidth !== null) $ssWriter->setDefaultColumnWidth($defaultColumnWidth);
$defaultRowHeight = $spoutParams["default_row_height"] ?? null;
if ($defaultRowHeight !== null) $ssWriter->setDefaultRowHeight($defaultRowHeight);
$defaultRowStyle = $spoutParams["default_row_style"] ?? null;
if ($defaultRowStyle !== null) $ssWriter->setDefaultRowStyle($defaultRowStyle);
$ssWriter->writeToStream($this->getResource());
$this->ssType = $ssType;
$this->ssWriter = $ssWriter;
$this->spoutParams = $spoutParams;
$this->typeNumeric = boolval($params["type_numeric"] ?? static::TYPE_NUMERIC);
$this->typeDate = boolval($params["type_date"] ?? static::TYPE_DATE);
$sheetParams = $params["sheet"] ?? static::SHEET_PARAMS;
$sheetName = $params["sheet_name"] ?? static::SHEET_NAME;
if ($sheetName !== null) $sheetParams["->setName"] = $sheetName;
$sheetViewParams = $params["sheet_view"] ?? static::SHEET_VIEW_PARAMS;
if ($sheetViewParams !== null) $sheetParams["view"] = $sheetViewParams;
$this->firstSheet = true;
$this->sheetParams = null;
$this->setSheet(null, $sheetParams);
}
const SS_TYPE_ODS = 1, SS_TYPE_XLSX = 2;
/** @var int type de fichier généré */
protected int $ssType;
protected WriterMultiSheetsAbstract $ssWriter;
protected ?array $spoutParams;
protected bool $typeNumeric;
protected bool $typeDate;
function setParams(?array $params): self {
if ($params !== null) {
if (array_key_exists("type_numeric", $params)) {
$this->typeNumeric = boolval($params["type_numeric"] ?? static::TYPE_NUMERIC);
}
if (array_key_exists("type_date", $params)) {
$this->typeDate = boolval($params["type_date"] ?? static::TYPE_DATE);
}
}
return $this;
}
protected bool $firstSheet;
protected ?array $sheetParams;
const STYLE_ROW = 0, STYLE_HEADER = 1;
protected int $rowStyle;
protected int $currentRow;
protected ?bool $differentOddEven = null;
protected int $oddEvenIndex = 0;
function setDifferentOddEven(bool $differentOddEven, ?bool $startWithOdd=null): self {
$this->differentOddEven = $differentOddEven;
if ($differentOddEven && $startWithOdd !== null) $this->oddEvenIndex = $startWithOdd? 1: 0;
return $this;
}
/**
* @param string|int|null $sheetName
*/
function setSheet($sheetName, ?array $sheetParams=null): self {
$sheet = $params["sheet"] ?? null;
$sheetName = $sheetName ?? $sheetParams["sheet_name"] ?? null;
$sheetViewParams = $sheetParams["sheet_view"] ?? null;
if ($sheet !== null) $sheetParams = $sheet;
if ($sheetName !== null) $sheetParams["->setName"] = $sheetName;
if ($sheetViewParams !== null) $sheetParams["view"] = $sheetViewParams;
$writer = $this->ssWriter;
if ($this->firstSheet) {
$this->firstSheet = false;
$sheet = $writer->getCurrentSheet();
} else {
$sheet = $writer->addNewSheetAndMakeItCurrent();
$this->wroteHeaders = false;
$this->built = false;
}
$this->rowStyle = self::STYLE_ROW;
$this->currentRow = 1;
switch ($this->ssType) {
case self::SS_TYPE_ODS:
# appliquer les paramètres de la feuille
$this->apply_params($sheet, $sheetParams, ref_builder_ods::PARAMS_SHEET);
break;
case self::SS_TYPE_XLSX:
# appliquer les paramètres de la feuille
$this->apply_params($sheet, $sheetParams, ref_builder_xlsx::PARAMS_SHEET);
# appliquer les paramètres de la vue de la feuille
$sheetViewParams =& $sheetParams["view"];
$sheetViewParams["->setFreezeRow"] ??= 2;
$sheet->setSheetView(self::apply_params(new SheetView(), $sheetViewParams, ref_builder_xlsx::PARAMS_SHEET_VIEW));
break;
}
self::set_defaults($sheetParams, "header_style", [
"font" => ["bold" => true],
"bg_color" => "gray",
]);
self::set_defaults($sheetParams, "odd_style", [
"wrap" => false,
]);
self::set_defaults($sheetParams, "even_style", [
"bg_color" => "light_gray",
"wrap" => false,
]);
$this->ensure_style($sheetParams["header_style"]);
$this->ensure_style($sheetParams["odd_style"]);
$this->ensure_style($sheetParams["even_style"]);
$this->sheetParams = $sheetParams;
if ($sheetParams !== null) {
if (array_key_exists("schema", $sheetParams)) {
$this->schema = $sheetParams["schema"] ?? null;
}
if (array_key_exists("headers", $sheetParams)) {
$this->headers = $sheetParams["headers"] ?? null;
}
if (array_key_exists("rows", $sheetParams)) {
$rows = $sheetParams["rows"] ?? null;
if (is_callable($rows)) $rows = $rows();
$this->rows = $rows;
}
if (array_key_exists("cook_func", $sheetParams)) {
$cookFunc = $sheetParams["cook_func"] ?? null;
$cookCtx = $cookArgs = null;
if ($cookFunc !== null) {
nur_func::ensure_func($cookFunc, $this, $cookArgs);
$cookCtx = nur_func::_prepare($cookFunc);
}
$this->cookCtx = $cookCtx;
$this->cookArgs = $cookArgs;
}
}
return $this;
}
/**
* les colonnes sont indexées sur 0 (e.g A = 0, B = 1, etc.)
* Les lignes sont indexées sur 1
*/
function mergeCells(int $topLeftCol, int $topLeftRow, int $bottomRightCol, int $bottomRightRow): void {
$this->ssWriter->mergeCells([$topLeftCol, $topLeftRow], [$bottomRightCol, $bottomRightRow]);
}
protected function isNumeric($value): bool {
if ($this->typeNumeric && is_numeric($value)) return true;
if (!is_string($value) && is_numeric($value)) return true;
return false;
}
protected function isDate(&$value, &$style): bool {
if ($value instanceof Date) {
$style ??= new Style();
$style->setFormat(self::DATE_FORMAT);
return true;
} elseif ($value instanceof DateTime) {
$style ??= new Style();
$style->setFormat(self::DATETIME_FORMAT);
return true;
} elseif (CellTypeHelper::isDateTimeOrDateInterval($value)) {
$style ??= new Style();
$style->setFormat(self::DATE_FORMAT);
return true;
}
if (!is_string($value) || !$this->typeDate) return false;
if (DateTime::isa_datetime($value, true)) {
$value = new DateTime($value);
$style ??= new Style();
$style->setFormat(self::DATETIME_FORMAT);
return true;
}
if (DateTime::isa_date($value, true)) {
$value = new Date($value);
$style ??= new Style();
$style->setFormat(self::DATE_FORMAT);
return true;
}
return false;
}
function _write(array $row, ?array $colStyles=null, ?array $rowStyle=null): void {
$rowParams = null;
if ($rowStyle !== null) {
# séparer rowParams (pour configurer l'instance de $row) et $rowStyle
# (pour appliquer un style sur la ligne)
foreach (array_keys(ref_builder::ROW_PARAMS) as $method) {
$value = $rowStyle[$method] ?? null;
unset($rowStyle[$method]);
if ($value !== null) $rowParams[$method] = $value;
}
if ($rowStyle === []) $rowStyle = null;
}
$sheetParams = $this->sheetParams;
$headerStyle = $sheetParams["header_style"] ?? null;
$oddStyle = $sheetParams["odd_style"] ?? null;
$evenStyle = $sheetParams["even_style"] ?? null;
$differentOddEven = $this->differentOddEven;
$differentOddEven ??= $sheetParams["different_odd_even"] ?? true;
$cells = [];
foreach ($row as $key => $col) {
$style = $colStyles[$key] ?? null;
self::ensure_style($style);
if ($col === null || $col === "") {
$type = Cell::TYPE_EMPTY;
} elseif ($this->isNumeric($col)) {
$type = Cell::TYPE_NUMERIC;
} elseif ($this->isDate($col, $style)) {
$type = Cell::TYPE_DATE;
} else {
$type = Cell::TYPE_STRING;
}
$cell = WriterEntityFactory::createCell($col, $style);
$cell->setType($type);
$cells[] = $cell;
}
if ($this->rowStyle === self::STYLE_HEADER) {
$rowStyle ??= $headerStyle;
} elseif ($differentOddEven && $this->oddEvenIndex % 2 == 0) {
$rowStyle ??= $evenStyle;
}
$rowStyle ??= $oddStyle;
self::ensure_style($rowStyle);
$row = WriterEntityFactory::createRow($cells, $rowStyle);
self::apply_params($row, $rowParams, ref_builder::ROW_PARAMS);
$mergeCells = $rowParams["merge_cells"] ?? null;
$mergeOffset = $rowParams["merge_offset"] ?? 0;
if ($mergeCells !== null) {
$currentRow = $this->currentRow;
foreach ($mergeCells as [$leftCol, $rightCol]) {
$this->mergeCells($leftCol + $mergeOffset, $currentRow, $rightCol + $mergeOffset, $currentRow);
}
}
$this->ssWriter->addRow($row);
$this->currentRow++;
if ($differentOddEven) $this->oddEvenIndex++;
}
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 {
$this->ssWriter->close();
$this->rewind();
return true;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace nulib\ext\spout;
use nulib\cl;
use nulib\file\tab\AbstractReader;
use OpenSpout\Reader\Common\Creator\ReaderEntityFactory;
class SpoutReader extends AbstractReader {
/** @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->ssType = $params["ss_type"] ?? null;
$this->allSheets = $params["all_sheets"] ?? true;
$wsname = static::WSNAME;
if ($params !== null && array_key_exists("wsname", $params)) {
# spécifié par l'utilisateur: $allSheets = false
$this->setWsname($params["wsname"]);
} elseif ($wsname !== null) {
# valeur non nulle de la classe: $allSheets = false
$this->setWsname($wsname);
} else {
# pas de valeur définie dans la classe, laisser $allSheets à sa valeur
# actuelle
$this->wsname = null;
}
$this->includeWsnames = cl::withn($params["include_wsnames"] ?? null);
$this->excludeWsnames = cl::withn($params["exclude_wsnames"] ?? null);
}
protected ?string $ssType;
/** @var bool faut-il retourner les lignes de toutes les feuilles? */
protected bool $allSheets;
function setAllSheets(bool $allSheets=true): self {
$this->allSheets = $allSheets;
return $this;
}
/**
* @var array|null si non null, liste de feuilles à inclure. n'est pris en
* compte que si $allSheets===true
*/
protected ?array $includeWsnames;
/**
* @var array|null si non null, liste de feuilles à exclure. n'est pris en
* compte que si $allSheets===true
*/
protected ?array $excludeWsnames;
protected $wsname;
/**
* @param string|int|null $wsname l'unique feuille à sélectionner
*
* NB: appeler cette méthode réinitialise $allSheets à false
*/
function setWsname($wsname): self {
$this->wsname = $wsname;
$this->allSheets = true;
return $this;
}
function getIterator() {
switch ($this->ssType) {
case "ods":
$ss = ReaderEntityFactory::createODSReader();
break;
case "xlsx":
$ss = ReaderEntityFactory::createXLSXReader();
break;
default:
$ss = ReaderEntityFactory::createReaderFromFile($this->input);
break;
}
$ss->open($this->input);
try {
$allSheets = $this->allSheets;
$includeWsnames = $this->includeWsnames;
$excludeWsnames = $this->excludeWsnames;
$wsname = $this->wsname;
$first = true;
foreach ($ss->getSheetIterator() as $ws) {
if ($allSheets) {
$wsname = $ws->getName();
$found = ($includeWsnames === null || in_array($wsname, $includeWsnames))
&& ($excludeWsnames === null || !in_array($wsname, $excludeWsnames));
} else {
$found = $wsname === null || $wsname === $ws->getName();
}
if ($found) {
if ($first) {
$first = false;
} else {
yield null;
# on garde le même schéma le cas échéant, mais supprimer headers
# pour permettre son recalcul
$this->headers = null;
}
$this->isrc = $this->idest = 0;
foreach ($ws->getRowIterator() as $row) {
$row = $row->toArray();
foreach ($row as &$col) {
$this->verifixCol($col);
}; unset($col);
if ($this->cookRow($row)) {
yield $row;
$this->idest++;
}
$this->isrc++;
}
}
}
} finally {
$ss->close();
}
}
}

11
src/ext/tab/SsBuilder.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace nulib\ext\tab;
use nulib\ext\spout\SpoutBuilder;
/**
* Class SsBuilder: construction d'une feuille de calcul, pour envoi à
* l'utilisateur
*/
class SsBuilder extends SpoutBuilder {
}

9
src/ext/tab/SsReader.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace nulib\ext\tab;
use nulib\ext\spout\SpoutReader;
use nulib\file\tab\TAbstractReader;
class SsReader extends SpoutReader {
use TAbstractReader;
}

View File

@ -0,0 +1,93 @@
<?php
namespace nulib\ref\ext\spout;
use OpenSpout\Common\Entity\Style\Color;
class ref_builder {
const PARAMS = [
# Builder
"output" => "?string",
"schema" => "?array",
"headers" => "?array",
"use_headers" => "bool",
"rows" => "?array",
"cook_func" => "?callable",
"ss_type" => "?string",
# SpoutBuilder
"type_numeric" => "bool",
"type_date" => "bool",
"spout" => self::PARAMS_SPOUT,
"sheet" => self::PARAMS_SHEET,
"sheet_name" => "?string",
"sheet_view" => self::PARAMS_SHEET_VIEW,
# TempStream
"max_memory" => "?int",
"throw_on_error" => "?bool",
];
const PARAMS_SPOUT = [
"->setColumnWidth" => ["float", ["int", null]],
"->setColumnWidthForRange" => ["int", "int", "int"],
"default_column_width" => "float",
"default_row_height" => "float",
"default_row_style" => self::STYLE,
];
const PARAMS_SHEET = [
"view" => self::PARAMS_SHEET_VIEW,
"->setName" => ["string"],
"->setIsVisible" => ["bool"],
"header_style" => self::STYLE,
"odd_style" => self::STYLE,
"even_style" => self::STYLE,
"different_odd_even" => "bool",
];
const PARAMS_SHEET_VIEW = [];
const ROW_PARAMS = [
"->setHeight" => ["float"],
"merge_cells" => "array",
"merge_offset" => "int",
];
const COLORS = [
"black" => Color::BLACK,
"white" => Color::WHITE,
"red" => Color::RED,
"dark_red" => Color::DARK_RED,
"orange" => Color::ORANGE,
"yellow" => Color::YELLOW,
"light_green" => Color::LIGHT_GREEN,
"green" => Color::GREEN,
"light_blue" => Color::LIGHT_BLUE,
"blue" => Color::BLUE,
"dark_blue" => Color::DARK_BLUE,
"purple" => Color::PURPLE,
"light_gray" => "EEEEEE",
"gray" => "B2B2B2",
];
const STYLE = [
"font" => [
"bold" => "bool",
"italic" => "bool",
"underline" => "bool",
"strikethrough" => "bool",
"name" => "string",
"size" => "int",
"color" => "string",
],
"bg_color" => "string",
"align" => "string",
"valign" => "string",
"wrap" => "bool",
"format" => "string",
"border" => [
"top" => ["color" => "string", "width" => "string", "style" => "string"],
"right" => ["color" => "string", "width" => "string", "style" => "string"],
"bottom" => ["color" => "string", "width" => "string", "style" => "string"],
"left" => ["color" => "string", "width" => "string", "style" => "string"],
],
];
}

View File

@ -0,0 +1,5 @@
<?php
namespace nulib\ref\ext\spout;
class ref_builder_ods extends ref_builder {
}

View File

@ -0,0 +1,36 @@
<?php
namespace nulib\ref\ext\spout;
class ref_builder_xlsx extends ref_builder {
const PARAMS_SHEET = [
"view" => self::PARAMS_SHEET_VIEW,
# copie de parent::SHEET
"->setName" => ["string"],
"->setIsVisible" => ["bool"],
"header_style" => self::STYLE,
"odd_style" => self::STYLE,
"even_style" => self::STYLE,
"different_odd_even" => "bool",
];
const PARAMS_SHEET_VIEW = [
"->setFreezeRow" => ["int"],
"->setFreezeColumn" => ["string"],
"->setZoomScale" => ["int"],
"->setShowFormulas" => ["bool"],
"->setShowGridLines" => ["bool"],
"->setShowRowColHeaders" => ["bool"],
"->setShowZeroes" => ["bool"],
"->setRightToLeft" => ["bool"],
"->setTabSelected" => ["bool"],
"->setShowOutlineSymbols" => ["bool"],
"->setDefaultGridColor" => ["bool"],
"->setView" => ["string"],
"->setTopLeftCell" => ["string"],
"->setColorId" => ["int"],
"->setZoomScaleNormal" => ["int"],
"->setZoomScalePageLayoutView" => ["int"],
"->setWorkbookViewId" => ["int"],
# copie de parent::PARAMS_SHEET_VIEW
];
}

0
tests/.gitignore vendored Normal file
View File

54
upstream-3.x/README.md Normal file
View File

@ -0,0 +1,54 @@
# OpenSpout
[![Latest Stable Version](https://poser.pugx.org/openspout/openspout/v/stable)](https://packagist.org/packages/openspout/openspout)
[![Build Status](https://github.com/openspout/openspout/actions/workflows/ci.yml/badge.svg)](https://github.com/openspout/openspout/actions/workflows/ci.yml)
[![Code Coverage](https://codecov.io/gh/openspout/openspout/coverage.svg?branch=main)](https://codecov.io/gh/openspout/openspout?branch=main)
[![Total Downloads](https://poser.pugx.org/openspout/openspout/downloads)](https://packagist.org/packages/openspout/openspout)
OpenSpout is a community driven fork of `box/spout`, a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB).
## Documentation
Documentation can be found at [https://openspout.readthedocs.io/en/latest/](https://openspout.readthedocs.io/en/latest/).
## Requirements
* PHP version 7.3 or higher
* PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled
## Upgrade from `box/spout`
1. Replace `box/spout` with `openspout/openspout` in your `composer.json`
2. Replace `Box\Spout` with `OpenSpout` in your code
## Upgrade guide
Version 3 introduced new functionality but also some breaking changes. If you want to upgrade your Spout codebase from version 2 please consult the [Upgrade guide](UPGRADE-3.0.md).
## Running tests
The `main` branch includes unit, functional and performance tests.
If you just want to check that everything is working as expected, executing the unit and functional tests is enough.
* `phpunit` - runs unit and functional tests
* `phpunit --group perf-tests` - only runs the performance tests
For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing).
> Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout)
## Copyright and License
This is a fork of Box's Spout library: https://github.com/box/spout
Code until and directly descending from commit [`cc42c1d`](https://github.com/openspout/openspout/commit/cc42c1d29fc5d29f07caeace99bd29dbb6d7c2f8)
is copyright of _Box, Inc._ and licensed under the Apache License, Version 2.0:
https://github.com/openspout/openspout/blob/cc42c1d29fc5d29f07caeace99bd29dbb6d7c2f8/LICENSE
Code created, edited and released after the commit mentioned above
is copyright of _openspout_ Github organization and licensed under MIT License.
https://github.com/openspout/openspout/blob/main/LICENSE

View File

@ -1,103 +1,33 @@
# Upgrade guide Upgrading from 2.x to 3.0
=========================
## Upgrading from 3.x to 4.0 Spout 3.0 introduced several backwards-incompatible changes. The upgrade from Spout 2.x to 3.0 must therefore be done with caution.
Beginning with v4, only actively supported [PHP version](https://www.php.net/supported-versions.php) will be supported.
Removing support for EOLed PHP versions as well adding support for new PHP versions will be included in MINOR releases.
### Most notable changes
1. OpenSpout is now fully typed
2. Classes and interfaces not consumed by the user are now marked as `@internal`
3. Classes used by the user are all `final`
### Reader & Writer objects
Both readers and writers have to be naturally instantiated with `new` keyword, passing the eventual needed `Options`
class as the first argument:
```php
use OpenSpout\Reader\CSV\Reader;
use OpenSpout\Reader\CSV\Options;
$options = new Options();
$options->FIELD_DELIMITER = '|';
$options->FIELD_ENCLOSURE = '@';
$reader = new Reader($options);
```
### Cell types on writes
Cell types are now handled with separate classes:
```php
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
$row = new Row([
new Cell\BooleanCell(true),
new Cell\DateIntervalCell(new DateInterval('P1D')),
new Cell\DateTimeCell(new DateTimeImmutable('now')),
new Cell\EmptyCell(null),
new Cell\FormulaCell('=SUM(A1:A2)'),
new Cell\NumericCell(3),
new Cell\StringCell('foo'),
]);
```
Auto-typing is still available though:
```php
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
$cell = Cell::fromValue(true); // Instance of Cell\BooleanCell
$row = Row::fromValues([
true,
new DateInterval('P1D'),
new DateTimeImmutable('now'),
null,
'=SUM(A1:A2)',
3,
'foo',
]);
```
## Upgrading from 2.x to 3.0
OpenSpout 3.0 introduced several backwards-incompatible changes. The upgrade from OpenSpout 2.x to 3.0 must therefore
be done with caution.
This guide is meant to ease this process. This guide is meant to ease this process.
### Most notable changes Most notable changes
--------------------
In 2.x, styles were applied per row; it was therefore impossible to apply different styles to cells in the same row. In 2.x, styles were applied per row; it was therefore impossible to apply different styles to cells in the same row.
With the 3.0 version, this is now possible: each cell can have its own style. With the 3.0 version, this is now possible: each cell can have its own style.
OpenSpout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, OpenSpout now Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
Finally, **_OpenSpout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community.
community.
### Reader changes
Reader changes
--------------
Creating a reader should now be done through the Reader `ReaderEntityFactory`, instead of using the `ReaderFactory`. Creating a reader should now be done through the Reader `ReaderEntityFactory`, instead of using the `ReaderFactory`.
Also, the `ReaderFactory::create($type)` method was removed and replaced by methods for each reader: Also, the `ReaderFactory::create($type)` method was removed and replaced by methods for each reader:
```php ```php
use OpenSpout\Reader\Common\Creator\ReaderEntityFactory; // namespace is no longer "OpenSpout\Reader" use OpenSpout\Reader\Common\Creator\ReaderEntityFactory; // namespace is no longer "OpenSpout\Reader"
...
$reader = ReaderEntityFactory::createXLSXReader(); // replaces ReaderFactory::create(Type::XLSX) $reader = ReaderEntityFactory::createXLSXReader(); // replaces ReaderFactory::create(Type::XLSX)
$reader = ReaderEntityFactory::createCSVReader(); // replaces ReaderFactory::create(Type::CSV) $reader = ReaderEntityFactory::createCSVReader(); // replaces ReaderFactory::create(Type::CSV)
$reader = ReaderEntityFactory::createODSReader(); // replaces ReaderFactory::create(Type::ODS) $reader = ReaderEntityFactory::createODSReader(); // replaces ReaderFactory::create(Type::ODS)
``` ```
When iterating over the spreadsheet rows, OpenSpout now returns `Row` objects, instead of an array containing row When iterating over the spreadsheet rows, Spout now returns `Row` objects, instead of an array containing row values. Accessing the row values should now be done this way:
values. Accessing the row values should now be done this way:
```php ```php
...
foreach ($reader->getSheetIterator() as $sheet) { foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) { // $row is a "Row" object, not an array foreach ($sheet->getRowIterator() as $row) { // $row is a "Row" object, not an array
$rowAsArray = $row->toArray(); // this is the 2.x equivalent $rowAsArray = $row->toArray(); // this is the 2.x equivalent
@ -108,22 +38,20 @@ foreach ($reader->getSheetIterator() as $sheet) {
} }
``` ```
### Writer changes Writer changes
--------------
Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`, Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`, instead of using the `WriterFactory`.
instead of using the `WriterFactory`.
Also, the `WriterFactory::create($type)` method was removed and replaced by methods for each writer: Also, the `WriterFactory::create($type)` method was removed and replaced by methods for each writer:
```php ```php
use OpenSpout\Writer\Common\Creator\WriterEntityFactory; // namespace is no longer "OpenSpout\Writer" use OpenSpout\Writer\Common\Creator\WriterEntityFactory; // namespace is no longer "OpenSpout\Writer"
...
$writer = WriterEntityFactory::createXLSXWriter(); // replaces WriterFactory::create(Type::XLSX) $writer = WriterEntityFactory::createXLSXWriter(); // replaces WriterFactory::create(Type::XLSX)
$writer = WriterEntityFactory::createCSVWriter(); // replaces WriterFactory::create(Type::CSV) $writer = WriterEntityFactory::createCSVWriter(); // replaces WriterFactory::create(Type::CSV)
$writer = WriterEntityFactory::createODSWriter(); // replaces WriterFactory::create(Type::ODS) $writer = WriterEntityFactory::createODSWriter(); // replaces WriterFactory::create(Type::ODS)
``` ```
Adding rows is also done differently: instead of passing an array, the writer now takes in a `Row` object (or an Adding rows is also done differently: instead of passing an array, the writer now takes in a `Row` object (or an array of `Row`). Creating such objects can easily be done this way:
array of `Row`). Creating such objects can easily be done this way:
```php ```php
// Adding a row from an array of values (2.x equivalent) // Adding a row from an array of values (2.x equivalent)
$cellValues = ['foo', 12345]; $cellValues = ['foo', 12345];
@ -137,8 +65,8 @@ $row2 = WriterEntityFactory::createRow([$cell1, $cell2]);
$writer->addRows([$row1, $row2]); $writer->addRows([$row1, $row2]);
``` ```
### Namespace changes for styles Namespace changes for styles
-----------------
The namespaces for styles have changed. Styles are still created by using a `builder` class. The namespaces for styles have changed. Styles are still created by using a `builder` class.
For the builder, please update your import statements to use the following namespaces: For the builder, please update your import statements to use the following namespaces:
@ -155,8 +83,7 @@ If your are using these classes directly via an import statement in your code, p
OpenSpout\Common\Entity\Style\Color OpenSpout\Common\Entity\Style\Color
OpenSpout\Common\Entity\Style\Style OpenSpout\Common\Entity\Style\Style
### Handling of empty rows Handling of empty rows
----------------------
In 2.x, empty rows were not added to the spreadsheet. In 2.x, empty rows were not added to the spreadsheet.
In 3.0, `addRow` now always writes a row to the spreadsheet: when the row does not contain any cells, an empty row In 3.0, `addRow` now always writes a row to the spreadsheet: when the row does not contain any cells, an empty row is created in the sheet.
is created in the sheet.

View File

@ -28,9 +28,8 @@
], ],
"homepage": "https://github.com/openspout/openspout", "homepage": "https://github.com/openspout/openspout",
"require": { "require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0", "php": "~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0",
"ext-dom": "*", "ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*", "ext-filter": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlreader": "*", "ext-xmlreader": "*",
@ -38,17 +37,14 @@
}, },
"require-dev": { "require-dev": {
"ext-zlib": "*", "ext-zlib": "*",
"friendsofphp/php-cs-fixer": "^3.65.0", "friendsofphp/php-cs-fixer": "^3.4",
"infection/infection": "^0.29.8", "phpstan/phpstan": "^1.4",
"phpbench/phpbench": "^1.3.1", "phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan": "^2.0.2", "phpunit/phpunit": "^9.5"
"phpstan/phpstan-phpunit": "^2.0.1",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^11.4.3"
}, },
"suggest": { "suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)", "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
"ext-mbstring": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)" "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -56,16 +52,13 @@
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": {
"OpenSpout\\Benchmarks\\": "benchmarks/"
},
"classmap": [ "classmap": [
"tests/" "tests/"
] ]
}, },
"config": { "config": {
"allow-plugins": { "platform": {
"infection/extension-installer": true "php": "7.3"
} }
}, },
"extra": { "extra": {

View File

@ -0,0 +1,147 @@
<?php
namespace OpenSpout\Autoloader;
/**
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
*/
class Psr4Autoloader
{
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
*
* @var array
*/
protected $prefixes = [];
/**
* Register loader with SPL autoloader stack.
*/
public function register()
{
spl_autoload_register([$this, 'loadClass']);
}
/**
* Adds a base directory for a namespace prefix.
*
* @param string $prefix the namespace prefix
* @param string $baseDir a base directory for class files in the
* namespace
* @param bool $prepend if true, prepend the base directory to the stack
* instead of appending it; this causes it to be searched first rather
* than last
*/
public function addNamespace($prefix, $baseDir, $prepend = false)
{
// normalize namespace prefix
$prefix = trim($prefix, '\\').'\\';
// normalize the base directory with a trailing separator
$baseDir = rtrim($baseDir, \DIRECTORY_SEPARATOR).'/';
// initialize the namespace prefix array
if (false === isset($this->prefixes[$prefix])) {
$this->prefixes[$prefix] = [];
}
// retain the base directory for the namespace prefix
if ($prepend) {
array_unshift($this->prefixes[$prefix], $baseDir);
} else {
$this->prefixes[$prefix][] = $baseDir;
}
}
/**
* Loads the class file for a given class name.
*
* @param string $class the fully-qualified class name
*
* @return mixed the mapped file name on success, or boolean false on
* failure
*/
public function loadClass($class)
{
// the current namespace prefix
$prefix = $class;
// work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name
while (($pos = strrpos($prefix, '\\')) !== false) {
// retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1);
// the rest is the relative class name
$relativeClass = substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
if (false !== $mappedFile) {
return $mappedFile;
}
// remove the trailing namespace separator for the next iteration
// of strrpos()
$prefix = rtrim($prefix, '\\');
}
// never found a mapped file
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix the namespace prefix
* @param string $relativeClass the relative class name
*
* @return mixed boolean false if no mapped file can be loaded, or the
* name of the mapped file that was loaded
*/
protected function loadMappedFile($prefix, $relativeClass)
{
// are there any base directories for this namespace prefix?
if (false === isset($this->prefixes[$prefix])) {
return false;
}
// look through base directories for this namespace prefix
foreach ($this->prefixes[$prefix] as $baseDir) {
// replace the namespace prefix with the base directory,
// replace namespace separators with directory separators
// in the relative class name, append with .php
$file = $baseDir
.str_replace('\\', '/', $relativeClass)
.'.php';
// if the mapped file exists, require it
if ($this->requireFile($file)) {
// yes, we're done
return $file;
}
}
// never found it
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file the file to require
*
* @return bool true if the file exists, false if not
*/
protected function requireFile($file)
{
if (file_exists($file)) {
require $file;
return true;
}
return false;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace OpenSpout\Autoloader;
require_once 'Psr4Autoloader.php';
/**
* @var string
* Full path to "src/Spout" which is what we want "OpenSpout" to map to
*/
$srcBaseDirectory = \dirname(__DIR__);
$loader = new Psr4Autoloader();
$loader->register();
$loader->addNamespace('OpenSpout', $srcBaseDirectory);

View File

@ -0,0 +1,48 @@
<?php
namespace OpenSpout\Common\Creator;
use OpenSpout\Common\Helper\EncodingHelper;
use OpenSpout\Common\Helper\FileSystemHelper;
use OpenSpout\Common\Helper\GlobalFunctionsHelper;
use OpenSpout\Common\Helper\StringHelper;
/**
* Factory to create helpers.
*/
class HelperFactory
{
/**
* @return GlobalFunctionsHelper
*/
public function createGlobalFunctionsHelper()
{
return new GlobalFunctionsHelper();
}
/**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
*
* @return FileSystemHelper
*/
public function createFileSystemHelper($baseFolderPath)
{
return new FileSystemHelper($baseFolderPath);
}
/**
* @return EncodingHelper
*/
public function createEncodingHelper(GlobalFunctionsHelper $globalFunctionsHelper)
{
return new EncodingHelper($globalFunctionsHelper);
}
/**
* @return StringHelper
*/
public function createStringHelper()
{
return new StringHelper();
}
}

View File

@ -0,0 +1,227 @@
<?php
namespace OpenSpout\Common\Entity;
use OpenSpout\Common\Entity\Style\Style;
use OpenSpout\Common\Helper\CellTypeHelper;
class Cell
{
/**
* Numeric cell type (whole numbers, fractional numbers, dates).
*/
public const TYPE_NUMERIC = 0;
/**
* String (text) cell type.
*/
public const TYPE_STRING = 1;
/**
* Formula cell type
* Not used at the moment.
*/
public const TYPE_FORMULA = 2;
/**
* Empty cell type.
*/
public const TYPE_EMPTY = 3;
/**
* Boolean cell type.
*/
public const TYPE_BOOLEAN = 4;
/**
* Date cell type.
*/
public const TYPE_DATE = 5;
/**
* Error cell type.
*/
public const TYPE_ERROR = 6;
/**
* The value of this cell.
*
* @var null|mixed
*/
protected $value;
/**
* The cell type.
*
* @var null|int
*/
protected $type;
/**
* The cell style.
*
* @var Style
*/
protected $style;
/**
* @param null|mixed $value
*/
public function __construct($value, Style $style = null)
{
$this->setValue($value);
$this->setStyle($style);
}
/**
* @return string
*/
public function __toString()
{
return (string) $this->getValue();
}
/**
* @param null|mixed $value
*/
public function setValue($value)
{
$this->value = $value;
$this->type = $this->detectType($value);
}
/**
* @return null|mixed
*/
public function getValue()
{
return !$this->isError() ? $this->value : null;
}
/**
* @return mixed
*/
public function getValueEvenIfError()
{
return $this->value;
}
/**
* @param null|Style $style
*/
public function setStyle($style)
{
$this->style = $style ?: new Style();
}
/**
* @return Style
*/
public function getStyle()
{
return $this->style;
}
/**
* @return null|int
*/
public function getType()
{
return $this->type;
}
/**
* @param int $type
*/
public function setType($type)
{
$this->type = $type;
}
/**
* @return bool
*/
public function isBoolean()
{
return self::TYPE_BOOLEAN === $this->type;
}
/**
* @return bool
*/
public function isEmpty()
{
return self::TYPE_EMPTY === $this->type;
}
/**
* @return bool
*/
public function isNumeric()
{
return self::TYPE_NUMERIC === $this->type;
}
/**
* @return bool
*/
public function isString()
{
return self::TYPE_STRING === $this->type;
}
/**
* @return bool
*/
public function isDate()
{
return self::TYPE_DATE === $this->type;
}
/**
* @return bool
*/
public function isFormula()
{
return self::TYPE_FORMULA === $this->type;
}
/**
* @return bool
*/
public function isError()
{
return self::TYPE_ERROR === $this->type;
}
/**
* Get the current value type.
*
* @param null|mixed $value
*
* @return int
*/
protected function detectType($value)
{
if (CellTypeHelper::isBoolean($value)) {
return self::TYPE_BOOLEAN;
}
if (CellTypeHelper::isEmpty($value)) {
return self::TYPE_EMPTY;
}
if (CellTypeHelper::isNumeric($value)) {
return self::TYPE_NUMERIC;
}
if (CellTypeHelper::isDateTimeOrDateInterval($value)) {
return self::TYPE_DATE;
}
if (CellTypeHelper::isFormula($value)) {
return self::TYPE_FORMULA;
}
if (CellTypeHelper::isNonEmptyString($value)) {
return self::TYPE_STRING;
}
return self::TYPE_ERROR;
}
}

View File

@ -0,0 +1,166 @@
<?php
namespace OpenSpout\Common\Entity;
use OpenSpout\Common\Entity\Style\Style;
class Row
{
/**
* The cells in this row.
*
* @var Cell[]
*/
protected $cells = [];
/**
* The row style.
*
* @var Style
*/
protected $style;
/**
* Row height (default is 15).
*
* @var string
*/
protected $height = '15';
/**
* Row constructor.
*
* @param Cell[] $cells
* @param null|Style $style
*/
public function __construct(array $cells, $style)
{
$this
->setCells($cells)
->setStyle($style)
;
}
/**
* @return Cell[] $cells
*/
public function getCells()
{
return $this->cells;
}
/**
* @param Cell[] $cells
*
* @return Row
*/
public function setCells(array $cells)
{
$this->cells = [];
foreach ($cells as $cell) {
$this->addCell($cell);
}
return $this;
}
/**
* @param int $cellIndex
*
* @return Row
*/
public function setCellAtIndex(Cell $cell, $cellIndex)
{
$this->cells[$cellIndex] = $cell;
return $this;
}
/**
* @param int $cellIndex
*
* @return null|Cell
*/
public function getCellAtIndex($cellIndex)
{
return $this->cells[$cellIndex] ?? null;
}
/**
* @return Row
*/
public function addCell(Cell $cell)
{
$this->cells[] = $cell;
return $this;
}
/**
* @return int
*/
public function getNumCells()
{
// When using "setCellAtIndex", it's possible to
// have "$this->cells" contain holes.
if (empty($this->cells)) {
return 0;
}
return max(array_keys($this->cells)) + 1;
}
/**
* @return Style
*/
public function getStyle()
{
return $this->style;
}
/**
* @param null|Style $style
*
* @return Row
*/
public function setStyle($style)
{
$this->style = $style ?: new Style();
return $this;
}
/**
* @return array The row values, as array
*/
public function toArray()
{
return array_map(function (Cell $cell) {
return $cell->getValue();
}, $this->cells);
}
/**
* Set row height.
*
* @param string $height
*
* @return Row
*/
public function setHeight($height)
{
$this->height = $height;
return $this;
}
/**
* Returns row height.
*
* @return string
*/
public function getHeight()
{
return $this->height;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace OpenSpout\Common\Entity\Style;
class Border
{
public const LEFT = 'left';
public const RIGHT = 'right';
public const TOP = 'top';
public const BOTTOM = 'bottom';
public const STYLE_NONE = 'none';
public const STYLE_SOLID = 'solid';
public const STYLE_DASHED = 'dashed';
public const STYLE_DOTTED = 'dotted';
public const STYLE_DOUBLE = 'double';
public const WIDTH_THIN = 'thin';
public const WIDTH_MEDIUM = 'medium';
public const WIDTH_THICK = 'thick';
/** @var array A list of BorderPart objects for this border. */
private $parts = [];
public function __construct(array $borderParts = [])
{
$this->setParts($borderParts);
}
/**
* @param string $name The name of the border part
*
* @return null|BorderPart
*/
public function getPart($name)
{
return $this->hasPart($name) ? $this->parts[$name] : null;
}
/**
* @param string $name The name of the border part
*
* @return bool
*/
public function hasPart($name)
{
return isset($this->parts[$name]);
}
/**
* @return array
*/
public function getParts()
{
return $this->parts;
}
/**
* Set BorderParts.
*
* @param array $parts
*/
public function setParts($parts)
{
$this->parts = [];
foreach ($parts as $part) {
$this->addPart($part);
}
}
/**
* @return Border
*/
public function addPart(BorderPart $borderPart)
{
$this->parts[$borderPart->getName()] = $borderPart;
return $this;
}
}

View File

@ -0,0 +1,181 @@
<?php
namespace OpenSpout\Common\Entity\Style;
use OpenSpout\Writer\Exception\Border\InvalidNameException;
use OpenSpout\Writer\Exception\Border\InvalidStyleException;
use OpenSpout\Writer\Exception\Border\InvalidWidthException;
class BorderPart
{
/**
* @var string the style of this border part
*/
protected $style;
/**
* @var string the name of this border part
*/
protected $name;
/**
* @var string the color of this border part
*/
protected $color;
/**
* @var string the width of this border part
*/
protected $width;
/**
* @var array allowed style constants for parts
*/
protected static $allowedStyles = [
'none',
'solid',
'dashed',
'dotted',
'double',
];
/**
* @var array allowed names constants for border parts
*/
protected static $allowedNames = [
'left',
'right',
'top',
'bottom',
];
/**
* @var array allowed width constants for border parts
*/
protected static $allowedWidths = [
'thin',
'medium',
'thick',
];
/**
* @param string $name @see BorderPart::$allowedNames
* @param string $color A RGB color code
* @param string $width @see BorderPart::$allowedWidths
* @param string $style @see BorderPart::$allowedStyles
*
* @throws InvalidNameException
* @throws InvalidStyleException
* @throws InvalidWidthException
*/
public function __construct($name, $color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
{
$this->setName($name);
$this->setColor($color);
$this->setWidth($width);
$this->setStyle($style);
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name The name of the border part @see BorderPart::$allowedNames
*
* @throws InvalidNameException
*/
public function setName($name)
{
if (!\in_array($name, self::$allowedNames, true)) {
throw new InvalidNameException($name);
}
$this->name = $name;
}
/**
* @return string
*/
public function getStyle()
{
return $this->style;
}
/**
* @param string $style The style of the border part @see BorderPart::$allowedStyles
*
* @throws InvalidStyleException
*/
public function setStyle($style)
{
if (!\in_array($style, self::$allowedStyles, true)) {
throw new InvalidStyleException($style);
}
$this->style = $style;
}
/**
* @return string
*/
public function getColor()
{
return $this->color;
}
/**
* @param string $color The color of the border part @see Color::rgb()
*/
public function setColor($color)
{
$this->color = $color;
}
/**
* @return string
*/
public function getWidth()
{
return $this->width;
}
/**
* @param string $width The width of the border part @see BorderPart::$allowedWidths
*
* @throws InvalidWidthException
*/
public function setWidth($width)
{
if (!\in_array($width, self::$allowedWidths, true)) {
throw new InvalidWidthException($width);
}
$this->width = $width;
}
/**
* @return array
*/
public static function getAllowedStyles()
{
return self::$allowedStyles;
}
/**
* @return array
*/
public static function getAllowedNames()
{
return self::$allowedNames;
}
/**
* @return array
*/
public static function getAllowedWidths()
{
return self::$allowedWidths;
}
}

View File

@ -1,20 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Entity\Style; namespace OpenSpout\Common\Entity\Style;
/** /**
* This class provides constants to work with text alignment. * This class provides constants to work with text alignment.
*/ */
final class CellAlignment abstract class CellAlignment
{ {
public const LEFT = 'left'; public const LEFT = 'left';
public const RIGHT = 'right'; public const RIGHT = 'right';
public const CENTER = 'center'; public const CENTER = 'center';
public const JUSTIFY = 'justify'; public const JUSTIFY = 'justify';
private const VALID_ALIGNMENTS = [ private static $VALID_ALIGNMENTS = [
self::LEFT => 1, self::LEFT => 1,
self::RIGHT => 1, self::RIGHT => 1,
self::CENTER => 1, self::CENTER => 1,
@ -22,10 +20,12 @@ final class CellAlignment
]; ];
/** /**
* @param string $cellAlignment
*
* @return bool Whether the given cell alignment is valid * @return bool Whether the given cell alignment is valid
*/ */
public static function isValid(string $cellAlignment): bool public static function isValid($cellAlignment)
{ {
return isset(self::VALID_ALIGNMENTS[$cellAlignment]); return isset(self::$VALID_ALIGNMENTS[$cellAlignment]);
} }
} }

View File

@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Entity\Style; namespace OpenSpout\Common\Entity\Style;
use OpenSpout\Common\Exception\InvalidColorException; use OpenSpout\Common\Exception\InvalidColorException;
@ -9,11 +7,9 @@ use OpenSpout\Common\Exception\InvalidColorException;
/** /**
* This class provides constants and functions to work with colors. * This class provides constants and functions to work with colors.
*/ */
final class Color abstract class Color
{ {
/** /** Standard colors - based on Office Online */
* Standard colors - based on Office Online.
*/
public const BLACK = '000000'; public const BLACK = '000000';
public const WHITE = 'FFFFFF'; public const WHITE = 'FFFFFF';
public const RED = 'FF0000'; public const RED = 'FF0000';
@ -36,7 +32,7 @@ final class Color
* *
* @return string RGB color * @return string RGB color
*/ */
public static function rgb(int $red, int $green, int $blue): string public static function rgb($red, $green, $blue)
{ {
self::throwIfInvalidColorComponentValue($red); self::throwIfInvalidColorComponentValue($red);
self::throwIfInvalidColorComponentValue($green); self::throwIfInvalidColorComponentValue($green);
@ -57,7 +53,7 @@ final class Color
* *
* @return string ARGB color * @return string ARGB color
*/ */
public static function toARGB(string $rgbColor): string public static function toARGB($rgbColor)
{ {
return 'FF'.$rgbColor; return 'FF'.$rgbColor;
} }
@ -65,11 +61,13 @@ final class Color
/** /**
* Throws an exception is the color component value is outside of bounds (0 - 255). * Throws an exception is the color component value is outside of bounds (0 - 255).
* *
* @throws InvalidColorException * @param int $colorComponent
*
* @throws \OpenSpout\Common\Exception\InvalidColorException
*/ */
private static function throwIfInvalidColorComponentValue(int $colorComponent): void protected static function throwIfInvalidColorComponentValue($colorComponent)
{ {
if ($colorComponent < 0 || $colorComponent > 255) { if (!\is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
throw new InvalidColorException("The RGB components must be between 0 and 255. Received: {$colorComponent}"); throw new InvalidColorException("The RGB components must be between 0 and 255. Received: {$colorComponent}");
} }
} }
@ -81,7 +79,7 @@ final class Color
* *
* @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d" * @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d"
*/ */
private static function convertColorComponentToHex(int $colorComponent): string protected static function convertColorComponentToHex($colorComponent)
{ {
return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT); return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT);
} }

View File

@ -1,160 +1,159 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Entity\Style; namespace OpenSpout\Common\Entity\Style;
use OpenSpout\Common\Exception\InvalidArgumentException;
/** /**
* Represents a style to be applied to a cell. * Represents a style to be applied to a cell.
*/ */
final class Style class Style
{ {
/** /** Default values */
* Default values.
*/
public const DEFAULT_FONT_SIZE = 11; public const DEFAULT_FONT_SIZE = 11;
public const DEFAULT_FONT_COLOR = Color::BLACK; public const DEFAULT_FONT_COLOR = Color::BLACK;
public const DEFAULT_FONT_NAME = 'Arial'; public const DEFAULT_FONT_NAME = 'Arial';
/** @var int Style ID */ /** @var null|int Style ID */
private int $id = -1; private $id;
/** @var bool Whether the font should be bold */ /** @var bool Whether the font should be bold */
private bool $fontBold = false; private $fontBold = false;
/** @var bool Whether the bold property was set */ /** @var bool Whether the bold property was set */
private bool $hasSetFontBold = false; private $hasSetFontBold = false;
/** @var bool Whether the font should be italic */ /** @var bool Whether the font should be italic */
private bool $fontItalic = false; private $fontItalic = false;
/** @var bool Whether the italic property was set */ /** @var bool Whether the italic property was set */
private bool $hasSetFontItalic = false; private $hasSetFontItalic = false;
/** @var bool Whether the font should be underlined */ /** @var bool Whether the font should be underlined */
private bool $fontUnderline = false; private $fontUnderline = false;
/** @var bool Whether the underline property was set */ /** @var bool Whether the underline property was set */
private bool $hasSetFontUnderline = false; private $hasSetFontUnderline = false;
/** @var bool Whether the font should be struck through */ /** @var bool Whether the font should be struck through */
private bool $fontStrikethrough = false; private $fontStrikethrough = false;
/** @var bool Whether the strikethrough property was set */ /** @var bool Whether the strikethrough property was set */
private bool $hasSetFontStrikethrough = false; private $hasSetFontStrikethrough = false;
/** @var int Font size */ /** @var int Font size */
private int $fontSize = self::DEFAULT_FONT_SIZE; private $fontSize = self::DEFAULT_FONT_SIZE;
/** @var bool Whether the font size property was set */ /** @var bool Whether the font size property was set */
private bool $hasSetFontSize = false; private $hasSetFontSize = false;
/** @var string Font color */ /** @var string Font color */
private string $fontColor = self::DEFAULT_FONT_COLOR; private $fontColor = self::DEFAULT_FONT_COLOR;
/** @var bool Whether the font color property was set */ /** @var bool Whether the font color property was set */
private bool $hasSetFontColor = false; private $hasSetFontColor = false;
/** @var string Font name */ /** @var string Font name */
private string $fontName = self::DEFAULT_FONT_NAME; private $fontName = self::DEFAULT_FONT_NAME;
/** @var bool Whether the font name property was set */ /** @var bool Whether the font name property was set */
private bool $hasSetFontName = false; private $hasSetFontName = false;
/** @var bool Whether specific font properties should be applied */ /** @var bool Whether specific font properties should be applied */
private bool $shouldApplyFont = false; private $shouldApplyFont = false;
/** @var bool Whether specific cell alignment should be applied */ /** @var bool Whether specific cell alignment should be applied */
private bool $shouldApplyCellAlignment = false; private $shouldApplyCellAlignment = false;
/** @var string Cell alignment */ /** @var string Cell alignment */
private string $cellAlignment; private $cellAlignment;
/** @var bool Whether the cell alignment property was set */ /** @var bool Whether the cell alignment property was set */
private bool $hasSetCellAlignment = false; private $hasSetCellAlignment = false;
/** @var bool Whether specific cell vertical alignment should be applied */
private bool $shouldApplyCellVerticalAlignment = false;
/** @var string Cell vertical alignment */
private string $cellVerticalAlignment;
/** @var bool Whether the cell vertical alignment property was set */
private bool $hasSetCellVerticalAlignment = false;
/** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */ /** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */
private bool $shouldWrapText = false; private $shouldWrapText = false;
/** @var bool Whether the wrap text property was set */ /** @var bool Whether the wrap text property was set */
private bool $hasSetWrapText = false; private $hasSetWrapText = false;
/** @var int Text rotation */
private int $textRotation = 0;
/** @var bool Whether the text rotation property was set */
private bool $hasSetTextRotation = false;
/** @var bool Whether the cell should shrink to fit to content */ /** @var bool Whether the cell should shrink to fit to content */
private bool $shouldShrinkToFit = false; private $shouldShrinkToFit = false;
/** @var bool Whether the shouldShrinkToFit text property was set */ /** @var bool Whether the shouldShrinkToFit text property was set */
private bool $hasSetShrinkToFit = false; private $hasSetShrinkToFit = false;
private ?Border $border = null; /** @var null|Border */
private $border;
/** @var bool Whether border properties should be applied */
private $shouldApplyBorder = false;
/** @var null|string Background color */ /** @var null|string Background color */
private ?string $backgroundColor = null; private $backgroundColor;
/** @var bool */
private $hasSetBackgroundColor = false;
/** @var null|string Format */ /** @var null|string Format */
private ?string $format = null; private $format;
private bool $isRegistered = false; /** @var bool */
private $hasSetFormat = false;
private bool $isEmpty = true; /** @var bool */
private $isRegistered = false;
public function __sleep(): array /** @var bool */
private $isEmpty = true;
/**
* @return null|int
*/
public function getId()
{ {
$vars = get_object_vars($this);
unset($vars['id'], $vars['isRegistered']);
return array_keys($vars);
}
public function getId(): int
{
\assert(0 <= $this->id);
return $this->id; return $this->id;
} }
public function setId(int $id): self /**
* @param int $id
*
* @return Style
*/
public function setId($id)
{ {
$this->id = $id; $this->id = $id;
return $this; return $this;
} }
public function getBorder(): ?Border /**
* @return null|Border
*/
public function getBorder()
{ {
return $this->border; return $this->border;
} }
public function setBorder(Border $border): self /**
* @return Style
*/
public function setBorder(Border $border)
{ {
$this->shouldApplyBorder = true;
$this->border = $border; $this->border = $border;
$this->isEmpty = false; $this->isEmpty = false;
return $this; return $this;
} }
public function isFontBold(): bool /**
* @return bool
*/
public function shouldApplyBorder()
{
return $this->shouldApplyBorder;
}
/**
* @return bool
*/
public function isFontBold()
{ {
return $this->fontBold; return $this->fontBold;
} }
public function setFontBold(): self /**
* @return Style
*/
public function setFontBold()
{ {
$this->fontBold = true; $this->fontBold = true;
$this->hasSetFontBold = true; $this->hasSetFontBold = true;
@ -164,17 +163,26 @@ final class Style
return $this; return $this;
} }
public function hasSetFontBold(): bool /**
* @return bool
*/
public function hasSetFontBold()
{ {
return $this->hasSetFontBold; return $this->hasSetFontBold;
} }
public function isFontItalic(): bool /**
* @return bool
*/
public function isFontItalic()
{ {
return $this->fontItalic; return $this->fontItalic;
} }
public function setFontItalic(): self /**
* @return Style
*/
public function setFontItalic()
{ {
$this->fontItalic = true; $this->fontItalic = true;
$this->hasSetFontItalic = true; $this->hasSetFontItalic = true;
@ -184,17 +192,26 @@ final class Style
return $this; return $this;
} }
public function hasSetFontItalic(): bool /**
* @return bool
*/
public function hasSetFontItalic()
{ {
return $this->hasSetFontItalic; return $this->hasSetFontItalic;
} }
public function isFontUnderline(): bool /**
* @return bool
*/
public function isFontUnderline()
{ {
return $this->fontUnderline; return $this->fontUnderline;
} }
public function setFontUnderline(): self /**
* @return Style
*/
public function setFontUnderline()
{ {
$this->fontUnderline = true; $this->fontUnderline = true;
$this->hasSetFontUnderline = true; $this->hasSetFontUnderline = true;
@ -204,17 +221,26 @@ final class Style
return $this; return $this;
} }
public function hasSetFontUnderline(): bool /**
* @return bool
*/
public function hasSetFontUnderline()
{ {
return $this->hasSetFontUnderline; return $this->hasSetFontUnderline;
} }
public function isFontStrikethrough(): bool /**
* @return bool
*/
public function isFontStrikethrough()
{ {
return $this->fontStrikethrough; return $this->fontStrikethrough;
} }
public function setFontStrikethrough(): self /**
* @return Style
*/
public function setFontStrikethrough()
{ {
$this->fontStrikethrough = true; $this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true; $this->hasSetFontStrikethrough = true;
@ -224,20 +250,28 @@ final class Style
return $this; return $this;
} }
public function hasSetFontStrikethrough(): bool /**
* @return bool
*/
public function hasSetFontStrikethrough()
{ {
return $this->hasSetFontStrikethrough; return $this->hasSetFontStrikethrough;
} }
public function getFontSize(): int /**
* @return int
*/
public function getFontSize()
{ {
return $this->fontSize; return $this->fontSize;
} }
/** /**
* @param int $fontSize Font size, in pixels * @param int $fontSize Font size, in pixels
*
* @return Style
*/ */
public function setFontSize(int $fontSize): self public function setFontSize($fontSize)
{ {
$this->fontSize = $fontSize; $this->fontSize = $fontSize;
$this->hasSetFontSize = true; $this->hasSetFontSize = true;
@ -247,12 +281,18 @@ final class Style
return $this; return $this;
} }
public function hasSetFontSize(): bool /**
* @return bool
*/
public function hasSetFontSize()
{ {
return $this->hasSetFontSize; return $this->hasSetFontSize;
} }
public function getFontColor(): string /**
* @return string
*/
public function getFontColor()
{ {
return $this->fontColor; return $this->fontColor;
} }
@ -261,8 +301,10 @@ final class Style
* Sets the font color. * Sets the font color.
* *
* @param string $fontColor ARGB color (@see Color) * @param string $fontColor ARGB color (@see Color)
*
* @return Style
*/ */
public function setFontColor(string $fontColor): self public function setFontColor($fontColor)
{ {
$this->fontColor = $fontColor; $this->fontColor = $fontColor;
$this->hasSetFontColor = true; $this->hasSetFontColor = true;
@ -272,20 +314,28 @@ final class Style
return $this; return $this;
} }
public function hasSetFontColor(): bool /**
* @return bool
*/
public function hasSetFontColor()
{ {
return $this->hasSetFontColor; return $this->hasSetFontColor;
} }
public function getFontName(): string /**
* @return string
*/
public function getFontName()
{ {
return $this->fontName; return $this->fontName;
} }
/** /**
* @param string $fontName Name of the font to use * @param string $fontName Name of the font to use
*
* @return Style
*/ */
public function setFontName(string $fontName): self public function setFontName($fontName)
{ {
$this->fontName = $fontName; $this->fontName = $fontName;
$this->hasSetFontName = true; $this->hasSetFontName = true;
@ -295,30 +345,29 @@ final class Style
return $this; return $this;
} }
public function hasSetFontName(): bool /**
* @return bool
*/
public function hasSetFontName()
{ {
return $this->hasSetFontName; return $this->hasSetFontName;
} }
public function getCellAlignment(): string /**
* @return string
*/
public function getCellAlignment()
{ {
return $this->cellAlignment; return $this->cellAlignment;
} }
public function getCellVerticalAlignment(): string
{
return $this->cellVerticalAlignment;
}
/** /**
* @param string $cellAlignment The cell alignment * @param string $cellAlignment The cell alignment
*
* @return Style
*/ */
public function setCellAlignment(string $cellAlignment): self public function setCellAlignment($cellAlignment)
{ {
if (!CellAlignment::isValid($cellAlignment)) {
throw new InvalidArgumentException('Invalid cell alignment value');
}
$this->cellAlignment = $cellAlignment; $this->cellAlignment = $cellAlignment;
$this->hasSetCellAlignment = true; $this->hasSetCellAlignment = true;
$this->shouldApplyCellAlignment = true; $this->shouldApplyCellAlignment = true;
@ -328,54 +377,35 @@ final class Style
} }
/** /**
* @param string $cellVerticalAlignment The cell vertical alignment * @return bool
*/ */
public function setCellVerticalAlignment(string $cellVerticalAlignment): self public function hasSetCellAlignment()
{
if (!CellVerticalAlignment::isValid($cellVerticalAlignment)) {
throw new InvalidArgumentException('Invalid cell vertical alignment value');
}
$this->cellVerticalAlignment = $cellVerticalAlignment;
$this->hasSetCellVerticalAlignment = true;
$this->shouldApplyCellVerticalAlignment = true;
$this->isEmpty = false;
return $this;
}
public function hasSetCellAlignment(): bool
{ {
return $this->hasSetCellAlignment; return $this->hasSetCellAlignment;
} }
public function hasSetCellVerticalAlignment(): bool
{
return $this->hasSetCellVerticalAlignment;
}
/** /**
* @return bool Whether specific cell alignment should be applied * @return bool Whether specific cell alignment should be applied
*/ */
public function shouldApplyCellAlignment(): bool public function shouldApplyCellAlignment()
{ {
return $this->shouldApplyCellAlignment; return $this->shouldApplyCellAlignment;
} }
public function shouldApplyCellVerticalAlignment(): bool /**
{ * @return bool
return $this->shouldApplyCellVerticalAlignment; */
} public function shouldWrapText()
public function shouldWrapText(): bool
{ {
return $this->shouldWrapText; return $this->shouldWrapText;
} }
/** /**
* @param bool $shouldWrap Should the text be wrapped * @param bool $shouldWrap Should the text be wrapped
*
* @return Style
*/ */
public function setShouldWrapText(bool $shouldWrap = true): self public function setShouldWrapText($shouldWrap = true)
{ {
$this->shouldWrapText = $shouldWrap; $this->shouldWrapText = $shouldWrap;
$this->hasSetWrapText = true; $this->hasSetWrapText = true;
@ -384,37 +414,18 @@ final class Style
return $this; return $this;
} }
public function hasSetWrapText(): bool /**
* @return bool
*/
public function hasSetWrapText()
{ {
return $this->hasSetWrapText; return $this->hasSetWrapText;
} }
public function textRotation(): int
{
return $this->textRotation;
}
/**
* @param int $rotation Rotate text
*/
public function setTextRotation(int $rotation): self
{
$this->textRotation = $rotation;
$this->hasSetTextRotation = true;
$this->isEmpty = false;
return $this;
}
public function hasSetTextRotation(): bool
{
return $this->hasSetTextRotation;
}
/** /**
* @return bool Whether specific font properties should be applied * @return bool Whether specific font properties should be applied
*/ */
public function shouldApplyFont(): bool public function shouldApplyFont()
{ {
return $this->shouldApplyFont; return $this->shouldApplyFont;
} }
@ -423,36 +434,66 @@ final class Style
* Sets the background color. * Sets the background color.
* *
* @param string $color ARGB color (@see Color) * @param string $color ARGB color (@see Color)
*
* @return Style
*/ */
public function setBackgroundColor(string $color): self public function setBackgroundColor($color)
{ {
$this->hasSetBackgroundColor = true;
$this->backgroundColor = $color; $this->backgroundColor = $color;
$this->isEmpty = false; $this->isEmpty = false;
return $this; return $this;
} }
public function getBackgroundColor(): ?string /**
* @return null|string
*/
public function getBackgroundColor()
{ {
return $this->backgroundColor; return $this->backgroundColor;
} }
/** /**
* Sets format. * @return bool Whether the background color should be applied
*/ */
public function setFormat(string $format): self public function shouldApplyBackgroundColor()
{ {
return $this->hasSetBackgroundColor;
}
/**
* Sets format.
*
* @param string $format
*
* @return Style
*/
public function setFormat($format)
{
$this->hasSetFormat = true;
$this->format = $format; $this->format = $format;
$this->isEmpty = false; $this->isEmpty = false;
return $this; return $this;
} }
public function getFormat(): ?string /**
* @return null|string
*/
public function getFormat()
{ {
return $this->format; return $this->format;
} }
/**
* @return bool Whether format should be applied
*/
public function shouldApplyFormat()
{
return $this->hasSetFormat;
}
public function isRegistered(): bool public function isRegistered(): bool
{ {
return $this->isRegistered; return $this->isRegistered;
@ -464,6 +505,12 @@ final class Style
$this->isRegistered = true; $this->isRegistered = true;
} }
public function unmarkAsRegistered(): void
{
$this->setId(0);
$this->isRegistered = false;
}
public function isEmpty(): bool public function isEmpty(): bool
{ {
return $this->isEmpty; return $this->isEmpty;
@ -471,8 +518,12 @@ final class Style
/** /**
* Sets should shrink to fit. * Sets should shrink to fit.
*
* @param bool $shrinkToFit
*
* @return Style
*/ */
public function setShouldShrinkToFit(bool $shrinkToFit = true): self public function setShouldShrinkToFit($shrinkToFit = true)
{ {
$this->hasSetShrinkToFit = true; $this->hasSetShrinkToFit = true;
$this->shouldShrinkToFit = $shrinkToFit; $this->shouldShrinkToFit = $shrinkToFit;
@ -483,12 +534,15 @@ final class Style
/** /**
* @return bool Whether format should be applied * @return bool Whether format should be applied
*/ */
public function shouldShrinkToFit(): bool public function shouldShrinkToFit()
{ {
return $this->shouldShrinkToFit; return $this->shouldShrinkToFit;
} }
public function hasSetShrinkToFit(): bool /**
* @return bool
*/
public function hasSetShrinkToFit()
{ {
return $this->hasSetShrinkToFit; return $this->hasSetShrinkToFit;
} }

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Common\Exception;
class EncodingConversionException extends SpoutException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Common\Exception;
class IOException extends SpoutException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Common\Exception;
class InvalidArgumentException extends SpoutException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Common\Exception;
class InvalidColorException extends SpoutException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Common\Exception;
abstract class SpoutException extends \Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Common\Exception;
class UnsupportedTypeException extends SpoutException
{
}

View File

@ -0,0 +1,82 @@
<?php
namespace OpenSpout\Common\Helper;
/**
* This class provides helper functions to determine the type of the cell value.
*/
class CellTypeHelper
{
/**
* @param null|mixed $value
*
* @return bool Whether the given value is considered "empty"
*/
public static function isEmpty($value)
{
return null === $value || '' === $value;
}
/**
* @param mixed $value
*
* @return bool Whether the given value is a non empty string
*/
public static function isNonEmptyString($value)
{
return 'string' === \gettype($value) && '' !== $value;
}
/**
* Returns whether the given value is numeric.
* A numeric value is from type "integer" or "double" ("float" is not returned by gettype).
*
* @param mixed $value
*
* @return bool Whether the given value is numeric
*/
public static function isNumeric($value)
{
$valueType = \gettype($value);
return 'integer' === $valueType || 'double' === $valueType;
}
/**
* Returns whether the given value is boolean.
* "true"/"false" and 0/1 are not booleans.
*
* @param mixed $value
*
* @return bool Whether the given value is boolean
*/
public static function isBoolean($value)
{
return 'boolean' === \gettype($value);
}
/**
* Returns whether the given value is a DateTime or DateInterval object.
*
* @param mixed $value
*
* @return bool Whether the given value is a DateTime or DateInterval object
*/
public static function isDateTimeOrDateInterval($value)
{
return
$value instanceof \DateTimeInterface
|| $value instanceof \DateInterval
;
}
/**
* @param mixed $value
*
* @return bool
*/
public static function isFormula($value)
{
return \is_string($value) && isset($value[0]) && '=' === $value[0];
}
}

View File

@ -1,46 +1,40 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Helper; namespace OpenSpout\Common\Helper;
use Error;
use OpenSpout\Common\Exception\EncodingConversionException; use OpenSpout\Common\Exception\EncodingConversionException;
/** /**
* @internal * This class provides helper functions to work with encodings.
*/ */
final readonly class EncodingHelper class EncodingHelper
{ {
/** /** Definition of the encodings that can have a BOM */
* Definition of the encodings that can have a BOM.
*/
public const ENCODING_UTF8 = 'UTF-8'; public const ENCODING_UTF8 = 'UTF-8';
public const ENCODING_UTF16_LE = 'UTF-16LE'; public const ENCODING_UTF16_LE = 'UTF-16LE';
public const ENCODING_UTF16_BE = 'UTF-16BE'; public const ENCODING_UTF16_BE = 'UTF-16BE';
public const ENCODING_UTF32_LE = 'UTF-32LE'; public const ENCODING_UTF32_LE = 'UTF-32LE';
public const ENCODING_UTF32_BE = 'UTF-32BE'; public const ENCODING_UTF32_BE = 'UTF-32BE';
/** /** Definition of the BOMs for the different encodings */
* Definition of the BOMs for the different encodings.
*/
public const BOM_UTF8 = "\xEF\xBB\xBF"; public const BOM_UTF8 = "\xEF\xBB\xBF";
public const BOM_UTF16_LE = "\xFF\xFE"; public const BOM_UTF16_LE = "\xFF\xFE";
public const BOM_UTF16_BE = "\xFE\xFF"; public const BOM_UTF16_BE = "\xFE\xFF";
public const BOM_UTF32_LE = "\xFF\xFE\x00\x00"; public const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
public const BOM_UTF32_BE = "\x00\x00\xFE\xFF"; public const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
/** @var array<string, string> Map representing the encodings supporting BOMs (key) and their associated BOM (value) */ /** @var \OpenSpout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
private array $supportedEncodingsWithBom; protected $globalFunctionsHelper;
private bool $canUseIconv; /** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */
protected $supportedEncodingsWithBom;
private bool $canUseMbString; /**
* @param \OpenSpout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
public function __construct(bool $canUseIconv, bool $canUseMbString) */
public function __construct($globalFunctionsHelper)
{ {
$this->canUseIconv = $canUseIconv; $this->globalFunctionsHelper = $globalFunctionsHelper;
$this->canUseMbString = $canUseMbString;
$this->supportedEncodingsWithBom = [ $this->supportedEncodingsWithBom = [
self::ENCODING_UTF8 => self::BOM_UTF8, self::ENCODING_UTF8 => self::BOM_UTF8,
@ -51,14 +45,6 @@ final readonly class EncodingHelper
]; ];
} }
public static function factory(): self
{
return new self(
\function_exists('iconv'),
\function_exists('mb_convert_encoding'),
);
}
/** /**
* Returns the number of bytes to use as offset in order to skip the BOM. * Returns the number of bytes to use as offset in order to skip the BOM.
* *
@ -67,7 +53,7 @@ final readonly class EncodingHelper
* *
* @return int Bytes offset to apply to skip the BOM (0 means no BOM) * @return int Bytes offset to apply to skip the BOM (0 means no BOM)
*/ */
public function getBytesOffsetToSkipBOM($filePointer, string $encoding): int public function getBytesOffsetToSkipBOM($filePointer, $encoding)
{ {
$byteOffsetToSkipBom = 0; $byteOffsetToSkipBom = 0;
@ -87,11 +73,11 @@ final readonly class EncodingHelper
* @param string $string Non UTF-8 string to be converted * @param string $string Non UTF-8 string to be converted
* @param string $sourceEncoding The encoding used to encode the source string * @param string $sourceEncoding The encoding used to encode the source string
* *
* @return string The converted, UTF-8 string * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
* *
* @throws EncodingConversionException If conversion is not supported or if the conversion failed * @return string The converted, UTF-8 string
*/ */
public function attemptConversionToUTF8(?string $string, string $sourceEncoding): ?string public function attemptConversionToUTF8($string, $sourceEncoding)
{ {
return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8); return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8);
} }
@ -102,11 +88,11 @@ final readonly class EncodingHelper
* @param string $string UTF-8 string to be converted * @param string $string UTF-8 string to be converted
* @param string $targetEncoding The encoding the string should be re-encoded into * @param string $targetEncoding The encoding the string should be re-encoded into
* *
* @return string The converted string, encoded with the given encoding * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
* *
* @throws EncodingConversionException If conversion is not supported or if the conversion failed * @return string The converted string, encoded with the given encoding
*/ */
public function attemptConversionFromUTF8(?string $string, string $targetEncoding): ?string public function attemptConversionFromUTF8($string, $targetEncoding)
{ {
return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding); return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding);
} }
@ -119,17 +105,17 @@ final readonly class EncodingHelper
* *
* @return bool TRUE if the file has a BOM, FALSE otherwise * @return bool TRUE if the file has a BOM, FALSE otherwise
*/ */
private function hasBOM($filePointer, string $encoding): bool protected function hasBOM($filePointer, $encoding)
{ {
$hasBOM = false; $hasBOM = false;
rewind($filePointer); $this->globalFunctionsHelper->rewind($filePointer);
if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) { if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
$potentialBom = $this->supportedEncodingsWithBom[$encoding]; $potentialBom = $this->supportedEncodingsWithBom[$encoding];
$numBytesInBom = \strlen($potentialBom); $numBytesInBom = \strlen($potentialBom);
$hasBOM = (fgets($filePointer, $numBytesInBom + 1) === $potentialBom); $hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
} }
return $hasBOM; return $hasBOM;
@ -143,47 +129,25 @@ final readonly class EncodingHelper
* @param string $sourceEncoding The encoding used to encode the source string * @param string $sourceEncoding The encoding used to encode the source string
* @param string $targetEncoding The encoding the string should be re-encoded into * @param string $targetEncoding The encoding the string should be re-encoded into
* *
* @return string The converted string, encoded with the given encoding * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
* *
* @throws EncodingConversionException If conversion is not supported or if the conversion failed * @return string The converted string, encoded with the given encoding
*/ */
private function attemptConversion(?string $string, string $sourceEncoding, string $targetEncoding): ?string protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
{ {
// if source and target encodings are the same, it's a no-op // if source and target encodings are the same, it's a no-op
if (null === $string || $sourceEncoding === $targetEncoding) { if ($sourceEncoding === $targetEncoding) {
return $string; return $string;
} }
$convertedString = null; $convertedString = null;
if ($this->canUseIconv) { if ($this->canUseIconv()) {
set_error_handler(static function (): bool { $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
return true; } elseif ($this->canUseMbString()) {
}); $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
$convertedString = iconv($sourceEncoding, $targetEncoding, $string);
restore_error_handler();
} elseif ($this->canUseMbString) {
$errorMessage = null;
set_error_handler(static function ($nr, $message) use (&$errorMessage): bool {
$errorMessage = $message; // @codeCoverageIgnore
return true; // @codeCoverageIgnore
});
try {
$convertedString = mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
} catch (Error $error) {
$errorMessage = $error->getMessage();
}
restore_error_handler();
if (null !== $errorMessage) {
$convertedString = false;
}
} else { } else {
throw new EncodingConversionException("The conversion from {$sourceEncoding} to {$targetEncoding} is not supported. Please install \"iconv\" or \"mbstring\"."); throw new EncodingConversionException("The conversion from {$sourceEncoding} to {$targetEncoding} is not supported. Please install \"iconv\" or \"PHP Intl\".");
} }
if (false === $convertedString) { if (false === $convertedString) {
@ -192,4 +156,25 @@ final readonly class EncodingHelper
return $convertedString; return $convertedString;
} }
/**
* Returns whether "iconv" can be used.
*
* @return bool TRUE if "iconv" is available and can be used, FALSE otherwise
*/
protected function canUseIconv()
{
return $this->globalFunctionsHelper->function_exists('iconv');
}
/**
* Returns whether "mb_string" functions can be used.
* These functions come with the PHP Intl package.
*
* @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise
*/
protected function canUseMbString()
{
return $this->globalFunctionsHelper->function_exists('mb_convert_encoding');
}
} }

View File

@ -0,0 +1,37 @@
<?php
namespace OpenSpout\Common\Helper\Escaper;
/**
* Provides functions to escape and unescape data for CSV files.
*/
class CSV implements EscaperInterface
{
/**
* Escapes the given string to make it compatible with CSV.
*
* @codeCoverageIgnore
*
* @param string $string The string to escape
*
* @return string The escaped string
*/
public function escape($string)
{
return $string;
}
/**
* Unescapes the given string to make it compatible with CSV.
*
* @codeCoverageIgnore
*
* @param string $string The string to unescape
*
* @return string The unescaped string
*/
public function unescape($string)
{
return $string;
}
}

View File

@ -1,11 +1,9 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Helper\Escaper; namespace OpenSpout\Common\Helper\Escaper;
/** /**
* @internal * Interface EscaperInterface.
*/ */
interface EscaperInterface interface EscaperInterface
{ {
@ -16,7 +14,7 @@ interface EscaperInterface
* *
* @return string The escaped string * @return string The escaped string
*/ */
public function escape(string $string): string; public function escape($string);
/** /**
* Unescapes the given string to make it compatible with PHP. * Unescapes the given string to make it compatible with PHP.
@ -25,5 +23,5 @@ interface EscaperInterface
* *
* @return string The unescaped string * @return string The unescaped string
*/ */
public function unescape(string $string): string; public function unescape($string);
} }

View File

@ -0,0 +1,63 @@
<?php
namespace OpenSpout\Common\Helper\Escaper;
/**
* Provides functions to escape and unescape data for ODS files.
*/
class ODS implements EscaperInterface
{
/**
* Escapes the given string to make it compatible with XLSX.
*
* @param string $string The string to escape
*
* @return string The escaped string
*/
public function escape($string)
{
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
// single/double quotes (for XML attributes) need to be encoded.
if (\defined('ENT_DISALLOWED')) {
/**
* 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced.
* Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor.
*
* @see https://github.com/box/spout/issues/329
*/
$replacedString = htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
} else {
// We are on hhvm or any other engine that does not support ENT_DISALLOWED.
$escapedString = htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
// control characters values are from 0 to 1F (hex values) in the ASCII table
// some characters should not be escaped though: "\t", "\r" and "\n".
$regexPattern = '[\x00-\x08'.
// skipping "\t" (0x9) and "\n" (0xA)
'\x0B-\x0C'.
// skipping "\r" (0xD)
'\x0E-\x1F]';
$replacedString = preg_replace("/{$regexPattern}/", '<27>', $escapedString);
}
return $replacedString;
}
/**
* Unescapes the given string to make it compatible with XLSX.
*
* @param string $string The string to unescape
*
* @return string The unescaped string
*/
public function unescape($string)
{
// ==============
// = WARNING =
// ==============
// It is assumed that the given string has already had its XML entities decoded.
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
// Therefore there is no need to call "htmlspecialchars_decode()".
return $string;
}
}

View File

@ -1,25 +1,23 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Helper\Escaper; namespace OpenSpout\Common\Helper\Escaper;
/** /**
* @internal * Provides functions to escape and unescape data for XLSX files.
*/ */
final class XLSX implements EscaperInterface class XLSX implements EscaperInterface
{ {
/** @var bool Whether the escaper has already been initialized */ /** @var bool Whether the escaper has already been initialized */
private bool $isAlreadyInitialized = false; private $isAlreadyInitialized = false;
/** @var string Regex pattern to detect control characters that need to be escaped */ /** @var string Regex pattern to detect control characters that need to be escaped */
private string $escapableControlCharactersPattern; private $escapableControlCharactersPattern;
/** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */ /** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
private array $controlCharactersEscapingMap; private $controlCharactersEscapingMap;
/** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */ /** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
private array $controlCharactersEscapingReverseMap; private $controlCharactersEscapingReverseMap;
/** /**
* Escapes the given string to make it compatible with XLSX. * Escapes the given string to make it compatible with XLSX.
@ -28,12 +26,11 @@ final class XLSX implements EscaperInterface
* *
* @return string The escaped string * @return string The escaped string
*/ */
public function escape(string $string): string public function escape($string)
{ {
$this->initIfNeeded(); $this->initIfNeeded();
$escapedString = $this->escapeControlCharacters($string); $escapedString = $this->escapeControlCharacters($string);
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as // @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
// single/double quotes (for XML attributes) need to be encoded. // single/double quotes (for XML attributes) need to be encoded.
return htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8'); return htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8');
@ -46,7 +43,7 @@ final class XLSX implements EscaperInterface
* *
* @return string The unescaped string * @return string The unescaped string
*/ */
public function unescape(string $string): string public function unescape($string)
{ {
$this->initIfNeeded(); $this->initIfNeeded();
@ -62,7 +59,7 @@ final class XLSX implements EscaperInterface
/** /**
* Initializes the control characters if not already done. * Initializes the control characters if not already done.
*/ */
private function initIfNeeded(): void protected function initIfNeeded()
{ {
if (!$this->isAlreadyInitialized) { if (!$this->isAlreadyInitialized) {
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern(); $this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
@ -76,7 +73,7 @@ final class XLSX implements EscaperInterface
/** /**
* @return string Regex pattern containing all escapable control characters * @return string Regex pattern containing all escapable control characters
*/ */
private function getEscapableControlCharactersPattern(): string protected function getEscapableControlCharactersPattern()
{ {
// control characters values are from 0 to 1F (hex values) in the ASCII table // control characters values are from 0 to 1F (hex values) in the ASCII table
// some characters should not be escaped though: "\t", "\r" and "\n". // some characters should not be escaped though: "\t", "\r" and "\n".
@ -98,16 +95,16 @@ final class XLSX implements EscaperInterface
* *
* @return string[] * @return string[]
*/ */
private function getControlCharactersEscapingMap(): array protected function getControlCharactersEscapingMap()
{ {
$controlCharactersEscapingMap = []; $controlCharactersEscapingMap = [];
// control characters values are from 0 to 1F (hex values) in the ASCII table // control characters values are from 0 to 1F (hex values) in the ASCII table
for ($charValue = 0x00; $charValue <= 0x1F; ++$charValue) { for ($charValue = 0x00; $charValue <= 0x1F; ++$charValue) {
$character = \chr($charValue); $character = \chr($charValue);
if (1 === preg_match("/{$this->escapableControlCharactersPattern}/", $character)) { if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
$charHexValue = dechex($charValue); $charHexValue = dechex($charValue);
$escapedChar = '_x'.\sprintf('%04s', strtoupper($charHexValue)).'_'; $escapedChar = '_x'.sprintf('%04s', strtoupper($charHexValue)).'_';
$controlCharactersEscapingMap[$escapedChar] = $character; $controlCharactersEscapingMap[$escapedChar] = $character;
} }
} }
@ -127,13 +124,15 @@ final class XLSX implements EscaperInterface
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 * @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
* *
* @param string $string String to escape * @param string $string String to escape
*
* @return string
*/ */
private function escapeControlCharacters(string $string): string protected function escapeControlCharacters($string)
{ {
$escapedString = $this->escapeEscapeCharacter($string); $escapedString = $this->escapeEscapeCharacter($string);
// if no control characters // if no control characters
if (1 !== preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) { if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
return $escapedString; return $escapedString;
} }
@ -149,7 +148,7 @@ final class XLSX implements EscaperInterface
* *
* @return string The escaped string * @return string The escaped string
*/ */
private function escapeEscapeCharacter(string $string): string protected function escapeEscapeCharacter($string)
{ {
return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string); return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
} }
@ -166,8 +165,10 @@ final class XLSX implements EscaperInterface
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 * @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
* *
* @param string $string String to unescape * @param string $string String to unescape
*
* @return string
*/ */
private function unescapeControlCharacters(string $string): string protected function unescapeControlCharacters($string)
{ {
$unescapedString = $string; $unescapedString = $string;
@ -186,7 +187,7 @@ final class XLSX implements EscaperInterface
* *
* @return string The unescaped string * @return string The unescaped string
*/ */
private function unescapeEscapeCharacter(string $string): string protected function unescapeEscapeCharacter($string)
{ {
return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string); return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
} }

View File

@ -1,34 +1,24 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Helper; namespace OpenSpout\Common\Helper;
use OpenSpout\Common\Exception\IOException; use OpenSpout\Common\Exception\IOException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/** /**
* @internal * This class provides helper functions to help with the file system operations
* like files/folders creation & deletion.
*/ */
final readonly class FileSystemHelper implements FileSystemHelperInterface class FileSystemHelper implements FileSystemHelperInterface
{ {
/** @var string Real path of the base folder where all the I/O can occur */ /** @var string Real path of the base folder where all the I/O can occur */
private string $baseFolderRealPath; protected $baseFolderRealPath;
/** /**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur * @param string $baseFolderPath The path of the base folder where all the I/O can occur
*/ */
public function __construct(string $baseFolderPath) public function __construct(string $baseFolderPath)
{ {
$realpath = realpath($baseFolderPath); $this->baseFolderRealPath = realpath($baseFolderPath);
\assert(false !== $realpath);
$this->baseFolderRealPath = $realpath;
}
public function getBaseFolderRealPath(): string
{
return $this->baseFolderRealPath;
} }
/** /**
@ -37,27 +27,19 @@ final readonly class FileSystemHelper implements FileSystemHelperInterface
* @param string $parentFolderPath The parent folder path under which the folder is going to be created * @param string $parentFolderPath The parent folder path under which the folder is going to be created
* @param string $folderName The name of the folder to create * @param string $folderName The name of the folder to create
* *
* @return string Path of the created folder * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
* *
* @throws IOException If unable to create the folder or if the folder path is not inside of the base folder * @return string Path of the created folder
*/ */
public function createFolder(string $parentFolderPath, string $folderName): string public function createFolder($parentFolderPath, $folderName)
{ {
$this->throwIfOperationNotInBaseFolder($parentFolderPath); $this->throwIfOperationNotInBaseFolder($parentFolderPath);
$folderPath = $parentFolderPath.\DIRECTORY_SEPARATOR.$folderName; $folderPath = $parentFolderPath.'/'.$folderName;
$errorMessage = '';
set_error_handler(static function ($nr, $message) use (&$errorMessage): bool {
$errorMessage = $message;
return true;
});
$wasCreationSuccessful = mkdir($folderPath, 0777, true); $wasCreationSuccessful = mkdir($folderPath, 0777, true);
restore_error_handler();
if (!$wasCreationSuccessful) { if (!$wasCreationSuccessful) {
throw new IOException("Unable to create folder: {$folderPath} - {$errorMessage}"); throw new IOException("Unable to create folder: {$folderPath}");
} }
return $folderPath; return $folderPath;
@ -71,27 +53,19 @@ final readonly class FileSystemHelper implements FileSystemHelperInterface
* @param string $fileName The name of the file to create * @param string $fileName The name of the file to create
* @param string $fileContents The contents of the file to create * @param string $fileContents The contents of the file to create
* *
* @return string Path of the created file * @throws \OpenSpout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
* *
* @throws IOException If unable to create the file or if the file path is not inside of the base folder * @return string Path of the created file
*/ */
public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
{ {
$this->throwIfOperationNotInBaseFolder($parentFolderPath); $this->throwIfOperationNotInBaseFolder($parentFolderPath);
$filePath = $parentFolderPath.\DIRECTORY_SEPARATOR.$fileName; $filePath = $parentFolderPath.'/'.$fileName;
$errorMessage = '';
set_error_handler(static function ($nr, $message) use (&$errorMessage): bool {
$errorMessage = $message;
return true;
});
$wasCreationSuccessful = file_put_contents($filePath, $fileContents); $wasCreationSuccessful = file_put_contents($filePath, $fileContents);
restore_error_handler();
if (false === $wasCreationSuccessful) { if (false === $wasCreationSuccessful) {
throw new IOException("Unable to create file: {$filePath} - {$errorMessage}"); throw new IOException("Unable to create file: {$filePath}");
} }
return $filePath; return $filePath;
@ -102,9 +76,9 @@ final readonly class FileSystemHelper implements FileSystemHelperInterface
* *
* @param string $filePath Path of the file to delete * @param string $filePath Path of the file to delete
* *
* @throws IOException If the file path is not inside of the base folder * @throws \OpenSpout\Common\Exception\IOException If the file path is not inside of the base folder
*/ */
public function deleteFile(string $filePath): void public function deleteFile($filePath)
{ {
$this->throwIfOperationNotInBaseFolder($filePath); $this->throwIfOperationNotInBaseFolder($filePath);
@ -118,15 +92,15 @@ final readonly class FileSystemHelper implements FileSystemHelperInterface
* *
* @param string $folderPath Path of the folder to delete * @param string $folderPath Path of the folder to delete
* *
* @throws IOException If the folder path is not inside of the base folder * @throws \OpenSpout\Common\Exception\IOException If the folder path is not inside of the base folder
*/ */
public function deleteFolderRecursively(string $folderPath): void public function deleteFolderRecursively($folderPath)
{ {
$this->throwIfOperationNotInBaseFolder($folderPath); $this->throwIfOperationNotInBaseFolder($folderPath);
$itemIterator = new RecursiveIteratorIterator( $itemIterator = new \RecursiveIteratorIterator(
new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS), new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST \RecursiveIteratorIterator::CHILD_FIRST
); );
foreach ($itemIterator as $item) { foreach ($itemIterator as $item) {
@ -147,16 +121,16 @@ final readonly class FileSystemHelper implements FileSystemHelperInterface
* *
* @param string $operationFolderPath The path of the folder where the I/O operation should occur * @param string $operationFolderPath The path of the folder where the I/O operation should occur
* *
* @throws IOException If the folder where the I/O operation should occur * @throws \OpenSpout\Common\Exception\IOException If the folder where the I/O operation should occur
* is not inside the base folder or the base folder does not exist * is not inside the base folder or the base folder does not exist
*/ */
private function throwIfOperationNotInBaseFolder(string $operationFolderPath): void protected function throwIfOperationNotInBaseFolder(string $operationFolderPath)
{ {
$operationFolderRealPath = realpath($operationFolderPath); $operationFolderRealPath = realpath($operationFolderPath);
if (false === $operationFolderRealPath) { if (!$this->baseFolderRealPath) {
throw new IOException("Folder not found: {$operationFolderRealPath}"); throw new IOException("The base folder path is invalid: {$this->baseFolderRealPath}");
} }
$isInBaseFolder = str_starts_with($operationFolderRealPath, $this->baseFolderRealPath); $isInBaseFolder = (0 === strpos($operationFolderRealPath, $this->baseFolderRealPath));
if (!$isInBaseFolder) { if (!$isInBaseFolder) {
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}"); throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
} }

View File

@ -1,13 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Common\Helper; namespace OpenSpout\Common\Helper;
use OpenSpout\Common\Exception\IOException;
/** /**
* @internal * This interface describes helper functions to help with the file system operations
* like files/folders creation & deletion.
*/ */
interface FileSystemHelperInterface interface FileSystemHelperInterface
{ {
@ -17,11 +14,11 @@ interface FileSystemHelperInterface
* @param string $parentFolderPath The parent folder path under which the folder is going to be created * @param string $parentFolderPath The parent folder path under which the folder is going to be created
* @param string $folderName The name of the folder to create * @param string $folderName The name of the folder to create
* *
* @return string Path of the created folder * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
* *
* @throws IOException If unable to create the folder or if the folder path is not inside of the base folder * @return string Path of the created folder
*/ */
public function createFolder(string $parentFolderPath, string $folderName): string; public function createFolder($parentFolderPath, $folderName);
/** /**
* Creates a file with the given name and content in the given folder. * Creates a file with the given name and content in the given folder.
@ -31,27 +28,27 @@ interface FileSystemHelperInterface
* @param string $fileName The name of the file to create * @param string $fileName The name of the file to create
* @param string $fileContents The contents of the file to create * @param string $fileContents The contents of the file to create
* *
* @return string Path of the created file * @throws \OpenSpout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
* *
* @throws IOException If unable to create the file or if the file path is not inside of the base folder * @return string Path of the created file
*/ */
public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string; public function createFileWithContents($parentFolderPath, $fileName, $fileContents);
/** /**
* Delete the file at the given path. * Delete the file at the given path.
* *
* @param string $filePath Path of the file to delete * @param string $filePath Path of the file to delete
* *
* @throws IOException If the file path is not inside of the base folder * @throws \OpenSpout\Common\Exception\IOException If the file path is not inside of the base folder
*/ */
public function deleteFile(string $filePath): void; public function deleteFile($filePath);
/** /**
* Delete the folder at the given path as well as all its contents. * Delete the folder at the given path as well as all its contents.
* *
* @param string $folderPath Path of the folder to delete * @param string $folderPath Path of the folder to delete
* *
* @throws IOException If the folder path is not inside of the base folder * @throws \OpenSpout\Common\Exception\IOException If the folder path is not inside of the base folder
*/ */
public function deleteFolderRecursively(string $folderPath): void; public function deleteFolderRecursively($folderPath);
} }

View File

@ -0,0 +1,371 @@
<?php
namespace OpenSpout\Common\Helper;
/**
* This class wraps global functions to facilitate testing.
*
* @codeCoverageIgnore
*/
class GlobalFunctionsHelper
{
/**
* Wrapper around global function fopen().
*
* @see fopen()
*
* @param string $fileName
* @param string $mode
*
* @return bool|resource
*/
public function fopen($fileName, $mode)
{
return fopen($fileName, $mode);
}
/**
* Wrapper around global function fgets().
*
* @see fgets()
*
* @param resource $handle
* @param null|int $length
*
* @return string
*/
public function fgets($handle, $length = null)
{
return fgets($handle, $length);
}
/**
* Wrapper around global function fputs().
*
* @see fputs()
*
* @param resource $handle
* @param string $string
*
* @return int
*/
public function fputs($handle, $string)
{
return fwrite($handle, $string);
}
/**
* Wrapper around global function fflush().
*
* @see fflush()
*
* @param resource $handle
*
* @return bool
*/
public function fflush($handle)
{
return fflush($handle);
}
/**
* Wrapper around global function fseek().
*
* @see fseek()
*
* @param resource $handle
* @param int $offset
*
* @return int
*/
public function fseek($handle, $offset)
{
return fseek($handle, $offset);
}
/**
* Wrapper around global function fgetcsv().
*
* @see fgetcsv()
*
* @param resource $handle
* @param null|int $length
* @param null|string $delimiter
* @param null|string $enclosure
*
* @return array|false
*/
public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null)
{
/**
* PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
* To fix that, simply disable the escape character.
*
* @see https://bugs.php.net/bug.php?id=43225
* @see http://tools.ietf.org/html/rfc4180
*/
$escapeCharacter = \PHP_VERSION_ID >= 70400 ? '' : "\0";
return fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter);
}
/**
* Wrapper around global function fputcsv().
*
* @see fputcsv()
*
* @param resource $handle
* @param null|string $delimiter
* @param null|string $enclosure
*
* @return false|int
*/
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
{
/**
* PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
* To fix that, simply disable the escape character.
*
* @see https://bugs.php.net/bug.php?id=43225
* @see http://tools.ietf.org/html/rfc4180
*/
$escapeCharacter = \PHP_VERSION_ID >= 70400 ? '' : "\0";
return fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter);
}
/**
* Wrapper around global function fwrite().
*
* @see fwrite()
*
* @param resource $handle
* @param string $string
*
* @return int
*/
public function fwrite($handle, $string)
{
return fwrite($handle, $string);
}
/**
* Wrapper around global function fclose().
*
* @see fclose()
*
* @param resource $handle
*
* @return bool
*/
public function fclose($handle)
{
return fclose($handle);
}
/**
* Wrapper around global function rewind().
*
* @see rewind()
*
* @param resource $handle
*
* @return bool
*/
public function rewind($handle)
{
return rewind($handle);
}
/**
* Wrapper around global function file_exists().
*
* @see file_exists()
*
* @param string $fileName
*
* @return bool
*/
public function file_exists($fileName)
{
return file_exists($fileName);
}
/**
* Wrapper around global function file_get_contents().
*
* @see file_get_contents()
*
* @param string $filePath
*
* @return string
*/
public function file_get_contents($filePath)
{
$realFilePath = $this->convertToUseRealPath($filePath);
return file_get_contents($realFilePath);
}
/**
* Wrapper around global function feof().
*
* @see feof()
*
* @param resource $handle
*
* @return bool
*/
public function feof($handle)
{
return feof($handle);
}
/**
* Wrapper around global function is_readable().
*
* @see is_readable()
*
* @param string $fileName
*
* @return bool
*/
public function is_readable($fileName)
{
return is_readable($fileName);
}
/**
* Wrapper around global function basename().
*
* @see basename()
*
* @param string $path
* @param string $suffix
*
* @return string
*/
public function basename($path, $suffix = '')
{
return basename($path, $suffix);
}
/**
* Wrapper around global function header().
*
* @see header()
*
* @param string $string
*/
public function header($string)
{
header($string);
}
/**
* Wrapper around global function ob_end_clean().
*
* @see ob_end_clean()
*/
public function ob_end_clean()
{
if (ob_get_length() > 0) {
ob_end_clean();
}
}
/**
* Wrapper around global function iconv().
*
* @see iconv()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
*
* @return bool|string the converted string or FALSE on failure
*/
public function iconv($string, $sourceEncoding, $targetEncoding)
{
return iconv($sourceEncoding, $targetEncoding, $string);
}
/**
* Wrapper around global function mb_convert_encoding().
*
* @see mb_convert_encoding()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
*
* @return bool|string the converted string or FALSE on failure
*/
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
{
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
}
/**
* Wrapper around global function stream_get_wrappers().
*
* @see stream_get_wrappers()
*
* @return array
*/
public function stream_get_wrappers()
{
return stream_get_wrappers();
}
/**
* Wrapper around global function function_exists().
*
* @see function_exists()
*
* @param string $functionName
*
* @return bool
*/
public function function_exists($functionName)
{
return \function_exists($functionName);
}
/**
* Updates the given file path to use a real path.
* This is to avoid issues on some Windows setup.
*
* @param string $filePath File path
*
* @return string The file path using a real path
*/
protected function convertToUseRealPath($filePath)
{
$realFilePath = $filePath;
if ($this->isZipStream($filePath)) {
if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
$documentPath = $matches[1];
$documentInsideZipPath = $matches[2];
$realFilePath = 'zip://'.realpath($documentPath).'#'.$documentInsideZipPath;
}
} else {
$realFilePath = realpath($filePath);
}
return $realFilePath;
}
/**
* Returns whether the given path is a zip stream.
*
* @param string $path Path pointing to a document
*
* @return bool TRUE if path is a zip stream, FALSE otherwise
*/
protected function isZipStream($path)
{
return 0 === strpos($path, 'zip://');
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace OpenSpout\Common\Helper;
/**
* This class provides helper functions to work with strings and multibyte strings.
*
* @codeCoverageIgnore
*/
class StringHelper
{
/** @var bool Whether the mbstring extension is loaded */
protected $hasMbstringSupport;
/** @var bool Whether the code is running with PHP7 or older versions */
private $isRunningPhp7OrOlder;
/** @var array Locale info, used for number formatting */
private $localeInfo;
public function __construct()
{
$this->hasMbstringSupport = \extension_loaded('mbstring');
$this->isRunningPhp7OrOlder = version_compare(PHP_VERSION, '8.0.0') < 0;
$this->localeInfo = localeconv();
}
/**
* Returns the length of the given string.
* It uses the multi-bytes function is available.
*
* @see strlen
* @see mb_strlen
*
* @param string $string
*
* @return int
*/
public function getStringLength($string)
{
return $this->hasMbstringSupport ? mb_strlen($string) : \strlen($string);
}
/**
* Returns the position of the first occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
*
* @see strpos
* @see mb_strpos
*
* @param string $char Needle
* @param string $string Haystack
*
* @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharFirstOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char);
return (false !== $position) ? $position : -1;
}
/**
* Returns the position of the last occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
*
* @see strrpos
* @see mb_strrpos
*
* @param string $char Needle
* @param string $string Haystack
*
* @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharLastOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char);
return (false !== $position) ? $position : -1;
}
/**
* Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format.
*
* Formatting of float values is locale dependent in PHP < 8.
* Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34).
* However, float values must be formatted with no thousands separator and a "." as decimal point
* to work properly. This method can be used to convert the value to the correct format before storing it.
*
* @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8.
*
* @param float|int $numericValue
*
* @return float|int|string
*/
public function formatNumericValue($numericValue)
{
if ($this->isRunningPhp7OrOlder && \is_float($numericValue)) {
return str_replace(
[$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
['', '.'],
(string) $numericValue
);
}
return $numericValue;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace OpenSpout\Common\Manager;
abstract class OptionsManagerAbstract implements OptionsManagerInterface
{
public const PREFIX_OPTION = 'OPTION_';
/** @var string[] List of all supported option names */
private $supportedOptions = [];
/** @var array Associative array [OPTION_NAME => OPTION_VALUE] */
private $options = [];
/**
* OptionsManagerAbstract constructor.
*/
public function __construct()
{
$this->supportedOptions = $this->getSupportedOptions();
$this->setDefaultOptions();
}
/**
* Sets the given option, if this option is supported.
*
* @param string $optionName
* @param mixed $optionValue
*/
public function setOption($optionName, $optionValue)
{
if (\in_array($optionName, $this->supportedOptions, true)) {
$this->options[$optionName] = $optionValue;
}
}
/**
* Add an option to the internal list of options
* Used only for mergeCells() for now.
*
* @param mixed $optionName
* @param mixed $optionValue
*/
public function addOption($optionName, $optionValue)
{
if (\in_array($optionName, $this->supportedOptions, true)) {
if (!isset($this->options[$optionName])) {
$this->options[$optionName] = [];
} elseif (!\is_array($this->options[$optionName])) {
$this->options[$optionName] = [$this->options[$optionName]];
}
$this->options[$optionName][] = $optionValue;
}
}
/**
* @param string $optionName
*
* @return null|mixed The set option or NULL if no option with given name found
*/
public function getOption($optionName)
{
$optionValue = null;
if (isset($this->options[$optionName])) {
$optionValue = $this->options[$optionName];
}
return $optionValue;
}
/**
* @return array List of supported options
*/
abstract protected function getSupportedOptions();
/**
* Sets the default options.
* To be overriden by child classes.
*/
abstract protected function setDefaultOptions();
}

View File

@ -0,0 +1,31 @@
<?php
namespace OpenSpout\Common\Manager;
/**
* Interface OptionsManagerInterface.
*/
interface OptionsManagerInterface
{
/**
* @param string $optionName
* @param mixed $optionValue
*/
public function setOption($optionName, $optionValue);
/**
* @param string $optionName
*
* @return null|mixed The set option or NULL if no option with given name found
*/
public function getOption($optionName);
/**
* Add an option to the internal list of options
* Used only for mergeCells() for now.
*
* @param mixed $optionName
* @param mixed $optionValue
*/
public function addOption($optionName, $optionValue);
}

View File

@ -0,0 +1,13 @@
<?php
namespace OpenSpout\Common;
/**
* This class references the supported types.
*/
abstract class Type
{
public const CSV = 'csv';
public const XLSX = 'xlsx';
public const ODS = 'ods';
}

View File

@ -0,0 +1,98 @@
<?php
namespace OpenSpout\Reader\CSV\Creator;
use OpenSpout\Common\Creator\HelperFactory;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Common\Helper\GlobalFunctionsHelper;
use OpenSpout\Common\Manager\OptionsManagerInterface;
use OpenSpout\Reader\Common\Creator\InternalEntityFactoryInterface;
use OpenSpout\Reader\CSV\RowIterator;
use OpenSpout\Reader\CSV\Sheet;
use OpenSpout\Reader\CSV\SheetIterator;
/**
* Factory to create entities.
*/
class InternalEntityFactory implements InternalEntityFactoryInterface
{
/** @var HelperFactory */
private $helperFactory;
public function __construct(HelperFactory $helperFactory)
{
$this->helperFactory = $helperFactory;
}
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param OptionsManagerInterface $optionsManager
* @param GlobalFunctionsHelper $globalFunctionsHelper
*
* @return SheetIterator
*/
public function createSheetIterator($filePointer, $optionsManager, $globalFunctionsHelper)
{
$rowIterator = $this->createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper);
$sheet = $this->createSheet($rowIterator);
return new SheetIterator($sheet);
}
/**
* @param Cell[] $cells
*
* @return Row
*/
public function createRow(array $cells = [])
{
return new Row($cells, null);
}
/**
* @param mixed $cellValue
*
* @return Cell
*/
public function createCell($cellValue)
{
return new Cell($cellValue);
}
/**
* @return Row
*/
public function createRowFromArray(array $cellValues = [])
{
$cells = array_map(function ($cellValue) {
return $this->createCell($cellValue);
}, $cellValues);
return $this->createRow($cells);
}
/**
* @param RowIterator $rowIterator
*
* @return Sheet
*/
private function createSheet($rowIterator)
{
return new Sheet($rowIterator);
}
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param OptionsManagerInterface $optionsManager
* @param GlobalFunctionsHelper $globalFunctionsHelper
*
* @return RowIterator
*/
private function createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper)
{
$encodingHelper = $this->helperFactory->createEncodingHelper($globalFunctionsHelper);
return new RowIterator($filePointer, $optionsManager, $encodingHelper, $this, $globalFunctionsHelper);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace OpenSpout\Reader\CSV\Manager;
use OpenSpout\Common\Helper\EncodingHelper;
use OpenSpout\Common\Manager\OptionsManagerAbstract;
use OpenSpout\Reader\Common\Entity\Options;
/**
* CSV Reader options manager.
*/
class OptionsManager extends OptionsManagerAbstract
{
/**
* {@inheritdoc}
*/
protected function getSupportedOptions()
{
return [
Options::SHOULD_FORMAT_DATES,
Options::SHOULD_PRESERVE_EMPTY_ROWS,
Options::FIELD_DELIMITER,
Options::FIELD_ENCLOSURE,
Options::ENCODING,
];
}
/**
* {@inheritdoc}
*/
protected function setDefaultOptions()
{
$this->setOption(Options::SHOULD_FORMAT_DATES, false);
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
$this->setOption(Options::FIELD_DELIMITER, ',');
$this->setOption(Options::FIELD_ENCLOSURE, '"');
$this->setOption(Options::ENCODING, EncodingHelper::ENCODING_UTF8);
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace OpenSpout\Reader\CSV;
use OpenSpout\Common\Exception\IOException;
use OpenSpout\Common\Helper\GlobalFunctionsHelper;
use OpenSpout\Common\Manager\OptionsManagerInterface;
use OpenSpout\Reader\Common\Creator\InternalEntityFactoryInterface;
use OpenSpout\Reader\Common\Entity\Options;
use OpenSpout\Reader\CSV\Creator\InternalEntityFactory;
use OpenSpout\Reader\ReaderAbstract;
/**
* This class provides support to read data from a CSV file.
*/
class Reader extends ReaderAbstract
{
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @var SheetIterator To iterator over the CSV unique "sheet" */
protected $sheetIterator;
/** @var string Original value for the "auto_detect_line_endings" INI value */
protected $originalAutoDetectLineEndings;
/** @var bool Whether the code is running with PHP >= 8.1 */
private $isRunningAtLeastPhp81;
public function __construct(
OptionsManagerInterface $optionsManager,
GlobalFunctionsHelper $globalFunctionsHelper,
InternalEntityFactoryInterface $entityFactory
) {
parent::__construct($optionsManager, $globalFunctionsHelper, $entityFactory);
$this->isRunningAtLeastPhp81 = version_compare(PHP_VERSION, '8.1.0') >= 0;
}
/**
* Sets the field delimiter for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldDelimiter Character that delimits fields
*
* @return Reader
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
return $this;
}
/**
* Sets the field enclosure for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldEnclosure Character that enclose fields
*
* @return Reader
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
return $this;
}
/**
* Sets the encoding of the CSV file to be read.
* Needs to be called before opening the reader.
*
* @param string $encoding Encoding of the CSV file to be read
*
* @return Reader
*/
public function setEncoding($encoding)
{
$this->optionsManager->setOption(Options::ENCODING, $encoding);
return $this;
}
/**
* Returns whether stream wrappers are supported.
*
* @return bool
*/
protected function doesSupportStreamWrapper()
{
return true;
}
/**
* Opens the file at the given path to make it ready to be read.
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
*
* @param string $filePath Path of the CSV file to be read
*
* @throws \OpenSpout\Common\Exception\IOException
*/
protected function openReader($filePath)
{
// "auto_detect_line_endings" is deprecated in PHP 8.1
if (!$this->isRunningAtLeastPhp81) {
$this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings');
ini_set('auto_detect_line_endings', '1');
}
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException("Could not open file {$filePath} for reading.");
}
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->sheetIterator = $entityFactory->createSheetIterator(
$this->filePointer,
$this->optionsManager,
$this->globalFunctionsHelper
);
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
protected function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*/
protected function closeReader()
{
if (\is_resource($this->filePointer)) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
// "auto_detect_line_endings" is deprecated in PHP 8.1
if (!$this->isRunningAtLeastPhp81) {
ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
}
}
}

View File

@ -1,53 +1,76 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\CSV; namespace OpenSpout\Reader\CSV;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row; use OpenSpout\Common\Entity\Row;
use OpenSpout\Common\Exception\EncodingConversionException;
use OpenSpout\Common\Helper\EncodingHelper; use OpenSpout\Common\Helper\EncodingHelper;
use OpenSpout\Common\Helper\GlobalFunctionsHelper;
use OpenSpout\Common\Manager\OptionsManagerInterface;
use OpenSpout\Reader\Common\Entity\Options;
use OpenSpout\Reader\CSV\Creator\InternalEntityFactory;
use OpenSpout\Reader\RowIteratorInterface; use OpenSpout\Reader\RowIteratorInterface;
/** /**
* Iterate over CSV rows. * Iterate over CSV rows.
*/ */
final class RowIterator implements RowIteratorInterface class RowIterator implements RowIteratorInterface
{ {
/** /**
* Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accommodates for very long lines). * Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines).
*/ */
public const MAX_READ_BYTES_PER_LINE = 0; public const MAX_READ_BYTES_PER_LINE = 0;
/** @var null|resource Pointer to the CSV file to read */ /** @var null|resource Pointer to the CSV file to read */
private $filePointer; protected $filePointer;
/** @var int Number of read rows */ /** @var int Number of read rows */
private int $numReadRows = 0; protected $numReadRows = 0;
/** @var null|Row Buffer used to store the current row, while checking if there are more rows to read */ /** @var null|Row Buffer used to store the current row, while checking if there are more rows to read */
private ?Row $rowBuffer = null; protected $rowBuffer;
/** @var bool Indicates whether all rows have been read */ /** @var bool Indicates whether all rows have been read */
private bool $hasReachedEndOfFile = false; protected $hasReachedEndOfFile = false;
private readonly Options $options; /** @var string Defines the character used to delimit fields (one character only) */
protected $fieldDelimiter;
/** @var EncodingHelper Helper to work with different encodings */ /** @var string Defines the character used to enclose fields (one character only) */
private readonly EncodingHelper $encodingHelper; protected $fieldEnclosure;
/** @var string Encoding of the CSV file to be read */
protected $encoding;
/** @var bool Whether empty rows should be returned or skipped */
protected $shouldPreserveEmptyRows;
/** @var \OpenSpout\Common\Helper\EncodingHelper Helper to work with different encodings */
protected $encodingHelper;
/** @var \OpenSpout\Reader\CSV\Creator\InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var \OpenSpout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** /**
* @param resource $filePointer Pointer to the CSV file to read * @param resource $filePointer Pointer to the CSV file to read
*/ */
public function __construct( public function __construct(
$filePointer, $filePointer,
Options $options, OptionsManagerInterface $optionsManager,
EncodingHelper $encodingHelper EncodingHelper $encodingHelper,
InternalEntityFactory $entityFactory,
GlobalFunctionsHelper $globalFunctionsHelper
) { ) {
$this->filePointer = $filePointer; $this->filePointer = $filePointer;
$this->options = $options; $this->fieldDelimiter = $optionsManager->getOption(Options::FIELD_DELIMITER);
$this->fieldEnclosure = $optionsManager->getOption(Options::FIELD_ENCLOSURE);
$this->encoding = $optionsManager->getOption(Options::ENCODING);
$this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
$this->encodingHelper = $encodingHelper; $this->encodingHelper = $encodingHelper;
$this->entityFactory = $entityFactory;
$this->globalFunctionsHelper = $globalFunctionsHelper;
} }
/** /**
@ -55,6 +78,7 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.rewind.php * @see http://php.net/manual/en/iterator.rewind.php
*/ */
#[\ReturnTypeWillChange]
public function rewind(): void public function rewind(): void
{ {
$this->rewindAndSkipBom(); $this->rewindAndSkipBom();
@ -70,9 +94,10 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.valid.php * @see http://php.net/manual/en/iterator.valid.php
*/ */
#[\ReturnTypeWillChange]
public function valid(): bool public function valid(): bool
{ {
return null !== $this->filePointer && !$this->hasReachedEndOfFile; return $this->filePointer && !$this->hasReachedEndOfFile;
} }
/** /**
@ -80,11 +105,12 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.next.php * @see http://php.net/manual/en/iterator.next.php
* *
* @throws EncodingConversionException If unable to convert data to UTF-8 * @throws \OpenSpout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
*/ */
#[\ReturnTypeWillChange]
public function next(): void public function next(): void
{ {
$this->hasReachedEndOfFile = feof($this->filePointer); $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if (!$this->hasReachedEndOfFile) { if (!$this->hasReachedEndOfFile) {
$this->readDataForNextRow(); $this->readDataForNextRow();
@ -96,6 +122,7 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.current.php * @see http://php.net/manual/en/iterator.current.php
*/ */
#[\ReturnTypeWillChange]
public function current(): ?Row public function current(): ?Row
{ {
return $this->rowBuffer; return $this->rowBuffer;
@ -106,27 +133,37 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.key.php * @see http://php.net/manual/en/iterator.key.php
*/ */
#[\ReturnTypeWillChange]
public function key(): int public function key(): int
{ {
return $this->numReadRows; return $this->numReadRows;
} }
/** /**
* This rewinds and skips the BOM if inserted at the beginning of the file * Cleans up what was created to iterate over the object.
* by moving the file pointer after it, so that it is not read.
*/ */
private function rewindAndSkipBom(): void #[\ReturnTypeWillChange]
public function end(): void
{ {
$byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->options->ENCODING); // do nothing
// sets the cursor after the BOM (0 means no BOM, so rewind it)
fseek($this->filePointer, $byteOffsetToSkipBom);
} }
/** /**
* @throws EncodingConversionException If unable to convert data to UTF-8 * This rewinds and skips the BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*/ */
private function readDataForNextRow(): void protected function rewindAndSkipBom()
{
$byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding);
// sets the cursor after the BOM (0 means no BOM, so rewind it)
$this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom);
}
/**
* @throws \OpenSpout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
*/
protected function readDataForNextRow()
{ {
do { do {
$rowData = $this->getNextUTF8EncodedRow(); $rowData = $this->getNextUTF8EncodedRow();
@ -134,10 +171,8 @@ final class RowIterator implements RowIteratorInterface
if (false !== $rowData) { if (false !== $rowData) {
// array_map will replace NULL values by empty strings // array_map will replace NULL values by empty strings
$rowDataBufferAsArray = array_map('\strval', $rowData); $rowDataBufferAsArray = array_map(function ($value) { return (string) $value; }, $rowData);
$this->rowBuffer = new Row(array_map(static function ($cellValue) { $this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
return Cell::fromValue($cellValue);
}, $rowDataBufferAsArray), null);
++$this->numReadRows; ++$this->numReadRows;
} else { } else {
// If we reach this point, it means end of file was reached. // If we reach this point, it means end of file was reached.
@ -147,19 +182,20 @@ final class RowIterator implements RowIteratorInterface
} }
/** /**
* @param array<int, null|string>|bool $currentRowData * @param array|bool $currentRowData
* *
* @return bool Whether the data for the current row can be returned or if we need to keep reading * @return bool Whether the data for the current row can be returned or if we need to keep reading
*/ */
private function shouldReadNextRow($currentRowData): bool protected function shouldReadNextRow($currentRowData)
{ {
$hasSuccessfullyFetchedRowData = (false !== $currentRowData); $hasSuccessfullyFetchedRowData = (false !== $currentRowData);
$hasNowReachedEndOfFile = feof($this->filePointer); $hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
$isEmptyLine = $this->isEmptyLine($currentRowData); $isEmptyLine = $this->isEmptyLine($currentRowData);
return return
(!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) (!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile)
|| (!$this->options->SHOULD_PRESERVE_EMPTY_ROWS && $isEmptyLine); || (!$this->shouldPreserveEmptyRows && $isEmptyLine)
;
} }
/** /**
@ -167,25 +203,19 @@ final class RowIterator implements RowIteratorInterface
* As fgetcsv() does not manage correctly encoding for non UTF-8 data, * As fgetcsv() does not manage correctly encoding for non UTF-8 data,
* we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes). * we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes).
* *
* @return array<int, null|string>|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read * @throws \OpenSpout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* *
* @throws EncodingConversionException If unable to convert data to UTF-8 * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
*/ */
private function getNextUTF8EncodedRow(): array|false protected function getNextUTF8EncodedRow()
{ {
$encodedRowData = fgetcsv( $encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
$this->filePointer,
self::MAX_READ_BYTES_PER_LINE,
$this->options->FIELD_DELIMITER,
$this->options->FIELD_ENCLOSURE,
''
);
if (false === $encodedRowData) { if (false === $encodedRowData) {
return false; return false;
} }
foreach ($encodedRowData as $cellIndex => $cellValue) { foreach ($encodedRowData as $cellIndex => $cellValue) {
switch ($this->options->ENCODING) { switch ($this->encoding) {
case EncodingHelper::ENCODING_UTF16_LE: case EncodingHelper::ENCODING_UTF16_LE:
case EncodingHelper::ENCODING_UTF32_LE: case EncodingHelper::ENCODING_UTF32_LE:
// remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
@ -201,18 +231,18 @@ final class RowIterator implements RowIteratorInterface
break; break;
} }
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->options->ENCODING); $encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
} }
return $encodedRowData; return $encodedRowData;
} }
/** /**
* @param array<int, null|string>|bool $lineData Array containing the cells value for the line * @param array|bool $lineData Array containing the cells value for the line
* *
* @return bool Whether the given line is empty * @return bool Whether the given line is empty
*/ */
private function isEmptyLine($lineData): bool protected function isEmptyLine($lineData)
{ {
return \is_array($lineData) && 1 === \count($lineData) && null === $lineData[0]; return \is_array($lineData) && 1 === \count($lineData) && null === $lineData[0];
} }

View File

@ -1,18 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\CSV; namespace OpenSpout\Reader\CSV;
use OpenSpout\Reader\SheetInterface; use OpenSpout\Reader\SheetInterface;
/** class Sheet implements SheetInterface
* @implements SheetInterface<RowIterator>
*/
final readonly class Sheet implements SheetInterface
{ {
/** @var RowIterator To iterate over the CSV's rows */ /** @var \OpenSpout\Reader\CSV\RowIterator To iterate over the CSV's rows */
private RowIterator $rowIterator; protected $rowIterator;
/** /**
* @param RowIterator $rowIterator Corresponding row iterator * @param RowIterator $rowIterator Corresponding row iterator
@ -22,7 +17,10 @@ final readonly class Sheet implements SheetInterface
$this->rowIterator = $rowIterator; $this->rowIterator = $rowIterator;
} }
public function getRowIterator(): RowIterator /**
* @return \OpenSpout\Reader\CSV\RowIterator
*/
public function getRowIterator()
{ {
return $this->rowIterator; return $this->rowIterator;
} }
@ -30,7 +28,7 @@ final readonly class Sheet implements SheetInterface
/** /**
* @return int Index of the sheet * @return int Index of the sheet
*/ */
public function getIndex(): int public function getIndex()
{ {
return 0; return 0;
} }
@ -38,7 +36,7 @@ final readonly class Sheet implements SheetInterface
/** /**
* @return string Name of the sheet - empty string since CSV does not support that * @return string Name of the sheet - empty string since CSV does not support that
*/ */
public function getName(): string public function getName()
{ {
return ''; return '';
} }
@ -46,7 +44,15 @@ final readonly class Sheet implements SheetInterface
/** /**
* @return bool Always TRUE as there is only one sheet * @return bool Always TRUE as there is only one sheet
*/ */
public function isActive(): bool public function isActive()
{
return true;
}
/**
* @return bool Always TRUE as the only sheet is always visible
*/
public function isVisible()
{ {
return true; return true;
} }

View File

@ -1,26 +1,24 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\CSV; namespace OpenSpout\Reader\CSV;
use OpenSpout\Reader\SheetIteratorInterface; use OpenSpout\Reader\SheetIteratorInterface;
/** /**
* @implements SheetIteratorInterface<Sheet> * Iterate over CSV unique "sheet".
*/ */
final class SheetIterator implements SheetIteratorInterface class SheetIterator implements SheetIteratorInterface
{ {
/** @var Sheet The CSV unique "sheet" */ /** @var Sheet The CSV unique "sheet" */
private readonly Sheet $sheet; protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */ /** @var bool Whether the unique "sheet" has already been read */
private bool $hasReadUniqueSheet = false; protected $hasReadUniqueSheet = false;
/** /**
* @param Sheet $sheet Corresponding unique sheet * @param Sheet $sheet Corresponding unique sheet
*/ */
public function __construct(Sheet $sheet) public function __construct($sheet)
{ {
$this->sheet = $sheet; $this->sheet = $sheet;
} }
@ -30,6 +28,7 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @see http://php.net/manual/en/iterator.rewind.php * @see http://php.net/manual/en/iterator.rewind.php
*/ */
#[\ReturnTypeWillChange]
public function rewind(): void public function rewind(): void
{ {
$this->hasReadUniqueSheet = false; $this->hasReadUniqueSheet = false;
@ -40,6 +39,7 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @see http://php.net/manual/en/iterator.valid.php * @see http://php.net/manual/en/iterator.valid.php
*/ */
#[\ReturnTypeWillChange]
public function valid(): bool public function valid(): bool
{ {
return !$this->hasReadUniqueSheet; return !$this->hasReadUniqueSheet;
@ -50,6 +50,7 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @see http://php.net/manual/en/iterator.next.php * @see http://php.net/manual/en/iterator.next.php
*/ */
#[\ReturnTypeWillChange]
public function next(): void public function next(): void
{ {
$this->hasReadUniqueSheet = true; $this->hasReadUniqueSheet = true;
@ -60,6 +61,7 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @see http://php.net/manual/en/iterator.current.php * @see http://php.net/manual/en/iterator.current.php
*/ */
#[\ReturnTypeWillChange]
public function current(): Sheet public function current(): Sheet
{ {
return $this->sheet; return $this->sheet;
@ -70,8 +72,18 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @see http://php.net/manual/en/iterator.key.php * @see http://php.net/manual/en/iterator.key.php
*/ */
#[\ReturnTypeWillChange]
public function key(): int public function key(): int
{ {
return 1; return 1;
} }
/**
* Cleans up what was created to iterate over the object.
*/
#[\ReturnTypeWillChange]
public function end(): void
{
// do nothing
}
} }

View File

@ -0,0 +1,26 @@
<?php
namespace OpenSpout\Reader\Common\Creator;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
/**
* Interface EntityFactoryInterface.
*/
interface InternalEntityFactoryInterface
{
/**
* @param Cell[] $cells
*
* @return Row
*/
public function createRow(array $cells = []);
/**
* @param mixed $cellValue
*
* @return Cell
*/
public function createCell($cellValue);
}

View File

@ -0,0 +1,72 @@
<?php
namespace OpenSpout\Reader\Common\Creator;
use OpenSpout\Common\Exception\UnsupportedTypeException;
use OpenSpout\Common\Type;
use OpenSpout\Reader\ReaderInterface;
/**
* Factory to create external entities.
*/
class ReaderEntityFactory
{
/**
* Creates a reader by file extension.
*
* @param string $path The path to the spreadsheet file. Supported extensions are .csv, .ods and .xlsx
*
* @throws \OpenSpout\Common\Exception\UnsupportedTypeException
*
* @return ReaderInterface
*/
public static function createReaderFromFile(string $path)
{
return ReaderFactory::createFromFile($path);
}
/**
* This creates an instance of a CSV reader.
*
* @return \OpenSpout\Reader\CSV\Reader
*/
public static function createCSVReader()
{
try {
return ReaderFactory::createFromType(Type::CSV);
} catch (UnsupportedTypeException $e) {
// should never happen
return null;
}
}
/**
* This creates an instance of a XLSX reader.
*
* @return \OpenSpout\Reader\XLSX\Reader
*/
public static function createXLSXReader()
{
try {
return ReaderFactory::createFromType(Type::XLSX);
} catch (UnsupportedTypeException $e) {
// should never happen
return null;
}
}
/**
* This creates an instance of a ODS reader.
*
* @return \OpenSpout\Reader\ODS\Reader
*/
public static function createODSReader()
{
try {
return ReaderFactory::createFromType(Type::ODS);
} catch (UnsupportedTypeException $e) {
// should never happen
return null;
}
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace OpenSpout\Reader\Common\Creator;
use OpenSpout\Common\Creator\HelperFactory;
use OpenSpout\Common\Exception\UnsupportedTypeException;
use OpenSpout\Common\Type;
use OpenSpout\Reader\CSV\Creator\InternalEntityFactory as CSVInternalEntityFactory;
use OpenSpout\Reader\CSV\Manager\OptionsManager as CSVOptionsManager;
use OpenSpout\Reader\CSV\Reader as CSVReader;
use OpenSpout\Reader\ODS\Creator\HelperFactory as ODSHelperFactory;
use OpenSpout\Reader\ODS\Creator\InternalEntityFactory as ODSInternalEntityFactory;
use OpenSpout\Reader\ODS\Creator\ManagerFactory as ODSManagerFactory;
use OpenSpout\Reader\ODS\Manager\OptionsManager as ODSOptionsManager;
use OpenSpout\Reader\ODS\Reader as ODSReader;
use OpenSpout\Reader\ReaderInterface;
use OpenSpout\Reader\XLSX\Creator\HelperFactory as XLSXHelperFactory;
use OpenSpout\Reader\XLSX\Creator\InternalEntityFactory as XLSXInternalEntityFactory;
use OpenSpout\Reader\XLSX\Creator\ManagerFactory as XLSXManagerFactory;
use OpenSpout\Reader\XLSX\Manager\OptionsManager as XLSXOptionsManager;
use OpenSpout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory;
use OpenSpout\Reader\XLSX\Reader as XLSXReader;
/**
* This factory is used to create readers, based on the type of the file to be read.
* It supports CSV, XLSX and ODS formats.
*/
class ReaderFactory
{
/**
* Creates a reader by file extension.
*
* @param string $path The path to the spreadsheet file. Supported extensions are .csv,.ods and .xlsx
*
* @throws \OpenSpout\Common\Exception\UnsupportedTypeException
*
* @return ReaderInterface
*/
public static function createFromFile(string $path)
{
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
return self::createFromType($extension);
}
/**
* This creates an instance of the appropriate reader, given the type of the file to be read.
*
* @param string $readerType Type of the reader to instantiate
*
* @throws \OpenSpout\Common\Exception\UnsupportedTypeException
*
* @return ReaderInterface
*/
public static function createFromType($readerType)
{
switch ($readerType) {
case Type::CSV: return self::createCSVReader();
case Type::XLSX: return self::createXLSXReader();
case Type::ODS: return self::createODSReader();
default:
throw new UnsupportedTypeException('No readers supporting the given type: '.$readerType);
}
}
/**
* @return CSVReader
*/
private static function createCSVReader()
{
$optionsManager = new CSVOptionsManager();
$helperFactory = new HelperFactory();
$entityFactory = new CSVInternalEntityFactory($helperFactory);
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
return new CSVReader($optionsManager, $globalFunctionsHelper, $entityFactory);
}
/**
* @return XLSXReader
*/
private static function createXLSXReader()
{
$optionsManager = new XLSXOptionsManager();
$helperFactory = new XLSXHelperFactory();
$managerFactory = new XLSXManagerFactory($helperFactory, new CachingStrategyFactory());
$entityFactory = new XLSXInternalEntityFactory($managerFactory, $helperFactory);
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
return new XLSXReader($optionsManager, $globalFunctionsHelper, $entityFactory, $managerFactory);
}
/**
* @return ODSReader
*/
private static function createODSReader()
{
$optionsManager = new ODSOptionsManager();
$helperFactory = new ODSHelperFactory();
$managerFactory = new ODSManagerFactory();
$entityFactory = new ODSInternalEntityFactory($helperFactory, $managerFactory);
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
return new ODSReader($optionsManager, $globalFunctionsHelper, $entityFactory);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace OpenSpout\Reader\Common\Entity;
/**
* Readers' options holder.
*/
abstract class Options
{
// Common options
public const SHOULD_FORMAT_DATES = 'shouldFormatDates';
public const SHOULD_PRESERVE_EMPTY_ROWS = 'shouldPreserveEmptyRows';
// CSV specific options
public const FIELD_DELIMITER = 'fieldDelimiter';
public const FIELD_ENCLOSURE = 'fieldEnclosure';
public const ENCODING = 'encoding';
// XLSX specific options
public const TEMP_FOLDER = 'tempFolder';
public const SHOULD_USE_1904_DATES = 'shouldUse1904Dates';
}

View File

@ -1,26 +1,51 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\Common\Manager; namespace OpenSpout\Reader\Common\Manager;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row; use OpenSpout\Common\Entity\Row;
use OpenSpout\Reader\Common\Creator\InternalEntityFactoryInterface;
class RowManager
{
/** @var InternalEntityFactoryInterface Factory to create entities */
private $entityFactory;
/** /**
* @internal * @param InternalEntityFactoryInterface $entityFactory Factory to create entities
*/ */
final class RowManager public function __construct(InternalEntityFactoryInterface $entityFactory)
{ {
$this->entityFactory = $entityFactory;
}
/**
* Detect whether a row is considered empty.
* An empty row has all of its cells empty.
*
* @return bool
*/
public function isEmpty(Row $row)
{
foreach ($row->getCells() as $cell) {
if (!$cell->isEmpty()) {
return false;
}
}
return true;
}
/** /**
* Fills the missing indexes of a row with empty cells. * Fills the missing indexes of a row with empty cells.
*
* @return Row
*/ */
public function fillMissingIndexesWithEmptyCells(Row $row): void public function fillMissingIndexesWithEmptyCells(Row $row)
{ {
$numCells = $row->getNumCells(); $numCells = $row->getNumCells();
if (0 === $numCells) { if (0 === $numCells) {
return; return $row;
} }
$rowCells = $row->getCells(); $rowCells = $row->getCells();
@ -37,7 +62,7 @@ final class RowManager
for ($cellIndex = 0; $cellIndex < $maxCellIndex; ++$cellIndex) { for ($cellIndex = 0; $cellIndex < $maxCellIndex; ++$cellIndex) {
if (!isset($rowCells[$cellIndex])) { if (!isset($rowCells[$cellIndex])) {
$row->setCellAtIndex(Cell::fromValue(''), $cellIndex); $row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
$needsSorting = true; $needsSorting = true;
} }
} }
@ -47,5 +72,7 @@ final class RowManager
ksort($rowCells); ksort($rowCells);
$row->setCells($rowCells); $row->setCells($rowCells);
} }
return $row;
} }
} }

View File

@ -1,17 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\Common; namespace OpenSpout\Reader\Common;
use OpenSpout\Reader\Exception\XMLProcessingException;
use OpenSpout\Reader\Wrapper\XMLReader; use OpenSpout\Reader\Wrapper\XMLReader;
use ReflectionMethod;
/** /**
* @internal * Helps process XML files.
*/ */
final class XMLProcessor class XMLProcessor
{ {
// Node types // Node types
public const NODE_TYPE_START = XMLReader::ELEMENT; public const NODE_TYPE_START = XMLReader::ELEMENT;
@ -25,16 +21,16 @@ final class XMLProcessor
public const PROCESSING_CONTINUE = 1; public const PROCESSING_CONTINUE = 1;
public const PROCESSING_STOP = 2; public const PROCESSING_STOP = 2;
/** @var XMLReader The XMLReader object that will help read sheet's XML data */ /** @var \OpenSpout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
private readonly XMLReader $xmlReader; protected $xmlReader;
/** @var array<string, array{reflectionMethod: ReflectionMethod, reflectionObject: object}> Registered callbacks */ /** @var array Registered callbacks */
private array $callbacks = []; private $callbacks = [];
/** /**
* @param XMLReader $xmlReader XMLReader object * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object
*/ */
public function __construct(XMLReader $xmlReader) public function __construct($xmlReader)
{ {
$this->xmlReader = $xmlReader; $this->xmlReader = $xmlReader;
} }
@ -43,8 +39,10 @@ final class XMLProcessor
* @param string $nodeName A callback may be triggered when a node with this name is read * @param string $nodeName A callback may be triggered when a node with this name is read
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* @param callable $callback Callback to execute when the read node has the given name and type * @param callable $callback Callback to execute when the read node has the given name and type
*
* @return XMLProcessor
*/ */
public function registerCallback(string $nodeName, int $nodeType, $callback): self public function registerCallback($nodeName, $nodeType, $callback)
{ {
$callbackKey = $this->getCallbackKey($nodeName, $nodeType); $callbackKey = $this->getCallbackKey($nodeName, $nodeType);
$this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback); $this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback);
@ -56,9 +54,9 @@ final class XMLProcessor
* Resumes the reading of the XML file where it was left off. * Resumes the reading of the XML file where it was left off.
* Stops whenever a callback indicates that reading should stop or at the end of the file. * Stops whenever a callback indicates that reading should stop or at the end of the file.
* *
* @throws XMLProcessingException * @throws \OpenSpout\Reader\Exception\XMLProcessingException
*/ */
public function readUntilStopped(): void public function readUntilStopped()
{ {
while ($this->xmlReader->read()) { while ($this->xmlReader->read()) {
$nodeType = $this->xmlReader->nodeType; $nodeType = $this->xmlReader->nodeType;
@ -84,7 +82,7 @@ final class XMLProcessor
* *
* @return string Key used to store the associated callback * @return string Key used to store the associated callback
*/ */
private function getCallbackKey(string $nodeName, int $nodeType): string private function getCallbackKey($nodeName, $nodeType)
{ {
return "{$nodeName}{$nodeType}"; return "{$nodeName}{$nodeType}";
} }
@ -97,13 +95,13 @@ final class XMLProcessor
* *
* @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME] * @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME]
* *
* @return array{reflectionMethod: ReflectionMethod, reflectionObject: object} Associative array containing the elements needed to invoke the callback using Reflection * @return array Associative array containing the elements needed to invoke the callback using Reflection
*/ */
private function getInvokableCallbackData($callback): array private function getInvokableCallbackData($callback)
{ {
$callbackObject = $callback[0]; $callbackObject = $callback[0];
$callbackMethodName = $callback[1]; $callbackMethodName = $callback[1];
$reflectionMethod = new ReflectionMethod($callbackObject, $callbackMethodName); $reflectionMethod = new \ReflectionMethod(\get_class($callbackObject), $callbackMethodName);
$reflectionMethod->setAccessible(true); $reflectionMethod->setAccessible(true);
return [ return [
@ -117,9 +115,9 @@ final class XMLProcessor
* @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed * @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* *
* @return null|array{reflectionMethod: ReflectionMethod, reflectionObject: object} Callback data to be used for execution when a node of the given name/type is read or NULL if none found * @return null|array Callback data to be used for execution when a node of the given name/type is read or NULL if none found
*/ */
private function getRegisteredCallbackData(string $nodeNamePossiblyWithPrefix, string $nodeNameWithoutPrefix, int $nodeType): ?array private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType)
{ {
// With prefixed nodes, we should match if (by order of preference): // With prefixed nodes, we should match if (by order of preference):
// 1. the callback was registered with the prefixed node name (e.g. "x:worksheet") // 1. the callback was registered with the prefixed node name (e.g. "x:worksheet")
@ -138,12 +136,12 @@ final class XMLProcessor
} }
/** /**
* @param array{reflectionMethod: ReflectionMethod, reflectionObject: object} $callbackData Associative array containing data to invoke the callback using Reflection * @param array $callbackData Associative array containing data to invoke the callback using Reflection
* @param XMLReader[] $args Arguments to pass to the callback * @param array $args Arguments to pass to the callback
* *
* @return int Callback response * @return int Callback response
*/ */
private function invokeCallback(array $callbackData, array $args): int private function invokeCallback($callbackData, $args)
{ {
$reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD]; $reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
$callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT]; $callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT];

View File

@ -0,0 +1,30 @@
<?php
namespace OpenSpout\Reader\Exception;
use Throwable;
class InvalidValueException extends ReaderException
{
/** @var mixed */
private $invalidValue;
/**
* @param mixed $invalidValue
* @param string $message
* @param int $code
*/
public function __construct($invalidValue, $message = '', $code = 0, Throwable $previous = null)
{
$this->invalidValue = $invalidValue;
parent::__construct($message, $code, $previous);
}
/**
* @return mixed
*/
public function getInvalidValue()
{
return $this->invalidValue;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Reader\Exception;
class IteratorNotRewindableException extends ReaderException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Reader\Exception;
class NoSheetsFoundException extends ReaderException
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace OpenSpout\Reader\Exception;
use OpenSpout\Common\Exception\SpoutException;
abstract class ReaderException extends SpoutException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Reader\Exception;
class ReaderNotOpenedException extends ReaderException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Reader\Exception;
class SharedStringNotFoundException extends ReaderException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace OpenSpout\Reader\Exception;
class XMLProcessingException extends ReaderException
{
}

View File

@ -0,0 +1,14 @@
<?php
namespace OpenSpout\Reader;
/**
* Interface IteratorInterface.
*/
interface IteratorInterface extends \Iterator
{
/**
* Cleans up what was created to iterate over the object.
*/
public function end();
}

View File

@ -0,0 +1,43 @@
<?php
namespace OpenSpout\Reader\ODS\Creator;
use OpenSpout\Reader\ODS\Helper\CellValueFormatter;
use OpenSpout\Reader\ODS\Helper\SettingsHelper;
/**
* Factory to create helpers.
*/
class HelperFactory extends \OpenSpout\Common\Creator\HelperFactory
{
/**
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
*
* @return CellValueFormatter
*/
public function createCellValueFormatter($shouldFormatDates)
{
$escaper = $this->createStringsEscaper();
return new CellValueFormatter($shouldFormatDates, $escaper);
}
/**
* @param InternalEntityFactory $entityFactory
*
* @return SettingsHelper
*/
public function createSettingsHelper($entityFactory)
{
return new SettingsHelper($entityFactory);
}
/**
* @return \OpenSpout\Common\Helper\Escaper\ODS
*/
public function createStringsEscaper()
{
// @noinspection PhpUnnecessaryFullyQualifiedNameInspection
return new \OpenSpout\Common\Helper\Escaper\ODS();
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace OpenSpout\Reader\ODS\Creator;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Reader\Common\Creator\InternalEntityFactoryInterface;
use OpenSpout\Reader\Common\Entity\Options;
use OpenSpout\Reader\Common\XMLProcessor;
use OpenSpout\Reader\ODS\RowIterator;
use OpenSpout\Reader\ODS\Sheet;
use OpenSpout\Reader\ODS\SheetIterator;
use OpenSpout\Reader\Wrapper\XMLReader;
/**
* Factory to create entities.
*/
class InternalEntityFactory implements InternalEntityFactoryInterface
{
/** @var HelperFactory */
private $helperFactory;
/** @var ManagerFactory */
private $managerFactory;
public function __construct(HelperFactory $helperFactory, ManagerFactory $managerFactory)
{
$this->helperFactory = $helperFactory;
$this->managerFactory = $managerFactory;
}
/**
* @param string $filePath Path of the file to be read
* @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
*
* @return SheetIterator
*/
public function createSheetIterator($filePath, $optionsManager)
{
$escaper = $this->helperFactory->createStringsEscaper();
$settingsHelper = $this->helperFactory->createSettingsHelper($this);
return new SheetIterator($filePath, $optionsManager, $escaper, $settingsHelper, $this);
}
/**
* @param XMLReader $xmlReader XML Reader
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
* @param string $sheetName Name of the sheet
* @param bool $isSheetActive Whether the sheet was defined as active
* @param bool $isSheetVisible Whether the sheet is visible
* @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
*
* @return Sheet
*/
public function createSheet($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible, $optionsManager)
{
$rowIterator = $this->createRowIterator($xmlReader, $optionsManager);
return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible);
}
/**
* @param Cell[] $cells
*
* @return Row
*/
public function createRow(array $cells = [])
{
return new Row($cells, null);
}
/**
* @param mixed $cellValue
*
* @return Cell
*/
public function createCell($cellValue)
{
return new Cell($cellValue);
}
/**
* @return XMLReader
*/
public function createXMLReader()
{
return new XMLReader();
}
/**
* @return \ZipArchive
*/
public function createZipArchive()
{
return new \ZipArchive();
}
/**
* @param XMLReader $xmlReader XML Reader
* @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
*
* @return RowIterator
*/
private function createRowIterator($xmlReader, $optionsManager)
{
$shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES);
$cellValueFormatter = $this->helperFactory->createCellValueFormatter($shouldFormatDates);
$xmlProcessor = $this->createXMLProcessor($xmlReader);
$rowManager = $this->managerFactory->createRowManager($this);
return new RowIterator($xmlReader, $optionsManager, $cellValueFormatter, $xmlProcessor, $rowManager, $this);
}
/**
* @param XMLReader $xmlReader
*
* @return XMLProcessor
*/
private function createXMLProcessor($xmlReader)
{
return new XMLProcessor($xmlReader);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace OpenSpout\Reader\ODS\Creator;
use OpenSpout\Reader\Common\Manager\RowManager;
/**
* Factory to create managers.
*/
class ManagerFactory
{
/**
* @param InternalEntityFactory $entityFactory Factory to create entities
*
* @return RowManager
*/
public function createRowManager($entityFactory)
{
return new RowManager($entityFactory);
}
}

View File

@ -1,26 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\ODS\Helper; namespace OpenSpout\Reader\ODS\Helper;
use DateInterval;
use DateTimeImmutable;
use DOMElement;
use DOMNode;
use DOMText;
use Exception;
use OpenSpout\Common\Helper\Escaper\ODS;
use OpenSpout\Reader\Exception\InvalidValueException; use OpenSpout\Reader\Exception\InvalidValueException;
/** /**
* @internal * This class provides helper functions to format cell values.
*/ */
final readonly class CellValueFormatter class CellValueFormatter
{ {
/** /** Definition of all possible cell types */
* Definition of all possible cell types.
*/
public const CELL_TYPE_STRING = 'string'; public const CELL_TYPE_STRING = 'string';
public const CELL_TYPE_FLOAT = 'float'; public const CELL_TYPE_FLOAT = 'float';
public const CELL_TYPE_BOOLEAN = 'boolean'; public const CELL_TYPE_BOOLEAN = 'boolean';
@ -30,9 +19,7 @@ final readonly class CellValueFormatter
public const CELL_TYPE_PERCENTAGE = 'percentage'; public const CELL_TYPE_PERCENTAGE = 'percentage';
public const CELL_TYPE_VOID = 'void'; public const CELL_TYPE_VOID = 'void';
/** /** Definition of XML nodes names used to parse data */
* Definition of XML nodes names used to parse data.
*/
public const XML_NODE_P = 'p'; public const XML_NODE_P = 'p';
public const XML_NODE_TEXT_A = 'text:a'; public const XML_NODE_TEXT_A = 'text:a';
public const XML_NODE_TEXT_SPAN = 'text:span'; public const XML_NODE_TEXT_SPAN = 'text:span';
@ -40,9 +27,7 @@ final readonly class CellValueFormatter
public const XML_NODE_TEXT_TAB = 'text:tab'; public const XML_NODE_TEXT_TAB = 'text:tab';
public const XML_NODE_TEXT_LINE_BREAK = 'text:line-break'; public const XML_NODE_TEXT_LINE_BREAK = 'text:line-break';
/** /** Definition of XML attributes used to parse data */
* Definition of XML attributes used to parse data.
*/
public const XML_ATTRIBUTE_TYPE = 'office:value-type'; public const XML_ATTRIBUTE_TYPE = 'office:value-type';
public const XML_ATTRIBUTE_VALUE = 'office:value'; public const XML_ATTRIBUTE_VALUE = 'office:value';
public const XML_ATTRIBUTE_BOOLEAN_VALUE = 'office:boolean-value'; public const XML_ATTRIBUTE_BOOLEAN_VALUE = 'office:boolean-value';
@ -51,26 +36,24 @@ final readonly class CellValueFormatter
public const XML_ATTRIBUTE_CURRENCY = 'office:currency'; public const XML_ATTRIBUTE_CURRENCY = 'office:currency';
public const XML_ATTRIBUTE_C = 'text:c'; public const XML_ATTRIBUTE_C = 'text:c';
/** /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
* List of XML nodes representing whitespaces and their corresponding value. protected $shouldFormatDates;
*/
private const WHITESPACE_XML_NODES = [ /** @var \OpenSpout\Common\Helper\Escaper\ODS Used to unescape XML data */
protected $escaper;
/** @var array List of XML nodes representing whitespaces and their corresponding value */
private static $WHITESPACE_XML_NODES = [
self::XML_NODE_TEXT_S => ' ', self::XML_NODE_TEXT_S => ' ',
self::XML_NODE_TEXT_TAB => "\t", self::XML_NODE_TEXT_TAB => "\t",
self::XML_NODE_TEXT_LINE_BREAK => "\n", self::XML_NODE_TEXT_LINE_BREAK => "\n",
]; ];
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
private bool $shouldFormatDates;
/** @var ODS Used to unescape XML data */
private ODS $escaper;
/** /**
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @param ODS $escaper Used to unescape XML data * @param \OpenSpout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data
*/ */
public function __construct(bool $shouldFormatDates, ODS $escaper) public function __construct($shouldFormatDates, $escaper)
{ {
$this->shouldFormatDates = $shouldFormatDates; $this->shouldFormatDates = $shouldFormatDates;
$this->escaper = $escaper; $this->escaper = $escaper;
@ -81,32 +64,52 @@ final readonly class CellValueFormatter
* *
* @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13 * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
* *
* @return bool|DateInterval|DateTimeImmutable|float|int|string The value associated with the cell, empty string if cell's type is void/undefined * @param \DOMElement $node
* *
* @throws InvalidValueException If the node value is not valid * @throws InvalidValueException If the node value is not valid
*
* @return bool|\DateInterval|\DateTime|float|int|string The value associated with the cell, empty string if cell's type is void/undefined
*/ */
public function extractAndFormatNodeValue(DOMElement $node): bool|DateInterval|DateTimeImmutable|float|int|string public function extractAndFormatNodeValue($node)
{ {
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE); $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE);
return match ($cellType) { switch ($cellType) {
self::CELL_TYPE_STRING => $this->formatStringCellValue($node), case self::CELL_TYPE_STRING:
self::CELL_TYPE_FLOAT => $this->formatFloatCellValue($node), return $this->formatStringCellValue($node);
self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($node),
self::CELL_TYPE_DATE => $this->formatDateCellValue($node), case self::CELL_TYPE_FLOAT:
self::CELL_TYPE_TIME => $this->formatTimeCellValue($node), return $this->formatFloatCellValue($node);
self::CELL_TYPE_CURRENCY => $this->formatCurrencyCellValue($node),
self::CELL_TYPE_PERCENTAGE => $this->formatPercentageCellValue($node), case self::CELL_TYPE_BOOLEAN:
default => '', return $this->formatBooleanCellValue($node);
};
case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($node);
case self::CELL_TYPE_TIME:
return $this->formatTimeCellValue($node);
case self::CELL_TYPE_CURRENCY:
return $this->formatCurrencyCellValue($node);
case self::CELL_TYPE_PERCENTAGE:
return $this->formatPercentageCellValue($node);
case self::CELL_TYPE_VOID:
default:
return '';
}
} }
/** /**
* Returns the cell String value. * Returns the cell String value.
* *
* @param \DOMElement $node
*
* @return string The value associated with the cell * @return string The value associated with the cell
*/ */
private function formatStringCellValue(DOMElement $node): string protected function formatStringCellValue($node)
{ {
$pNodeValues = []; $pNodeValues = [];
$pNodes = $node->getElementsByTagName(self::XML_NODE_P); $pNodes = $node->getElementsByTagName(self::XML_NODE_P);
@ -123,9 +126,11 @@ final readonly class CellValueFormatter
/** /**
* Returns the cell Numeric value from the given node. * Returns the cell Numeric value from the given node.
* *
* @param \DOMElement $node
*
* @return float|int The value associated with the cell * @return float|int The value associated with the cell
*/ */
private function formatFloatCellValue(DOMElement $node): float|int protected function formatFloatCellValue($node)
{ {
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
@ -138,19 +143,27 @@ final readonly class CellValueFormatter
/** /**
* Returns the cell Boolean value from the given node. * Returns the cell Boolean value from the given node.
* *
* @param \DOMElement $node
*
* @return bool The value associated with the cell * @return bool The value associated with the cell
*/ */
private function formatBooleanCellValue(DOMElement $node): bool protected function formatBooleanCellValue($node)
{ {
return (bool) $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE); $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE);
return (bool) $nodeValue;
} }
/** /**
* Returns the cell Date value from the given node. * Returns the cell Date value from the given node.
* *
* @param \DOMElement $node
*
* @throws InvalidValueException If the value is not a valid date * @throws InvalidValueException If the value is not a valid date
*
* @return \DateTime|string The value associated with the cell
*/ */
private function formatDateCellValue(DOMElement $node): DateTimeImmutable|string protected function formatDateCellValue($node)
{ {
// The XML node looks like this: // The XML node looks like this:
// <table:table-cell calcext:value-type="date" office:date-value="2016-05-19T16:39:00" office:value-type="date"> // <table:table-cell calcext:value-type="date" office:date-value="2016-05-19T16:39:00" office:value-type="date">
@ -166,9 +179,9 @@ final readonly class CellValueFormatter
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
try { try {
$cellValue = new DateTimeImmutable($nodeValue); $cellValue = new \DateTime($nodeValue);
} catch (Exception $previous) { } catch (\Exception $e) {
throw new InvalidValueException($nodeValue, '', 0, $previous); throw new InvalidValueException($nodeValue);
} }
} }
@ -178,11 +191,13 @@ final readonly class CellValueFormatter
/** /**
* Returns the cell Time value from the given node. * Returns the cell Time value from the given node.
* *
* @return DateInterval|string The value associated with the cell * @param \DOMElement $node
* *
* @throws InvalidValueException If the value is not a valid time * @throws InvalidValueException If the value is not a valid time
*
* @return \DateInterval|string The value associated with the cell
*/ */
private function formatTimeCellValue(DOMElement $node): DateInterval|string protected function formatTimeCellValue($node)
{ {
// The XML node looks like this: // The XML node looks like this:
// <table:table-cell calcext:value-type="time" office:time-value="PT13H24M00S" office:value-type="time"> // <table:table-cell calcext:value-type="time" office:time-value="PT13H24M00S" office:value-type="time">
@ -198,9 +213,9 @@ final readonly class CellValueFormatter
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
try { try {
$cellValue = new DateInterval($nodeValue); $cellValue = new \DateInterval($nodeValue);
} catch (Exception $previous) { } catch (\Exception $e) {
throw new InvalidValueException($nodeValue, '', 0, $previous); throw new InvalidValueException($nodeValue);
} }
} }
@ -210,9 +225,11 @@ final readonly class CellValueFormatter
/** /**
* Returns the cell Currency value from the given node. * Returns the cell Currency value from the given node.
* *
* @param \DOMElement $node
*
* @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR") * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR")
*/ */
private function formatCurrencyCellValue(DOMElement $node): string protected function formatCurrencyCellValue($node)
{ {
$value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); $value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
$currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY); $currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY);
@ -223,22 +240,29 @@ final readonly class CellValueFormatter
/** /**
* Returns the cell Percentage value from the given node. * Returns the cell Percentage value from the given node.
* *
* @param \DOMElement $node
*
* @return float|int The value associated with the cell * @return float|int The value associated with the cell
*/ */
private function formatPercentageCellValue(DOMElement $node): float|int protected function formatPercentageCellValue($node)
{ {
// percentages are formatted like floats // percentages are formatted like floats
return $this->formatFloatCellValue($node); return $this->formatFloatCellValue($node);
} }
private function extractTextValueFromNode(DOMNode $pNode): string /**
* @param \DOMNode $pNode
*
* @return string
*/
private function extractTextValueFromNode($pNode)
{ {
$textValue = ''; $textValue = '';
foreach ($pNode->childNodes as $childNode) { foreach ($pNode->childNodes as $childNode) {
if ($childNode instanceof DOMText) { if ($childNode instanceof \DOMText) {
$textValue .= $childNode->nodeValue; $textValue .= $childNode->nodeValue;
} elseif ($this->isWhitespaceNode($childNode->nodeName) && $childNode instanceof DOMElement) { } elseif ($this->isWhitespaceNode($childNode->nodeName)) {
$textValue .= $this->transformWhitespaceNode($childNode); $textValue .= $this->transformWhitespaceNode($childNode);
} elseif (self::XML_NODE_TEXT_A === $childNode->nodeName || self::XML_NODE_TEXT_SPAN === $childNode->nodeName) { } elseif (self::XML_NODE_TEXT_A === $childNode->nodeName || self::XML_NODE_TEXT_SPAN === $childNode->nodeName) {
$textValue .= $this->extractTextValueFromNode($childNode); $textValue .= $this->extractTextValueFromNode($childNode);
@ -253,10 +277,14 @@ final readonly class CellValueFormatter
* - <text:s /> * - <text:s />
* - <text:tab /> * - <text:tab />
* - <text:line-break />. * - <text:line-break />.
*
* @param string $nodeName
*
* @return bool
*/ */
private function isWhitespaceNode(string $nodeName): bool private function isWhitespaceNode($nodeName)
{ {
return isset(self::WHITESPACE_XML_NODES[$nodeName]); return isset(self::$WHITESPACE_XML_NODES[$nodeName]);
} }
/** /**
@ -269,15 +297,15 @@ final readonly class CellValueFormatter
* *
* @see https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415200_253892949 * @see https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415200_253892949
* *
* @param DOMElement $node The XML node representing a whitespace * @param \DOMElement $node The XML node representing a whitespace
* *
* @return string The corresponding whitespace value * @return string The corresponding whitespace value
*/ */
private function transformWhitespaceNode(DOMElement $node): string private function transformWhitespaceNode($node)
{ {
$countAttribute = $node->getAttribute(self::XML_ATTRIBUTE_C); // only defined for "<text:s>" $countAttribute = $node->getAttribute(self::XML_ATTRIBUTE_C); // only defined for "<text:s>"
$numWhitespaces = '' !== $countAttribute ? (int) $countAttribute : 1; $numWhitespaces = (!empty($countAttribute)) ? (int) $countAttribute : 1;
return str_repeat(self::WHITESPACE_XML_NODES[$node->nodeName], $numWhitespaces); return str_repeat(self::$WHITESPACE_XML_NODES[$node->nodeName], $numWhitespaces);
} }
} }

View File

@ -1,34 +1,41 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\ODS\Helper; namespace OpenSpout\Reader\ODS\Helper;
use OpenSpout\Reader\Exception\XMLProcessingException; use OpenSpout\Reader\Exception\XMLProcessingException;
use OpenSpout\Reader\Wrapper\XMLReader; use OpenSpout\Reader\ODS\Creator\InternalEntityFactory;
/** /**
* @internal * This class provides helper functions to extract data from the "settings.xml" file.
*/ */
final class SettingsHelper class SettingsHelper
{ {
public const SETTINGS_XML_FILE_PATH = 'settings.xml'; public const SETTINGS_XML_FILE_PATH = 'settings.xml';
/** /** Definition of XML nodes name and attribute used to parse settings data */
* Definition of XML nodes name and attribute used to parse settings data.
*/
public const XML_NODE_CONFIG_ITEM = 'config:config-item'; public const XML_NODE_CONFIG_ITEM = 'config:config-item';
public const XML_ATTRIBUTE_CONFIG_NAME = 'config:name'; public const XML_ATTRIBUTE_CONFIG_NAME = 'config:name';
public const XML_ATTRIBUTE_VALUE_ACTIVE_TABLE = 'ActiveTable'; public const XML_ATTRIBUTE_VALUE_ACTIVE_TABLE = 'ActiveTable';
/** @var InternalEntityFactory Factory to create entities */
private $entityFactory;
/**
* @param InternalEntityFactory $entityFactory Factory to create entities
*/
public function __construct($entityFactory)
{
$this->entityFactory = $entityFactory;
}
/** /**
* @param string $filePath Path of the file to be read * @param string $filePath Path of the file to be read
* *
* @return null|string Name of the sheet that was defined as active or NULL if none found * @return null|string Name of the sheet that was defined as active or NULL if none found
*/ */
public function getActiveSheetName(string $filePath): ?string public function getActiveSheetName($filePath)
{ {
$xmlReader = new XMLReader(); $xmlReader = $this->entityFactory->createXMLReader();
if (false === $xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH)) { if (false === $xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH)) {
return null; return null;
} }
@ -43,7 +50,7 @@ final class SettingsHelper
break; break;
} }
} }
} catch (XMLProcessingException) { // @codeCoverageIgnore } catch (XMLProcessingException $exception) {
// do nothing // do nothing
} }

View File

@ -0,0 +1,32 @@
<?php
namespace OpenSpout\Reader\ODS\Manager;
use OpenSpout\Common\Manager\OptionsManagerAbstract;
use OpenSpout\Reader\Common\Entity\Options;
/**
* ODS Reader options manager.
*/
class OptionsManager extends OptionsManagerAbstract
{
/**
* {@inheritdoc}
*/
protected function getSupportedOptions()
{
return [
Options::SHOULD_FORMAT_DATES,
Options::SHOULD_PRESERVE_EMPTY_ROWS,
];
}
/**
* {@inheritdoc}
*/
protected function setDefaultOptions()
{
$this->setOption(Options::SHOULD_FORMAT_DATES, false);
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace OpenSpout\Reader\ODS;
use OpenSpout\Common\Exception\IOException;
use OpenSpout\Reader\ODS\Creator\InternalEntityFactory;
use OpenSpout\Reader\ReaderAbstract;
/**
* This class provides support to read data from a ODS file.
*/
class Reader extends ReaderAbstract
{
/** @var \ZipArchive */
protected $zip;
/** @var SheetIterator To iterator over the ODS sheets */
protected $sheetIterator;
/**
* Returns whether stream wrappers are supported.
*
* @return bool
*/
protected function doesSupportStreamWrapper()
{
return false;
}
/**
* Opens the file at the given file path to make it ready to be read.
*
* @param string $filePath Path of the file to be read
*
* @throws \OpenSpout\Common\Exception\IOException If the file at the given path or its content cannot be read
* @throws \OpenSpout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
*/
protected function openReader($filePath)
{
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->zip = $entityFactory->createZipArchive();
if (true === $this->zip->open($filePath)) {
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->sheetIterator = $entityFactory->createSheetIterator($filePath, $this->optionsManager);
} else {
throw new IOException("Could not open {$filePath} for reading.");
}
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
protected function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*/
protected function closeReader()
{
if (null !== $this->zip) {
$this->zip->close();
}
}
}

View File

@ -1,81 +1,103 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\ODS; namespace OpenSpout\Reader\ODS;
use DOMElement;
use OpenSpout\Common\Entity\Cell; use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row; use OpenSpout\Common\Entity\Row;
use OpenSpout\Common\Exception\IOException; use OpenSpout\Common\Exception\IOException;
use OpenSpout\Common\Manager\OptionsManagerInterface;
use OpenSpout\Reader\Common\Entity\Options;
use OpenSpout\Reader\Common\Manager\RowManager;
use OpenSpout\Reader\Common\XMLProcessor; use OpenSpout\Reader\Common\XMLProcessor;
use OpenSpout\Reader\Exception\InvalidValueException; use OpenSpout\Reader\Exception\InvalidValueException;
use OpenSpout\Reader\Exception\IteratorNotRewindableException; use OpenSpout\Reader\Exception\IteratorNotRewindableException;
use OpenSpout\Reader\Exception\SharedStringNotFoundException; use OpenSpout\Reader\Exception\XMLProcessingException;
use OpenSpout\Reader\IteratorInterface;
use OpenSpout\Reader\ODS\Creator\InternalEntityFactory;
use OpenSpout\Reader\ODS\Helper\CellValueFormatter; use OpenSpout\Reader\ODS\Helper\CellValueFormatter;
use OpenSpout\Reader\RowIteratorInterface;
use OpenSpout\Reader\Wrapper\XMLReader; use OpenSpout\Reader\Wrapper\XMLReader;
final class RowIterator implements RowIteratorInterface class RowIterator implements IteratorInterface
{ {
/** /** Definition of XML nodes names used to parse data */
* Definition of XML nodes names used to parse data.
*/
public const XML_NODE_TABLE = 'table:table'; public const XML_NODE_TABLE = 'table:table';
public const XML_NODE_ROW = 'table:table-row'; public const XML_NODE_ROW = 'table:table-row';
public const XML_NODE_CELL = 'table:table-cell'; public const XML_NODE_CELL = 'table:table-cell';
public const MAX_COLUMNS_EXCEL = 16384; public const MAX_COLUMNS_EXCEL = 16384;
/** /** Definition of XML attribute used to parse data */
* Definition of XML attribute used to parse data.
*/
public const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated'; public const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated';
public const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated'; public const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
private readonly Options $options; /** @var \OpenSpout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader;
/** @var XMLProcessor Helper Object to process XML nodes */ /** @var \OpenSpout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
private readonly XMLProcessor $xmlProcessor; protected $xmlProcessor;
/** @var CellValueFormatter Helper to format cell values */ /** @var bool Whether empty rows should be returned or skipped */
private readonly CellValueFormatter $cellValueFormatter; protected $shouldPreserveEmptyRows;
/** @var Helper\CellValueFormatter Helper to format cell values */
protected $cellValueFormatter;
/** @var RowManager Manages rows */
protected $rowManager;
/** @var InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var bool Whether the iterator has already been rewound once */ /** @var bool Whether the iterator has already been rewound once */
private bool $hasAlreadyBeenRewound = false; protected $hasAlreadyBeenRewound = false;
/** @var Row The currently processed row */ /** @var Row The currently processed row */
private Row $currentlyProcessedRow; protected $currentlyProcessedRow;
/** @var null|Row Buffer used to store the current row, while checking if there are more rows to read */ /** @var null|Row Buffer used to store the current row, while checking if there are more rows to read */
private ?Row $rowBuffer = null; protected $rowBuffer;
/** @var bool Indicates whether all rows have been read */ /** @var bool Indicates whether all rows have been read */
private bool $hasReachedEndOfFile = false; protected $hasReachedEndOfFile = false;
/** @var int Last row index processed (one-based) */ /** @var int Last row index processed (one-based) */
private int $lastRowIndexProcessed = 0; protected $lastRowIndexProcessed = 0;
/** @var int Row index to be processed next (one-based) */ /** @var int Row index to be processed next (one-based) */
private int $nextRowIndexToBeProcessed = 1; protected $nextRowIndexToBeProcessed = 1;
/** @var null|Cell Last processed cell (because when reading cell at column N+1, cell N is processed) */ /** @var null|Cell Last processed cell (because when reading cell at column N+1, cell N is processed) */
private ?Cell $lastProcessedCell = null; protected $lastProcessedCell;
/** @var int Number of times the last processed row should be repeated */ /** @var int Number of times the last processed row should be repeated */
private int $numRowsRepeated = 1; protected $numRowsRepeated = 1;
/** @var int Number of times the last cell value should be copied to the cells on its right */ /** @var int Number of times the last cell value should be copied to the cells on its right */
private int $numColumnsRepeated = 1; protected $numColumnsRepeated = 1;
/** @var bool Whether at least one cell has been read for the row currently being processed */ /** @var bool Whether at least one cell has been read for the row currently being processed */
private bool $hasAlreadyReadOneCellInCurrentRow = false; protected $hasAlreadyReadOneCellInCurrentRow = false;
/**
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
* @param OptionsManagerInterface $optionsManager Reader's options manager
* @param CellValueFormatter $cellValueFormatter Helper to format cell values
* @param XMLProcessor $xmlProcessor Helper to process XML files
* @param RowManager $rowManager Manages rows
* @param InternalEntityFactory $entityFactory Factory to create entities
*/
public function __construct( public function __construct(
Options $options, XMLReader $xmlReader,
OptionsManagerInterface $optionsManager,
CellValueFormatter $cellValueFormatter, CellValueFormatter $cellValueFormatter,
XMLProcessor $xmlProcessor XMLProcessor $xmlProcessor,
RowManager $rowManager,
InternalEntityFactory $entityFactory
) { ) {
$this->xmlReader = $xmlReader;
$this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
$this->cellValueFormatter = $cellValueFormatter; $this->cellValueFormatter = $cellValueFormatter;
$this->entityFactory = $entityFactory;
$this->rowManager = $rowManager;
// Register all callbacks to process different nodes when reading the XML file // Register all callbacks to process different nodes when reading the XML file
$this->xmlProcessor = $xmlProcessor; $this->xmlProcessor = $xmlProcessor;
@ -83,7 +105,6 @@ final class RowIterator implements RowIteratorInterface
$this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']);
$this->options = $options;
} }
/** /**
@ -92,8 +113,9 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.rewind.php * @see http://php.net/manual/en/iterator.rewind.php
* *
* @throws IteratorNotRewindableException If the iterator is rewound more than once * @throws \OpenSpout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
*/ */
#[\ReturnTypeWillChange]
public function rewind(): void public function rewind(): void
{ {
// Because sheet and row data is located in the file, we can't rewind both the // Because sheet and row data is located in the file, we can't rewind both the
@ -117,6 +139,7 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.valid.php * @see http://php.net/manual/en/iterator.valid.php
*/ */
#[\ReturnTypeWillChange]
public function valid(): bool public function valid(): bool
{ {
return !$this->hasReachedEndOfFile; return !$this->hasReachedEndOfFile;
@ -127,9 +150,10 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.next.php * @see http://php.net/manual/en/iterator.next.php
* *
* @throws SharedStringNotFoundException If a shared string was not found * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws IOException If unable to read the sheet data XML * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML
*/ */
#[\ReturnTypeWillChange]
public function next(): void public function next(): void
{ {
if ($this->doesNeedDataForNextRowToBeProcessed()) { if ($this->doesNeedDataForNextRowToBeProcessed()) {
@ -144,6 +168,7 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.current.php * @see http://php.net/manual/en/iterator.current.php
*/ */
#[\ReturnTypeWillChange]
public function current(): Row public function current(): Row
{ {
return $this->rowBuffer; return $this->rowBuffer;
@ -154,11 +179,21 @@ final class RowIterator implements RowIteratorInterface
* *
* @see http://php.net/manual/en/iterator.key.php * @see http://php.net/manual/en/iterator.key.php
*/ */
#[\ReturnTypeWillChange]
public function key(): int public function key(): int
{ {
return $this->lastRowIndexProcessed; return $this->lastRowIndexProcessed;
} }
/**
* Cleans up what was created to iterate over the object.
*/
#[\ReturnTypeWillChange]
public function end(): void
{
$this->xmlReader->close();
}
/** /**
* Returns whether we need data for the next row to be processed. * Returns whether we need data for the next row to be processed.
* We DO need to read data if: * We DO need to read data if:
@ -168,34 +203,39 @@ final class RowIterator implements RowIteratorInterface
* *
* @return bool whether we need data for the next row to be processed * @return bool whether we need data for the next row to be processed
*/ */
private function doesNeedDataForNextRowToBeProcessed(): bool protected function doesNeedDataForNextRowToBeProcessed()
{ {
$hasReadAtLeastOneRow = (0 !== $this->lastRowIndexProcessed); $hasReadAtLeastOneRow = (0 !== $this->lastRowIndexProcessed);
return return
!$hasReadAtLeastOneRow !$hasReadAtLeastOneRow
|| $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1; || $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1
;
} }
/** /**
* @throws SharedStringNotFoundException If a shared string was not found * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws IOException If unable to read the sheet data XML * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML
*/ */
private function readDataForNextRow(): void protected function readDataForNextRow()
{ {
$this->currentlyProcessedRow = new Row([], null); $this->currentlyProcessedRow = $this->entityFactory->createRow();
try {
$this->xmlProcessor->readUntilStopped(); $this->xmlProcessor->readUntilStopped();
} catch (XMLProcessingException $exception) {
throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
}
$this->rowBuffer = $this->currentlyProcessedRow; $this->rowBuffer = $this->currentlyProcessedRow;
} }
/** /**
* @param XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
* *
* @return int A return code that indicates what action should the processor take next * @return int A return code that indicates what action should the processor take next
*/ */
private function processRowStartingNode(XMLReader $xmlReader): int protected function processRowStartingNode($xmlReader)
{ {
// Reset data from current row // Reset data from current row
$this->hasAlreadyReadOneCellInCurrentRow = false; $this->hasAlreadyReadOneCellInCurrentRow = false;
@ -207,16 +247,16 @@ final class RowIterator implements RowIteratorInterface
} }
/** /**
* @param XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
* *
* @return int A return code that indicates what action should the processor take next * @return int A return code that indicates what action should the processor take next
*/ */
private function processCellStartingNode(XMLReader $xmlReader): int protected function processCellStartingNode($xmlReader)
{ {
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader); $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
// NOTE: expand() will automatically decode all XML entities of the child nodes // NOTE: expand() will automatically decode all XML entities of the child nodes
/** @var DOMElement $node */ /** @var \DOMElement $node */
$node = $xmlReader->expand(); $node = $xmlReader->expand();
$currentCell = $this->getCell($node); $currentCell = $this->getCell($node);
@ -237,12 +277,12 @@ final class RowIterator implements RowIteratorInterface
/** /**
* @return int A return code that indicates what action should the processor take next * @return int A return code that indicates what action should the processor take next
*/ */
private function processRowEndingNode(): int protected function processRowEndingNode()
{ {
$isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRow, $this->lastProcessedCell); $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRow, $this->lastProcessedCell);
// if the fetched row is empty and we don't want to preserve it... // if the fetched row is empty and we don't want to preserve it...
if (!$this->options->SHOULD_PRESERVE_EMPTY_ROWS && $isEmptyRow) { if (!$this->shouldPreserveEmptyRows && $isEmptyRow) {
// ... skip it // ... skip it
return XMLProcessor::PROCESSING_CONTINUE; return XMLProcessor::PROCESSING_CONTINUE;
} }
@ -275,7 +315,7 @@ final class RowIterator implements RowIteratorInterface
/** /**
* @return int A return code that indicates what action should the processor take next * @return int A return code that indicates what action should the processor take next
*/ */
private function processTableEndingNode(): int protected function processTableEndingNode()
{ {
// The closing "</table:table>" marks the end of the file // The closing "</table:table>" marks the end of the file
$this->hasReachedEndOfFile = true; $this->hasReachedEndOfFile = true;
@ -284,11 +324,11 @@ final class RowIterator implements RowIteratorInterface
} }
/** /**
* @param XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
* *
* @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing * @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing
*/ */
private function getNumRowsRepeatedForCurrentNode(XMLReader $xmlReader): int protected function getNumRowsRepeatedForCurrentNode($xmlReader)
{ {
$numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED); $numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
@ -296,11 +336,11 @@ final class RowIterator implements RowIteratorInterface
} }
/** /**
* @param XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
* *
* @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
*/ */
private function getNumColumnsRepeatedForCurrentNode(XMLReader $xmlReader): int protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
{ {
$numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED); $numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
@ -310,15 +350,18 @@ final class RowIterator implements RowIteratorInterface
/** /**
* Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node. * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
* *
* @param \DOMElement $node
*
* @return Cell The cell set with the associated with the cell * @return Cell The cell set with the associated with the cell
*/ */
private function getCell(DOMElement $node): Cell protected function getCell($node)
{ {
try { try {
$cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node); $cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node);
$cell = Cell::fromValue($cellValue); $cell = $this->entityFactory->createCell($cellValue);
} catch (InvalidValueException $exception) { } catch (InvalidValueException $exception) {
$cell = new Cell\ErrorCell($exception->getInvalidValue(), null); $cell = $this->entityFactory->createCell($exception->getInvalidValue());
$cell->setType(Cell::TYPE_ERROR);
} }
return $cell; return $cell;
@ -330,14 +373,16 @@ final class RowIterator implements RowIteratorInterface
* After finishing processing each cell, the last read cell is not part of the * After finishing processing each cell, the last read cell is not part of the
* row data yet (as we still need to apply the "num-columns-repeated" attribute). * row data yet (as we still need to apply the "num-columns-repeated" attribute).
* *
* @param Row $currentRow
* @param null|Cell $lastReadCell The last read cell * @param null|Cell $lastReadCell The last read cell
* *
* @return bool Whether the row is empty * @return bool Whether the row is empty
*/ */
private function isEmptyRow(Row $currentRow, ?Cell $lastReadCell): bool protected function isEmptyRow($currentRow, $lastReadCell)
{ {
return return
$currentRow->isEmpty() $this->rowManager->isEmpty($currentRow)
&& (null === $lastReadCell || $lastReadCell instanceof Cell\EmptyCell); && (!isset($lastReadCell) || $lastReadCell->isEmpty())
;
} }
} }

View File

@ -1,30 +1,31 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\ODS; namespace OpenSpout\Reader\ODS;
use OpenSpout\Reader\SheetWithVisibilityInterface; use OpenSpout\Reader\SheetInterface;
/** /**
* @implements SheetWithVisibilityInterface<RowIterator> * Represents a sheet within a ODS file.
*/ */
final readonly class Sheet implements SheetWithVisibilityInterface class Sheet implements SheetInterface
{ {
/** @var RowIterator To iterate over sheet's rows */ /** @var \OpenSpout\Reader\ODS\RowIterator To iterate over sheet's rows */
private RowIterator $rowIterator; protected $rowIterator;
/** @var int ID of the sheet */
protected $id;
/** @var int Index of the sheet, based on order in the workbook (zero-based) */ /** @var int Index of the sheet, based on order in the workbook (zero-based) */
private int $index; protected $index;
/** @var string Name of the sheet */ /** @var string Name of the sheet */
private string $name; protected $name;
/** @var bool Whether the sheet was the active one */ /** @var bool Whether the sheet was the active one */
private bool $isActive; protected $isActive;
/** @var bool Whether the sheet is visible */ /** @var bool Whether the sheet is visible */
private bool $isVisible; protected $isVisible;
/** /**
* @param RowIterator $rowIterator The corresponding row iterator * @param RowIterator $rowIterator The corresponding row iterator
@ -33,7 +34,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
* @param bool $isSheetActive Whether the sheet was defined as active * @param bool $isSheetActive Whether the sheet was defined as active
* @param bool $isSheetVisible Whether the sheet is visible * @param bool $isSheetVisible Whether the sheet is visible
*/ */
public function __construct(RowIterator $rowIterator, int $sheetIndex, string $sheetName, bool $isSheetActive, bool $isSheetVisible) public function __construct($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible)
{ {
$this->rowIterator = $rowIterator; $this->rowIterator = $rowIterator;
$this->index = $sheetIndex; $this->index = $sheetIndex;
@ -42,7 +43,10 @@ final readonly class Sheet implements SheetWithVisibilityInterface
$this->isVisible = $isSheetVisible; $this->isVisible = $isSheetVisible;
} }
public function getRowIterator(): RowIterator /**
* @return \OpenSpout\Reader\ODS\RowIterator
*/
public function getRowIterator()
{ {
return $this->rowIterator; return $this->rowIterator;
} }
@ -50,7 +54,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
/** /**
* @return int Index of the sheet, based on order in the workbook (zero-based) * @return int Index of the sheet, based on order in the workbook (zero-based)
*/ */
public function getIndex(): int public function getIndex()
{ {
return $this->index; return $this->index;
} }
@ -58,7 +62,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
/** /**
* @return string Name of the sheet * @return string Name of the sheet
*/ */
public function getName(): string public function getName()
{ {
return $this->name; return $this->name;
} }
@ -66,7 +70,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
/** /**
* @return bool Whether the sheet was defined as active * @return bool Whether the sheet was defined as active
*/ */
public function isActive(): bool public function isActive()
{ {
return $this->isActive; return $this->isActive;
} }
@ -74,7 +78,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
/** /**
* @return bool Whether the sheet is visible * @return bool Whether the sheet is visible
*/ */
public function isVisible(): bool public function isVisible()
{ {
return $this->isVisible; return $this->isVisible;
} }

View File

@ -1,31 +1,24 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\ODS; namespace OpenSpout\Reader\ODS;
use DOMElement;
use OpenSpout\Common\Exception\IOException; use OpenSpout\Common\Exception\IOException;
use OpenSpout\Common\Helper\Escaper\ODS;
use OpenSpout\Reader\Common\XMLProcessor;
use OpenSpout\Reader\Exception\XMLProcessingException; use OpenSpout\Reader\Exception\XMLProcessingException;
use OpenSpout\Reader\ODS\Helper\CellValueFormatter; use OpenSpout\Reader\IteratorInterface;
use OpenSpout\Reader\ODS\Creator\InternalEntityFactory;
use OpenSpout\Reader\ODS\Helper\SettingsHelper; use OpenSpout\Reader\ODS\Helper\SettingsHelper;
use OpenSpout\Reader\SheetIteratorInterface;
use OpenSpout\Reader\Wrapper\XMLReader; use OpenSpout\Reader\Wrapper\XMLReader;
/** /**
* @implements SheetIteratorInterface<Sheet> * Iterate over ODS sheet.
*/ */
final class SheetIterator implements SheetIteratorInterface class SheetIterator implements IteratorInterface
{ {
public const CONTENT_XML_FILE_PATH = 'content.xml'; public const CONTENT_XML_FILE_PATH = 'content.xml';
public const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'; public const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0';
/** /** Definition of XML nodes name and attribute used to parse sheet data */
* Definition of XML nodes name and attribute used to parse sheet data.
*/
public const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles'; public const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles';
public const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties'; public const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties';
public const XML_NODE_TABLE = 'table:table'; public const XML_NODE_TABLE = 'table:table';
@ -35,37 +28,45 @@ final class SheetIterator implements SheetIteratorInterface
public const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display'; public const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display';
/** @var string Path of the file to be read */ /** @var string Path of the file to be read */
private readonly string $filePath; protected $filePath;
private readonly Options $options; /** @var \OpenSpout\Common\Manager\OptionsManagerInterface Reader's options manager */
protected $optionsManager;
/** @var InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var XMLReader The XMLReader object that will help read sheet's XML data */ /** @var XMLReader The XMLReader object that will help read sheet's XML data */
private readonly XMLReader $xmlReader; protected $xmlReader;
/** @var ODS Used to unescape XML data */ /** @var \OpenSpout\Common\Helper\Escaper\ODS Used to unescape XML data */
private readonly ODS $escaper; protected $escaper;
/** @var bool Whether there are still at least a sheet to be read */ /** @var bool Whether there are still at least a sheet to be read */
private bool $hasFoundSheet; protected $hasFoundSheet;
/** @var int The index of the sheet being read (zero-based) */ /** @var int The index of the sheet being read (zero-based) */
private int $currentSheetIndex; protected $currentSheetIndex;
/** @var string The name of the sheet that was defined as active */ /** @var string The name of the sheet that was defined as active */
private readonly ?string $activeSheetName; protected $activeSheetName;
/** @var array<string, bool> Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */ /** @var array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */
private array $sheetsVisibility; protected $sheetsVisibility;
public function __construct( /**
string $filePath, * @param string $filePath Path of the file to be read
Options $options, * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager
ODS $escaper, * @param \OpenSpout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data
SettingsHelper $settingsHelper * @param SettingsHelper $settingsHelper Helper to get data from "settings.xml"
) { * @param InternalEntityFactory $entityFactory Factory to create entities
*/
public function __construct($filePath, $optionsManager, $escaper, $settingsHelper, $entityFactory)
{
$this->filePath = $filePath; $this->filePath = $filePath;
$this->options = $options; $this->optionsManager = $optionsManager;
$this->xmlReader = new XMLReader(); $this->entityFactory = $entityFactory;
$this->xmlReader = $entityFactory->createXMLReader();
$this->escaper = $escaper; $this->escaper = $escaper;
$this->activeSheetName = $settingsHelper->getActiveSheetName($filePath); $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
} }
@ -75,9 +76,10 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @see http://php.net/manual/en/iterator.rewind.php * @see http://php.net/manual/en/iterator.rewind.php
* *
* @throws IOException If unable to open the XML file containing sheets' data * @throws \OpenSpout\Common\Exception\IOException If unable to open the XML file containing sheets' data
*/ */
public function rewind(): void #[\ReturnTypeWillChange]
public function rewind()
{ {
$this->xmlReader->close(); $this->xmlReader->close();
@ -101,15 +103,13 @@ final class SheetIterator implements SheetIteratorInterface
* Checks if current position is valid. * Checks if current position is valid.
* *
* @see http://php.net/manual/en/iterator.valid.php * @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/ */
public function valid(): bool #[\ReturnTypeWillChange]
public function valid()
{ {
$valid = $this->hasFoundSheet; return $this->hasFoundSheet;
if (!$valid) {
$this->xmlReader->close();
}
return $valid;
} }
/** /**
@ -117,7 +117,8 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @see http://php.net/manual/en/iterator.next.php * @see http://php.net/manual/en/iterator.next.php
*/ */
public function next(): void #[\ReturnTypeWillChange]
public function next()
{ {
$this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
@ -130,29 +131,27 @@ final class SheetIterator implements SheetIteratorInterface
* Return the current element. * Return the current element.
* *
* @see http://php.net/manual/en/iterator.current.php * @see http://php.net/manual/en/iterator.current.php
*
* @return \OpenSpout\Reader\ODS\Sheet
*/ */
public function current(): Sheet #[\ReturnTypeWillChange]
public function current()
{ {
$escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME); $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
\assert(null !== $escapedSheetName);
$sheetName = $this->escaper->unescape($escapedSheetName); $sheetName = $this->escaper->unescape($escapedSheetName);
$isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName); $isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName);
$sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME); $sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME);
\assert(null !== $sheetStyleName);
$isSheetVisible = $this->isSheetVisible($sheetStyleName); $isSheetVisible = $this->isSheetVisible($sheetStyleName);
return new Sheet( return $this->entityFactory->createSheet(
new RowIterator( $this->xmlReader,
$this->options,
new CellValueFormatter($this->options->SHOULD_FORMAT_DATES, new ODS()),
new XMLProcessor($this->xmlReader)
),
$this->currentSheetIndex, $this->currentSheetIndex,
$sheetName, $sheetName,
$isSheetActive, $isSheetActive,
$isSheetVisible $isSheetVisible,
$this->optionsManager
); );
} }
@ -160,33 +159,44 @@ final class SheetIterator implements SheetIteratorInterface
* Return the key of the current element. * Return the key of the current element.
* *
* @see http://php.net/manual/en/iterator.key.php * @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/ */
public function key(): int #[\ReturnTypeWillChange]
public function key()
{ {
return $this->currentSheetIndex + 1; return $this->currentSheetIndex + 1;
} }
/**
* Cleans up what was created to iterate over the object.
*/
#[\ReturnTypeWillChange]
public function end()
{
$this->xmlReader->close();
}
/** /**
* Extracts the visibility of the sheets. * Extracts the visibility of the sheets.
* *
* @return array<string, bool> Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] * @return array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE]
*/ */
private function readSheetsVisibility(): array private function readSheetsVisibility()
{ {
$sheetsVisibility = []; $sheetsVisibility = [];
$this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES); $this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES);
/** @var \DOMElement $automaticStylesNode */
$automaticStylesNode = $this->xmlReader->expand(); $automaticStylesNode = $this->xmlReader->expand();
\assert($automaticStylesNode instanceof DOMElement);
$tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES); $tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES);
/** @var \DOMElement $tableStyleNode */
foreach ($tableStyleNodes as $tableStyleNode) { foreach ($tableStyleNodes as $tableStyleNode) {
$isSheetVisible = ('false' !== $tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY)); $isSheetVisible = ('false' !== $tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY));
$parentStyleNode = $tableStyleNode->parentNode; $parentStyleNode = $tableStyleNode->parentNode;
\assert($parentStyleNode instanceof DOMElement);
$styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME); $styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME);
$sheetsVisibility[$styleName] = $isSheetVisible; $sheetsVisibility[$styleName] = $isSheetVisible;
@ -204,13 +214,14 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @return bool Whether the current sheet was defined as the active one * @return bool Whether the current sheet was defined as the active one
*/ */
private function isSheetActive(string $sheetName, int $sheetIndex, ?string $activeSheetName): bool private function isSheetActive($sheetName, $sheetIndex, $activeSheetName)
{ {
// The given sheet is active if its name matches the defined active sheet's name // The given sheet is active if its name matches the defined active sheet's name
// or if no information about the active sheet was found, it defaults to the first sheet. // or if no information about the active sheet was found, it defaults to the first sheet.
return return
(null === $activeSheetName && 0 === $sheetIndex) (null === $activeSheetName && 0 === $sheetIndex)
|| ($activeSheetName === $sheetName); || ($activeSheetName === $sheetName)
;
} }
/** /**
@ -220,7 +231,7 @@ final class SheetIterator implements SheetIteratorInterface
* *
* @return bool Whether the current sheet is visible * @return bool Whether the current sheet is visible
*/ */
private function isSheetVisible(string $sheetStyleName): bool private function isSheetVisible($sheetStyleName)
{ {
return $this->sheetsVisibility[$sheetStyleName] ?? return $this->sheetsVisibility[$sheetStyleName] ??
true; true;

View File

@ -1,22 +1,65 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader; namespace OpenSpout\Reader;
use OpenSpout\Common\Exception\IOException; use OpenSpout\Common\Exception\IOException;
use OpenSpout\Reader\Exception\ReaderException; use OpenSpout\Common\Helper\GlobalFunctionsHelper;
use OpenSpout\Common\Manager\OptionsManagerInterface;
use OpenSpout\Reader\Common\Creator\InternalEntityFactoryInterface;
use OpenSpout\Reader\Common\Entity\Options;
use OpenSpout\Reader\Exception\ReaderNotOpenedException; use OpenSpout\Reader\Exception\ReaderNotOpenedException;
/** abstract class ReaderAbstract implements ReaderInterface
* @template T of SheetIteratorInterface
*
* @implements ReaderInterface<T>
*/
abstract class AbstractReader implements ReaderInterface
{ {
/** @var bool Indicates whether the stream is currently open */ /** @var bool Indicates whether the stream is currently open */
private bool $isStreamOpened = false; protected $isStreamOpened = false;
/** @var InternalEntityFactoryInterface Factory to create entities */
protected $entityFactory;
/** @var \OpenSpout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @var OptionsManagerInterface Writer options manager */
protected $optionsManager;
public function __construct(
OptionsManagerInterface $optionsManager,
GlobalFunctionsHelper $globalFunctionsHelper,
InternalEntityFactoryInterface $entityFactory
) {
$this->optionsManager = $optionsManager;
$this->globalFunctionsHelper = $globalFunctionsHelper;
$this->entityFactory = $entityFactory;
}
/**
* Sets whether date/time values should be returned as PHP objects or be formatted as strings.
*
* @param bool $shouldFormatDates
*
* @return ReaderAbstract
*/
public function setShouldFormatDates($shouldFormatDates)
{
$this->optionsManager->setOption(Options::SHOULD_FORMAT_DATES, $shouldFormatDates);
return $this;
}
/**
* Sets whether empty rows should be returned or skipped.
*
* @param bool $shouldPreserveEmptyRows
*
* @return ReaderAbstract
*/
public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
{
$this->optionsManager->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, $shouldPreserveEmptyRows);
return $this;
}
/** /**
* Prepares the reader to read the given file. It also makes sure * Prepares the reader to read the given file. It also makes sure
@ -24,9 +67,9 @@ abstract class AbstractReader implements ReaderInterface
* *
* @param string $filePath Path of the file to be read * @param string $filePath Path of the file to be read
* *
* @throws IOException If the file at the given path does not exist, is not readable or is corrupted * @throws \OpenSpout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
*/ */
public function open(string $filePath): void public function open($filePath)
{ {
if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) { if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) {
throw new IOException("Could not open {$filePath} for reading! Stream wrapper used is not supported for this type of file."); throw new IOException("Could not open {$filePath} for reading! Stream wrapper used is not supported for this type of file.");
@ -34,10 +77,10 @@ abstract class AbstractReader implements ReaderInterface
if (!$this->isPhpStream($filePath)) { if (!$this->isPhpStream($filePath)) {
// we skip the checks if the provided file path points to a PHP stream // we skip the checks if the provided file path points to a PHP stream
if (!file_exists($filePath)) { if (!$this->globalFunctionsHelper->file_exists($filePath)) {
throw new IOException("Could not open {$filePath} for reading! File does not exist."); throw new IOException("Could not open {$filePath} for reading! File does not exist.");
} }
if (!is_readable($filePath)) { if (!$this->globalFunctionsHelper->is_readable($filePath)) {
throw new IOException("Could not open {$filePath} for reading! File is not readable."); throw new IOException("Could not open {$filePath} for reading! File is not readable.");
} }
} }
@ -46,66 +89,86 @@ abstract class AbstractReader implements ReaderInterface
$fileRealPath = $this->getFileRealPath($filePath); $fileRealPath = $this->getFileRealPath($filePath);
$this->openReader($fileRealPath); $this->openReader($fileRealPath);
$this->isStreamOpened = true; $this->isStreamOpened = true;
} catch (ReaderException $exception) { } catch (\Exception $exception) {
throw new IOException( throw new IOException("Could not open {$filePath} for reading! ({$exception->getMessage()})");
"Could not open {$filePath} for reading!",
0,
$exception
);
} }
} }
/**
* Returns an iterator to iterate over sheets.
*
* @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
*
* @return SheetIteratorInterface To iterate over sheets
*/
public function getSheetIterator()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Reader should be opened first.');
}
return $this->getConcreteSheetIterator();
}
/** /**
* Closes the reader, preventing any additional reading. * Closes the reader, preventing any additional reading.
*/ */
final public function close(): void public function close()
{ {
if ($this->isStreamOpened) { if ($this->isStreamOpened) {
$this->closeReader(); $this->closeReader();
$sheetIterator = $this->getConcreteSheetIterator();
if (null !== $sheetIterator) {
$sheetIterator->end();
}
$this->isStreamOpened = false; $this->isStreamOpened = false;
} }
} }
/** /**
* Returns whether stream wrappers are supported. * Returns whether stream wrappers are supported.
*
* @return bool
*/ */
abstract protected function doesSupportStreamWrapper(): bool; abstract protected function doesSupportStreamWrapper();
/** /**
* Opens the file at the given file path to make it ready to be read. * Opens the file at the given file path to make it ready to be read.
* *
* @param string $filePath Path of the file to be read * @param string $filePath Path of the file to be read
*/ */
abstract protected function openReader(string $filePath): void; abstract protected function openReader($filePath);
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIteratorInterface To iterate over sheets
*/
abstract protected function getConcreteSheetIterator();
/** /**
* Closes the reader. To be used after reading the file. * Closes the reader. To be used after reading the file.
*/ */
abstract protected function closeReader(): void; abstract protected function closeReader();
final protected function ensureStreamOpened(): void
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Reader should be opened first.');
}
}
/** /**
* Returns the real path of the given path. * Returns the real path of the given path.
* If the given path is a valid stream wrapper, returns the path unchanged. * If the given path is a valid stream wrapper, returns the path unchanged.
*
* @param string $filePath
*
* @return string
*/ */
private function getFileRealPath(string $filePath): string protected function getFileRealPath($filePath)
{ {
if ($this->isSupportedStreamWrapper($filePath)) { if ($this->isSupportedStreamWrapper($filePath)) {
return $filePath; return $filePath;
} }
// Need to use realpath to fix "Can't open file" on some Windows setup // Need to use realpath to fix "Can't open file" on some Windows setup
$realpath = realpath($filePath); return realpath($filePath);
\assert(false !== $realpath);
return $realpath;
} }
/** /**
@ -116,10 +179,10 @@ abstract class AbstractReader implements ReaderInterface
* *
* @return null|string The stream wrapper scheme or NULL if not a stream wrapper * @return null|string The stream wrapper scheme or NULL if not a stream wrapper
*/ */
private function getStreamWrapperScheme(string $filePath): ?string protected function getStreamWrapperScheme($filePath)
{ {
$streamScheme = null; $streamScheme = null;
if (1 === preg_match('/^(\w+):\/\//', $filePath, $matches)) { if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
$streamScheme = $matches[1]; $streamScheme = $matches[1];
} }
@ -134,7 +197,7 @@ abstract class AbstractReader implements ReaderInterface
* *
* @return bool Whether the given path is an unsupported stream wrapper * @return bool Whether the given path is an unsupported stream wrapper
*/ */
private function isStreamWrapper(string $filePath): bool protected function isStreamWrapper($filePath)
{ {
return null !== $this->getStreamWrapperScheme($filePath); return null !== $this->getStreamWrapperScheme($filePath);
} }
@ -148,11 +211,13 @@ abstract class AbstractReader implements ReaderInterface
* *
* @return bool Whether the given path is an supported stream wrapper * @return bool Whether the given path is an supported stream wrapper
*/ */
private function isSupportedStreamWrapper(string $filePath): bool protected function isSupportedStreamWrapper($filePath)
{ {
$streamScheme = $this->getStreamWrapperScheme($filePath); $streamScheme = $this->getStreamWrapperScheme($filePath);
return null === $streamScheme || \in_array($streamScheme, stream_get_wrappers(), true); return (null !== $streamScheme) ?
\in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers(), true) :
true;
} }
/** /**
@ -162,7 +227,7 @@ abstract class AbstractReader implements ReaderInterface
* *
* @return bool Whether the given path maps to a PHP stream * @return bool Whether the given path maps to a PHP stream
*/ */
private function isPhpStream(string $filePath): bool protected function isPhpStream($filePath)
{ {
$streamScheme = $this->getStreamWrapperScheme($filePath); $streamScheme = $this->getStreamWrapperScheme($filePath);

View File

@ -1,13 +1,9 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader; namespace OpenSpout\Reader;
use OpenSpout\Common\Exception\IOException;
/** /**
* @template T of SheetIteratorInterface * Interface ReaderInterface.
*/ */
interface ReaderInterface interface ReaderInterface
{ {
@ -17,21 +13,21 @@ interface ReaderInterface
* *
* @param string $filePath Path of the file to be read * @param string $filePath Path of the file to be read
* *
* @throws IOException * @throws \OpenSpout\Common\Exception\IOException
*/ */
public function open(string $filePath): void; public function open($filePath);
/** /**
* Returns an iterator to iterate over sheets. * Returns an iterator to iterate over sheets.
* *
* @return T * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
* *
* @throws Exception\ReaderNotOpenedException If called before opening the reader * @return SheetIteratorInterface To iterate over sheets
*/ */
public function getSheetIterator(): SheetIteratorInterface; public function getSheetIterator();
/** /**
* Closes the reader, preventing any additional reading. * Closes the reader, preventing any additional reading.
*/ */
public function close(): void; public function close();
} }

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader;
use OpenSpout\Common\Entity\Row;
interface RowIteratorInterface extends IteratorInterface
{
/**
* Cleans up what was created to iterate over the object.
*/
#[\ReturnTypeWillChange]
public function end();
/**
* @return null|Row
*/
#[\ReturnTypeWillChange]
public function current();
}

View File

@ -0,0 +1,34 @@
<?php
namespace OpenSpout\Reader;
/**
* Interface SheetInterface.
*/
interface SheetInterface
{
/**
* @return IteratorInterface iterator to iterate over the sheet's rows
*/
public function getRowIterator();
/**
* @return int Index of the sheet
*/
public function getIndex();
/**
* @return string Name of the sheet
*/
public function getName();
/**
* @return bool Whether the sheet was defined as active
*/
public function isActive();
/**
* @return bool Whether the sheet is visible
*/
public function isVisible();
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader;
/**
* Interface IteratorInterface.
*/
interface SheetIteratorInterface extends IteratorInterface
{
/**
* Cleans up what was created to iterate over the object.
*/
#[\ReturnTypeWillChange]
public function end();
/**
* @return null|SheetInterface
*/
#[\ReturnTypeWillChange]
public function current();
}

View File

@ -1,24 +1,22 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\Wrapper; namespace OpenSpout\Reader\Wrapper;
use OpenSpout\Reader\Exception\XMLProcessingException; use OpenSpout\Reader\Exception\XMLProcessingException;
/** /**
* @internal * Trait XMLInternalErrorsHelper.
*/ */
trait XMLInternalErrorsHelper trait XMLInternalErrorsHelper
{ {
/** @var bool Stores whether XML errors were initially stored internally - used to reset */ /** @var bool Stores whether XML errors were initially stored internally - used to reset */
private bool $initialUseInternalErrorsValue; protected $initialUseInternalErrorsValue;
/** /**
* To avoid displaying lots of warning/error messages on screen, * To avoid displaying lots of warning/error messages on screen,
* stores errors internally instead. * stores errors internally instead.
*/ */
private function useXMLInternalErrors(): void protected function useXMLInternalErrors()
{ {
libxml_clear_errors(); libxml_clear_errors();
$this->initialUseInternalErrorsValue = libxml_use_internal_errors(true); $this->initialUseInternalErrorsValue = libxml_use_internal_errors(true);
@ -28,9 +26,9 @@ trait XMLInternalErrorsHelper
* Throws an XMLProcessingException if an error occured. * Throws an XMLProcessingException if an error occured.
* It also always resets the "libxml_use_internal_errors" setting back to its initial value. * It also always resets the "libxml_use_internal_errors" setting back to its initial value.
* *
* @throws XMLProcessingException * @throws \OpenSpout\Reader\Exception\XMLProcessingException
*/ */
private function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(): void protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured()
{ {
if ($this->hasXMLErrorOccured()) { if ($this->hasXMLErrorOccured()) {
$this->resetXMLInternalErrorsSetting(); $this->resetXMLInternalErrorsSetting();
@ -41,7 +39,7 @@ trait XMLInternalErrorsHelper
$this->resetXMLInternalErrorsSetting(); $this->resetXMLInternalErrorsSetting();
} }
private function resetXMLInternalErrorsSetting(): void protected function resetXMLInternalErrorsSetting()
{ {
libxml_use_internal_errors($this->initialUseInternalErrorsValue); libxml_use_internal_errors($this->initialUseInternalErrorsValue);
} }
@ -51,7 +49,7 @@ trait XMLInternalErrorsHelper
* *
* @return bool TRUE if an error occured, FALSE otherwise * @return bool TRUE if an error occured, FALSE otherwise
*/ */
private function hasXMLErrorOccured(): bool private function hasXMLErrorOccured()
{ {
return false !== libxml_get_last_error(); return false !== libxml_get_last_error();
} }
@ -61,11 +59,11 @@ trait XMLInternalErrorsHelper
* *
* @see libxml_get_last_error * @see libxml_get_last_error
* *
* @return string Last XML error message or null if no error * @return null|string Last XML error message or null if no error
*/ */
private function getLastXMLErrorMessage(): string private function getLastXMLErrorMessage()
{ {
$errorMessage = ''; $errorMessage = null;
$error = libxml_get_last_error(); $error = libxml_get_last_error();
if (false !== $error) { if (false !== $error) {

View File

@ -1,17 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace OpenSpout\Reader\Wrapper; namespace OpenSpout\Reader\Wrapper;
use OpenSpout\Common\Exception\IOException;
use OpenSpout\Reader\Exception\XMLProcessingException;
use ZipArchive;
/** /**
* @internal * Wrapper around the built-in XMLReader.
*
* @see \XMLReader
*/ */
final class XMLReader extends \XMLReader class XMLReader extends \XMLReader
{ {
use XMLInternalErrorsHelper; use XMLInternalErrorsHelper;
@ -25,13 +21,14 @@ final class XMLReader extends \XMLReader
* *
* @return bool TRUE on success or FALSE on failure * @return bool TRUE on success or FALSE on failure
*/ */
public function openFileInZip(string $zipFilePath, string $fileInsideZipPath): bool public function openFileInZip($zipFilePath, $fileInsideZipPath)
{ {
$wasOpenSuccessful = false; $wasOpenSuccessful = false;
$realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath); $realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath);
// We need to check first that the file we are trying to read really exist because: // We need to check first that the file we are trying to read really exist because:
// - PHP emits a warning when trying to open a file that does not exist. // - PHP emits a warning when trying to open a file that does not exist.
// - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779)
if ($this->fileExistsWithinZip($realPathURI)) { if ($this->fileExistsWithinZip($realPathURI)) {
$wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET);
} }
@ -48,17 +45,12 @@ final class XMLReader extends \XMLReader
* *
* @return string The real path URI * @return string The real path URI
*/ */
public function getRealPathURIForFileInZip(string $zipFilePath, string $fileInsideZipPath): string public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath)
{ {
// The file path should not start with a '/', otherwise it won't be found // The file path should not start with a '/', otherwise it won't be found
$fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/'); $fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/');
$realpath = realpath($zipFilePath); return self::ZIP_WRAPPER.realpath($zipFilePath).'#'.$fileInsideZipPathWithoutLeadingSlash;
if (false === $realpath) {
throw new IOException("Could not open {$zipFilePath} for reading! File does not exist.");
}
return self::ZIP_WRAPPER.$realpath.'#'.$fileInsideZipPathWithoutLeadingSlash;
} }
/** /**
@ -66,9 +58,12 @@ final class XMLReader extends \XMLReader
* *
* @see \XMLReader::read * @see \XMLReader::read
* *
* @throws XMLProcessingException If an error/warning occurred * @throws \OpenSpout\Reader\Exception\XMLProcessingException If an error/warning occurred
*
* @return bool TRUE on success or FALSE on failure
*/ */
public function read(): bool #[\ReturnTypeWillChange]
public function read()
{ {
$this->useXMLInternalErrors(); $this->useXMLInternalErrors();
@ -84,11 +79,11 @@ final class XMLReader extends \XMLReader
* *
* @param string $nodeName Name of the node to find * @param string $nodeName Name of the node to find
* *
* @return bool TRUE on success or FALSE on failure * @throws \OpenSpout\Reader\Exception\XMLProcessingException If an error/warning occurred
* *
* @throws XMLProcessingException If an error/warning occurred * @return bool TRUE on success or FALSE on failure
*/ */
public function readUntilNodeFound(string $nodeName): bool public function readUntilNodeFound($nodeName)
{ {
do { do {
$wasReadSuccessful = $this->read(); $wasReadSuccessful = $this->read();
@ -105,9 +100,12 @@ final class XMLReader extends \XMLReader
* *
* @param null|string $localName The name of the next node to move to * @param null|string $localName The name of the next node to move to
* *
* @throws XMLProcessingException If an error/warning occurred * @throws \OpenSpout\Reader\Exception\XMLProcessingException If an error/warning occurred
*
* @return bool TRUE on success or FALSE on failure
*/ */
public function next($localName = null): bool #[\ReturnTypeWillChange]
public function next($localName = null)
{ {
$this->useXMLInternalErrors(); $this->useXMLInternalErrors();
@ -119,17 +117,21 @@ final class XMLReader extends \XMLReader
} }
/** /**
* @param string $nodeName
*
* @return bool Whether the XML Reader is currently positioned on the starting node with given name * @return bool Whether the XML Reader is currently positioned on the starting node with given name
*/ */
public function isPositionedOnStartingNode(string $nodeName): bool public function isPositionedOnStartingNode($nodeName)
{ {
return $this->isPositionedOnNode($nodeName, self::ELEMENT); return $this->isPositionedOnNode($nodeName, self::ELEMENT);
} }
/** /**
* @param string $nodeName
*
* @return bool Whether the XML Reader is currently positioned on the ending node with given name * @return bool Whether the XML Reader is currently positioned on the ending node with given name
*/ */
public function isPositionedOnEndingNode(string $nodeName): bool public function isPositionedOnEndingNode($nodeName)
{ {
return $this->isPositionedOnNode($nodeName, self::END_ELEMENT); return $this->isPositionedOnNode($nodeName, self::END_ELEMENT);
} }
@ -137,7 +139,7 @@ final class XMLReader extends \XMLReader
/** /**
* @return string The name of the current node, un-prefixed * @return string The name of the current node, un-prefixed
*/ */
public function getCurrentNodeName(): string public function getCurrentNodeName()
{ {
return $this->localName; return $this->localName;
} }
@ -149,16 +151,16 @@ final class XMLReader extends \XMLReader
* *
* @return bool TRUE if the file exists, FALSE otherwise * @return bool TRUE if the file exists, FALSE otherwise
*/ */
private function fileExistsWithinZip(string $zipStreamURI): bool protected function fileExistsWithinZip($zipStreamURI)
{ {
$doesFileExists = false; $doesFileExists = false;
$pattern = '/zip:\/\/([^#]+)#(.*)/'; $pattern = '/zip:\/\/([^#]+)#(.*)/';
if (1 === preg_match($pattern, $zipStreamURI, $matches)) { if (preg_match($pattern, $zipStreamURI, $matches)) {
$zipFilePath = $matches[1]; $zipFilePath = $matches[1];
$innerFilePath = $matches[2]; $innerFilePath = $matches[2];
$zip = new ZipArchive(); $zip = new \ZipArchive();
if (true === $zip->open($zipFilePath)) { if (true === $zip->open($zipFilePath)) {
$doesFileExists = (false !== $zip->locateName($innerFilePath)); $doesFileExists = (false !== $zip->locateName($innerFilePath));
$zip->close(); $zip->close();
@ -169,9 +171,12 @@ final class XMLReader extends \XMLReader
} }
/** /**
* @param string $nodeName
* @param int $nodeType
*
* @return bool Whether the XML Reader is currently positioned on the node with given name and type * @return bool Whether the XML Reader is currently positioned on the node with given name and type
*/ */
private function isPositionedOnNode(string $nodeName, int $nodeType): bool private function isPositionedOnNode($nodeName, $nodeType)
{ {
/** /**
* In some cases, the node has a prefix (for instance, "<sheet>" can also be "<x:sheet>"). * In some cases, the node has a prefix (for instance, "<sheet>" can also be "<x:sheet>").
@ -179,7 +184,7 @@ final class XMLReader extends \XMLReader
* *
* @see https://github.com/box/spout/issues/233 * @see https://github.com/box/spout/issues/233
*/ */
$hasPrefix = str_contains($nodeName, ':'); $hasPrefix = (false !== strpos($nodeName, ':'));
$currentNodeName = ($hasPrefix) ? $this->name : $this->localName; $currentNodeName = ($hasPrefix) ? $this->name : $this->localName;
return $this->nodeType === $nodeType && $currentNodeName === $nodeName; return $this->nodeType === $nodeType && $currentNodeName === $nodeName;

View File

@ -0,0 +1,38 @@
<?php
namespace OpenSpout\Reader\XLSX\Creator;
use OpenSpout\Common\Helper\Escaper;
use OpenSpout\Reader\XLSX\Helper\CellValueFormatter;
use OpenSpout\Reader\XLSX\Manager\SharedStringsManager;
use OpenSpout\Reader\XLSX\Manager\StyleManager;
/**
* Factory to create helpers.
*/
class HelperFactory extends \OpenSpout\Common\Creator\HelperFactory
{
/**
* @param SharedStringsManager $sharedStringsManager Manages shared strings
* @param StyleManager $styleManager Manages styles
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @param bool $shouldUse1904Dates Whether date/time values should use a calendar starting in 1904 instead of 1900
*
* @return CellValueFormatter
*/
public function createCellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates)
{
$escaper = $this->createStringsEscaper();
return new CellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper);
}
/**
* @return Escaper\XLSX
*/
public function createStringsEscaper()
{
// @noinspection PhpUnnecessaryFullyQualifiedNameInspection
return new Escaper\XLSX();
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace OpenSpout\Reader\XLSX\Creator;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Reader\Common\Creator\InternalEntityFactoryInterface;
use OpenSpout\Reader\Common\Entity\Options;
use OpenSpout\Reader\Common\XMLProcessor;
use OpenSpout\Reader\Wrapper\XMLReader;
use OpenSpout\Reader\XLSX\Manager\SharedStringsManager;
use OpenSpout\Reader\XLSX\RowIterator;
use OpenSpout\Reader\XLSX\Sheet;
use OpenSpout\Reader\XLSX\SheetIterator;
/**
* Factory to create entities.
*/
class InternalEntityFactory implements InternalEntityFactoryInterface
{
/** @var HelperFactory */
private $helperFactory;
/** @var ManagerFactory */
private $managerFactory;
public function __construct(ManagerFactory $managerFactory, HelperFactory $helperFactory)
{
$this->managerFactory = $managerFactory;
$this->helperFactory = $helperFactory;
}
/**
* @param string $filePath Path of the file to be read
* @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @param SharedStringsManager $sharedStringsManager Manages shared strings
*
* @return SheetIterator
*/
public function createSheetIterator($filePath, $optionsManager, $sharedStringsManager)
{
$sheetManager = $this->managerFactory->createSheetManager(
$filePath,
$optionsManager,
$sharedStringsManager,
$this
);
return new SheetIterator($sheetManager);
}
/**
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
* @param string $sheetName Name of the sheet
* @param bool $isSheetActive Whether the sheet was defined as active
* @param bool $isSheetVisible Whether the sheet is visible
* @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @param SharedStringsManager $sharedStringsManager Manages shared strings
*
* @return Sheet
*/
public function createSheet(
$filePath,
$sheetDataXMLFilePath,
$sheetIndex,
$sheetName,
$isSheetActive,
$isSheetVisible,
$optionsManager,
$sharedStringsManager
) {
$rowIterator = $this->createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager);
return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible);
}
/**
* @param Cell[] $cells
*
* @return Row
*/
public function createRow(array $cells = [])
{
return new Row($cells, null);
}
/**
* @param mixed $cellValue
*
* @return Cell
*/
public function createCell($cellValue)
{
return new Cell($cellValue);
}
/**
* @return \ZipArchive
*/
public function createZipArchive()
{
return new \ZipArchive();
}
/**
* @return XMLReader
*/
public function createXMLReader()
{
return new XMLReader();
}
/**
* @param XMLReader $xmlReader
*
* @return XMLProcessor
*/
public function createXMLProcessor($xmlReader)
{
return new XMLProcessor($xmlReader);
}
/**
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @param SharedStringsManager $sharedStringsManager Manages shared strings
*
* @return RowIterator
*/
private function createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager)
{
$xmlReader = $this->createXMLReader();
$xmlProcessor = $this->createXMLProcessor($xmlReader);
$styleManager = $this->managerFactory->createStyleManager($filePath, $this);
$rowManager = $this->managerFactory->createRowManager($this);
$shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES);
$shouldUse1904Dates = $optionsManager->getOption(Options::SHOULD_USE_1904_DATES);
$cellValueFormatter = $this->helperFactory->createCellValueFormatter(
$sharedStringsManager,
$styleManager,
$shouldFormatDates,
$shouldUse1904Dates
);
$shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
return new RowIterator(
$filePath,
$sheetDataXMLFilePath,
$shouldPreserveEmptyRows,
$xmlReader,
$xmlProcessor,
$cellValueFormatter,
$rowManager,
$this
);
}
}

Some files were not shown because too many files have changed in this diff Show More