Skip to content

Commit

Permalink
Adding Writer::noEnclosure method
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Dec 10, 2024
1 parent 42a8b36 commit be9fef0
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 30 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ All Notable changes to `Csv` will be documented in this file

- `XMLConverter::formatter`
- `HTMLConverter::formatter`
- `Writer::encloseNone`
- `Writer::encloseRelax`
- `Writer::noEnclosure`

### Deprecated

Expand All @@ -16,6 +19,7 @@ All Notable changes to `Csv` will be documented in this file
### Fixed

- `JsonConverter::formatter` now accepts callable before only `Closure` where accepted.
- The protected property `Writer::$enclose_all` is no longer a boolean but an integer

### Removed

Expand Down
1 change: 0 additions & 1 deletion docs/9.0/converter/html.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ This method sets the optional attribute name for the field name on the HTML `td`

<p class="message-info">If none is used or an empty string is given, the field name information won't be exported to the HTML table.</p>


### HTMLConverter::formatter

<p class="message-info">New feature introduced in version <code>9.20.0</code></p>
Expand Down
2 changes: 1 addition & 1 deletion docs/9.0/converter/xml.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public XMLConverter::formatter(?callback $formatter): mixed
```

This method allow to apply a callback prior to converting your collection individual item.
This callback allows you to specify how each item will be converted. The formatter should
This callback allows you to specify how each item will be converted. The formatter should
return an associative array suitable for conversion.

## Conversion
Expand Down
15 changes: 12 additions & 3 deletions docs/9.0/writer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,28 @@ By default, `getFlushTreshold` returns `null`.
<p class="message-info"><code>Writer::insertAll</code> always flushes its buffer when all records are inserted, regardless of the threshold value.</p>
<p class="message-info">If set to <code>null</code> the inner flush mechanism of PHP's <code>fputcsv</code> will be used.</p>

## Force Enclosure
## Enclosure

<p class="message-info">Since version <code>9.10.0</code> you can provide control the presence of enclosure around all records.</p>
<p class="message-info">Since version <code>9.20.0</code> generating CSV without enclosure is allowed.</p>

```php
public Writer::forceEnclosure(): self
public Writer::relaxEnclosure(): self
public Writer::noEnclosure(): self
public Writer::encloseAll(): bool
public Writer::encloseSelective(): bool
public Writer::encloseNone(): bool
```

By default, the `Writer` adds enclosures only around records that requires them. For all other records no enclosure character is present,
With this feature, you can force the enclosure to be present on every record entry or CSV cell. The inclusion will respect
the presence of enclosures inside the cell content as well as the presence of PHP's escape character.
With this feature, you can:

- force the enclosure to be present on every record entry or CSV cell;
- completely remove the presence of enclosure;

<p class="message-warning">When this feature is activated the <code>$escape</code> character is completely ignored (it is the same as setting it to the empty string.)</p>
<p class="message-warning">When no enclosure is used, the library <strong>DO NOT CHECK</strong> for multiline fields.</p>

```php
<?php
Expand Down
7 changes: 4 additions & 3 deletions src/HTMLConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace League\Csv;

use Closure;
use Deprecated;
use DOMDocument;
use DOMElement;
Expand All @@ -30,8 +31,8 @@ class HTMLConverter
/** table id attribute value. */
protected string $id_value = '';
protected XMLConverter $xml_converter;
/** @var ?callable(array, array-key): array */
protected mixed $formatter = null;
/** @var ?Closure(array, array-key): array */
protected ?Closure $formatter = null;

