Skip to content

Commit

Permalink
Merge pull request #1918 from hydephp/bring-media-assets-into-the-hyd…
Browse files Browse the repository at this point in the history
…e-kernel

[2.x] Major performance and data handling improvements to media assets
  • Loading branch information
caendesilva authored Aug 3, 2024
2 parents 74eed1f + c3bcc78 commit a85d5d3
Show file tree
Hide file tree
Showing 13 changed files with 627 additions and 334 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<section id="hyde-kernel-filesystem-methods">

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

#### `filesystem()`

Expand Down Expand Up @@ -56,7 +56,7 @@ Hyde::pathToRelative(string $path): string
No description provided.

```php
Hyde::assets(): Illuminate\Support\Collection
Hyde::assets(): \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile>
```

<!-- End generated docs for Hyde\Foundation\Concerns\ForwardsFilesystem -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public function pathToRelative(string $path): string
return $this->filesystem->pathToRelative($path);
}

/** @return \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile> */
public function assets(): Collection
{
return $this->filesystem->assets();
Expand Down
102 changes: 79 additions & 23 deletions packages/framework/src/Support/Filesystem/MediaFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@

use Hyde\Hyde;
use Hyde\Facades\Config;
use Hyde\Facades\Filesystem;
use Illuminate\Support\Collection;
use Hyde\Framework\Exceptions\FileNotFoundException;
use Illuminate\Support\Str;

use function Hyde\unslash;
use function Hyde\path_join;
use function Hyde\trim_slashes;
use function extension_loaded;
use function file_exists;
use function array_merge;
use function filesize;
use function pathinfo;
use function is_file;

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

/** @return \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile> The array keys are the filenames relative to the _media/ directory */
public static function all(): Collection
public readonly int $length;
public readonly string $mimeType;
public readonly string $hash;

public function __construct(string $path)
{
return Hyde::assets();
parent::__construct($this->getNormalizedPath($path));

$this->length = $this->findContentLength();
$this->mimeType = $this->findMimeType();
$this->hash = $this->findHash();
}

/** @return array<string> Array of filenames relative to the _media/ directory */
/**
* Get an array of media asset filenames relative to the `_media/` directory.
*
* @return array<int, string> {@example `['app.css', 'images/logo.svg']`}
*/
public static function files(): array
{
return static::all()->keys()->all();
}

/**
* Get a collection of all media files, parsed into `MediaFile` instances, keyed by the filenames relative to the `_media/` directory.
*
* @return \Illuminate\Support\Collection<string, \Hyde\Support\Filesystem\MediaFile>
*/
public static function all(): Collection
{
return Hyde::assets();
}

/**
* Get the absolute path to the media source directory, or a file within it.
*/
Expand All @@ -60,11 +79,12 @@ public static function outputPath(string $path = ''): string
return Hyde::sitePath(Hyde::getMediaOutputDirectory());
}

$path = unslash($path);

return Hyde::sitePath(Hyde::getMediaOutputDirectory()."/$path");
return Hyde::sitePath(path_join(Hyde::getMediaOutputDirectory(), unslash($path)));
}

/**
* Get the path to the media file relative to the media directory.
*/
public function getIdentifier(): string
{
return Str::after($this->getPath(), Hyde::getMediaDirectory().'/');
Expand All @@ -75,21 +95,60 @@ public function toArray(): array
return array_merge(parent::toArray(), [
'length' => $this->getContentLength(),
'mimeType' => $this->getMimeType(),
'hash' => $this->getHash(),
]);
}

