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
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.
Spout 3.0 introduced several backwards-incompatible changes. The upgrade from Spout 2.x to 3.0 must therefore be done with caution.
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.
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
makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
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.
Finally, **_OpenSpout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the
community.
### Reader changes
Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community.
Reader changes
--------------
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:
```php
use OpenSpout\Reader\Common\Creator\ReaderEntityFactory; // namespace is no longer "OpenSpout\Reader"
...
$reader = ReaderEntityFactory::createXLSXReader(); // replaces ReaderFactory::create(Type::XLSX)
$reader = ReaderEntityFactory::createCSVReader(); // replaces ReaderFactory::create(Type::CSV)
$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
values. Accessing the row values should now be done this way:
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:
```php
...
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) { // $row is a "Row" object, not an array
$rowAsArray = $row->toArray(); // this is the 2.x equivalent
@ -108,22 +38,20 @@ foreach ($reader->getSheetIterator() as $sheet) {
}
```
### Writer changes
Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`,
instead of using the `WriterFactory`.
Writer changes
--------------
Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`, instead of using the `WriterFactory`.
Also, the `WriterFactory::create($type)` method was removed and replaced by methods for each writer:
```php
use OpenSpout\Writer\Common\Creator\WriterEntityFactory; // namespace is no longer "OpenSpout\Writer"
...
$writer = WriterEntityFactory::createXLSXWriter(); // replaces WriterFactory::create(Type::XLSX)
$writer = WriterEntityFactory::createCSVWriter(); // replaces WriterFactory::create(Type::CSV)
$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
array of `Row`). Creating such objects can easily be done this way:
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:
```php
// Adding a row from an array of values (2.x equivalent)
$cellValues = ['foo', 12345];
@ -137,8 +65,8 @@ $row2 = WriterEntityFactory::createRow([$cell1, $cell2]);
$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.
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\Style
### Handling of empty rows
Handling of empty rows
----------------------
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
is created in the sheet.
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.

View File

@ -28,9 +28,8 @@
],
"homepage": "https://github.com/openspout/openspout",
"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-fileinfo": "*",
"ext-filter": "*",
"ext-libxml": "*",
"ext-xmlreader": "*",
@ -38,17 +37,14 @@
},
"require-dev": {
"ext-zlib": "*",
"friendsofphp/php-cs-fixer": "^3.65.0",
"infection/infection": "^0.29.8",
"phpbench/phpbench": "^1.3.1",
"phpstan/phpstan": "^2.0.2",
"phpstan/phpstan-phpunit": "^2.0.1",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^11.4.3"
"friendsofphp/php-cs-fixer": "^3.4",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
"ext-mbstring": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
"ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
},
"autoload": {
"psr-4": {
@ -56,16 +52,13 @@
}
},
"autoload-dev": {
"psr-4": {
"OpenSpout\\Benchmarks\\": "benchmarks/"
},
"classmap": [
"tests/"
]
},
"config": {
"allow-plugins": {
"infection/extension-installer": true
"platform": {
"php": "7.3"
}
},
"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
declare(strict_types=1);
namespace OpenSpout\Common\Entity\Style;
/**
* This class provides constants to work with text alignment.
*/
final class CellAlignment
abstract class CellAlignment
{
public const LEFT = 'left';
public const RIGHT = 'right';
public const CENTER = 'center';
public const JUSTIFY = 'justify';
private const VALID_ALIGNMENTS = [
private static $VALID_ALIGNMENTS = [
self::LEFT => 1,
self::RIGHT => 1,
self::CENTER => 1,
@ -22,10 +20,12 @@ final class CellAlignment
];
/**
* @param string $cellAlignment
*
* @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
declare(strict_types=1);
namespace OpenSpout\Common\Entity\Style;
use OpenSpout\Common\Exception\InvalidColorException;
@ -9,11 +7,9 @@ use OpenSpout\Common\Exception\InvalidColorException;
/**
* 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 WHITE = 'FFFFFF';
public const RED = 'FF0000';
@ -36,7 +32,7 @@ final class 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($green);
@ -57,7 +53,7 @@ final class Color
*
* @return string ARGB color
*/
public static function toARGB(string $rgbColor): string
public static function toARGB($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 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}");
}
}
@ -81,7 +79,7 @@ final class Color
*
* @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);
}

View File

@ -1,160 +1,159 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Common\Entity\Style;
use OpenSpout\Common\Exception\InvalidArgumentException;
/**
* 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_COLOR = Color::BLACK;
public const DEFAULT_FONT_NAME = 'Arial';
/** @var int Style ID */
private int $id = -1;
/** @var null|int Style ID */
private $id;
/** @var bool Whether the font should be bold */
private bool $fontBold = false;
private $fontBold = false;
/** @var bool Whether the bold property was set */
private bool $hasSetFontBold = false;
private $hasSetFontBold = false;
/** @var bool Whether the font should be italic */
private bool $fontItalic = false;
private $fontItalic = false;
/** @var bool Whether the italic property was set */
private bool $hasSetFontItalic = false;
private $hasSetFontItalic = false;
/** @var bool Whether the font should be underlined */
private bool $fontUnderline = false;
private $fontUnderline = false;
/** @var bool Whether the underline property was set */
private bool $hasSetFontUnderline = false;
private $hasSetFontUnderline = false;
/** @var bool Whether the font should be struck through */
private bool $fontStrikethrough = false;
private $fontStrikethrough = false;
/** @var bool Whether the strikethrough property was set */
private bool $hasSetFontStrikethrough = false;
private $hasSetFontStrikethrough = false;
/** @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 */
private bool $hasSetFontSize = false;
private $hasSetFontSize = false;
/** @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 */
private bool $hasSetFontColor = false;
private $hasSetFontColor = false;
/** @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 */
private bool $hasSetFontName = false;
private $hasSetFontName = false;
/** @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 */
private bool $shouldApplyCellAlignment = false;
private $shouldApplyCellAlignment = false;
/** @var string Cell alignment */
private string $cellAlignment;
private $cellAlignment;
/** @var bool Whether the cell alignment property was set */
private bool $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;
private $hasSetCellAlignment = false;
/** @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 */
private bool $hasSetWrapText = false;
/** @var int Text rotation */
private int $textRotation = 0;
/** @var bool Whether the text rotation property was set */
private bool $hasSetTextRotation = false;
private $hasSetWrapText = false;
/** @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 */
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 */
private ?string $backgroundColor = null;
private $backgroundColor;
/** @var bool */
private $hasSetBackgroundColor = false;
/** @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;
}
public function setId(int $id): self
/**
* @param int $id
*
* @return Style
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
public function getBorder(): ?Border
/**
* @return null|Border
*/
public function getBorder()
{
return $this->border;
}
public function setBorder(Border $border): self
/**
* @return Style
*/
public function setBorder(Border $border)
{
$this->shouldApplyBorder = true;
$this->border = $border;
$this->isEmpty = false;
return $this;
}
public function isFontBold(): bool
/**
* @return bool
*/
public function shouldApplyBorder()
{
return $this->shouldApplyBorder;
}
/**
* @return bool
*/
public function isFontBold()
{
return $this->fontBold;
}
public function setFontBold(): self
/**
* @return Style
*/
public function setFontBold()
{
$this->fontBold = true;
$this->hasSetFontBold = true;
@ -164,17 +163,26 @@ final class Style
return $this;
}
public function hasSetFontBold(): bool
/**
* @return bool
*/
public function hasSetFontBold()
{
return $this->hasSetFontBold;
}
public function isFontItalic(): bool
/**
* @return bool
*/
public function isFontItalic()
{
return $this->fontItalic;
}
public function setFontItalic(): self
/**
* @return Style
*/
public function setFontItalic()
{
$this->fontItalic = true;
$this->hasSetFontItalic = true;
@ -184,17 +192,26 @@ final class Style
return $this;
}
public function hasSetFontItalic(): bool
/**
* @return bool
*/
public function hasSetFontItalic()
{
return $this->hasSetFontItalic;
}
public function isFontUnderline(): bool
/**
* @return bool
*/
public function isFontUnderline()
{
return $this->fontUnderline;
}
public function setFontUnderline(): self
/**
* @return Style
*/
public function setFontUnderline()
{
$this->fontUnderline = true;
$this->hasSetFontUnderline = true;
@ -204,17 +221,26 @@ final class Style
return $this;
}
public function hasSetFontUnderline(): bool
/**
* @return bool
*/
public function hasSetFontUnderline()
{
return $this->hasSetFontUnderline;
}
public function isFontStrikethrough(): bool
/**
* @return bool
*/
public function isFontStrikethrough()
{
return $this->fontStrikethrough;
}
public function setFontStrikethrough(): self
/**
* @return Style
*/
public function setFontStrikethrough()
{
$this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true;
@ -224,20 +250,28 @@ final class Style
return $this;
}
public function hasSetFontStrikethrough(): bool
/**
* @return bool
*/
public function hasSetFontStrikethrough()
{
return $this->hasSetFontStrikethrough;
}
public function getFontSize(): int
/**
* @return int
*/
public function getFontSize()
{
return $this->fontSize;
}
/**
* @param int $fontSize Font size, in pixels
*
* @return Style
*/
public function setFontSize(int $fontSize): self
public function setFontSize($fontSize)
{
$this->fontSize = $fontSize;
$this->hasSetFontSize = true;
@ -247,12 +281,18 @@ final class Style
return $this;
}
public function hasSetFontSize(): bool
/**
* @return bool
*/
public function hasSetFontSize()
{
return $this->hasSetFontSize;
}
public function getFontColor(): string
/**
* @return string
*/
public function getFontColor()
{
return $this->fontColor;
}
@ -261,8 +301,10 @@ final class Style
* Sets the font color.
*
* @param string $fontColor ARGB color (@see Color)
*
* @return Style
*/
public function setFontColor(string $fontColor): self
public function setFontColor($fontColor)
{
$this->fontColor = $fontColor;
$this->hasSetFontColor = true;
@ -272,20 +314,28 @@ final class Style
return $this;
}
public function hasSetFontColor(): bool
/**
* @return bool
*/
public function hasSetFontColor()
{
return $this->hasSetFontColor;
}
public function getFontName(): string
/**
* @return string
*/
public function getFontName()
{
return $this->fontName;
}
/**
* @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->hasSetFontName = true;
@ -295,30 +345,29 @@ final class Style
return $this;
}
public function hasSetFontName(): bool
/**
* @return bool
*/
public function hasSetFontName()
{
return $this->hasSetFontName;
}
public function getCellAlignment(): string
/**
* @return string
*/
public function getCellAlignment()
{
return $this->cellAlignment;
}
public function getCellVerticalAlignment(): string
{
return $this->cellVerticalAlignment;
}
/**
* @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->hasSetCellAlignment = 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
{
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
public function hasSetCellAlignment()
{
return $this->hasSetCellAlignment;
}
public function hasSetCellVerticalAlignment(): bool
{
return $this->hasSetCellVerticalAlignment;
}
/**
* @return bool Whether specific cell alignment should be applied
*/
public function shouldApplyCellAlignment(): bool
public function shouldApplyCellAlignment()
{
return $this->shouldApplyCellAlignment;
}
public function shouldApplyCellVerticalAlignment(): bool
{
return $this->shouldApplyCellVerticalAlignment;
}
public function shouldWrapText(): bool
/**
* @return bool
*/
public function shouldWrapText()
{
return $this->shouldWrapText;
}
/**
* @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->hasSetWrapText = true;
@ -384,37 +414,18 @@ final class Style
return $this;
}
public function hasSetWrapText(): bool
/**
* @return bool
*/
public function 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
*/
public function shouldApplyFont(): bool
public function shouldApplyFont()
{
return $this->shouldApplyFont;
}
@ -423,36 +434,66 @@ final class Style
* Sets the background 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->isEmpty = false;
return $this;
}
public function getBackgroundColor(): ?string
/**
* @return null|string
*/
public function getBackgroundColor()
{
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->isEmpty = false;
return $this;
}
public function getFormat(): ?string
/**
* @return null|string
*/
public function getFormat()
{
return $this->format;
}
/**
* @return bool Whether format should be applied
*/
public function shouldApplyFormat()
{
return $this->hasSetFormat;
}
public function isRegistered(): bool
{
return $this->isRegistered;
@ -464,6 +505,12 @@ final class Style
$this->isRegistered = true;
}
public function unmarkAsRegistered(): void
{
$this->setId(0);
$this->isRegistered = false;
}
public function isEmpty(): bool
{
return $this->isEmpty;
@ -471,8 +518,12 @@ final class Style
/**
* 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->shouldShrinkToFit = $shrinkToFit;
@ -483,12 +534,15 @@ final class Style
/**
* @return bool Whether format should be applied
*/
public function shouldShrinkToFit(): bool
public function shouldShrinkToFit()
{
return $this->shouldShrinkToFit;
}
public function hasSetShrinkToFit(): bool
/**
* @return bool
*/
public function 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
declare(strict_types=1);
namespace OpenSpout\Common\Helper;
use Error;
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_UTF16_LE = 'UTF-16LE';
public const ENCODING_UTF16_BE = 'UTF-16BE';
public const ENCODING_UTF32_LE = 'UTF-32LE';
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_UTF16_LE = "\xFF\xFE";
public const BOM_UTF16_BE = "\xFE\xFF";
public const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
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) */
private array $supportedEncodingsWithBom;
/** @var \OpenSpout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
private bool $canUseIconv;
/** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */
protected $supportedEncodingsWithBom;
private bool $canUseMbString;
public function __construct(bool $canUseIconv, bool $canUseMbString)
/**
* @param \OpenSpout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($globalFunctionsHelper)
{
$this->canUseIconv = $canUseIconv;
$this->canUseMbString = $canUseMbString;
$this->globalFunctionsHelper = $globalFunctionsHelper;
$this->supportedEncodingsWithBom = [
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.
*
@ -67,7 +53,7 @@ final readonly class EncodingHelper
*
* @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;
@ -87,11 +73,11 @@ final readonly class EncodingHelper
* @param string $string Non UTF-8 string to be converted
* @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);
}
@ -102,11 +88,11 @@ final readonly class EncodingHelper
* @param string $string UTF-8 string to be converted
* @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);
}
@ -119,17 +105,17 @@ final readonly class EncodingHelper
*
* @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;
rewind($filePointer);
$this->globalFunctionsHelper->rewind($filePointer);
if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
$potentialBom = $this->supportedEncodingsWithBom[$encoding];
$numBytesInBom = \strlen($potentialBom);
$hasBOM = (fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
}
return $hasBOM;
@ -143,47 +129,25 @@ final readonly class EncodingHelper
* @param string $sourceEncoding The encoding used to encode the source string
* @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 (null === $string || $sourceEncoding === $targetEncoding) {
if ($sourceEncoding === $targetEncoding) {
return $string;
}
$convertedString = null;
if ($this->canUseIconv) {
set_error_handler(static function (): bool {
return true;
});
$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;
}
if ($this->canUseIconv()) {
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
} elseif ($this->canUseMbString()) {
$convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
} 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) {
@ -192,4 +156,25 @@ final readonly class EncodingHelper
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
declare(strict_types=1);
namespace OpenSpout\Common\Helper\Escaper;
/**
* @internal
* Interface EscaperInterface.
*/
interface EscaperInterface
{
@ -16,7 +14,7 @@ interface EscaperInterface
*
* @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.
@ -25,5 +23,5 @@ interface EscaperInterface
*
* @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
declare(strict_types=1);
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 */
private bool $isAlreadyInitialized = false;
private $isAlreadyInitialized = false;
/** @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) */
private array $controlCharactersEscapingMap;
private $controlCharactersEscapingMap;
/** @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.
@ -28,12 +26,11 @@ final class XLSX implements EscaperInterface
*
* @return string The escaped string
*/
public function escape(string $string): string
public function escape($string)
{
$this->initIfNeeded();
$escapedString = $this->escapeControlCharacters($string);
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
// single/double quotes (for XML attributes) need to be encoded.
return htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8');
@ -46,7 +43,7 @@ final class XLSX implements EscaperInterface
*
* @return string The unescaped string
*/
public function unescape(string $string): string
public function unescape($string)
{
$this->initIfNeeded();
@ -62,7 +59,7 @@ final class XLSX implements EscaperInterface
/**
* Initializes the control characters if not already done.
*/
private function initIfNeeded(): void
protected function initIfNeeded()
{
if (!$this->isAlreadyInitialized) {
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
@ -76,7 +73,7 @@ final class XLSX implements EscaperInterface
/**
* @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
// some characters should not be escaped though: "\t", "\r" and "\n".
@ -98,16 +95,16 @@ final class XLSX implements EscaperInterface
*
* @return string[]
*/
private function getControlCharactersEscapingMap(): array
protected function getControlCharactersEscapingMap()
{
$controlCharactersEscapingMap = [];
// control characters values are from 0 to 1F (hex values) in the ASCII table
for ($charValue = 0x00; $charValue <= 0x1F; ++$charValue) {
$character = \chr($charValue);
if (1 === preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
$charHexValue = dechex($charValue);
$escapedChar = '_x'.\sprintf('%04s', strtoupper($charHexValue)).'_';
$escapedChar = '_x'.sprintf('%04s', strtoupper($charHexValue)).'_';
$controlCharactersEscapingMap[$escapedChar] = $character;
}
}
@ -127,13 +124,15 @@ final class XLSX implements EscaperInterface
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to escape
*
* @return string
*/
private function escapeControlCharacters(string $string): string
protected function escapeControlCharacters($string)
{
$escapedString = $this->escapeEscapeCharacter($string);
// if no control characters
if (1 !== preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
return $escapedString;
}
@ -149,7 +148,7 @@ final class XLSX implements EscaperInterface
*
* @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);
}
@ -166,8 +165,10 @@ final class XLSX implements EscaperInterface
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to unescape
*
* @return string
*/
private function unescapeControlCharacters(string $string): string
protected function unescapeControlCharacters($string)
{
$unescapedString = $string;
@ -186,7 +187,7 @@ final class XLSX implements EscaperInterface
*
* @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);
}

View File

@ -1,34 +1,24 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Common\Helper;
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 */
private string $baseFolderRealPath;
protected $baseFolderRealPath;
/**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
*/
public function __construct(string $baseFolderPath)
{
$realpath = realpath($baseFolderPath);
\assert(false !== $realpath);
$this->baseFolderRealPath = $realpath;
}
public function getBaseFolderRealPath(): string
{
return $this->baseFolderRealPath;
$this->baseFolderRealPath = realpath($baseFolderPath);
}
/**
@ -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 $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);
$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);
restore_error_handler();
if (!$wasCreationSuccessful) {
throw new IOException("Unable to create folder: {$folderPath} - {$errorMessage}");
throw new IOException("Unable to create folder: {$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 $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);
$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);
restore_error_handler();
if (false === $wasCreationSuccessful) {
throw new IOException("Unable to create file: {$filePath} - {$errorMessage}");
throw new IOException("Unable to create file: {$filePath}");
}
return $filePath;
@ -102,9 +76,9 @@ final readonly class FileSystemHelper implements FileSystemHelperInterface
*
* @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);
@ -118,15 +92,15 @@ final readonly class FileSystemHelper implements FileSystemHelperInterface
*
* @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);
$itemIterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
$itemIterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
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
*
* @throws IOException If the folder where the I/O operation should occur
* is not inside the base folder or the base folder does not exist
* @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
*/
private function throwIfOperationNotInBaseFolder(string $operationFolderPath): void
protected function throwIfOperationNotInBaseFolder(string $operationFolderPath)
{
$operationFolderRealPath = realpath($operationFolderPath);
if (false === $operationFolderRealPath) {
throw new IOException("Folder not found: {$operationFolderRealPath}");
if (!$this->baseFolderRealPath) {
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) {
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
}

View File

@ -1,13 +1,10 @@
<?php
declare(strict_types=1);
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
{
@ -17,11 +14,11 @@ interface FileSystemHelperInterface
* @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
*
* @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.
@ -31,27 +28,27 @@ interface FileSystemHelperInterface
* @param string $fileName The name 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.
*
* @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.
*
* @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
declare(strict_types=1);
namespace OpenSpout\Reader\CSV;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Common\Exception\EncodingConversionException;
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;
/**
* 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;
/** @var null|resource Pointer to the CSV file to read */
private $filePointer;
protected $filePointer;
/** @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 */
private ?Row $rowBuffer = null;
protected $rowBuffer;
/** @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 */
private readonly EncodingHelper $encodingHelper;
/** @var string Defines the character used to enclose fields (one character only) */
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
*/
public function __construct(
$filePointer,
Options $options,
EncodingHelper $encodingHelper
OptionsManagerInterface $optionsManager,
EncodingHelper $encodingHelper,
InternalEntityFactory $entityFactory,
GlobalFunctionsHelper $globalFunctionsHelper
) {
$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->entityFactory = $entityFactory;
$this->globalFunctionsHelper = $globalFunctionsHelper;
}
/**
@ -55,6 +78,7 @@ final class RowIterator implements RowIteratorInterface
*
* @see http://php.net/manual/en/iterator.rewind.php
*/
#[\ReturnTypeWillChange]
public function rewind(): void
{
$this->rewindAndSkipBom();
@ -70,9 +94,10 @@ final class RowIterator implements RowIteratorInterface
*
* @see http://php.net/manual/en/iterator.valid.php
*/
#[\ReturnTypeWillChange]
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
*
* @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
{
$this->hasReachedEndOfFile = feof($this->filePointer);
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if (!$this->hasReachedEndOfFile) {
$this->readDataForNextRow();
@ -96,6 +122,7 @@ final class RowIterator implements RowIteratorInterface
*
* @see http://php.net/manual/en/iterator.current.php
*/
#[\ReturnTypeWillChange]
public function current(): ?Row
{
return $this->rowBuffer;
@ -106,27 +133,37 @@ final class RowIterator implements RowIteratorInterface
*
* @see http://php.net/manual/en/iterator.key.php
*/
#[\ReturnTypeWillChange]
public function key(): int
{
return $this->numReadRows;
}
/**
* 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.
* Cleans up what was created to iterate over the object.
*/
private function rewindAndSkipBom(): void
#[\ReturnTypeWillChange]
public function end(): void
{
$byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->options->ENCODING);
// sets the cursor after the BOM (0 means no BOM, so rewind it)
fseek($this->filePointer, $byteOffsetToSkipBom);
// do nothing
}
/**
* @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 {
$rowData = $this->getNextUTF8EncodedRow();
@ -134,10 +171,8 @@ final class RowIterator implements RowIteratorInterface
if (false !== $rowData) {
// array_map will replace NULL values by empty strings
$rowDataBufferAsArray = array_map('\strval', $rowData);
$this->rowBuffer = new Row(array_map(static function ($cellValue) {
return Cell::fromValue($cellValue);
}, $rowDataBufferAsArray), null);
$rowDataBufferAsArray = array_map(function ($value) { return (string) $value; }, $rowData);
$this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
++$this->numReadRows;
} else {
// 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
*/
private function shouldReadNextRow($currentRowData): bool
protected function shouldReadNextRow($currentRowData)
{
$hasSuccessfullyFetchedRowData = (false !== $currentRowData);
$hasNowReachedEndOfFile = feof($this->filePointer);
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
$isEmptyLine = $this->isEmptyLine($currentRowData);
return
(!$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,
* 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(
$this->filePointer,
self::MAX_READ_BYTES_PER_LINE,
$this->options->FIELD_DELIMITER,
$this->options->FIELD_ENCLOSURE,
''
);
$encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
if (false === $encodedRowData) {
return false;
}
foreach ($encodedRowData as $cellIndex => $cellValue) {
switch ($this->options->ENCODING) {
switch ($this->encoding) {
case EncodingHelper::ENCODING_UTF16_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
@ -201,18 +231,18 @@ final class RowIterator implements RowIteratorInterface
break;
}
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->options->ENCODING);
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
}
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
*/
private function isEmptyLine($lineData): bool
protected function isEmptyLine($lineData)
{
return \is_array($lineData) && 1 === \count($lineData) && null === $lineData[0];
}

View File

@ -1,18 +1,13 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader\CSV;
use OpenSpout\Reader\SheetInterface;
/**
* @implements SheetInterface<RowIterator>
*/
final readonly class Sheet implements SheetInterface
class Sheet implements SheetInterface
{
/** @var RowIterator To iterate over the CSV's rows */
private RowIterator $rowIterator;
/** @var \OpenSpout\Reader\CSV\RowIterator To iterate over the CSV's rows */
protected $rowIterator;
/**
* @param RowIterator $rowIterator Corresponding row iterator
@ -22,7 +17,10 @@ final readonly class Sheet implements SheetInterface
$this->rowIterator = $rowIterator;
}
public function getRowIterator(): RowIterator
/**
* @return \OpenSpout\Reader\CSV\RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
@ -30,7 +28,7 @@ final readonly class Sheet implements SheetInterface
/**
* @return int Index of the sheet
*/
public function getIndex(): int
public function getIndex()
{
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
*/
public function getName(): string
public function getName()
{
return '';
}
@ -46,7 +44,15 @@ final readonly class Sheet implements SheetInterface
/**
* @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;
}

View File

@ -1,26 +1,24 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader\CSV;
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" */
private readonly Sheet $sheet;
protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */
private bool $hasReadUniqueSheet = false;
protected $hasReadUniqueSheet = false;
/**
* @param Sheet $sheet Corresponding unique sheet
*/
public function __construct(Sheet $sheet)
public function __construct($sheet)
{
$this->sheet = $sheet;
}
@ -30,6 +28,7 @@ final class SheetIterator implements SheetIteratorInterface
*
* @see http://php.net/manual/en/iterator.rewind.php
*/
#[\ReturnTypeWillChange]
public function rewind(): void
{
$this->hasReadUniqueSheet = false;
@ -40,6 +39,7 @@ final class SheetIterator implements SheetIteratorInterface
*
* @see http://php.net/manual/en/iterator.valid.php
*/
#[\ReturnTypeWillChange]
public function valid(): bool
{
return !$this->hasReadUniqueSheet;
@ -50,6 +50,7 @@ final class SheetIterator implements SheetIteratorInterface
*
* @see http://php.net/manual/en/iterator.next.php
*/
#[\ReturnTypeWillChange]
public function next(): void
{
$this->hasReadUniqueSheet = true;
@ -60,6 +61,7 @@ final class SheetIterator implements SheetIteratorInterface
*
* @see http://php.net/manual/en/iterator.current.php
*/
#[\ReturnTypeWillChange]
public function current(): Sheet
{
return $this->sheet;
@ -70,8 +72,18 @@ final class SheetIterator implements SheetIteratorInterface
*
* @see http://php.net/manual/en/iterator.key.php
*/
#[\ReturnTypeWillChange]
public function key(): int
{
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
declare(strict_types=1);
namespace OpenSpout\Reader\Common\Manager;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Reader\Common\Creator\InternalEntityFactoryInterface;
/**
* @internal
*/
final class RowManager
class RowManager
{
/** @var InternalEntityFactoryInterface Factory to create entities */
private $entityFactory;
/**
* @param InternalEntityFactoryInterface $entityFactory Factory to create entities
*/
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.
*
* @return Row
*/
public function fillMissingIndexesWithEmptyCells(Row $row): void
public function fillMissingIndexesWithEmptyCells(Row $row)
{
$numCells = $row->getNumCells();
if (0 === $numCells) {
return;
return $row;
}
$rowCells = $row->getCells();
@ -37,7 +62,7 @@ final class RowManager
for ($cellIndex = 0; $cellIndex < $maxCellIndex; ++$cellIndex) {
if (!isset($rowCells[$cellIndex])) {
$row->setCellAtIndex(Cell::fromValue(''), $cellIndex);
$row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
$needsSorting = true;
}
}
@ -47,5 +72,7 @@ final class RowManager
ksort($rowCells);
$row->setCells($rowCells);
}
return $row;
}
}

View File

@ -1,17 +1,13 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader\Common;
use OpenSpout\Reader\Exception\XMLProcessingException;
use OpenSpout\Reader\Wrapper\XMLReader;
use ReflectionMethod;
/**
* @internal
* Helps process XML files.
*/
final class XMLProcessor
class XMLProcessor
{
// Node types
public const NODE_TYPE_START = XMLReader::ELEMENT;
@ -25,16 +21,16 @@ final class XMLProcessor
public const PROCESSING_CONTINUE = 1;
public const PROCESSING_STOP = 2;
/** @var XMLReader The XMLReader object that will help read sheet's XML data */
private readonly XMLReader $xmlReader;
/** @var \OpenSpout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader;
/** @var array<string, array{reflectionMethod: ReflectionMethod, reflectionObject: object}> Registered callbacks */
private array $callbacks = [];
/** @var array Registered 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;
}
@ -43,8 +39,10 @@ final class XMLProcessor
* @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 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);
$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.
* 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()) {
$nodeType = $this->xmlReader->nodeType;
@ -84,7 +82,7 @@ final class XMLProcessor
*
* @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}";
}
@ -97,13 +95,13 @@ final class XMLProcessor
*
* @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];
$callbackMethodName = $callback[1];
$reflectionMethod = new ReflectionMethod($callbackObject, $callbackMethodName);
$reflectionMethod = new \ReflectionMethod(\get_class($callbackObject), $callbackMethodName);
$reflectionMethod->setAccessible(true);
return [
@ -117,9 +115,9 @@ final class XMLProcessor
* @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed
* @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):
// 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 XMLReader[] $args Arguments to pass to the callback
* @param array $callbackData Associative array containing data to invoke the callback using Reflection
* @param array $args Arguments to pass to the callback
*
* @return int Callback response
*/
private function invokeCallback(array $callbackData, array $args): int
private function invokeCallback($callbackData, $args)
{
$reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
$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
declare(strict_types=1);
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;
/**
* @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_FLOAT = 'float';
public const CELL_TYPE_BOOLEAN = 'boolean';
@ -30,9 +19,7 @@ final readonly class CellValueFormatter
public const CELL_TYPE_PERCENTAGE = 'percentage';
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_TEXT_A = 'text:a';
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_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_VALUE = 'office: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_C = 'text:c';
/**
* List of XML nodes representing whitespaces and their corresponding value.
*/
private const WHITESPACE_XML_NODES = [
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
protected $shouldFormatDates;
/** @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_TAB => "\t",
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 ODS $escaper Used to unescape XML data
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @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->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
*
* @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
*
* @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);
return match ($cellType) {
self::CELL_TYPE_STRING => $this->formatStringCellValue($node),
self::CELL_TYPE_FLOAT => $this->formatFloatCellValue($node),
self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($node),
self::CELL_TYPE_DATE => $this->formatDateCellValue($node),
self::CELL_TYPE_TIME => $this->formatTimeCellValue($node),
self::CELL_TYPE_CURRENCY => $this->formatCurrencyCellValue($node),
self::CELL_TYPE_PERCENTAGE => $this->formatPercentageCellValue($node),
default => '',
};
switch ($cellType) {
case self::CELL_TYPE_STRING:
return $this->formatStringCellValue($node);
case self::CELL_TYPE_FLOAT:
return $this->formatFloatCellValue($node);
case self::CELL_TYPE_BOOLEAN:
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.
*
* @param \DOMElement $node
*
* @return string The value associated with the cell
*/
private function formatStringCellValue(DOMElement $node): string
protected function formatStringCellValue($node)
{
$pNodeValues = [];
$pNodes = $node->getElementsByTagName(self::XML_NODE_P);
@ -123,9 +126,11 @@ final readonly class CellValueFormatter
/**
* Returns the cell Numeric value from the given node.
*
* @param \DOMElement $node
*
* @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);
@ -138,19 +143,27 @@ final readonly class CellValueFormatter
/**
* Returns the cell Boolean value from the given node.
*
* @param \DOMElement $node
*
* @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.
*
* @param \DOMElement $node
*
* @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:
// <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);
try {
$cellValue = new DateTimeImmutable($nodeValue);
} catch (Exception $previous) {
throw new InvalidValueException($nodeValue, '', 0, $previous);
$cellValue = new \DateTime($nodeValue);
} catch (\Exception $e) {
throw new InvalidValueException($nodeValue);
}
}
@ -178,11 +191,13 @@ final readonly class CellValueFormatter
/**
* 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
*
* @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:
// <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);
try {
$cellValue = new DateInterval($nodeValue);
} catch (Exception $previous) {
throw new InvalidValueException($nodeValue, '', 0, $previous);
$cellValue = new \DateInterval($nodeValue);
} catch (\Exception $e) {
throw new InvalidValueException($nodeValue);
}
}
@ -210,9 +225,11 @@ final readonly class CellValueFormatter
/**
* 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")
*/
private function formatCurrencyCellValue(DOMElement $node): string
protected function formatCurrencyCellValue($node)
{
$value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
$currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY);
@ -223,22 +240,29 @@ final readonly class CellValueFormatter
/**
* Returns the cell Percentage value from the given node.
*
* @param \DOMElement $node
*
* @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
return $this->formatFloatCellValue($node);
}
private function extractTextValueFromNode(DOMNode $pNode): string
/**
* @param \DOMNode $pNode
*
* @return string
*/
private function extractTextValueFromNode($pNode)
{
$textValue = '';
foreach ($pNode->childNodes as $childNode) {
if ($childNode instanceof DOMText) {
if ($childNode instanceof \DOMText) {
$textValue .= $childNode->nodeValue;
} elseif ($this->isWhitespaceNode($childNode->nodeName) && $childNode instanceof DOMElement) {
} elseif ($this->isWhitespaceNode($childNode->nodeName)) {
$textValue .= $this->transformWhitespaceNode($childNode);
} elseif (self::XML_NODE_TEXT_A === $childNode->nodeName || self::XML_NODE_TEXT_SPAN === $childNode->nodeName) {
$textValue .= $this->extractTextValueFromNode($childNode);
@ -253,10 +277,14 @@ final readonly class CellValueFormatter
* - <text:s />
* - <text:tab />
* - <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
*
* @param DOMElement $node The XML node representing a whitespace
* @param \DOMElement $node The XML node representing a whitespace
*
* @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>"
$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
declare(strict_types=1);
namespace OpenSpout\Reader\ODS\Helper;
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';
/**
* 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_ATTRIBUTE_CONFIG_NAME = 'config:name';
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
*
* @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)) {
return null;
}
@ -43,7 +50,7 @@ final class SettingsHelper
break;
}
}
} catch (XMLProcessingException) { // @codeCoverageIgnore
} catch (XMLProcessingException $exception) {
// 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
declare(strict_types=1);
namespace OpenSpout\Reader\ODS;
use DOMElement;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Row;
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\Exception\InvalidValueException;
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\RowIteratorInterface;
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_ROW = 'table:table-row';
public const XML_NODE_CELL = 'table:table-cell';
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_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 */
private readonly XMLProcessor $xmlProcessor;
/** @var \OpenSpout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
protected $xmlProcessor;
/** @var CellValueFormatter Helper to format cell values */
private readonly CellValueFormatter $cellValueFormatter;
/** @var bool Whether empty rows should be returned or skipped */
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 */
private bool $hasAlreadyBeenRewound = false;
protected $hasAlreadyBeenRewound = false;
/** @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 */
private ?Row $rowBuffer = null;
protected $rowBuffer;
/** @var bool Indicates whether all rows have been read */
private bool $hasReachedEndOfFile = false;
protected $hasReachedEndOfFile = false;
/** @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) */
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) */
private ?Cell $lastProcessedCell = null;
protected $lastProcessedCell;
/** @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 */
private int $numColumnsRepeated = 1;
protected $numColumnsRepeated = 1;
/** @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(
Options $options,
XMLReader $xmlReader,
OptionsManagerInterface $optionsManager,
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->entityFactory = $entityFactory;
$this->rowManager = $rowManager;
// Register all callbacks to process different nodes when reading the XML file
$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_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
$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
*
* @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
{
// 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
*/
#[\ReturnTypeWillChange]
public function valid(): bool
{
return !$this->hasReachedEndOfFile;
@ -127,9 +150,10 @@ final class RowIterator implements RowIteratorInterface
*
* @see http://php.net/manual/en/iterator.next.php
*
* @throws SharedStringNotFoundException If a shared string was not found
* @throws IOException If unable to read the sheet data XML
* @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML
*/
#[\ReturnTypeWillChange]
public function next(): void
{
if ($this->doesNeedDataForNextRowToBeProcessed()) {
@ -144,6 +168,7 @@ final class RowIterator implements RowIteratorInterface
*
* @see http://php.net/manual/en/iterator.current.php
*/
#[\ReturnTypeWillChange]
public function current(): Row
{
return $this->rowBuffer;
@ -154,11 +179,21 @@ final class RowIterator implements RowIteratorInterface
*
* @see http://php.net/manual/en/iterator.key.php
*/
#[\ReturnTypeWillChange]
public function key(): int
{
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.
* 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
*/
private function doesNeedDataForNextRowToBeProcessed(): bool
protected function doesNeedDataForNextRowToBeProcessed()
{
$hasReadAtLeastOneRow = (0 !== $this->lastRowIndexProcessed);
return
!$hasReadAtLeastOneRow
|| $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1;
|| $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1
;
}
/**
* @throws SharedStringNotFoundException If a shared string was not found
* @throws IOException If unable to read the sheet data XML
* @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @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();
$this->xmlProcessor->readUntilStopped();
try {
$this->xmlProcessor->readUntilStopped();
} catch (XMLProcessingException $exception) {
throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
}
$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
*/
private function processRowStartingNode(XMLReader $xmlReader): int
protected function processRowStartingNode($xmlReader)
{
// Reset data from current row
$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
*/
private function processCellStartingNode(XMLReader $xmlReader): int
protected function processCellStartingNode($xmlReader)
{
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
// NOTE: expand() will automatically decode all XML entities of the child nodes
/** @var DOMElement $node */
/** @var \DOMElement $node */
$node = $xmlReader->expand();
$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
*/
private function processRowEndingNode(): int
protected function processRowEndingNode()
{
$isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRow, $this->lastProcessedCell);
// 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
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
*/
private function processTableEndingNode(): int
protected function processTableEndingNode()
{
// The closing "</table:table>" marks the end of the file
$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
*/
private function getNumRowsRepeatedForCurrentNode(XMLReader $xmlReader): int
protected function getNumRowsRepeatedForCurrentNode($xmlReader)
{
$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
*/
private function getNumColumnsRepeatedForCurrentNode(XMLReader $xmlReader): int
protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
{
$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.
*
* @param \DOMElement $node
*
* @return Cell The cell set with the associated with the cell
*/
private function getCell(DOMElement $node): Cell
protected function getCell($node)
{
try {
$cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node);
$cell = Cell::fromValue($cellValue);
$cell = $this->entityFactory->createCell($cellValue);
} catch (InvalidValueException $exception) {
$cell = new Cell\ErrorCell($exception->getInvalidValue(), null);
$cell = $this->entityFactory->createCell($exception->getInvalidValue());
$cell->setType(Cell::TYPE_ERROR);
}
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
* 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
*
* @return bool Whether the row is empty
*/
private function isEmptyRow(Row $currentRow, ?Cell $lastReadCell): bool
protected function isEmptyRow($currentRow, $lastReadCell)
{
return
$currentRow->isEmpty()
&& (null === $lastReadCell || $lastReadCell instanceof Cell\EmptyCell);
$this->rowManager->isEmpty($currentRow)
&& (!isset($lastReadCell) || $lastReadCell->isEmpty())
;
}
}

View File

@ -1,30 +1,31 @@
<?php
declare(strict_types=1);
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 */
private RowIterator $rowIterator;
/** @var \OpenSpout\Reader\ODS\RowIterator To iterate over sheet's rows */
protected $rowIterator;
/** @var int ID of the sheet */
protected $id;
/** @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 */
private string $name;
protected $name;
/** @var bool Whether the sheet was the active one */
private bool $isActive;
protected $isActive;
/** @var bool Whether the sheet is visible */
private bool $isVisible;
protected $isVisible;
/**
* @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 $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->index = $sheetIndex;
@ -42,7 +43,10 @@ final readonly class Sheet implements SheetWithVisibilityInterface
$this->isVisible = $isSheetVisible;
}
public function getRowIterator(): RowIterator
/**
* @return \OpenSpout\Reader\ODS\RowIterator
*/
public function getRowIterator()
{
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)
*/
public function getIndex(): int
public function getIndex()
{
return $this->index;
}
@ -58,7 +62,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
/**
* @return string Name of the sheet
*/
public function getName(): string
public function getName()
{
return $this->name;
}
@ -66,7 +70,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
/**
* @return bool Whether the sheet was defined as active
*/
public function isActive(): bool
public function isActive()
{
return $this->isActive;
}
@ -74,7 +78,7 @@ final readonly class Sheet implements SheetWithVisibilityInterface
/**
* @return bool Whether the sheet is visible
*/
public function isVisible(): bool
public function isVisible()
{
return $this->isVisible;
}

View File

@ -1,31 +1,24 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader\ODS;
use DOMElement;
use OpenSpout\Common\Exception\IOException;
use OpenSpout\Common\Helper\Escaper\ODS;
use OpenSpout\Reader\Common\XMLProcessor;
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\SheetIteratorInterface;
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 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_STYLE_TABLE_PROPERTIES = 'table-properties';
public const XML_NODE_TABLE = 'table:table';
@ -35,37 +28,45 @@ final class SheetIterator implements SheetIteratorInterface
public const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display';
/** @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 */
private readonly XMLReader $xmlReader;
protected $xmlReader;
/** @var ODS Used to unescape XML data */
private readonly ODS $escaper;
/** @var \OpenSpout\Common\Helper\Escaper\ODS Used to unescape XML data */
protected $escaper;
/** @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) */
private int $currentSheetIndex;
protected $currentSheetIndex;
/** @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] */
private array $sheetsVisibility;
/** @var array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */
protected $sheetsVisibility;
public function __construct(
string $filePath,
Options $options,
ODS $escaper,
SettingsHelper $settingsHelper
) {
/**
* @param string $filePath Path of the file to be read
* @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager
* @param \OpenSpout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data
* @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->options = $options;
$this->xmlReader = new XMLReader();
$this->optionsManager = $optionsManager;
$this->entityFactory = $entityFactory;
$this->xmlReader = $entityFactory->createXMLReader();
$this->escaper = $escaper;
$this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
}
@ -75,9 +76,10 @@ final class SheetIterator implements SheetIteratorInterface
*
* @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();
@ -101,15 +103,13 @@ final class SheetIterator implements SheetIteratorInterface
* Checks if current position is valid.
*
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid(): bool
#[\ReturnTypeWillChange]
public function valid()
{
$valid = $this->hasFoundSheet;
if (!$valid) {
$this->xmlReader->close();
}
return $valid;
return $this->hasFoundSheet;
}
/**
@ -117,7 +117,8 @@ final class SheetIterator implements SheetIteratorInterface
*
* @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);
@ -130,29 +131,27 @@ final class SheetIterator implements SheetIteratorInterface
* Return the current element.
*
* @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);
\assert(null !== $escapedSheetName);
$sheetName = $this->escaper->unescape($escapedSheetName);
$isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName);
$sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME);
\assert(null !== $sheetStyleName);
$isSheetVisible = $this->isSheetVisible($sheetStyleName);
return new Sheet(
new RowIterator(
$this->options,
new CellValueFormatter($this->options->SHOULD_FORMAT_DATES, new ODS()),
new XMLProcessor($this->xmlReader)
),
return $this->entityFactory->createSheet(
$this->xmlReader,
$this->currentSheetIndex,
$sheetName,
$isSheetActive,
$isSheetVisible
$isSheetVisible,
$this->optionsManager
);
}
@ -160,33 +159,44 @@ final class SheetIterator implements SheetIteratorInterface
* Return the key of the current element.
*
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key(): int
#[\ReturnTypeWillChange]
public function key()
{
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.
*
* @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 = [];
$this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES);
/** @var \DOMElement $automaticStylesNode */
$automaticStylesNode = $this->xmlReader->expand();
\assert($automaticStylesNode instanceof DOMElement);
$tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES);
/** @var \DOMElement $tableStyleNode */
foreach ($tableStyleNodes as $tableStyleNode) {
$isSheetVisible = ('false' !== $tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY));
$parentStyleNode = $tableStyleNode->parentNode;
\assert($parentStyleNode instanceof DOMElement);
$styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME);
$sheetsVisibility[$styleName] = $isSheetVisible;
@ -204,13 +214,14 @@ final class SheetIterator implements SheetIteratorInterface
*
* @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
// or if no information about the active sheet was found, it defaults to the first sheet.
return
(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
*/
private function isSheetVisible(string $sheetStyleName): bool
private function isSheetVisible($sheetStyleName)
{
return $this->sheetsVisibility[$sheetStyleName] ??
true;

View File

@ -1,22 +1,65 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader;
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;
/**
* @template T of SheetIteratorInterface
*
* @implements ReaderInterface<T>
*/
abstract class AbstractReader implements ReaderInterface
abstract class ReaderAbstract implements ReaderInterface
{
/** @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
@ -24,9 +67,9 @@ abstract class AbstractReader implements ReaderInterface
*
* @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))) {
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)) {
// 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.");
}
if (!is_readable($filePath)) {
if (!$this->globalFunctionsHelper->is_readable($filePath)) {
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);
$this->openReader($fileRealPath);
$this->isStreamOpened = true;
} catch (ReaderException $exception) {
throw new IOException(
"Could not open {$filePath} for reading!",
0,
$exception
);
} catch (\Exception $exception) {
throw new IOException("Could not open {$filePath} for reading! ({$exception->getMessage()})");
}
}
/**
* 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.
*/
final public function close(): void
public function close()
{
if ($this->isStreamOpened) {
$this->closeReader();
$sheetIterator = $this->getConcreteSheetIterator();
if (null !== $sheetIterator) {
$sheetIterator->end();
}
$this->isStreamOpened = false;
}
}
/**
* 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.
*
* @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.
*/
abstract protected function closeReader(): void;
final protected function ensureStreamOpened(): void
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Reader should be opened first.');
}
}
abstract protected function closeReader();
/**
* Returns the real path of the given path.
* 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)) {
return $filePath;
}
// Need to use realpath to fix "Can't open file" on some Windows setup
$realpath = realpath($filePath);
\assert(false !== $realpath);
return $realpath;
return realpath($filePath);
}
/**
@ -116,10 +179,10 @@ abstract class AbstractReader implements ReaderInterface
*
* @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;
if (1 === preg_match('/^(\w+):\/\//', $filePath, $matches)) {
if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
$streamScheme = $matches[1];
}
@ -134,7 +197,7 @@ abstract class AbstractReader implements ReaderInterface
*
* @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);
}
@ -148,11 +211,13 @@ abstract class AbstractReader implements ReaderInterface
*
* @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);
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
*/
private function isPhpStream(string $filePath): bool
protected function isPhpStream($filePath)
{
$streamScheme = $this->getStreamWrapperScheme($filePath);

View File

@ -1,13 +1,9 @@
<?php
declare(strict_types=1);
namespace OpenSpout\Reader;
use OpenSpout\Common\Exception\IOException;
/**
* @template T of SheetIteratorInterface
* Interface ReaderInterface.
*/
interface ReaderInterface
{
@ -17,21 +13,21 @@ interface ReaderInterface
*
* @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.
*
* @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.
*/
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
declare(strict_types=1);
namespace OpenSpout\Reader\Wrapper;
use OpenSpout\Reader\Exception\XMLProcessingException;
/**
* @internal
* Trait XMLInternalErrorsHelper.
*/
trait XMLInternalErrorsHelper
{
/** @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,
* stores errors internally instead.
*/
private function useXMLInternalErrors(): void
protected function useXMLInternalErrors()
{
libxml_clear_errors();
$this->initialUseInternalErrorsValue = libxml_use_internal_errors(true);
@ -28,9 +26,9 @@ trait XMLInternalErrorsHelper
* Throws an XMLProcessingException if an error occured.
* 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()) {
$this->resetXMLInternalErrorsSetting();
@ -41,7 +39,7 @@ trait XMLInternalErrorsHelper
$this->resetXMLInternalErrorsSetting();
}
private function resetXMLInternalErrorsSetting(): void
protected function resetXMLInternalErrorsSetting()
{
libxml_use_internal_errors($this->initialUseInternalErrorsValue);
}
@ -51,7 +49,7 @@ trait XMLInternalErrorsHelper
*
* @return bool TRUE if an error occured, FALSE otherwise
*/
private function hasXMLErrorOccured(): bool
private function hasXMLErrorOccured()
{
return false !== libxml_get_last_error();
}
@ -61,11 +59,11 @@ trait XMLInternalErrorsHelper
*
* @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();
if (false !== $error) {

View File

@ -1,17 +1,13 @@
<?php
declare(strict_types=1);
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;
@ -25,13 +21,14 @@ final class XMLReader extends \XMLReader
*
* @return bool TRUE on success or FALSE on failure
*/
public function openFileInZip(string $zipFilePath, string $fileInsideZipPath): bool
public function openFileInZip($zipFilePath, $fileInsideZipPath)
{
$wasOpenSuccessful = false;
$realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath);
// 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.
// - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779)
if ($this->fileExistsWithinZip($realPathURI)) {
$wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET);
}
@ -48,17 +45,12 @@ final class XMLReader extends \XMLReader
*
* @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
$fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/');
$realpath = realpath($zipFilePath);
if (false === $realpath) {
throw new IOException("Could not open {$zipFilePath} for reading! File does not exist.");
}
return self::ZIP_WRAPPER.$realpath.'#'.$fileInsideZipPathWithoutLeadingSlash;
return self::ZIP_WRAPPER.realpath($zipFilePath).'#'.$fileInsideZipPathWithoutLeadingSlash;
}
/**
@ -66,9 +58,12 @@ final class XMLReader extends \XMLReader
*
* @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();
@ -84,11 +79,11 @@ final class XMLReader extends \XMLReader
*
* @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 {
$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
*
* @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();
@ -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
*/
public function isPositionedOnStartingNode(string $nodeName): bool
public function isPositionedOnStartingNode($nodeName)
{
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
*/
public function isPositionedOnEndingNode(string $nodeName): bool
public function isPositionedOnEndingNode($nodeName)
{
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
*/
public function getCurrentNodeName(): string
public function getCurrentNodeName()
{
return $this->localName;
}
@ -149,16 +151,16 @@ final class XMLReader extends \XMLReader
*
* @return bool TRUE if the file exists, FALSE otherwise
*/
private function fileExistsWithinZip(string $zipStreamURI): bool
protected function fileExistsWithinZip($zipStreamURI)
{
$doesFileExists = false;
$pattern = '/zip:\/\/([^#]+)#(.*)/';
if (1 === preg_match($pattern, $zipStreamURI, $matches)) {
if (preg_match($pattern, $zipStreamURI, $matches)) {
$zipFilePath = $matches[1];
$innerFilePath = $matches[2];
$zip = new ZipArchive();
$zip = new \ZipArchive();
if (true === $zip->open($zipFilePath)) {
$doesFileExists = (false !== $zip->locateName($innerFilePath));
$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
*/
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>").
@ -179,7 +184,7 @@ final class XMLReader extends \XMLReader
*
* @see https://github.com/box/spout/issues/233
*/
$hasPrefix = str_contains($nodeName, ':');
$hasPrefix = (false !== strpos($nodeName, ':'));
$currentNodeName = ($hasPrefix) ? $this->name : $this->localName;
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