Skip to content

Commit 456c0e2

Browse files
committed
rework
1 parent 32cd824 commit 456c0e2

File tree

7 files changed

+138
-60
lines changed

7 files changed

+138
-60
lines changed

src/LiveComponent/src/EventListener/LiveComponentSubscriber.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@
4444
class LiveComponentSubscriber implements EventSubscriberInterface, ServiceSubscriberInterface
4545
{
4646
private const HTML_CONTENT_TYPE = 'application/vnd.live-component+html';
47-
4847
private const REDIRECT_HEADER = 'X-Live-Redirect';
49-
private const DOWNLOAD_HEADER = 'X-Live-Download';
5048

5149
public function __construct(
5250
private ContainerInterface $container,
@@ -258,13 +256,6 @@ public function onKernelView(ViewEvent $event): void
258256
return;
259257
}
260258

261-
if ($event->getControllerResult() instanceof BinaryFileResponse) {
262-
if (!$event->getControllerResult()->headers->has(self::DOWNLOAD_HEADER)) {
263-
264-
}
265-
$event->setResponse(new Response());
266-
}
267-
268259
$event->setResponse($this->createResponse($request->attributes->get('_mounted_component')));
269260
}
270261

src/LiveComponent/src/LiveDownloadResponse.php

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Symfony\UX\LiveComponent;
4+
5+
use Symfony\Component\HttpFoundation\BinaryFileResponse;
6+
use Symfony\Component\HttpFoundation\HeaderUtils;
7+
use Symfony\Component\HttpFoundation\StreamedResponse;
8+
9+
/**
10+
* @author Simon André <[email protected]>
11+
* @author Kevin Bond <[email protected]>
12+
*/
13+
final class LiveResponse
14+
{
15+
/**
16+
* @param string|\SplFileInfo $file The file to send as a response
17+
* @param string|null $filename The name of the file to send (defaults to the basename of the file)
18+
* @param string|null $contentType The content type of the file (defaults to `application/octet-stream`)
19+
*/
20+
public static function file(string|\SplFileInfo $file, ?string $filename = null, ?string $contentType = null, ?int $size = null): BinaryFileResponse
21+
{
22+
return new BinaryFileResponse($file, 200, [
23+
'Content-Disposition' => HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename ?? basename($file)),
24+
'Content-Type' => $contentType ?? 'application/octet-stream',
25+
'Content-Length' => $size ?? ($file instanceof \SplFileInfo ? $file->getSize() : null),
26+
]);
27+
}
28+
29+
/**
30+
* @param resource|Closure $file The file to stream as a response
31+
* @param string $filename The name of the file to send (defaults to the basename of the file)
32+
* @param string|null $contentType The content type of the file (defaults to `application/octet-stream`)
33+
* @param int|null $size The size of the file
34+
*/
35+
public static function streamFile(mixed $file, string $filename, ?string $contentType = null, ?int $size = null): StreamedResponse
36+
{
37+
if (!is_resource($file) && !$file instanceof \Closure) {
38+
throw new \InvalidArgumentException(sprintf('The file must be a resource or a closure, "%s" given.', get_debug_type($file)));
39+
}
40+
41+
return new StreamedResponse($file instanceof \Closure ? $file(...) : function () use ($file) {
42+
while (!feof($file)) {
43+
echo fread($file, 1024);
44+
}
45+
}, 200, [
46+
'Content-Disposition' => HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename),
47+
'Content-Type' => $contentType ?? 'application/octet-stream',
48+
'Content-Length' => $size,
49+
]);
50+
}
51+
}

src/LiveComponent/tests/Fixtures/Component/DownloadFileComponent.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use Symfony\UX\LiveComponent\Attribute\LiveAction;
1717
use Symfony\UX\LiveComponent\Attribute\LiveArg;
1818
use Symfony\UX\LiveComponent\DefaultActionTrait;
19-
use Symfony\UX\LiveComponent\LiveDownloadResponse;
19+
use Symfony\UX\LiveComponent\LiveResponse;
2020

