Skip to content

Commit a85d5d3

Browse files
authored
Merge pull request #1918 from hydephp/bring-media-assets-into-the-hyde-kernel
[2.x] Major performance and data handling improvements to media assets
2 parents 74eed1f + c3bcc78 commit a85d5d3

File tree

13 files changed

+627
-334
lines changed

13 files changed

+627
-334
lines changed

docs/_data/partials/hyde-pages-api/hyde-kernel-filesystem-methods.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<section id="hyde-kernel-filesystem-methods">
22

33
<!-- Start generated docs for Hyde\Foundation\Concerns\ForwardsFilesystem -->
4-
<!-- Generated by HydePHP DocGen script at 2024-07-28 11:20:12 in 0.11ms -->
4+
<!-- Generated by HydePHP DocGen script at 2024-08-01 10:01:06 in 0.13ms -->
55

66
#### `filesystem()`
77

@@ -56,7 +56,7 @@ Hyde::pathToRelative(string $path): string
5656
No description provided.
5757

5858
```php
59-
Hyde::assets(): Illuminate\Support\Collection
59+
Hyde::assets(): \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile>
6060
```
6161

6262
<!-- End generated docs for Hyde\Foundation\Concerns\ForwardsFilesystem -->

packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function pathToRelative(string $path): string
4444
return $this->filesystem->pathToRelative($path);
4545
}
4646

47+
/** @return \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile> */
4748
public function assets(): Collection
4849
{
4950
return $this->filesystem->assets();

packages/framework/src/Support/Filesystem/MediaFile.php

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@
66

77
use Hyde\Hyde;
88
use Hyde\Facades\Config;
9+
use Hyde\Facades\Filesystem;
910
use Illuminate\Support\Collection;
1011
use Hyde\Framework\Exceptions\FileNotFoundException;
1112
use Illuminate\Support\Str;
1213

1314
use function Hyde\unslash;
1415
use function Hyde\path_join;
16+
use function Hyde\trim_slashes;
1517
use function extension_loaded;
16-
use function file_exists;
1718
use function array_merge;
18-
use function filesize;
19-
use function pathinfo;
20-
use function is_file;
2119

2220
/**
2321
* File abstraction for a project media file.
@@ -27,18 +25,39 @@ class MediaFile extends ProjectFile
2725
/** @var array<string> The default extensions for media types */
2826
final public const EXTENSIONS = ['png', 'svg', 'jpg', 'jpeg', 'gif', 'ico', 'css', 'js'];
2927

30-
/** @return \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile> The array keys are the filenames relative to the _media/ directory */
31-
public static function all(): Collection
28+
public readonly int $length;
29+
public readonly string $mimeType;
30+
public readonly string $hash;
31+
32+
public function __construct(string $path)
3233
{
33-
return Hyde::assets();
34+
parent::__construct($this->getNormalizedPath($path));
35+
36+
$this->length = $this->findContentLength();
37+
$this->mimeType = $this->findMimeType();
38+
$this->hash = $this->findHash();
3439
}
3540

36-
/** @return array<string> Array of filenames relative to the _media/ directory */
41+
/**
42+
* Get an array of media asset filenames relative to the `_media/` directory.
43+
*
44+
* @return array<int, string> {@example `['app.css', 'images/logo.svg']`}
45+
*/
3746
public static function files(): array
3847
{
3948
return static::all()->keys()->all();
4049
}
4150

51+
/**
52+
* Get a collection of all media files, parsed into `MediaFile` instances, keyed by the filenames relative to the `_media/` directory.
53+
*
54+
* @return \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile>
55+
*/
56+
public static function all(): Collection
57+
{
58+
return Hyde::assets();
59+
}
60+
4261
/**
4362
* Get the absolute path to the media source directory, or a file within it.
4463
*/
@@ -60,11 +79,12 @@ public static function outputPath(string $path = ''): string
6079
return Hyde::sitePath(Hyde::getMediaOutputDirectory());
6180
}
6281

63-
$path = unslash($path);
64-
65-
return Hyde::sitePath(Hyde::getMediaOutputDirectory()."/$path");
82+
return Hyde::sitePath(path_join(Hyde::getMediaOutputDirectory(), unslash($path)));
6683
}
6784