public function getContentLength(): int
{
if (! is_file($this->getAbsolutePath())) {
throw new FileNotFoundException($this->path);
return $this->length;
}

public function getMimeType(): string
{
return $this->mimeType;
}

public function getHash(): string
{
return $this->hash;
}

/** @internal */
public static function getCacheBustKey(string $file): string
{
return Config::getBool('hyde.enable_cache_busting', true) && Filesystem::exists(static::sourcePath("$file"))
? '?v='.static::make($file)->getHash()
: '';
}

protected function getNormalizedPath(string $path): string
{
$path = Hyde::pathToRelative($path);

// Normalize paths using output directory to have source directory prefix
if (str_starts_with($path, Hyde::getMediaOutputDirectory()) && str_starts_with(Hyde::getMediaDirectory(), '_')) {
$path = '_'.$path;
}

return filesize($this->getAbsolutePath());
// Normalize the path to include the media directory
$path = static::sourcePath(trim_slashes(Str::after($path, Hyde::getMediaDirectory())));

if (Filesystem::missing($path)) {
throw new FileNotFoundException($path);
}

return $path;
}

public function getMimeType(): string
protected function findContentLength(): int
{
return Filesystem::size($this->getPath());
}

protected function findMimeType(): string
{
$extension = pathinfo($this->getAbsolutePath(), PATHINFO_EXTENSION);
$extension = $this->getExtension();

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

if (extension_loaded('fileinfo') && file_exists($this->getAbsolutePath())) {
return mime_content_type($this->getAbsolutePath());
if (extension_loaded('fileinfo') && Filesystem::exists($this->getPath())) {
return Filesystem::mimeType($this->getPath());
}

return 'text/plain';
}

/** @internal */
public static function getCacheBustKey(string $file): string
protected function findHash(): string
{
return Config::getBool('hyde.enable_cache_busting', true) && file_exists(MediaFile::sourcePath("$file"))
? '?v='.md5_file(MediaFile::sourcePath("$file"))
: '';
return Filesystem::hash($this->getPath(), 'crc32');
}
}
4 changes: 1 addition & 3 deletions packages/framework/src/Support/Filesystem/ProjectFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
use Hyde\Support\Concerns\Serializable;
use Hyde\Support\Contracts\SerializableContract;

use function pathinfo;

/**
* Filesystem abstraction for a file stored in the project.
*/
Expand Down Expand Up @@ -71,6 +69,6 @@ public function getContents(): string

public function getExtension(): string
{
return pathinfo($this->getAbsolutePath(), PATHINFO_EXTENSION);
return Filesystem::extension($this->getPath());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ public function testAssetsMethodGetsAllSiteAssetsAsArray()
'path' => '_media/app.css',
'length' => 123,
'mimeType' => 'text/css',
'hash' => hash_file('crc32', Hyde::path('_media/app.css')),
],
], $assets);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ public function testMediaLinkHelperUsesConfiguredMediaDirectory()

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

public function testMediaLinkHelperWithValidationAndNonExistingFile()
Expand Down
121 changes: 121 additions & 0 deletions packages/framework/tests/Feature/Support/MediaFileTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace Hyde\Framework\Testing\Feature\Support;

use Hyde\Hyde;
use Hyde\Testing\TestCase;
use Hyde\Support\Filesystem\MediaFile;
use Hyde\Framework\Exceptions\FileNotFoundException;