2121
/**
2222
* @author Simon André <[email protected]>
@@ -25,32 +25,32 @@
2525
class DownloadFileComponent
2626
{
2727
use DefaultActionTrait;
28-
29-
private const FILE_DIRECTORY = __DIR__.'/../files/';
28+
29+
private const FILE_DIRECTORY = __DIR__.'/../files/';
3030

3131
#[LiveAction]
3232
public function download(): BinaryFileResponse
3333
{
3434
$file = new \SplFileInfo(self::FILE_DIRECTORY.'/foo.json');
35-
36-
return new LiveDownloadResponse($file);
35+
36+
return LiveResponse::file($file);
3737
}
38-
38+
3939
#[LiveAction]
4040
public function generate(): BinaryFileResponse
4141
{
4242
$file = new \SplTempFileObject();
4343
$file->fwrite(file_get_contents(self::FILE_DIRECTORY.'/foo.json'));
44-
45-
return new LiveDownloadResponse($file, 'foo.json');
44+
45+
return LiveResponse::file($file, 'foo.json', size: 1000);
4646
}
47-
47+
4848
#[LiveAction]
4949
public function heavyFile(#[LiveArg] int $size): BinaryFileResponse
5050
{
5151
$file = new \SplFileInfo(self::FILE_DIRECTORY.'heavy.txt');
52-
53-
$response = new BinaryFileResponse($file);
52+
53+
$response = LiveResponse::file($file);
5454
$response->headers->set('Content-Length', 10000000); // 10MB
5555
}
5656
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
text

src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,6 @@ public function testCanDownloadFileFromComponentAction(): void
349349
'body' => ['data' => json_encode(['props' => $dehydrated->getProps()])],
350350
])
351351
->assertStatus(200)
352-
->assertHeaderContains('X-Live-Download', '1')
353352
->assertHeaderContains('Content-Type', 'application/octet-stream')
354353
->assertHeaderContains('Content-Disposition', 'attachment')
355354
->assertHeaderEquals('Content-Length', '21')
@@ -379,7 +378,6 @@ public function testCanDownloadGeneratedFileFromComponentAction(): void
379378
],
380379
])
381380
->assertStatus(200)
382-
->assertHeaderContains('X-Live-Download', '1')
383381
->assertHeaderContains('Content-Type', 'application/octet-stream')
384382
->assertHeaderContains('Content-Disposition', 'attachment')
385383
->assertHeaderEquals('Content-Length', '21')
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Symfony\UX\LiveComponent\Tests\Unit;
4+
5+
use Symfony\Component\HttpFoundation\BinaryFileResponse;
6+
use Symfony\Component\HttpFoundation\File\File;
7+
use Symfony\Component\HttpFoundation\StreamedResponse;
8+
use Symfony\UX\LiveComponent\LiveResponse;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class LiveResponseTest extends TestCase
12+
{
13+
public function testSendFileWithStringPath(): void
14+
{
15+
$filePath = __DIR__.'/../fixtures/files/test.txt';
16+
$response = LiveResponse::file($filePath);
17+
18+
$this->assertInstanceOf(BinaryFileResponse::class, $response);
19+
$this->assertEquals('attachment; filename=test.txt', $response->headers->get('Content-Disposition'));
20+
$this->assertEquals('application/octet-stream', $response->headers->get('Content-Type'));
21+
}
22+
23+
public function testSendFileWithSplFileInfo(): void
24+
{
25+
$file = new File(__DIR__.'/../fixtures/files/test.txt');
26+
$response = LiveResponse::file($file, 'custom-name.txt', 'text/plain');
27+
28+
$this->assertInstanceOf(BinaryFileResponse::class, $response);
29+
$this->assertEquals('attachment; filename=custom-name.txt', $response->headers->get('Content-Disposition'));
30+
$this->assertEquals('text/plain', $response->headers->get('Content-Type'));
31+
}
32+
33+
public function testSendFileWithSplTempFileObject(): void
34+
{
35+
$tempFile = new \SplTempFileObject();
36+
$tempFile->fwrite('Temporary content');
37+
$response = LiveResponse::file($tempFile, size: 17);
38+
39+
$this->assertInstanceOf(BinaryFileResponse::class, $response);
40+
$this->assertEquals('application/octet-stream', $response->headers->get('Content-Type'));
41+
$this->assertEquals(17, $response->headers->get('Content-Length'));
42+
}
43+
44+
public function testStreamFileWithResource(): void
45+
{
46+
$file = fopen(__DIR__.'/../fixtures/files/test.txt', 'rb');
47+
$response = LiveResponse::streamFile($file, 'streamed-file.txt');
48+
49+
$this->assertInstanceOf(StreamedResponse::class, $response);
50+
$this->assertEquals('attachment; filename=streamed-file.txt', $response->headers->get('Content-Disposition'));
51+
$this->assertEquals('application/octet-stream', $response->headers->get('Content-Type'));
52+
fclose($file);
53+
}
54+
55+
public function testStreamFileWithClosure(): void
56+
{
57+
$closure = function () {
58+
echo 'Streaming content';
59+
};
60+
61+
$response = LiveResponse::streamFile($closure, 'streamed-closure.txt', 'text/plain');
62+
63+
$this->assertInstanceOf(StreamedResponse::class, $response);
64+
$this->assertEquals('attachment; filename=streamed-closure.txt', $response->headers->get('Content-Disposition'));
65+
$this->assertEquals('text/plain', $response->headers->get('Content-Type'));
66+
}
67+
68+
public function testStreamFileWithInvalidType(): void
69+
{
70+
$this->expectException(\InvalidArgumentException::class);
71+
$this->expectExceptionMessage('The file must be a resource or a closure, "string" given.');
72+
73+
LiveResponse::streamFile('invalid-type', 'invalid.txt');
74+
}
75+
}

0 commit comments

Comments
 (0)