85+
/**
86+
* Get the path to the media file relative to the media directory.
87+
*/
6888
public function getIdentifier(): string
6989
{
7090
return Str::after($this->getPath(), Hyde::getMediaDirectory().'/');
@@ -75,21 +95,60 @@ public function toArray(): array
7595
return array_merge(parent::toArray(), [
7696
'length' => $this->getContentLength(),
7797
'mimeType' => $this->getMimeType(),
98+
'hash' => $this->getHash(),
7899
]);
79100
}
80101

81102
public function getContentLength(): int
82103
{
83-
if (! is_file($this->getAbsolutePath())) {
84-
throw new FileNotFoundException($this->path);
104+
return $this->length;
105+
}
106+
107+
public function getMimeType(): string
108+
{
109+
return $this->mimeType;
110+
}
111+
112+
public function getHash(): string
113+
{
114+
return $this->hash;
115+
}
116+
117+
/** @internal */
118+
public static function getCacheBustKey(string $file): string
119+
{
120+
return Config::getBool('hyde.enable_cache_busting', true) && Filesystem::exists(static::sourcePath("$file"))
121+
? '?v='.static::make($file)->getHash()
122+
: '';
123+
}
124+
125+
protected function getNormalizedPath(string $path): string
126+
{
127+
$path = Hyde::pathToRelative($path);
128+
129+
// Normalize paths using output directory to have source directory prefix
130+
if (str_starts_with($path, Hyde::getMediaOutputDirectory()) && str_starts_with(Hyde::getMediaDirectory(), '_')) {
131+
$path = '_'.$path;
85132
}
86133

87-
return filesize($this->getAbsolutePath());
134+
// Normalize the path to include the media directory
135+
$path = static::sourcePath(trim_slashes(Str::after($path, Hyde::getMediaDirectory())));
136+
137+
if (Filesystem::missing($path)) {
138+
throw new FileNotFoundException($path);
139+
}
140+
141+
return $path;
88142
}
89143

90-
public function getMimeType(): string
144+
protected function findContentLength(): int
145+
{
146+
return Filesystem::size($this->getPath());
147+
}
148+
149+
protected function findMimeType(): string
91150
{
92-
$extension = pathinfo($this->getAbsolutePath(), PATHINFO_EXTENSION);
151+
$extension = $this->getExtension();
93152

94153
// See if we can find a mime type for the extension instead of
95154
// having to rely on a PHP extension and filesystem lookups.
@@ -112,18 +171,15 @@ public function getMimeType(): string
112171
return $lookup[$extension];
113172
}
114173

115-
if (extension_loaded('fileinfo') && file_exists($this->getAbsolutePath())) {
116-
return mime_content_type($this->getAbsolutePath());
174+
if (extension_loaded('fileinfo') && Filesystem::exists($this->getPath())) {
175+
return Filesystem::mimeType($this->getPath());
117176
}
118177

119178
return 'text/plain';
120179
}
121180

122-
/** @internal */
123-
public static function getCacheBustKey(string $file): string
181+
protected function findHash(): string
124182
{
125-
return Config::getBool('hyde.enable_cache_busting', true) && file_exists(MediaFile::sourcePath("$file"))
126-
? '?v='.md5_file(MediaFile::sourcePath("$file"))
127-
: '';
183+
return Filesystem::hash($this->getPath(), 'crc32');
128184
}
129185
}

packages/framework/src/Support/Filesystem/ProjectFile.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
use Hyde\Support\Concerns\Serializable;
1212
use Hyde\Support\Contracts\SerializableContract;
1313

14-
use function pathinfo;
15-
1614
/**
1715
* Filesystem abstraction for a file stored in the project.
1816
*/
@@ -71,6 +69,6 @@ public function getContents(): string
7169

7270
public function getExtension(): string
7371
{
74-
return pathinfo($this->getAbsolutePath(), PATHINFO_EXTENSION);
72+
return Filesystem::extension($this->getPath());
7573
}
7674
}

