-
-
Notifications
You must be signed in to change notification settings - Fork 338
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve CallbackStreamFilter implementation
- Loading branch information
Showing
15 changed files
with
593 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,187 @@ | ||
--- | ||
layout: default | ||
title: Dynamic Stream Filter | ||
title: Custom Stream Filter | ||
--- | ||
|
||
# Callback Stream Filter | ||
# Custom Stream Filter | ||
|
||
<p class="message-info">Available since version <code>9.22.0</code></p> | ||
<p class="message-info">Available since version <code>9.21.0</code></p> | ||
|
||
Sometimes you may encounter a scenario where you need to create a specific stream filter | ||
to resolve a specific issue. Instead of having to put up with the hassle of creating a | ||
fully fledge stream filter, we are introducing a `CallbackStreamFilter`. This filter | ||
is a PHP stream filter which enables applying a callable onto the stream prior to it | ||
being actively consumed by the CSV process. | ||
|
||
## Usage with CSV objects | ||
## Registering the callbacks | ||
|
||
Out of the box, to work, the feature requires a callback and its associated unique filter name. | ||
|
||
```php | ||
use League\Csv\CallbackStreamFilter; | ||
|
||
CallbackStreamFilter::register('myapp.to.upper', strtoupper(...)); | ||
``` | ||
|
||
<p class="message-warning"><code>CallbackStreanFilter::register</code> register your callback | ||
globally. So you only need to register it once. Preferably in your container definition if you | ||
are using a framework.</p> | ||
|
||
The callback signature is the following | ||
|
||
```php | ||
callable(string $bucket [, mixed $params]): string | ||
``` | ||
|
||
- the `$bucket` parameter represents the chunk of the stream you will be operating on. | ||
- the `$params` represents additional parameter you may pass onto the callback when it is being attached. This parameter is optional | ||
|
||
Once registered you can re-use the filter via its `$filtername` with CSV documents or with a resource. | ||
|
||
You can always check for the existence of your registered filter by calling the `CallbackStreamFilter::isRegistered` method. | ||
The method will only return `true` for filter registered via the class. It will return `false` otherwise. | ||
|
||
```php | ||
CallbackStreamFilter::isRegistered('myapp.to.upper'); | ||
//returns true - exists; was registered in the previous example | ||
CallbackStreamFilter::isRegistered('myapp.to.lower'); | ||
//returns false - does not exist; is not registered by CallbackStreamFilter | ||
CallbackStreamFilter::isRegistered('string.tolower'); | ||
//returns false - exits, is registered by PHP itself not by CallbackStreamFilter | ||
``` | ||
|
||
Last but not least you can always list all the registered filter names by calling the | ||
|
||
```php | ||
CallbackStreamFilter::registeredFilterNames(); // returns a list | ||
``` | ||
|
||
<p class="message-info">To avoid conflict with already registered stream filters a best | ||
practice is to namespace your own filter by using a unique prefix. Instead of | ||
naming it <code>string.to.lower</code> you should name it <code><strong>myapp.</strong>string.to.lower</code> | ||
where <code>myapp</code> is specific for your own codebase.</p> | ||
|
||
## Applying the callback | ||
|
||
Out of the box, the filter can not work, it requires a unique name and a callback to be usable. | ||
Once registered you can re-use the filter with CSV documents or with a resource. | ||
Once registered you can use one of the following methods to attach your filter to your instance. | ||
|
||
let's imagine we have a CSV document with the return carrier character as the end of line character. | ||
This type of document is parsable by the package but only if you enable the deprecated `auto_detect_line_endings`. | ||
- `CallbackStreamFilter::appendOnReadTo` | ||
- `CallbackStreamFilter::appendOnWriteTo` | ||
- `CallbackStreamFilter::prependOnReadTo` | ||
- `CallbackStreamFilter::prependOnWriteTo` | ||
|
||
If you no longer want to rely on that feature since it emits a deprecation warning you can use the new | ||
`CallbackStreamFilter` instead by swaping the offending character with a modern alternative. | ||
Those static public methods will all add the filter to the stream filter queue attached to the structure | ||
(League/CSV objects or PHP stream resource). They all share the same signature and only differ in: | ||
|
||
- where in the queue the filter is added (at the top or at the bottom of the stream filter queue); | ||
- which mode (read or write) will be used; | ||
|
||
To illustrate their usage please check the two examples below, one with League CSV and another with | ||
PHP stream resources. | ||
|
||
## Usage with CSV objects | ||
|
||
Let's imagine we have a CSV document using the return carrier character (`\r`) as the end of line character. | ||
This type of document is parsable by the package but only if you enable the deprecated `auto_detect_line_endings` ini setting. | ||
|
||
If you no longer want to rely on that feature which has been deprecated since PHP 8.1 and will be | ||
removed from PHP once PHP9.0 is release, you can, as an alternative, use the `CallbackStreamFilter` | ||
instead by replacing the offending character with a supported alternative. | ||
|
||
```php | ||
use League\Csv\CallbackStreamFilter; | ||
use League\Csv\Reader; | ||
|
||
$csv = "title1,title2,title3\rcontent11,content12,content13\rcontent21,content22,content23\r"; | ||
$csv = "title1,title2,title3\r". | ||
. "content11,content12,content13\r" | ||
. "content21,content22,content23\r"; | ||
|
||
$document = Reader::createFromString($csv); | ||
CallbackStreamFilter::addTo( | ||
$document, | ||
'swap.carrier.return', | ||
$document->setHeaderOffset(0); | ||
|
||
CallbackStreamFilter::register( | ||
'myapp.replace.eol', | ||
fn (string $bucket): string => str_replace("\r", "\n", $bucket) | ||
); | ||
$document->setHeaderOffset(0); | ||
CallbackStreamFilter::appendOnReadTo($document, 'myapp.replace.eol'); | ||
|
||
return $document->first(); | ||
// returns ['title1' => 'content11', 'title2' => 'content12', 'title3' => 'content13'] | ||
// returns [ | ||
// 'title1' => 'content11', | ||
// 'title2' => 'content12', | ||
// 'title3' => 'content13', | ||
// ] | ||
``` | ||
|
||
The `addTo` method register the filter with the unique `swap.carrier.return` name and then attach | ||
it to the CSV document object on read. | ||
The `appendOnReadTo` method will check for the availability of the filter via its | ||
name `myapp.replace.eol`. If it is not present a `LogicException` will be | ||
thrown, otherwise it will attach the filter to the CSV document object at the | ||
bottom of the stream filter queue using the reading mode. | ||
|
||
<p class="message-warning">On read, the CSV document content is <strong>never changed or replaced</strong>. | ||
Conversely, the changes <strong>are persisted during writing</strong>.</p> | ||
However, on write, the changes <strong>are persisted</strong> into the created document.</p> | ||
|
||
## Usage with streams | ||
|
||
<p class="message-notice">In the following example we will use the optional <code>$params</code> parameter | ||
to add a specific behaviour to our callback</p> | ||
|
||
```php | ||
use League\Csv\CallbackStreamFilter; | ||
|
||
$csv = <<<CSV | ||
title1,title2,title3 | ||
content11,content12,content13 | ||
content21,content22,content23 | ||
CSV; | ||
|
||
$stream = tmpfile(); | ||
fwrite($stream, $csv); | ||
|
||
// We first check to see if the callback is not already registered | ||
// without the check a LogicException would be thrown on | ||
// usage or on callback registration | ||
if (!CallbackStreamFilter::isRegistered('myapp.replace.string')) { | ||
CallbackStreamFilter::register( | ||
'myapp.replace.string', | ||
function (string $bucket, array $params): string { | ||
return str_replace( | ||
$params['search'], | ||
$params['replace'], | ||
$bucket | ||
); | ||
} | ||
); | ||
} | ||
|
||
$streamReference = CallbackStreamFilter::appendOnReadTo($stream, 'myapp.replace.string', [ | ||
'search' => ['content', '1', '2', '3'], | ||
'replace' => ['contenu ', 'A', 'B', 'C'], | ||
]); | ||
|
||
rewind($stream); | ||
$data = []; | ||
while (($record = fgetcsv($stream, 1000, ',')) !== false) { | ||
$data[] = $record; | ||
} | ||
var_dump($data[1]); | ||
//returns ['contenu AA', 'contenu AB', 'contenu AC'] | ||
|
||
stream_filter_remove($streamReference); //we remove the stream | ||
|
||
rewind($stream); | ||
$altData = []; | ||
while (($record = fgetcsv($stream, 1000, ',')) !== false) { | ||
$altData[] = $record; | ||
} | ||
var_dump($altData[1]); | ||
//returns ['content11', 'content12', 'content13'] | ||
|
||
fclose($stream); | ||
``` | ||
|
||
Of course the `CallbackStreamFilter` can be use in other different scenario or with PHP stream resources. | ||
When using one of the `append*` or `prepend*` methods with a resource, the method | ||
returns a stream reference that you can use to remove the stream filter. When | ||
using the method with the `Reader` and/or the `Writer` class, the methods returns | ||
the CSV class instance because both classes manage automatically the lifecycle of the | ||
filter and automatically remove them on the class destruction. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.