Skip to content

Commit 156d868

Browse files
Add PSR-14 Event Dispatcher support
* PSR-14 (Event Manager) has been added * added tests * req symfony/var-dumper * Update README.md * Apply PHP-CS-Fixer fixes [skip ci] --------- Co-authored-by: GitHub Actions <actions@github.com>
1 parent bd5e5f9 commit 156d868

10 files changed

+447
-6
lines changed

.php-cs-fixer.cache

Lines changed: 0 additions & 1 deletion
This file was deleted.

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,49 @@ $rule->allow('image/*') // All image types
8686
->allow('application/pdf') // PDF files
8787
->allow('*/vnd.openxmlformats-officedocument.*'); // Office documents
8888
```
89+
### Event System
90+
91+
The library provides PSR-14 compatible events:
92+
93+
#### Available Events:
94+
- **`BeforeValidationEvent`** - Dispatched before validation starts
95+
- **`BeforeUploadEvent`** - Dispatched after validation, before file upload
96+
- **`AfterUploadEvent`** - Dispatched after successful file upload
97+
- **`UploadErrorEvent`** - Dispatched when any error occurs
98+
99+
#### Usage Example:
100+
```php
101+
use Enjoys\Upload\Event\AfterUploadEvent;
102+
use Psr\EventDispatcher\EventDispatcherInterface;
103+
104+
/** @var EventDispatcherInterface $dispatcher */
105+
106+
// Initialize with event dispatcher
107+
$upload = new UploadProcessing($uploadedFile, $filesystem, $dispatcher);
108+
109+
// Add event listener
110+
$dispatcher->addListener(
111+
AfterUploadEvent::class,
112+
function (AfterUploadEvent $event) {
113+
logger()->info("File uploaded to: " . $event->uploadProcessing->getTargetPath());
114+
}
115+
);
116+
117+
$upload->upload();
118+
```
119+
120+
#### Event Propagation:
121+
All events implement `StoppableEventInterface`. To stop further processing:
122+
```php
123+
$dispatcher->addListener(
124+
BeforeUploadEvent::class,
125+
function (BeforeUploadEvent $event) {
126+
if ($shouldStop) {
127+
$event->stopPropagation(); // Stops other listeners
128+
}
129+
}
130+
);
131+
```
89132

90133
### API Reference
91134

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
"require": {
1919
"php": "~8.2.0 | ~8.3.0 | ~8.4.0",
2020
"psr/http-message": "^1.0 | ^2.0",
21-
"league/flysystem": "^3.30.0"
21+
"league/flysystem": "^3.30.0",
22+
"psr/event-dispatcher": "^1.0"
2223
},
2324
"require-dev": {
2425
"vimeo/psalm": "^6.12.0",
25-
"phpunit/phpunit": "^11.5.24",
26+
"phpunit/phpunit": "^11.5.25",
2627
"infection/infection": "^0.29.14",
2728
"league/flysystem-memory": "^3.29.0",
29+
"symfony/var-dumper": "^6.0 | ^7.0",
2830
"guzzlehttp/psr7": "^2.7.1",
2931
"friendsofphp/php-cs-fixer": "~v3.75.0"
3032
},

src/Event/AbstractUploadEvent.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Enjoys\Upload\Event;
6+
7+
use Psr\EventDispatcher\StoppableEventInterface;
8+
9+
/**
10+
* Base abstract class for all upload-related events
11+
*
12+
* Provides common functionality for event propagation control
13+
* according to PSR-14 (Event Dispatcher) standard.
14+
*
15+
* All concrete upload events should extend this class to maintain
16+
* consistent behavior across the upload event system.
17+
*/
18+
abstract class AbstractUploadEvent implements StoppableEventInterface
19+
{
20+
/**
21+
* @var bool Flag indicating whether event propagation is stopped
22+
*/
23+
private bool $propagationStopped = false;
24+
25+
/**
26+
* Checks whether event propagation is stopped
27+
*
28+
* @return bool True if event propagation is stopped, false otherwise
29+
*/
30+
#[\Override]
31+
public function isPropagationStopped(): bool
32+
{
33+
return $this->propagationStopped;
34+
}
35+
36+
/**
37+
* Stops the event propagation
38+
*
39+
* When called, prevents the event from being passed to additional listeners.
40+
* This is useful when a listener has handled the event and wants to prevent
41+
* other listeners from processing it further.
42+
*/
43+
public function stopPropagation(): void
44+
{
45+
$this->propagationStopped = true;
46+
}
47+
}

src/Event/AfterUploadEvent.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Enjoys\Upload\Event;
6+
7+
use Enjoys\Upload\UploadProcessing;
8+
9+
/**
10+
* Event dispatched after a file has been successfully uploaded and processed
11+
*
12+
* This event provides access to the upload processing instance, allowing listeners
13+
* to retrieve information about the uploaded file(s), storage details, and any
14+
* processing results.
15+
*/
16+
final class AfterUploadEvent extends AbstractUploadEvent
17+
{
18+
/**
19+
* @param UploadProcessing $uploadProcessing The upload processing instance containing
20+
* details about the completed upload operation, including:
21+
* - Processed file metadata
22+
* - Storage information
23+
* - Any transformations applied
24+
* - Upload status and results
25+
*/
26+
public function __construct(public readonly UploadProcessing $uploadProcessing)
27+
{
28+
}
29+
}

src/Event/BeforeUploadEvent.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Enjoys\Upload\Event;
6+
7+
use Enjoys\Upload\UploadProcessing;
8+
9+
/**
10+
* Event dispatched before file upload processing begins
11+
*
12+
* This event allows listeners to modify upload parameters or perform validation
13+
* before the actual file processing occurs. The upload can be aborted by throwing
14+
* an exception from an event listener.
15+
*/
16+
final class BeforeUploadEvent extends AbstractUploadEvent
17+
{
18+
/**
19+
* @param UploadProcessing $uploadProcessing The upload processing instance containing:
20+
* - File metadata (name, size, type)
21+
* - Target storage configuration
22+
* - Processing options
23+
* - Validation rules
24+
*/
25+
public function __construct(public readonly UploadProcessing $uploadProcessing)
26+
{
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Enjoys\Upload\Event;
6+
7+
use Enjoys\Upload\UploadProcessing;
8+
9+
/**
10+
* Event dispatched before file validation occurs
11+
*
12+
* This event allows listeners to modify validation rules or perform
13+
* custom pre-validation checks before the standard validation process.
14+
* The upload can be aborted by throwing an exception from an event listener.
15+
*/
16+
final class BeforeValidationEvent extends AbstractUploadEvent
17+
{
18+
/**
19+
* @param UploadProcessing $uploadProcessing The upload processing instance containing:
20+
* - File metadata (name, size, temporary path)
21+
* - Current validation rules
22+
* - Upload configuration
23+
* - User-defined validation callbacks
24+
*/
25+
public function __construct(public readonly UploadProcessing $uploadProcessing)
26+
{
27+
}
28+
}

src/Event/UploadErrorEvent.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Enjoys\Upload\Event;
6+
7+
use Enjoys\Upload\UploadProcessing;
8+
use Throwable;
9+
10+
/**
11+
* Event dispatched when an error occurs during file upload processing
12+
*
13+
* This event provides access to both the upload processing instance and the exception
14+
* that caused the failure, allowing for error handling, logging, or recovery attempts.
15+
* Common error scenarios include:
16+
* - File validation failures
17+
* - Filesystem errors (permissions, quota exceeded)
18+
* - Processing errors (image manipulation, etc.)
19+
* - Network errors (for remote storage)
20+
*/
21+
final class UploadErrorEvent extends AbstractUploadEvent
22+
{
23+
/**
24+
* @param UploadProcessing $uploadProcessing The upload processing instance containing
25+
* details about the failed upload operation
26+
* @param Throwable $exception The exception that caused the upload to fail
27+
* with detailed error information and stack trace
28+
*/
29+
public function __construct(
30+
public readonly UploadProcessing $uploadProcessing,
31+
public readonly Throwable $exception
32+
) {
33+
}
34+
}

src/UploadProcessing.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44

55
namespace Enjoys\Upload;
66

7+
use Enjoys\Upload\Event\AfterUploadEvent;
8+
use Enjoys\Upload\Event\BeforeUploadEvent;
9+
use Enjoys\Upload\Event\BeforeValidationEvent;
10+
use Enjoys\Upload\Event\UploadErrorEvent;
11+
use Enjoys\Upload\Exception\RuleException;
712
use League\Flysystem\Filesystem;
813
use League\Flysystem\FilesystemException;
14+
use Psr\EventDispatcher\EventDispatcherInterface;
915
use Psr\Http\Message\UploadedFileInterface;
16+
use Throwable;
1017

1118
final class UploadProcessing
1219
{
1320
/**
14-
* @var string|null Final storage path (null until file is uploaded)
21+
* @var string|null Final storage path (null until a file is uploaded)
1522
*/
1623
private ?string $targetPath = null;
1724

@@ -29,10 +36,12 @@ final class UploadProcessing
2936
* @param UploadedFileInterface $uploadedFile The PSR-7 uploaded file to process
3037
* @param Filesystem $filesystem Flysystem instance that provides filesystem abstraction
3138
* (supports local, FTP, S3, and other storage systems)
39+
* @param EventDispatcherInterface|null $dispatcher Optional event dispatcher for handling upload-related events
3240
*/
3341
public function __construct(
3442
private readonly UploadedFileInterface $uploadedFile,
3543
private readonly Filesystem $filesystem,
44+
private readonly ?EventDispatcherInterface $dispatcher = null
3645
) {
3746
$this->fileInfo = new FileInfo($uploadedFile);
3847
}
@@ -42,16 +51,24 @@ public function __construct(
4251
*
4352
* @param string $targetPath The target directory path (defaults to '/')
4453
* @throws FilesystemException If there's an error during file system operations
54+
* @throws RuleException Thrown when validation fails
55+
* @throws Throwable
4556
*/
4657
public function upload(string $targetPath = '/'): void
4758
{
59+
$this->dispatcher?->dispatch(new BeforeValidationEvent($this));
4860
$this->validate();
4961

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

5265
$resource = $this->uploadedFile->getStream()->detach();
5366
try {
5467
$this->filesystem->writeStream($this->targetPath, $resource);
68+
$this->dispatcher?->dispatch(new AfterUploadEvent($this));
69+
} catch (Throwable $e) {
70+
$this->dispatcher?->dispatch(new UploadErrorEvent($this, $e));
71+
throw $e;
5572
} finally {
5673
if (is_resource($resource)) {
5774
fclose($resource);
@@ -61,6 +78,8 @@ public function upload(string $targetPath = '/'): void
6178

6279
/**
6380
* Validates the uploaded file against all registered rules
81+
*
82+
* @throws RuleException Thrown when validation fails
6483
*/
6584
private function validate(): void
6685
{
@@ -149,6 +168,4 @@ public function getRules(): array
149168
{
150169
return $this->rules;
151170
}
152-
153-
154171
}

0 commit comments

Comments
 (0)