[LiveComponent] Add support for downloading files from LiveActions (Experimental)

by @smnandre

Some issues have been detected in this pull request

Issues that can be fixed by applying a patch

Review the proposed patch then download it to apply it manually or execute the following command from the repository root directory:

curl https://fabbot.io/patch/symfony/ux/2483/456c0e2d2161ff8d2c51e9e95e2c09fe82b47031/cs.diff | patch -p0
diff -ru src/LiveComponent/src/EventListener/LiveComponentSubscriber.php src/LiveComponent/src/EventListener/LiveComponentSubscriber.php
--- src/LiveComponent/src/EventListener/LiveComponentSubscriber.php	2025-01-10 15:08:57.945964379 +0000
+++ src/LiveComponent/src/EventListener/LiveComponentSubscriber.php	2025-01-10 15:09:03.603975457 +0000
@@ -13,7 +13,6 @@
 
 use Psr\Container\ContainerInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\Exception\JsonException;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
diff -ru src/LiveComponent/src/LiveResponse.php src/LiveComponent/src/LiveResponse.php
--- src/LiveComponent/src/LiveResponse.php	2025-01-10 15:08:58.143964767 +0000
+++ src/LiveComponent/src/LiveResponse.php	2025-01-10 15:09:03.698975643 +0000
@@ -1,5 +1,14 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <[email protected]>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\LiveComponent;
 
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
@@ -13,9 +22,9 @@
 final class LiveResponse
 {
     /**
-     * @param string|\SplFileInfo $file The file to send as a response
-     * @param string|null $filename    The name of the file to send     (defaults to the basename of the file)
-     * @param string|null $contentType The content type of the file     (defaults to `application/octet-stream`)
+     * @param string|\SplFileInfo $file        The file to send as a response
+     * @param string|null         $filename    The name of the file to send     (defaults to the basename of the file)
+     * @param string|null         $contentType The content type of the file     (defaults to `application/octet-stream`)
      */
     public static function file(string|\SplFileInfo $file, ?string $filename = null, ?string $contentType = null, ?int $size = null): BinaryFileResponse
     {
@@ -27,15 +36,15 @@
     }
 
     /**
-     * @param resource|Closure $file   The file to stream as a response
-     * @param string $filename         The name of the file to send     (defaults to the basename of the file)
-     * @param string|null $contentType The content type of the file     (defaults to `application/octet-stream`)
-     * @param int|null $size           The size of the file
+     * @param resource|Closure $file        The file to stream as a response
+     * @param string           $filename    The name of the file to send     (defaults to the basename of the file)
+     * @param string|null      $contentType The content type of the file     (defaults to `application/octet-stream`)
+     * @param int|null         $size        The size of the file
      */
     public static function streamFile(mixed $file, string $filename, ?string $contentType = null, ?int $size = null): StreamedResponse
     {
-        if (!is_resource($file) && !$file instanceof \Closure) {
-            throw new \InvalidArgumentException(sprintf('The file must be a resource or a closure, "%s" given.', get_debug_type($file)));
+        if (!\is_resource($file) && !$file instanceof \Closure) {
+            throw new \InvalidArgumentException(\sprintf('The file must be a resource or a closure, "%s" given.', get_debug_type($file)));
         }
 
         return new StreamedResponse($file instanceof \Closure ? $file(...) : function () use ($file) {
diff -ru src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php
--- src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php	2025-01-10 15:09:00.005968413 +0000
+++ src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php	2025-01-10 15:09:04.261976745 +0000
@@ -381,9 +381,9 @@
             ->assertHeaderContains('Content-Type', 'application/octet-stream')
             ->assertHeaderContains('Content-Disposition', 'attachment')
             ->assertHeaderEquals('Content-Length', '21')
-            ->use(function(Browser $browser) {
+            ->use(function (Browser $browser) {
                 self::assertJson($browser->content());
-                self::assertSame(['foo' => 'bar'], \json_decode($browser->content(), true));
+                self::assertSame(['foo' => 'bar'], json_decode($browser->content(), true));
             });
     }
 
diff -ru src/LiveComponent/tests/Unit/LiveResponseTest.php src/LiveComponent/tests/Unit/LiveResponseTest.php
--- src/LiveComponent/tests/Unit/LiveResponseTest.php	2025-01-10 15:09:00.228968849 +0000
+++ src/LiveComponent/tests/Unit/LiveResponseTest.php	2025-01-10 15:09:04.361976941 +0000
@@ -1,12 +1,21 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <[email protected]>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\LiveComponent\Tests\Unit;
 
+use PHPUnit\Framework\TestCase;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\File\File;
 use Symfony\Component\HttpFoundation\StreamedResponse;
 use Symfony\UX\LiveComponent\LiveResponse;
-use PHPUnit\Framework\TestCase;
 
 class LiveResponseTest extends TestCase
 {
@@ -41,9 +50,9 @@
         $this->assertEquals(17, $response->headers->get('Content-Length'));
     }
 
-     public function testStreamFileWithResource(): void
+    public function testStreamFileWithResource(): void
     {
-        $file = fopen(__DIR__.'/../fixtures/files/test.txt', 'rb');
+        $file = fopen(__DIR__.'/../fixtures/files/test.txt', 'r');
         $response = LiveResponse::streamFile($file, 'streamed-file.txt');
 
         $this->assertInstanceOf(StreamedResponse::class, $response);

0
Common Typos

0
JSON Files Syntax

0
File Permissions

0
Merge Commits

Issues that can be fixed by applying a patch

Review the proposed patch then download it to apply it manually or execute the following command from the repository root directory:

curl https://fabbot.io/patch/symfony/ux/2483/456c0e2d2161ff8d2c51e9e95e2c09fe82b47031/exception_messages.diff | patch -p0
diff -ru ux.symfony.com/src/Twig/Components/DownloadFiles.php ux.symfony.com/src/Twig/Components/DownloadFiles.php
--- ux.symfony.com/src/Twig/Components/DownloadFiles.php	2025-01-10 15:09:01.799971925 +0000
+++ ux.symfony.com/src/Twig/Components/DownloadFiles.php	2025-01-10 15:09:06.155980454 +0000
@@ -49,7 +49,7 @@
             'csv' => $this->generateCsvReport($this->year),
             'json' => $this->generateJsonReport($this->year),
             'md' => $this->generateMarkdownReport($this->year),
-            default => throw new \InvalidArgumentException('Invalid format provided'),
+            default => throw new \InvalidArgumentException('Invalid format provided.'),
         };
 
         $file = new \SplTempFileObject();

0
Usage of void in test files

0
Use ::class whenever possible

0
Deprecation Messages