Skip to content

Commit 3991cf8

Browse files
authored
Add raw parameter on AdapterInterface and let OpenSpout exporter use it (#366)
* Add raw parameter on AdapterInterface and let OpenSpout exporter use it This prevents calling normalize() and render() on the column during export with the ExcelOpenSpoutExporter. This allows the exporter to decide how PHP values are represented in the output file. * Do not cache OptionsResolvers in `AbstractColumn::$resolversByClass` This fixes a tricky bug. When you initialize a BoolColumn for the second time, the previously configured OptionsResolver is used. In this resolver, the default `rightExpr` option value is a callable where $this refers to the BoolColumn that was created during the first initialization. This might be a different BoolColumn instance. The call to $this->getTrueValue() inside the `rightExpr` callable will then return the wrong value. I'm not sure if this resolver 'cache' was added after profiling or as a precautionary measure. If it was just a precaution, I would remove it as in this commit, as performance optimizations should always only be added as a result of profiling. If this was truly done to fix a performance issue, then a different fix is necessary.
1 parent 6efccf5 commit 3991cf8

File tree

11 files changed

+180
-84
lines changed

11 files changed

+180
-84
lines changed

src/Adapter/AbstractAdapter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct()
3131
$this->accessor = PropertyAccess::createPropertyAccessor();
3232
}
3333

