Skip to content
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
1 change: 0 additions & 1 deletion .php-cs-fixer.cache

This file was deleted.

43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,49 @@ $rule->allow('image/*') // All image types
->allow('application/pdf') // PDF files
->allow('*/vnd.openxmlformats-officedocument.*'); // Office documents
```
### Event System

The library provides PSR-14 compatible events:

#### Available Events:
- **`BeforeValidationEvent`** - Dispatched before validation starts
- **`BeforeUploadEvent`** - Dispatched after validation, before file upload
- **`AfterUploadEvent`** - Dispatched after successful file upload
- **`UploadErrorEvent`** - Dispatched when any error occurs

#### Usage Example:
```php
use Enjoys\Upload\Event\AfterUploadEvent;
use Psr\EventDispatcher\EventDispatcherInterface;

/** @var EventDispatcherInterface $dispatcher */

// Initialize with event dispatcher
$upload = new UploadProcessing($uploadedFile, $filesystem, $dispatcher);

// Add event listener
$dispatcher->addListener(
AfterUploadEvent::class,
function (AfterUploadEvent $event) {
logger()->info("File uploaded to: " . $event->uploadProcessing->getTargetPath());
}
);

$upload->upload();
```

#### Event Propagation:
All events implement `StoppableEventInterface`. To stop further processing:
```php
$dispatcher->addListener(
BeforeUploadEvent::class,
function (BeforeUploadEvent $event) {
if ($shouldStop) {
$event->stopPropagation(); // Stops other listeners
}
}
);
```

### API Reference

Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
"require": {
"php": "~8.2.0 | ~8.3.0 | ~8.4.0",
"psr/http-message": "^1.0 | ^2.0",
"league/flysystem": "^3.30.0"
"league/flysystem": "^3.30.0",
"psr/event-dispatcher": "^1.0"
},
"require-dev": {
"vimeo/psalm": "^6.12.0",
"phpunit/phpunit": "^11.5.24",
"phpunit/phpunit": "^11.5.25",
"infection/infection": "^0.29.14",
"league/flysystem-memory": "^3.29.0",
"symfony/var-dumper": "^6.0 | ^7.0",
"guzzlehttp/psr7": "^2.7.1",
"friendsofphp/php-cs-fixer": "~v3.75.0"
},
Expand Down
47 changes: 47 additions & 0 deletions src/Event/AbstractUploadEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Enjoys\Upload\Event;

use Psr\EventDispatcher\StoppableEventInterface;

/**
* Base abstract class for all upload-related events
*
* Provides common functionality for event propagation control
* according to PSR-14 (Event Dispatcher) standard.
*
* All concrete upload events should extend this class to maintain
* consistent behavior across the upload event system.
*/
abstract class AbstractUploadEvent implements StoppableEventInterface
{
/**
* @var bool Flag indicating whether event propagation is stopped
*/
private bool $propagationStopped = false;

/**
* Checks whether event propagation is stopped
*
* @return bool True if event propagation is stopped, false otherwise
*/
#[\Override]
public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}

/**
* Stops the event propagation
*
* When called, prevents the event from being passed to additional listeners.
* This is useful when a listener has handled the event and wants to prevent
* other listeners from processing it further.
*/
public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}
29 changes: 29 additions & 0 deletions src/Event/AfterUploadEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Enjoys\Upload\Event;

use Enjoys\Upload\UploadProcessing;

/**
* Event dispatched after a file has been successfully uploaded and processed
*
* This event provides access to the upload processing instance, allowing listeners
* to retrieve information about the uploaded file(s), storage details, and any
* processing results.
*/
final class AfterUploadEvent extends AbstractUploadEvent
{
/**
* @param UploadProcessing $uploadProcessing The upload processing instance containing
* details about the completed upload operation, including:
* - Processed file metadata
* - Storage information
* - Any transformations applied
* - Upload status and results
*/
public function __construct(public readonly UploadProcessing $uploadProcessing)
{
}
}
28 changes: 28 additions & 0 deletions src/Event/BeforeUploadEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Enjoys\Upload\Event;

use Enjoys\Upload\UploadProcessing;

/**
* Event dispatched before file upload processing begins
*
* This event allows listeners to modify upload parameters or perform validation
* before the actual file processing occurs. The upload can be aborted by throwing
* an exception from an event listener.
*/
final class BeforeUploadEvent extends AbstractUploadEvent
{
/**
* @param UploadProcessing $uploadProcessing The upload processing instance containing:
* - File metadata (name, size, type)
* - Target storage configuration
* - Processing options
* - Validation rules
*/
public function __construct(public readonly UploadProcessing $uploadProcessing)
{
}
}
28 changes: 28 additions & 0 deletions src/Event/BeforeValidationEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Enjoys\Upload\Event;

use Enjoys\Upload\UploadProcessing;

/**
* Event dispatched before file validation occurs
*
* This event allows listeners to modify validation rules or perform
* custom pre-validation checks before the standard validation process.
* The upload can be aborted by throwing an exception from an event listener.
*/
final class BeforeValidationEvent extends AbstractUploadEvent
{
/**
* @param UploadProcessing $uploadProcessing The upload processing instance containing:
* - File metadata (name, size, temporary path)
* - Current validation rules
* - Upload configuration
* - User-defined validation callbacks
*/
public function __construct(public readonly UploadProcessing $uploadProcessing)
{
}
}
34 changes: 34 additions & 0 deletions src/Event/UploadErrorEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Enjoys\Upload\Event;

use Enjoys\Upload\UploadProcessing;
use Throwable;

/**
* Event dispatched when an error occurs during file upload processing
*
* This event provides access to both the upload processing instance and the exception
* that caused the failure, allowing for error handling, logging, or recovery attempts.
* Common error scenarios include:
* - File validation failures
* - Filesystem errors (permissions, quota exceeded)
* - Processing errors (image manipulation, etc.)
* - Network errors (for remote storage)
*/
final class UploadErrorEvent extends AbstractUploadEvent
{
/**
* @param UploadProcessing $uploadProcessing The upload processing instance containing
* details about the failed upload operation
* @param Throwable $exception The exception that caused the upload to fail
* with detailed error information and stack trace
*/
public function __construct(
public readonly UploadProcessing $uploadProcessing,
public readonly Throwable $exception
) {
}
}
23 changes: 20 additions & 3 deletions src/UploadProcessing.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@

namespace Enjoys\Upload;

use Enjoys\Upload\Event\AfterUploadEvent;
use Enjoys\Upload\Event\BeforeUploadEvent;
use Enjoys\Upload\Event\BeforeValidationEvent;
use Enjoys\Upload\Event\UploadErrorEvent;
use Enjoys\Upload\Exception\RuleException;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\UploadedFileInterface;
use Throwable;

final class UploadProcessing
{
/**
* @var string|null Final storage path (null until file is uploaded)
* @var string|null Final storage path (null until a file is uploaded)
*/
private ?string $targetPath = null;

Expand All @@ -29,10 +36,12 @@ final class UploadProcessing
* @param UploadedFileInterface $uploadedFile The PSR-7 uploaded file to process
* @param Filesystem $filesystem Flysystem instance that provides filesystem abstraction
* (supports local, FTP, S3, and other storage systems)
* @param EventDispatcherInterface|null $dispatcher Optional event dispatcher for handling upload-related events
*/
public function __construct(
private readonly UploadedFileInterface $uploadedFile,
private readonly Filesystem $filesystem,
private readonly ?EventDispatcherInterface $dispatcher = null
) {
$this->fileInfo = new FileInfo($uploadedFile);
}
Expand All @@ -42,16 +51,24 @@ public function __construct(
*
* @param string $targetPath The target directory path (defaults to '/')
* @throws FilesystemException If there's an error during file system operations
* @throws RuleException Thrown when validation fails
* @throws Throwable
*/
public function upload(string $targetPath = '/'): void
{
$this->dispatcher?->dispatch(new BeforeValidationEvent($this));
$this->validate();

$this->dispatcher?->dispatch(new BeforeUploadEvent($this));
$this->targetPath = rtrim($targetPath, '/') . '/' . $this->fileInfo->getFilename();

$resource = $this->uploadedFile->getStream()->detach();
try {
$this->filesystem->writeStream($this->targetPath, $resource);
$this->dispatcher?->dispatch(new AfterUploadEvent($this));
} catch (Throwable $e) {
$this->dispatcher?->dispatch(new UploadErrorEvent($this, $e));
throw $e;
} finally {
if (is_resource($resource)) {
fclose($resource);
Expand All @@ -61,6 +78,8 @@ public function upload(string $targetPath = '/'): void

/**
* Validates the uploaded file against all registered rules
*
* @throws RuleException Thrown when validation fails
*/
private function validate(): void
{
Expand Down Expand Up @@ -149,6 +168,4 @@ public function getRules(): array
{
return $this->rules;
}


}
Loading