diff --git a/README.md b/README.md index 8d03687..4bf913f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,61 @@ $post->getVersionedFileUrl('file'); // returns https://www.example.com/storage/p $post->deleteFile('file'); ``` +### Using with underscore translatable +This package ships with support for the [underscore translatable](github.com/esign/laravel-underscore-translatable) package. + +Make sure to include the file, filename and mime columns within the `translatable` array: +```php +use Esign\ModelFiles\Concerns\HasFiles; +use Esign\UnderscoreTranslatable\UnderscoreTranslatable; +use Illuminate\Database\Eloquent\Model; + +class UnderscoreTranslatablePost extends Model +{ + use HasFiles; + use UnderscoreTranslatable; + + public $translatable = [ + 'document', + 'document_filename', + 'document_mime', + ]; +} +``` + +Next up, your migrations should look like the following: +```php +Schema::create('posts', function (Blueprint $table) { + $table->id(); + $table->boolean('document_en')->default(0); + $table->boolean('document_nl')->default(0); + $table->string('document_filename_en')->nullable(); + $table->string('document_filename_nl')->nullable(); + $table->string('document_mime_en')->nullable(); + $table->string('document_mime_nl')->nullable(); +}); +``` + +You may now use the internal methods using the default or the specific locale: +```php +$post->hasFile('document'); // returns true/false +$post->getFolderPath('document'); // returns posts/document_en +$post->getFilePath('document'); // returns posts/document_en/1.pdf +$post->getFileMime('document'); // returns application/pdf +$post->getFileExtension('document'); // returns pdf +$post->getFileUrl('document'); // returns https://www.example.com/storage/posts/document_en/1.pdf +$post->getVersionedFileUrl('document'); // returns https://www.example.com/storage/posts/document_en/1.pdf?t=1675776047 +``` + +```php +$post->hasFile('document_en'); // returns true/false +$post->getFolderPath('document_en'); // returns posts/document_en +$post->getFilePath('document_en'); // returns posts/document_en/1.pdf +$post->getFileMime('document_en'); // returns application/pdf +$post->getFileExtension('document_en'); // returns pdf +$post->getFileUrl('document_en'); // returns https://www.example.com/storage/posts/document_en/1.pdf +$post->getVersionedFileUrl('document_en'); // returns https://www.example.com/storage/posts/document_en/1.pdf?t=1675776047 +``` ### Testing diff --git a/composer.json b/composer.json index 493384f..7af671d 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": "^8.0", + "esign/laravel-underscore-translatable": "^1.4", "illuminate/http": "^8.74|^9.0|^10.0", "illuminate/support": "^8.0|^9.0|^10.0" }, diff --git a/src/Concerns/HasFiles.php b/src/Concerns/HasFiles.php index cc1f6d7..ed5cc7f 100644 --- a/src/Concerns/HasFiles.php +++ b/src/Concerns/HasFiles.php @@ -2,10 +2,13 @@ namespace Esign\ModelFiles\Concerns; +use BadMethodCallException; use Esign\ModelFiles\Exceptions\ModelNotPersistedException; +use Esign\UnderscoreTranslatable\UnderscoreTranslatable; use Illuminate\Http\File; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; trait HasFiles { @@ -13,12 +16,17 @@ trait HasFiles public function hasFile(string $column): bool { - return (bool) $this->getAttribute($column); + return (bool) $this->getAttribute( + $this->guessFileColumn($column) + ); } public function setHasFile(string $column, bool $value): static { - return $this->setAttribute($column, $value); + return $this->setAttribute( + $this->guessFileColumn($column), + $value + ); } public function getFileName(string $column): ?string @@ -63,7 +71,7 @@ public function getFilePath(string $column): ?string public function getFolderPath(string $column): string { - return $this->getTable() . '/' . $column; + return $this->getTable() . '/' . $this->guessFileColumn($column); } public function getFileUrl(string $column): ?string @@ -143,16 +151,72 @@ protected function ensureModelIsPersisted(): void } } + protected function guessFileColumn(string $column): string + { + if ( + $this->usesTrait(UnderscoreTranslatable::class) && + $translatedColumnName = $this->guessUnderscoreTranslatableColumn($column) + ) { + return $translatedColumnName; + } + + return $column; + } + protected function guessFileNameColumn(string $column): string { + if ( + $this->usesTrait(UnderscoreTranslatable::class) && + $translatedColumnName = $this->guessUnderscoreTranslatableColumn($column, 'filename') + ) { + return $translatedColumnName; + } + return "{$column}_filename"; } protected function guessFileMimeColumn(string $column): string { + if ( + $this->usesTrait(UnderscoreTranslatable::class) && + $translatedColumnName = $this->guessUnderscoreTranslatableColumn($column, 'mime') + ) { + return $translatedColumnName; + } + return "{$column}_mime"; } + protected function usesTrait(string $className): bool + { + return in_array($className, class_uses_recursive($this)); + } + + protected function ensureTraitIsUsed(string $className): void + { + if (! $this->usesTrait($className)) { + throw new BadMethodCallException("The {$className} trait must be used to call this method."); + } + } + + protected function guessUnderscoreTranslatableColumn(string $column, ?string $columnSuffix = null): ?string + { + $this->ensureTraitIsUsed(UnderscoreTranslatable::class); + + $columnSuffix = $columnSuffix ? "_{$columnSuffix}" : null; + + if ($this->isTranslatableAttribute($column)) { + return $this->getTranslatableAttributeName("{$column}{$columnSuffix}"); + } + + $columnWithoutPossibleLocaleSuffix = Str::beforeLast($column, '_'); + if ($this->isTranslatableAttribute($columnWithoutPossibleLocaleSuffix)) { + return $this->getTranslatableAttributeName("{$columnWithoutPossibleLocaleSuffix}{$columnSuffix}"); + } + + return null; + } + public function usingFileDisk(string $fileDisk): static { $this->fileDisk = $fileDisk; diff --git a/tests/Feature/Concerns/HasFilesTest.php b/tests/Feature/Concerns/HasFilesTest.php index 8b1f7e3..e4e2ae6 100644 --- a/tests/Feature/Concerns/HasFilesTest.php +++ b/tests/Feature/Concerns/HasFilesTest.php @@ -5,12 +5,34 @@ use Esign\ModelFiles\Exceptions\ModelNotPersistedException; use Esign\ModelFiles\Tests\Support\Models\Post; use Esign\ModelFiles\Tests\TestCase; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Http\File; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; class HasFilesTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + Schema::create('posts', function (Blueprint $table) { + $table->id(); + $table->boolean('document')->default(false); + $table->string('document_filename')->nullable(); + $table->string('document_mime')->nullable(); + $table->timestamps(); + }); + } + + protected function tearDown(): void + { + Schema::dropIfExists('posts'); + + parent::tearDown(); + } + /** @test */ public function it_can_check_if_it_has_a_file() { @@ -263,4 +285,18 @@ public function it_can_store_jpeg_as_jpg() $this->it_can_store_an_uploaded_file_using_the_guessed_extension_instead_of_the_one_provided_in_the_client_name(); $this->it_can_store_a_file_using_the_guessed_extension_instead_of_the_one_provided_in_the_client_name(); } + + protected function createPostWithDocument( + bool $document, + ?string $documentFilename, + ?string $documentMime, + array $attributes = [], + ): Post { + return Post::create([ + 'document' => $document, + 'document_filename' => $documentFilename, + 'document_mime' => $documentMime, + ...$attributes, + ]); + } } diff --git a/tests/Feature/Concerns/UnderscoreTranslatableSupportTest.php b/tests/Feature/Concerns/UnderscoreTranslatableSupportTest.php new file mode 100644 index 0000000..a730b3a --- /dev/null +++ b/tests/Feature/Concerns/UnderscoreTranslatableSupportTest.php @@ -0,0 +1,366 @@ +id(); + $table->boolean('document_en')->default(false); + $table->string('document_filename_en')->nullable(); + $table->string('document_mime_en')->nullable(); + $table->timestamps(); + }); + } + + protected function tearDown(): void + { + Schema::dropIfExists('underscore_translatable_posts'); + + parent::tearDown(); + } + + /** @test */ + public function it_can_check_if_it_has_a_file_for_the_default_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertTrue($postA->hasFile('document')); + $this->assertFalse($postB->hasFile('document')); + } + + /** @test */ + public function it_can_check_if_it_has_a_file_for_a_specific_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertTrue($postA->hasFile('document_en')); + $this->assertFalse($postB->hasFile('document_en')); + } + + /** @test */ + public function it_can_set_that_it_has_a_file_for_the_default_locale() + { + $post = $this->createPostWithDocument(false, null, null); + + $post = $post->setHasFile('document', true); + + $this->assertTrue($post->hasFile('document')); + } + + /** @test */ + public function it_can_set_that_it_has_a_file_for_a_specific_locale() + { + $post = $this->createPostWithDocument(false, null, null); + + $post = $post->setHasFile('document_en', true); + + $this->assertTrue($post->hasFile('document_en')); + } + + /** @test */ + public function it_can_get_the_file_name_for_the_default_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals('my-document.pdf', $postA->getFileName('document')); + $this->assertEquals(null, $postB->getFileName('document')); + } + + /** @test */ + public function it_can_get_the_file_name_for_a_specific_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals('my-document.pdf', $postA->getFileName('document_en')); + $this->assertEquals(null, $postB->getFileName('document_en')); + } + + /** @test */ + public function it_can_set_the_file_name_for_the_default_locale() + { + $post = $this->createPostWithDocument(false, null, null); + + $post = $post->setFileName('document', 'my-document.pdf'); + + $this->assertEquals('my-document.pdf', $post->getFileName('document')); + } + + + /** @test */ + public function it_can_set_the_file_name_for_a_specific_locale() + { + $post = $this->createPostWithDocument(false, null, null); + + $post = $post->setFileName('document_en', 'my-document.pdf'); + + $this->assertEquals('my-document.pdf', $post->getFileName('document_en')); + } + + /** @test */ + public function it_can_get_the_file_extension_for_the_default_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals('pdf', $postA->getFileExtension('document')); + $this->assertEquals(null, $postB->getFileExtension('document')); + } + + /** @test */ + public function it_can_get_the_file_extension_for_a_specific_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals('pdf', $postA->getFileExtension('document_en')); + $this->assertEquals(null, $postB->getFileExtension('document_en')); + } + + /** @test */ + public function it_can_get_the_file_mime_for_the_default_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals('application/pdf', $postA->getFileMime('document')); + $this->assertEquals(null, $postB->getFileMime('document')); + } + + /** @test */ + public function it_can_get_the_file_mime_for_a_specific_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals('application/pdf', $postA->getFileMime('document_en')); + $this->assertEquals(null, $postB->getFileMime('document_en')); + } + + /** @test */ + public function it_can_set_the_file_mime_for_the_default_locale() + { + $post = $this->createPostWithDocument(false, null, null); + + $post->setFileMime('document', 'application/pdf'); + + $this->assertEquals('application/pdf', $post->getFileMime('document')); + } + + /** @test */ + public function it_can_set_the_file_mime_for_a_specific_locale() + { + $post = $this->createPostWithDocument(false, null, null); + + $post->setFileMime('document_en', 'application/pdf'); + + $this->assertEquals('application/pdf', $post->getFileMime('document_en')); + } + + /** @test */ + public function it_can_get_the_file_path_for_the_default_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals( + "underscore_translatable_posts/document_en/{$postA->getKey()}.pdf", + $postA->getFilePath('document') + ); + $this->assertEquals(null, $postB->getFilePath('document')); + } + + /** @test */ + public function it_can_get_the_file_path_for_a_specific_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals( + "underscore_translatable_posts/document_en/{$postA->getKey()}.pdf", + $postA->getFilePath('document_en') + ); + $this->assertEquals(null, $postB->getFilePath('document_en')); + } + + /** @test */ + public function it_can_get_the_folder_path_for_the_default_locale() + { + $post = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + + $this->assertEquals( + 'underscore_translatable_posts/document_en', + $post->getFolderPath('document') + ); + } + + /** @test */ + public function it_can_get_the_folder_path_for_a_specific_locale() + { + $post = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + + $this->assertEquals( + 'underscore_translatable_posts/document_en', + $post->getFolderPath('document_en') + ); + } + + /** @test */ + public function it_can_get_the_file_url_for_the_default_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals( + "http://localhost/storage/underscore_translatable_posts/document_en/{$postA->getKey()}.pdf", + $postA->getFileUrl('document') + ); + $this->assertEquals(null, $postB->getFileUrl('document')); + } + + /** @test */ + public function it_can_get_the_file_url_for_a_specific_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals( + "http://localhost/storage/underscore_translatable_posts/document_en/{$postA->getKey()}.pdf", + $postA->getFileUrl('document_en') + ); + $this->assertEquals(null, $postB->getFileUrl('document_en')); + } + + /** @test */ + public function it_can_get_the_versioned_file_url_for_the_default_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals( + "http://localhost/storage/underscore_translatable_posts/document_en/{$postA->getKey()}.pdf?t={$postA->updated_at->timestamp}", + $postA->getVersionedFileUrl('document') + ); + $this->assertEquals(null, $postB->getVersionedFileUrl('document')); + } + + /** @test */ + public function it_can_get_the_versioned_file_url_for_a_specific_locale() + { + $postA = $this->createPostWithDocument(true, 'my-document.pdf', 'application/pdf'); + $postB = $this->createPostWithDocument(false, null, null); + + $this->assertEquals( + "http://localhost/storage/underscore_translatable_posts/document_en/{$postA->getKey()}.pdf?t={$postA->updated_at->timestamp}", + $postA->getVersionedFileUrl('document_en') + ); + $this->assertEquals(null, $postB->getVersionedFileUrl('document_en')); + } + + /** @test */ + public function it_can_store_a_file_for_the_default_locale() + { + Storage::fake(); + $post = $this->createPostWithDocument(false, null, null); + $file = UploadedFile::fake()->create('my-document.pdf', 1000, 'application/pdf'); + + $updatedPost = $post->storeFile('document', $file); + + Storage::assertExists($post->getFilePath('document')); + $this->assertInstanceOf(UnderscoreTranslatablePost::class, $updatedPost); + $this->assertDatabaseHas(UnderscoreTranslatablePost::class, [ + 'id' => $post->getKey(), + 'document_en' => true, + 'document_filename_en' => 'my-document.pdf', + 'document_mime_en' => 'application/pdf', + ]); + } + + /** @test */ + public function it_can_store_a_file_for_a_specific_locale() + { + Storage::fake(); + $post = $this->createPostWithDocument(false, null, null); + $file = UploadedFile::fake()->create('my-document.pdf', 1000, 'application/pdf'); + + $updatedPost = $post->storeFile('document_en', $file); + + Storage::assertExists($post->getFilePath('document_en')); + $this->assertInstanceOf(UnderscoreTranslatablePost::class, $updatedPost); + $this->assertDatabaseHas(UnderscoreTranslatablePost::class, [ + 'id' => $post->getKey(), + 'document_en' => true, + 'document_filename_en' => 'my-document.pdf', + 'document_mime_en' => 'application/pdf', + ]); + } + + /** @test */ + public function it_can_delete_a_file_for_the_default_locale() + { + Storage::fake(); + $post = $this->createPostWithDocument(false, null, null); + $file = UploadedFile::fake()->create('my-document.pdf', 1000, 'application/pdf'); + $post->storeFile('document', $file); + + $updatedPost = $post->deleteFile('document'); + + Storage::assertMissing($post->getFilePath('document')); + $this->assertInstanceOf(UnderscoreTranslatablePost::class, $updatedPost); + $this->assertDatabaseHas(UnderscoreTranslatablePost::class, [ + 'id' => $post->getKey(), + 'document_en' => false, + 'document_filename_en' => null, + 'document_mime_en' => null, + ]); + } + + /** @test */ + public function it_can_delete_a_file_for_a_specific_locale() + { + Storage::fake(); + $post = $this->createPostWithDocument(false, null, null); + $file = UploadedFile::fake()->create('my-document.pdf', 1000, 'application/pdf'); + $post->storeFile('document_en', $file); + + $updatedPost = $post->deleteFile('document_en'); + + Storage::assertMissing($post->getFilePath('document_en')); + $this->assertInstanceOf(UnderscoreTranslatablePost::class, $updatedPost); + $this->assertDatabaseHas(UnderscoreTranslatablePost::class, [ + 'id' => $post->getKey(), + 'document_en' => false, + 'document_filename_en' => null, + 'document_mime_en' => null, + ]); + } + + protected function createPostWithDocument( + bool $document, + ?string $documentFilename, + ?string $documentMime, + array $attributes = [], + ): UnderscoreTranslatablePost { + return UnderscoreTranslatablePost::create([ + 'document_en' => $document, + 'document_filename_en' => $documentFilename, + 'document_mime_en' => $documentMime, + ...$attributes, + ]); + } +} diff --git a/tests/Support/Models/UnderscoreTranslatablePost.php b/tests/Support/Models/UnderscoreTranslatablePost.php new file mode 100644 index 0000000..bd9ba1b --- /dev/null +++ b/tests/Support/Models/UnderscoreTranslatablePost.php @@ -0,0 +1,21 @@ +id(); - $table->boolean('document')->default(false); - $table->string('document_filename')->nullable(); - $table->string('document_mime')->nullable(); - $table->timestamps(); - }); - } - - protected function tearDown(): void - { - Schema::dropIfExists('posts'); - - parent::tearDown(); - } - - protected function createPostWithDocument( - bool $document, - ?string $documentFilename, - ?string $documentMime, - array $attributes = [], - ): Post { - return Post::create([ - 'document' => $document, - 'document_filename' => $documentFilename, - 'document_mime' => $documentMime, - ...$attributes, - ]); - } }