/**
* @covers \Hyde\Support\Filesystem\MediaFile
*
* @see \Hyde\Framework\Testing\Unit\Support\MediaFileUnitTest
*/
class MediaFileTest extends TestCase
{
public function testMediaFileCreationAndBasicProperties()
{
$this->file('_media/test.txt', 'Hello, World!');

$mediaFile = MediaFile::make('test.txt');

$this->assertInstanceOf(MediaFile::class, $mediaFile);
$this->assertSame('test.txt', $mediaFile->getName());
$this->assertSame('_media/test.txt', $mediaFile->getPath());
$this->assertSame(Hyde::path('_media/test.txt'), $mediaFile->getAbsolutePath());
$this->assertSame('Hello, World!', $mediaFile->getContents());
$this->assertSame('txt', $mediaFile->getExtension());

$this->assertSame([
'name' => 'test.txt',
'path' => '_media/test.txt',
'length' => 13,
'mimeType' => 'text/plain',
'hash' => 'dffed8e6',
], $mediaFile->toArray());
}

public function testMediaFileDiscovery()
{
// App.css is a default file
$this->file('_media/image.png', 'PNG content');
$this->file('_media/style.css', 'CSS content');
$this->file('_media/script.js', 'JS content');

$allFiles = MediaFile::all();

$this->assertCount(4, $allFiles);
$this->assertArrayHasKey('image.png', $allFiles);
$this->assertArrayHasKey('style.css', $allFiles);
$this->assertArrayHasKey('script.js', $allFiles);

$fileNames = MediaFile::files();
$this->assertSame(['image.png', 'app.css', 'style.css', 'script.js'], $fileNames);
}

public function testMediaFileProperties()
{
$content = str_repeat('a', 1024); // 1KB content
$this->file('_media/large_file.txt', $content);

$mediaFile = MediaFile::make('large_file.txt');

$this->assertSame(1024, $mediaFile->getContentLength());
$this->assertSame('text/plain', $mediaFile->getMimeType());
$this->assertSame(hash('crc32', $content), $mediaFile->getHash());
}

public function testMediaFilePathHandling()
{
$this->file('_media/subfolder/nested_file.txt', 'Nested content');

$mediaFile = MediaFile::make('subfolder/nested_file.txt');

$this->assertSame('subfolder/nested_file.txt', $mediaFile->getIdentifier());
$this->assertSame('_media/subfolder/nested_file.txt', $mediaFile->getPath());
}

public function testMediaFileExceptionHandling()
{
$this->expectException(FileNotFoundException::class);
MediaFile::make('non_existent_file.txt');
}

public function testMediaDirectoryCustomization()
{
Hyde::setMediaDirectory('custom_media');

$this->file('custom_media/custom_file.txt', 'Custom content');

$mediaFile = MediaFile::make('custom_file.txt');

$this->assertSame('custom_media/custom_file.txt', $mediaFile->getPath());
$this->assertSame(Hyde::path('custom_media/custom_file.txt'), $mediaFile->getAbsolutePath());

Hyde::setMediaDirectory('_media');
}

public function testMediaFileOutputPaths()
{
$this->assertSame(Hyde::path('_site/media'), MediaFile::outputPath());
$this->assertSame(Hyde::path('_site/media/test.css'), MediaFile::outputPath('test.css'));

Hyde::setOutputDirectory('custom_output');
$this->assertSame(Hyde::path('custom_output/media'), MediaFile::outputPath());

Hyde::setOutputDirectory('_site');
}

public function testMediaFileCacheBusting()
{
$this->file('_media/cachebust_test.js', 'console.log("Hello");');

$cacheBustKey = MediaFile::getCacheBustKey('cachebust_test.js');

$this->assertStringStartsWith('?v=', $cacheBustKey);
$this->assertSame('?v=cd5de5e7', $cacheBustKey); // Expect CRC32 hash
}
}
4 changes: 2 additions & 2 deletions packages/framework/tests/Unit/Facades/AssetFacadeUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function testHasMediaFileHelperReturnsTrueForExistingFile()
public function testMediaLinkReturnsMediaPathWithCacheKey()
{
$this->assertIsString($path = Asset::mediaLink('app.css'));
$this->assertSame('media/app.css?v='.md5_file(Hyde::path('_media/app.css')), $path);
$this->assertSame('media/app.css?v='.hash_file('crc32', Hyde::path('_media/app.css')), $path);
}

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

$this->assertIsString($path);
$this->assertSame('assets/app.css?v='.md5_file(Hyde::path('_assets/app.css')), $path);
$this->assertSame('assets/app.css?v='.hash_file('crc32', Hyde::path('_assets/app.css')), $path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

namespace Hyde\Framework\Testing\Unit\Foundation;

use Mockery;
use Hyde\Foundation\Kernel\Filesystem;
use Hyde\Hyde;
use Hyde\Support\Filesystem\MediaFile;
use Hyde\Testing\UnitTestCase;
use Illuminate\Support\Collection;
use Illuminate\Filesystem\Filesystem as BaseFilesystem;

/**
* @covers \Hyde\Foundation\Kernel\Filesystem
Expand All @@ -24,6 +26,12 @@ protected function setUp(): void
{
parent::setUp();
$this->filesystem = new TestableFilesystem(Hyde::getInstance());

$mock = Mockery::mock(BaseFilesystem::class)->makePartial();
$mock->shouldReceive('missing')->andReturn(false)->byDefault();
$mock->shouldReceive('size')->andReturn(100)->byDefault();
$mock->shouldReceive('hash')->andReturn('hash')->byDefault();
app()->instance(BaseFilesystem::class, $mock);
}

public function testAssetsMethodReturnsSameInstanceOnSubsequentCalls()
Expand Down
Loading

0 comments on commit a85d5d3

Please sign in to comment.