Skip to content

Commit

Permalink
Merge pull request #3 from cmatosbc/tests
Browse files Browse the repository at this point in the history
Compression/Decompression on the fly; Better error handling.
  • Loading branch information
cmatosbc authored Dec 10, 2024
2 parents f4098e4 + ebca293 commit 56532c7
Show file tree
Hide file tree
Showing 10 changed files with 585 additions and 56 deletions.
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

0 comments on commit 56532c7

Please sign in to comment.