public static function create(): self
{
Expand Down Expand Up @@ -174,7 +175,7 @@ public function td(string $fieldname_attribute_name): self
public function formatter(?callable $formatter): self
{
$clone = clone $this;
$clone->formatter = $formatter;
$clone->formatter = ($formatter instanceof Closure || null === $formatter) ? $formatter : $formatter(...);

return $clone;
}
Expand Down
6 changes: 3 additions & 3 deletions src/JsonConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ final class JsonConverter
public readonly int $depth;
/** @var int<1, max> */
public readonly int $indentSize;
/** @var ?callable(T, array-key): mixed */
public readonly mixed $formatter;
/** @var ?Closure(T, array-key): mixed */
public readonly ?Closure $formatter;
/** @var int<1, max> */
public readonly int $chunkSize;
/** @var non-empty-string */
Expand Down Expand Up @@ -151,7 +151,7 @@ private function __construct(int $flags, int $depth, int $indentSize, ?callable
$this->flags = $flags;
$this->depth = $depth;
$this->indentSize = $indentSize;
$this->formatter = $formatter;
$this->formatter = ($formatter instanceof Closure || null === $formatter) ? $formatter : $formatter(...);
$this->chunkSize = $chunkSize;

// Initialize settings and closure to use for conversion.
Expand Down
2 changes: 1 addition & 1 deletion src/Query/Constraint/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static function filterOn(
}

if (is_callable($operator)) {
return new self($column, Closure::fromCallable($operator), $value);
return new self($column, $operator(...), $value);
}

return new self(
Expand Down
16 changes: 7 additions & 9 deletions src/Query/Ordering/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,13 @@ public static function sortOn(
default => throw new QueryException('Unknown or unsupported ordering operator value: '.$direction),
};

if (is_callable($callback)) {
$callback = Closure::fromCallable($callback);
}

return new self(
$operator,
$column,
$callback ?? static fn (mixed $first, mixed $second): int => $first <=> $second
);
$callback = match (true) {
null === $callback => static fn (mixed $first, mixed $second): int => $first <=> $second,
$callback instanceof Closure => $callback,
default => $callback(...),
};

return new self($operator, $column, $callback);
}

/**
Expand Down
42 changes: 36 additions & 6 deletions src/Writer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
*/
class Writer extends AbstractCsv implements TabularDataWriter
{
protected const ENCLOSE_ALL = 1;
protected const ENCLOSE_SELECTIVE = 0;
protected const ENCLOSE_NONE = -2;

protected const STREAM_FILTER_MODE = STREAM_FILTER_WRITE;
/** @var array<callable> callable collection to format the record before insertion. */
protected array $formatters = [];
Expand All @@ -38,7 +42,7 @@ class Writer extends AbstractCsv implements TabularDataWriter
protected string $newline = "\n";
protected int $flush_counter = 0;
protected ?int $flush_threshold = null;
protected bool $enclose_all = false;
protected int $enclose_all = self::ENCLOSE_SELECTIVE;
/** @var array{0:array<string>,1:array<string>} */
protected array $enclosure_replace = [[], []];
/** @var Closure(array): (int|false) */
Expand All @@ -54,14 +58,15 @@ protected function resetProperties(): void
];

$this->insertRecord = fn (array $record): int|false => match ($this->enclose_all) {
true => $this->document->fwrite(implode(
self::ENCLOSE_ALL => $this->document->fwrite(implode(
$this->delimiter,
array_map(
fn ($content) => $this->enclosure.$content.$this->enclosure,
str_replace($this->enclosure_replace[0], $this->enclosure_replace[1], $record)
)
).$this->newline),
false => $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape, $this->newline),
self::ENCLOSE_NONE => $this->document->fwrite(implode($this->delimiter, $record).$this->newline),
default => $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape, $this->newline),
};
}

Expand All @@ -86,7 +91,24 @@ public function getFlushThreshold(): ?int
*/
public function encloseAll(): bool
{
return $this->enclose_all;
return self::ENCLOSE_ALL === $this->enclose_all;
}

/**
* Tells whether new entries will never be enclosed on writing.
*/
public function encloseNone(): bool
{
return self::ENCLOSE_NONE === $this->enclose_all;
}

/**
* Tells whether new entries will be selectively enclosed on writing
* if the field content requires encoding.
*/
public function encloseSelective(): bool
{
return self::ENCLOSE_SELECTIVE === $this->enclose_all;
}

/**
Expand Down Expand Up @@ -206,15 +228,23 @@ public function setFlushThreshold(?int $threshold): self

public function relaxEnclosure(): self
{
$this->enclose_all = false;
$this->enclose_all = self::ENCLOSE_SELECTIVE;
$this->resetProperties();

return $this;
}

public function forceEnclosure(): self
{
$this->enclose_all = true;
$this->enclose_all = self::ENCLOSE_ALL;
$this->resetProperties();

return $this;
}

public function noEnclosure(): self
{
$this->enclose_all = self::ENCLOSE_NONE;
$this->resetProperties();

return $this;
Expand Down
42 changes: 42 additions & 0 deletions src/WriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ public function testInsertNormalFile(): void
{
$csv = Writer::createFromPath(__DIR__.'/../test_files/foo.csv', 'a+');
$csv->insertOne(['jane', 'doe', 'jane.doe@example.com']);

self::assertFalse($csv->encloseAll());
self::assertFalse($csv->encloseNone());
self::assertTrue($csv->encloseSelective());
self::assertStringContainsString('jane,doe,jane.doe@example.com', $csv->toString());
}

Expand Down Expand Up @@ -303,6 +307,44 @@ public function testEncloseAll(): void
"1996"|"Jeep"|"Grand Cherokee"|"MUST SELL!
air, moon roof, loaded"|"4799.00"
CSV;

self::assertTrue($csv->encloseAll());
self::assertFalse($csv->encloseNone());
self::assertFalse($csv->encloseSelective());
self::assertStringContainsString($expected, $csv->toString());
}

public function testEncloseNothing(): void
{
/**
* @see https://en.wikipedia.org/wiki/Comma-separated_values#Example
*/
$records = [
['Year', 'Make', 'Model', 'Description', 'Price'],
[1997, 'Ford', 'E350', 'ac,abs,moon', '3000.00'],
[1999, 'Chevy', 'Venture "Extended Edition"', null, '4900.00'],
[1999, 'Chevy', 'Venture "Extended Edition, Very Large"', null, '5000.00'],
[1996, 'Jeep', 'Grand Cherokee', 'MUST SELL!
air, moon roof, loaded', '4799.00'],
];

$csv = Writer::createFromString();
$csv->setDelimiter('|');
$csv->noEnclosure();
$csv->insertAll($records);

$expected = <<<CSV
Year|Make|Model|Description|Price
1997|Ford|E350|ac,abs,moon|3000.00
1999|Chevy|Venture "Extended Edition"||4900.00
1999|Chevy|Venture "Extended Edition, Very Large"||5000.00
1996|Jeep|Grand Cherokee|MUST SELL!
air, moon roof, loaded|4799.00
CSV;

self::assertFalse($csv->encloseAll());
self::assertTrue($csv->encloseNone());
self::assertFalse($csv->encloseSelective());
self::assertStringContainsString($expected, $csv->toString());
}
}
7 changes: 4 additions & 3 deletions src/XMLConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace League\Csv;

use Closure;
use DOMAttr;
use DOMDocument;
use DOMElement;
Expand All @@ -34,8 +35,8 @@ class XMLConverter
protected string $column_attr = '';
/** XML offset attribute name. */
protected string $offset_attr = '';
/** @var ?callable(array, array-key): array */
public mixed $formatter = null;
/** @var ?Closure(array, array-key): array */
public ?Closure $formatter = null;

public static function create(): self
{
Expand Down Expand Up @@ -161,7 +162,7 @@ public function rootElement(string $node_name): self
public function formatter(?callable $formatter): self
{
$clone = clone $this;
$clone->formatter = $formatter;
$clone->formatter = ($formatter instanceof Closure || null === $formatter) ? $formatter : $formatter(...);

return $clone;
}
Expand Down

0 comments on commit be9fef0

Please sign in to comment.