packages/framework/tests/Feature/Foundation/FilesystemTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ public function testAssetsMethodGetsAllSiteAssetsAsArray()
377377
'path' => '_media/app.css',
378378
'length' => 123,
379379
'mimeType' => 'text/css',
380+
'hash' => hash_file('crc32', Hyde::path('_media/app.css')),
380381
],
381382
], $assets);
382383
}

packages/framework/tests/Feature/Foundation/HyperlinksTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ public function testMediaLinkHelperUsesConfiguredMediaDirectory()
107107

108108
public function testMediaLinkHelperWithValidationAndExistingFile()
109109
{
110-
$this->file('_media/foo');
111-
$this->assertSame('media/foo?v=d41d8cd98f00b204e9800998ecf8427e', $this->class->mediaLink('foo', true));
110+
$this->file('_media/foo', 'test');
111+
$this->assertSame('media/foo?v=accf8b33', $this->class->mediaLink('foo', true));
112112
}
113113

114114
public function testMediaLinkHelperWithValidationAndNonExistingFile()
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hyde\Framework\Testing\Feature\Support;
6+
7+
use Hyde\Hyde;
8+
use Hyde\Testing\TestCase;
9+
use Hyde\Support\Filesystem\MediaFile;
10+
use Hyde\Framework\Exceptions\FileNotFoundException;
11+
12+
/**
13+
* @covers \Hyde\Support\Filesystem\MediaFile
14+
*
15+
* @see \Hyde\Framework\Testing\Unit\Support\MediaFileUnitTest
16+
*/
17+
class MediaFileTest extends TestCase
18+
{
19+
public function testMediaFileCreationAndBasicProperties()
20+
{
21+
$this->file('_media/test.txt', 'Hello, World!');
22+
23+
$mediaFile = MediaFile::make('test.txt');
24+
25+
$this->assertInstanceOf(MediaFile::class, $mediaFile);
26+
$this->assertSame('test.txt', $mediaFile->getName());
27+
$this->assertSame('_media/test.txt', $mediaFile->getPath());
28+
$this->assertSame(Hyde::path('_media/test.txt'), $mediaFile->getAbsolutePath());
29+
$this->assertSame('Hello, World!', $mediaFile->getContents());
30+
$this->assertSame('txt', $mediaFile->getExtension());
31+
32+
$this->assertSame([
33+
'name' => 'test.txt',
34+
'path' => '_media/test.txt',
35+
'length' => 13,
36+
'mimeType' => 'text/plain',
37+
'hash' => 'dffed8e6',
38+
], $mediaFile->toArray());
39+
}
40+
41+
public function testMediaFileDiscovery()
42+
{
43+
// App.css is a default file
44+
$this->file('_media/image.png', 'PNG content');
45+
$this->file('_media/style.css', 'CSS content');
46+
$this->file('_media/script.js', 'JS content');
47+
48+
$allFiles = MediaFile::all();
49+
50+
$this->assertCount(4, $allFiles);
51+
$this->assertArrayHasKey('image.png', $allFiles);
52+
$this->assertArrayHasKey('style.css', $allFiles);
53+
$this->assertArrayHasKey('script.js', $allFiles);
54+
55+
$fileNames = MediaFile::files();
56+
$this->assertSame(['image.png', 'app.css', 'style.css', 'script.js'], $fileNames);
57+
}
58+
59+
public function testMediaFileProperties()
60+
{
61+
$content = str_repeat('a', 1024); // 1KB content
62+
$this->file('_media/large_file.txt', $content);
63+
64+
$mediaFile = MediaFile::make('large_file.txt');
65+
66+
$this->assertSame(1024, $mediaFile->getContentLength());
67+
$this->assertSame('text/plain', $mediaFile->getMimeType());
68+
$this->assertSame(hash('crc32', $content), $mediaFile->getHash());
69+
}
70+
71+
public function testMediaFilePathHandling()
72+
{
73+
$this->file('_media/subfolder/nested_file.txt', 'Nested content');
74+
75+
$mediaFile = MediaFile::make('subfolder/nested_file.txt');
76+
77+
$this->assertSame('subfolder/nested_file.txt', $mediaFile->getIdentifier());
78+
$this->assertSame('_media/subfolder/nested_file.txt', $mediaFile->getPath());
79+
}
80+
81+
public function testMediaFileExceptionHandling()
82+
{
83+
$this->expectException(FileNotFoundException::class);
84+
MediaFile::make('non_existent_file.txt');
85+
}
86+
87+
public function testMediaDirectoryCustomization()
88+
{
89+
Hyde::setMediaDirectory('custom_media');
90+
91+
$this->file('custom_media/custom_file.txt', 'Custom content');
92+
93+
$mediaFile = MediaFile::make('custom_file.txt');
94+
95+
$this->assertSame('custom_media/custom_file.txt', $mediaFile->getPath());
96+
$this->assertSame(Hyde::path('custom_media/custom_file.txt'), $mediaFile->getAbsolutePath());
97+
98+
Hyde::setMediaDirectory('_media');
99+
}
100+
101+
public function testMediaFileOutputPaths()
102+
{
103+
$this->assertSame(Hyde::path('_site/media'), MediaFile::outputPath());
104+
$this->assertSame(Hyde::path('_site/media/test.css'), MediaFile::outputPath('test.css'));
105+
106+
Hyde::setOutputDirectory('custom_output');
107+
$this->assertSame(Hyde::path('custom_output/media'), MediaFile::outputPath());
108+
109+
Hyde::setOutputDirectory('_site');
110+
}
111+
112+
public function testMediaFileCacheBusting()
113+
{
114+
$this->file('_media/cachebust_test.js', 'console.log("Hello");');
115+
116+
$cacheBustKey = MediaFile::getCacheBustKey('cachebust_test.js');
117+
118+
$this->assertStringStartsWith('?v=', $cacheBustKey);
119+
$this->assertSame('?v=cd5de5e7', $cacheBustKey); // Expect CRC32 hash
120+
}
121+
}