34-
final public function getData(DataTableState $state): ResultSetInterface
34+
final public function getData(DataTableState $state, bool $raw = false): ResultSetInterface
3535
{
3636
$query = new AdapterQuery($state);
3737

@@ -41,7 +41,7 @@ final public function getData(DataTableState $state): ResultSetInterface
4141
$transformer = $state->getDataTable()->getTransformer();
4242
$identifier = $query->getIdentifierPropertyPath();
4343

44-
$data = (function () use ($query, $identifier, $transformer, $propertyMap) {
44+
$data = (function () use ($query, $identifier, $transformer, $propertyMap, $raw) {
4545
foreach ($this->getResults($query) as $result) {
4646
$row = [];
4747
if (!empty($identifier)) {
@@ -51,7 +51,7 @@ final public function getData(DataTableState $state): ResultSetInterface
5151
/** @var AbstractColumn $column */
5252
foreach ($propertyMap as list($column, $mapping)) {
5353
$value = ($mapping && $this->accessor->isReadable($result, $mapping)) ? $this->accessor->getValue($result, $mapping) : null;
54-
$row[$column->getName()] = $column->transform($value, $result);
54+
$row[$column->getName()] = $column->transform($value, $result, raw: $raw);
5555
}
5656
if (null !== $transformer) {
5757
$row = call_user_func($transformer, $row, $result);

src/Adapter/AdapterInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ public function configure(array $options): void;
3131
/**
3232
* Processes a datatable's state into a result set fit for further processing.
3333
*/
34-
public function getData(DataTableState $state): ResultSetInterface;
34+
public function getData(DataTableState $state, bool $raw = false): ResultSetInterface;
3535
}

src/Adapter/ArrayAdapter.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function configure(array $options): void
3333
$this->accessor = PropertyAccess::createPropertyAccessor();
3434
}
3535

36-
public function getData(DataTableState $state): ResultSetInterface
36+
public function getData(DataTableState $state, bool $raw = false): ResultSetInterface
3737
{
3838
// Very basic implementation of sorting
3939
try {
@@ -64,7 +64,7 @@ public function getData(DataTableState $state): ResultSetInterface
6464
}
6565
}
6666

67-
$data = iterator_to_array($this->processData($state, $this->data, $map));
67+
$data = iterator_to_array($this->processData($state, $this->data, $map, $raw));
6868

6969
$length = $state->getLength() ?? 0;
7070
$page = $length > 0 ? array_slice($data, $state->getStart(), $state->getLength()) : $data;
@@ -77,12 +77,12 @@ public function getData(DataTableState $state): ResultSetInterface
7777
* @param array<string, string> $map
7878
* @return \Generator<mixed[]>
7979
*/
80-
protected function processData(DataTableState $state, array $data, array $map): \Generator
80+
protected function processData(DataTableState $state, array $data, array $map, bool $raw): \Generator
8181
{
8282
$transformer = $state->getDataTable()->getTransformer();
8383
$search = $state->getGlobalSearch() ?: '';
8484
foreach ($data as $result) {
85-
if ($row = $this->processRow($state, $result, $map, $search)) {
85+
if ($row = $this->processRow($state, $result, $map, $search, $raw)) {
8686
if (null !== $transformer) {
8787
$row = call_user_func($transformer, $row, $result);
8888
}
@@ -96,13 +96,13 @@ protected function processData(DataTableState $state, array $data, array $map):
9696
* @param array<string, string> $map
9797
* @return mixed[]|null
9898
*/
99-
protected function processRow(DataTableState $state, array $result, array $map, string $search): ?array
99+
protected function processRow(DataTableState $state, array $result, array $map, string $search, bool $raw): ?array
100100
{
101101
$row = [];
102102
$match = empty($search);
103103
foreach ($state->getDataTable()->getColumns() as $column) {
104104
$value = (!empty($propertyPath = $map[$column->getName()]) && $this->accessor->isReadable($result, $propertyPath)) ? $this->accessor->getValue($result, $propertyPath) : null;
105-
$value = $column->transform($value, $result);
105+
$value = $column->transform($value, $result, raw: $raw);
106106
if (!$match) {
107107
$match = (false !== mb_stripos($value, $search));
108108
}

src/Column/AbstractColumn.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@
2424
*/
2525
abstract class AbstractColumn
2626
{
27-
/** @var array<string, OptionsResolver> */
28-
private static array $resolversByClass = [];
29-
3027
private string $name;
3128
private int $index;
3229
private DataTable $dataTable;
@@ -43,21 +40,19 @@ public function initialize(string $name, int $index, array $options, DataTable $
4340
$this->index = $index;
4441
$this->dataTable = $dataTable;
4542

46-
$class = get_class($this);
47-
if (!isset(self::$resolversByClass[$class])) {
48-
self::$resolversByClass[$class] = new OptionsResolver();
49-
$this->configureOptions(self::$resolversByClass[$class]);
50-
}
51-
$this->options = self::$resolversByClass[$class]->resolve($options);
43+
$resolver = new OptionsResolver();
44+
$this->configureOptions($resolver);
45+
$this->options = $resolver->resolve($options);
5246
}
5347

5448
/**
5549
* The transform function is responsible for converting column-appropriate input to a datatables-usable type.
5650
*
5751
* @param mixed $value The single value of the column, if mapping makes it possible to derive one
5852
* @param mixed $context All relevant data of the entire row
53+
* @param bool $raw if true, will not normalize the value to string and will not call `AbstractColumn::render()`
5954
*/
60-
public function transform(mixed $value = null, mixed $context = null): mixed
55+
public function transform(mixed $value = null, mixed $context = null, bool $raw = false): mixed
6156
{
6257
$data = $this->getData();
6358
if (is_callable($data)) {
@@ -66,7 +61,7 @@ public function transform(mixed $value = null, mixed $context = null): mixed
6661
$value = $data;
6762
}
6863

69-
return $this->render($this->normalize($value), $context);
64+
return ($raw) ? $value : $this->render($this->normalize($value), $context);
7065
}
7166

7267
/**

src/Exporter/AbstractDataTableExporter.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@
1414

1515
use Symfony\Component\OptionsResolver\OptionsResolver;
1616

17+
/**
18+
* Default implementation for DataTableExporterInterface.
19+
*/
1720
abstract class AbstractDataTableExporter implements DataTableExporterInterface
1821
{
1922
#[\Override]
2023
public function configureColumnOptions(OptionsResolver $resolver): void
2124
{
2225
}
26+
27+
public function supportsRawData(): bool
28+
{
29+
return false;
30+
}
2331
}

src/Exporter/DataTableExporterInterface.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,16 @@ public function getName(): string;
4646
* `exporterOptions` of AbstractColumn.
4747
*/
4848
public function configureColumnOptions(OptionsResolver $resolver): void;
49+
50+
/**
51+
* Returns whether the exporter supports non-string data.
52+
*
53+
* The exporter should convert input types to the appropriate output types (e.g. an
54+
* int becomes a number type in Excel). Non-supported types should be cast to a
55+
* string.
56+
*
57+
* When this is true, `AbstractColumn::normalize()` and `AbstractColumn::render()`
58+
* will not be called.
59+
*/
60+
public function supportsRawData(): bool;
4961
}

src/Exporter/DataTableExporterManager.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ public function getExporter(): DataTableExporterInterface
9090
*/
9191
public function getExport(): \SplFileInfo
9292
{
93-
return $this->getExporter()->export($this->getColumnNames(), $this->getAllData(), $this->getColumnOptions());
93+
$exporter = $this->getExporter();
94+
95+
return $exporter->export($this->getColumnNames(), $this->getAllData($exporter->supportsRawData()), $this->getColumnOptions());
9496
}
9597

9698
/**
@@ -133,11 +135,11 @@ private function getColumnNames(): array
133135
* A Generator is created in order to remove the 'DT_RowId' key
134136
* which is created by some adapters (e.g. ORMAdapter).
135137
*/
136-
private function getAllData(): \Iterator
138+
private function getAllData(bool $raw): \Iterator
137139
{
138140
$data = $this->dataTable
139141
->getAdapter()
140-
->getData($this->dataTable->getState()->setStart(0)->setLength(null))
142+
->getData($this->dataTable->getState()->setStart(0)->setLength(null), raw: $raw)
141143
->getData();
142144

143145
foreach ($data as $row) {

src/Exporter/Excel/ExcelOpenSpoutExporter.php

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ public function export(array $columnNames, \Iterator $data, array $columnOptions
5555
}
5656

5757
if (is_string($value)) {
58-
// The data that we get may contain rich HTML. But OpenSpout does not support this.
59-
// We just strip all HTML tags and unescape the remaining text.
60-
$value = htmlspecialchars_decode(strip_tags($value), ENT_QUOTES | ENT_SUBSTITUTE);
58+
// Previously, we stripped HTML tags and unescaped the value, because the value was passed through
59+
// AbstractColumn::render() which would have escaped special chars and could have added HTML tags.
60+
//
61+
// Now that we have raw data, we don't need to do that anymore.
62+
//
63+
// $value = htmlspecialchars_decode(strip_tags($value), ENT_QUOTES | ENT_SUBSTITUTE);
6164

6265
// Excel has a limit of 32,767 characters per cell
6366
if (mb_strlen($value) > static::MAX_CHARACTERS_PER_CELL) {
@@ -67,7 +70,8 @@ public function export(array $columnNames, \Iterator $data, array $columnOptions
6770
}
6871

6972
// Do not wrap text
70-
$row->addCell(Cell::fromValue($value, $this->resolveStyleOption($options['style'], $value)));
73+
$style = $this->resolveStyleOption($options['style'], $value);
74+
$row->addCell($this->normalizeToCell($value, $style));
7175

7276
next($columnOptions);
7377
}
@@ -108,6 +112,25 @@ private function resolveStyleOption(Style|callable $style, mixed $value): Style
108112
return $style instanceof Style ? $style : $style($value);
109113
}
110114

115+
private function normalizeToCell(mixed $value, ?Style $style = null): Cell
116+
{
117+
if (
118+
is_scalar($value) // (bool, int, float, string)
119+
|| null === $value
120+
|| $value instanceof \DateTimeInterface
121+
|| $value instanceof \DateInterval
122+
) {
123+
return Cell::fromValue($value, $style);
124+
} else {
125+
// Try casting to string, else put an error message in the cell
126+
try {
127+
return Cell::fromValue((string) $value, $style);
128+
} catch (\Throwable $e) {
129+
return Cell::fromValue($e->getMessage(), (new Style())->setFontItalic());
130+
}
131+
}
132+
}
133+
111134
public function getMimeType(): string
112135
{
113136
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
@@ -130,4 +153,10 @@ public function configureColumnOptions(OptionsResolver $resolver): void
130153
->setAllowedTypes('columnWidth', ['int', 'float'])
131154
;
132155
}
156+
157+
#[\Override]
158+
public function supportsRawData(): bool
159+
{
160+
return true;
161+
}
133162
}

0 commit comments

Comments
 (0)