Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compression/Decompression on the fly; Better error handling. #3

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 97 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@ Penelope is designed to handle large file operations efficiently by utilizing PH
- **Flexible**: Support for both synchronous and asynchronous operations
- **Transformable**: Apply custom transformations during read/write operations
- **Progress Tracking**: Monitor write progress in real-time
- **Compression Support**: Built-in support for gzip, bzip2, and deflate compression
- **Error Resilience**: Robust error handling with retry mechanisms and logging

## 📋 Requirements

- PHP 8.1 or higher (Fiber support required)
- Composer for dependency management
- PHP Extensions:
- `zlib` for gzip/deflate compression
- `bz2` for bzip2 compression (optional)

## 🛠 Installation

```bash
composer require cmatosbc/penelope

# For bzip2 support (Ubuntu/Debian)
sudo apt-get install php-bz2
```

## 📖 Usage
Expand Down Expand Up @@ -57,47 +65,95 @@ while ($fiber->isSuspended()) {
}
```

### Basic File Writing
### Compression Support

```php
$handler = new AsyncFileHandler('output.txt', 'w');
use Penelope\Compression\CompressionHandler;

// Synchronous write
$written = $handler->writeSync($data);
// Create a compression handler (gzip, bzip2, or deflate)
$compression = new CompressionHandler('gzip', 6); // level 6 compression

// Asynchronous write with progress tracking
$fiber = $handler->writeAsync($data);
// Compress data
$compressed = $compression->compress($data);

$progress = $fiber->start();
while ($fiber->isSuspended()) {
$progress = $fiber->resume();
if ($progress !== null) {
echo "Progress: {$progress['progress']}%\n";
}
}
// Decompress data
$decompressed = $compression->decompress($compressed);

// Get file extension for compressed files
$extension = $compression->getFileExtension(); // Returns .gz for gzip
```

### Data Transformation
### Error Handling with Retries

```php
$handler = new AsyncFileHandler('data.txt', 'r');

// Set up a transformation (e.g., remove whitespace)
$handler->setTransformCallable(function(string $chunk): string {
return preg_replace('/\s+/', '', $chunk);
});
use Penelope\Error\ErrorHandler;
use Penelope\Error\RetryPolicy;
use Psr\Log\LoggerInterface;

// Create a retry policy with custom settings
$retryPolicy = new RetryPolicy(
maxAttempts: 3, // Maximum number of retry attempts
delayMs: 100, // Initial delay between retries in milliseconds
backoffMultiplier: 2.0, // Multiplier for exponential backoff
maxDelayMs: 5000 // Maximum delay between retries
);

// Create an error handler with custom logger (optional)
$errorHandler = new ErrorHandler($logger, $retryPolicy);

// Execute an operation with retry logic
try {
$result = $errorHandler->executeWithRetry(
function() {
// Your operation here
return $someResult;
},
'Reading file chunk'
);
} catch (\RuntimeException $e) {
// Handle final failure after all retries
}
```

// Read with transformation
$fiber = $handler->readAsync();
$content = '';
### Combining Features

$chunk = $fiber->start();
while ($chunk !== null || $fiber->isSuspended()) {
if ($chunk !== null) {
$content .= $chunk; // Chunk is already transformed
}
$chunk = $fiber->resume();
}
```php
use Penelope\AsyncFileHandler;
use Penelope\Compression\CompressionHandler;
use Penelope\Error\ErrorHandler;
use Penelope\Error\RetryPolicy;

// Set up handlers
$compression = new CompressionHandler('gzip');
$retryPolicy = new RetryPolicy(maxAttempts: 3);
$errorHandler = new ErrorHandler(null, $retryPolicy);
$fileHandler = new AsyncFileHandler('large_file.txt', 'r');

// Read and compress file with retry logic
$errorHandler->executeWithRetry(
function() use ($fileHandler, $compression) {
$fiber = $fileHandler->readAsync();
$compressedContent = '';

// Start reading
$chunk = $fiber->start();
if ($chunk !== null) {
$compressedContent .= $compression->compress($chunk);
}

// Continue reading
while ($fiber->isSuspended()) {
$chunk = $fiber->resume();
if ($chunk !== null) {
$compressedContent .= $compression->compress($chunk);
}
}

// Write compressed content
file_put_contents('output.gz', $compressedContent);
},
'Compressing file'
);
```

## 🎯 Use Cases
Expand All @@ -116,30 +172,19 @@ while ($chunk = $fiber->resume()) {
}
```

### 2. Real-time Data Transformation
Ideal for data sanitization, format conversion, or content filtering:

```php
$handler = new AsyncFileHandler('input.csv', 'r');
$handler->setTransformCallable(function($chunk) {
return str_replace(',', ';', $chunk); // Convert CSV to semicolon-separated
});
```
### 2. File Compression and Archiving

### 3. Progress Monitoring
Perfect for long-running file operations in web applications:
- Compress large log files for archival
- Create compressed backups of data files
- Stream compressed data to remote storage
- Process and compress multiple files in parallel

```php
$handler = new AsyncFileHandler('large_file.txt', 'w');
$fiber = $handler->writeAsync($data);
### 3. Error-Resilient Operations

while ($fiber->isSuspended()) {
$progress = $fiber->resume();
if ($progress !== null) {
updateProgressBar($progress['progress']);
}
}
```
- Retry failed network file transfers
- Handle intermittent I/O errors gracefully
- Log detailed error information for debugging
- Implement progressive backoff for rate-limited operations

## 🔍 Performance

Expand All @@ -154,7 +199,7 @@ Based on our benchmarks with a 100MB file:

```bash
composer install
./vendor/bin/phpunit
./vendor/bin/phpunit --testdox
```

## 🤝 Contributing
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"license": "gpl-3.0-or-later",
"minimum-stability": "dev",
"require": {
"php": ">=8.1"
"php": ">=8.1",
"psr/log": "^3.0",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
Expand Down
12 changes: 9 additions & 3 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
bootstrap="tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache"
displayDetailsOnTestsThatTriggerDeprecations="false"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true">
<testsuites>
<testsuite name="Penelope Test Suite">
<directory>tests</directory>
<testsuite name="Async File Handler">
<file>tests/AsyncFileHandlerTest.php</file>
</testsuite>
<testsuite name="Compression">
<directory>tests/Compression</directory>
</testsuite>
<testsuite name="Performance">
<file>tests/PerformanceTest.php</file>
</testsuite>
</testsuites>
<source>
Expand Down
97 changes: 97 additions & 0 deletions src/Compression/CompressionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace Penelope\Compression;

use RuntimeException;

class CompressionHandler implements CompressionInterface
{
private string $algorithm;
private int $level;

/**
* @param string $algorithm Compression algorithm ('gzip', 'bzip2', or 'deflate')
* @param int $level Compression level (1-9, where 9 is maximum compression)
*/
public function __construct(string $algorithm = 'gzip', int $level = 6)
{
if (!in_array($algorithm, ['gzip', 'bzip2', 'deflate'])) {
throw new RuntimeException("Unsupported compression algorithm: {$algorithm}");
}

if ($level < 1 || $level > 9) {
throw new RuntimeException("Compression level must be between 1 and 9");
}

$this->algorithm = $algorithm;
$this->level = $level;

// Verify that the required extension is loaded
$this->verifyExtension();
}

public function compress(string $data): string
{
switch ($this->algorithm) {
case 'gzip':
return gzencode($data, $this->level);
case 'bzip2':
return bzcompress($data, $this->level);
case 'deflate':
return gzdeflate($data, $this->level);
default:
throw new RuntimeException("Unsupported compression algorithm");
}
}

public function decompress(string $data): string
{
switch ($this->algorithm) {
case 'gzip':
$result = gzdecode($data);
break;
case 'bzip2':
$result = bzdecompress($data);
break;
case 'deflate':
$result = gzinflate($data);
break;
default:
throw new RuntimeException("Unsupported compression algorithm");
}

if ($result === false) {
throw new RuntimeException("Failed to decompress data");
}

return $result;
}

public function getAlgorithm(): string
{
return $this->algorithm;
}

public function getFileExtension(): string
{
return match($this->algorithm) {
'gzip' => '.gz',
'bzip2' => '.bz2',
'deflate' => '.zz',
default => throw new RuntimeException("Unsupported compression algorithm"),
};
}

private function verifyExtension(): void
{
$required = match($this->algorithm) {
'gzip', 'deflate' => 'zlib',
'bzip2' => 'bz2',
default => throw new RuntimeException("Unsupported compression algorithm"),
};

if (!extension_loaded($required)) {
throw new RuntimeException("Required extension not loaded: {$required}");
}
}
}
32 changes: 32 additions & 0 deletions src/Compression/CompressionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Penelope\Compression;

interface CompressionInterface
{
/**
* Compress the given data
* @param string $data Data to compress
* @return string Compressed data
*/
public function compress(string $data): string;

/**
* Decompress the given data
* @param string $data Data to decompress
* @return string Decompressed data
*/
public function decompress(string $data): string;

/**
* Get the compression algorithm name
* @return string
*/
public function getAlgorithm(): string;

/**
* Get the file extension associated with this compression
* @return string
*/
public function getFileExtension(): string;
}
Loading
Loading