packages/framework/tests/Unit/Facades/AssetFacadeUnitTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function testHasMediaFileHelperReturnsTrueForExistingFile()
4949
public function testMediaLinkReturnsMediaPathWithCacheKey()
5050
{
5151
$this->assertIsString($path = Asset::mediaLink('app.css'));
52-
$this->assertSame('media/app.css?v='.md5_file(Hyde::path('_media/app.css')), $path);
52+
$this->assertSame('media/app.css?v='.hash_file('crc32', Hyde::path('_media/app.css')), $path);
5353
}
5454

5555
public function testMediaLinkReturnsMediaPathWithoutCacheKeyIfCacheBustingIsDisabled()
@@ -72,6 +72,6 @@ public function testMediaLinkSupportsCustomMediaDirectories()
7272
$path = Asset::mediaLink('app.css');
7373

7474
$this->assertIsString($path);
75-
$this->assertSame('assets/app.css?v='.md5_file(Hyde::path('_assets/app.css')), $path);
75+
$this->assertSame('assets/app.css?v='.hash_file('crc32', Hyde::path('_assets/app.css')), $path);
7676
}
7777
}

packages/framework/tests/Unit/Foundation/FilesystemHasMediaFilesTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
namespace Hyde\Framework\Testing\Unit\Foundation;
66

7+
use Mockery;
78
use Hyde\Foundation\Kernel\Filesystem;
89
use Hyde\Hyde;
910
use Hyde\Support\Filesystem\MediaFile;
1011
use Hyde\Testing\UnitTestCase;
1112
use Illuminate\Support\Collection;
13+
use Illuminate\Filesystem\Filesystem as BaseFilesystem;
1214

1315
/**
1416
* @covers \Hyde\Foundation\Kernel\Filesystem
@@ -24,6 +26,12 @@ protected function setUp(): void
2426
{
2527
parent::setUp();
2628
$this->filesystem = new TestableFilesystem(Hyde::getInstance());
29+
30+
$mock = Mockery::mock(BaseFilesystem::class)->makePartial();
31+
$mock->shouldReceive('missing')->andReturn(false)->byDefault();
32+
$mock->shouldReceive('size')->andReturn(100)->byDefault();
33+
$mock->shouldReceive('hash')->andReturn('hash')->byDefault();
34+
app()->instance(BaseFilesystem::class, $mock);
2735
}
2836

2937
public function testAssetsMethodReturnsSameInstanceOnSubsequentCalls()

0 commit comments

Comments
 (0)