Add image to form

This commit is contained in:
philipp lang 2024-01-12 23:29:18 +01:00
parent e011b52534
commit 97fabf0f24
9 changed files with 374 additions and 3 deletions

4
.gitmodules vendored
View File

@ -10,3 +10,7 @@
[submodule "packages/adrema-plugin"] [submodule "packages/adrema-plugin"]
path = packages/adrema-plugin path = packages/adrema-plugin
url = https://git.zoomyboy.de/silva/adrema-plugin.git url = https://git.zoomyboy.de/silva/adrema-plugin.git
[submodule "packages/medialibrary-helper"]
path = packages/medialibrary-helper
url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git
branch = version2

View File

@ -28,6 +28,7 @@ class FormStoreAction
'registration_until' => 'present|nullable|date', 'registration_until' => 'present|nullable|date',
'mail_top' => 'nullable|string', 'mail_top' => 'nullable|string',
'mail_bottom' => 'nullable|string', 'mail_bottom' => 'nullable|string',
'header_image' => 'required|exclude',
]; ];
} }
@ -36,7 +37,10 @@ class FormStoreAction
*/ */
public function handle(array $attributes): Form public function handle(array $attributes): Form
{ {
return Form::create($attributes); return tap(
Form::create($attributes),
fn ($form) => $form->setDeferredUploads(request()->input('header_image'))
);
} }
/** /**
@ -48,6 +52,7 @@ class FormStoreAction
...$this->globalValidationAttributes(), ...$this->globalValidationAttributes(),
'from' => 'Start', 'from' => 'Start',
'to' => 'Ende', 'to' => 'Ende',
'header_image' => 'Bild',
]; ];
} }

View File

@ -7,11 +7,16 @@ use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Zoomyboy\MedialibraryHelper\DefersUploads;
class Form extends Model class Form extends Model implements HasMedia
{ {
use HasFactory; use HasFactory;
use Sluggable; use Sluggable;
use InteractsWithMedia;
use DefersUploads;
public $guarded = []; public $guarded = [];
@ -26,6 +31,14 @@ class Form extends Model
]; ];
} }
public function registerMediaCollections(): void
{
$this->addMediaCollection('headerImage')
->singleFile()
->maxWidth(fn () => 500)
->forceFileName(fn (Form $model, string $name) => $model->slug);
}
/** @var array<int, string> */ /** @var array<int, string> */
public $dates = ['from', 'to', 'registration_from', 'registration_until']; public $dates = ['from', 'to', 'registration_from', 'registration_until'];

View File

@ -20,6 +20,13 @@
"options": { "options": {
"symlink": true "symlink": true
} }
},
{
"type": "path",
"url": "./packages/medialibrary-helper",
"options": {
"symlink": true
}
} }
], ],
"license": "MIT", "license": "MIT",
@ -53,7 +60,8 @@
"zoomyboy/laravel-nami": "dev-master", "zoomyboy/laravel-nami": "dev-master",
"zoomyboy/osm": "1.0.3", "zoomyboy/osm": "1.0.3",
"zoomyboy/phone": "^1.0", "zoomyboy/phone": "^1.0",
"zoomyboy/tex": "dev-main as 1.0" "zoomyboy/tex": "dev-main as 1.0",
"zoomyboy/medialibrary-helper": "dev-master as 1.0"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",

267
config/media-library.php Normal file
View File

@ -0,0 +1,267 @@
<?php
return [
/*
* The disk on which to store added files and derived images by default. Choose
* one or more of the disks you've configured in config/filesystems.php.
*/
'disk_name' => env('MEDIA_DISK', 'public'),
/*
* The maximum file size of an item in bytes.
* Adding a larger file will result in an exception.
*/
'max_file_size' => 1024 * 1024 * 10, // 10MB
/*
* This queue connection will be used to generate derived and responsive images.
* Leave empty to use the default queue connection.
*/
'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
/*
* This queue will be used to generate derived and responsive images.
* Leave empty to use the default queue.
*/
'queue_name' => '',
/*
* By default all conversions will be performed on a queue.
*/
'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
/*
* The fully qualified class name of the media model.
*/
'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class,
/*
* When enabled, media collections will be serialised using the default
* laravel model serialization behaviour.
*
* Keep this option disabled if using Media Library Pro components (https://medialibrary.pro)
*/
'use_default_collection_serialization' => false,
/*
* The fully qualified class name of the model used for temporary uploads.
*
* This model is only used in Media Library Pro (https://medialibrary.pro)
*/
'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class,
/*
* When enabled, Media Library Pro will only process temporary uploads that were uploaded
* in the same session. You can opt to disable this for stateless usage of
* the pro components.
*/
'enable_temporary_uploads_session_affinity' => true,
/*
* When enabled, Media Library pro will generate thumbnails for uploaded file.
*/
'generate_thumbnails_for_temporary_uploads' => true,
/*
* This is the class that is responsible for naming generated files.
*/
'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class,
/*
* The class that contains the strategy for determining a media file's path.
*/
'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class,
/*
* The class that contains the strategy for determining how to remove files.
*/
'file_remover_class' => Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover::class,
/*
* Here you can specify which path generator should be used for the given class.
*/
'custom_path_generators' => [
// Model::class => PathGenerator::class
// or
// 'model_morph_alias' => PathGenerator::class
],
/*
* When urls to files get generated, this class will be called. Use the default
* if your files are stored locally above the site root or on s3.
*/
'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class,
/*
* Moves media on updating to keep path consistent. Enable it only with a custom
* PathGenerator that uses, for example, the media UUID.
*/
'moves_media_on_update' => false,
/*
* Whether to activate versioning when urls to files get generated.
* When activated, this attaches a ?v=xx query string to the URL.
*/
'version_urls' => false,
/*
* The media library will try to optimize all converted images by removing
* metadata and applying a little bit of compression. These are
* the optimizers that will be used by default.
*/
'image_optimizers' => [
Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [
'-m85', // set maximum quality to 85%
'--force', // ensure that progressive generation is always done also if a little bigger
'--strip-all', // this strips out all text information such as comments and EXIF data
'--all-progressive', // this will make sure the resulting image is a progressive one
],
Spatie\ImageOptimizer\Optimizers\Pngquant::class => [
'--force', // required parameter for this package
],
Spatie\ImageOptimizer\Optimizers\Optipng::class => [
'-i0', // this will result in a non-interlaced, progressive scanned image
'-o2', // this set the optimization level to two (multiple IDAT compression trials)
'-quiet', // required parameter for this package
],
Spatie\ImageOptimizer\Optimizers\Svgo::class => [
'--disable=cleanupIDs', // disabling because it is known to cause troubles
],
Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [
'-b', // required parameter for this package
'-O3', // this produces the slowest but best results
],
Spatie\ImageOptimizer\Optimizers\Cwebp::class => [
'-m 6', // for the slowest compression method in order to get the best compression.
'-pass 10', // for maximizing the amount of analysis pass.
'-mt', // multithreading for some speed improvements.
'-q 90', //quality factor that brings the least noticeable changes.
],
Spatie\ImageOptimizer\Optimizers\Avifenc::class => [
'-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63).
'-j all', // number of jobs (worker threads, "all" uses all available cores).
'--min 0', // min quantizer for color (0-63).
'--max 63', // max quantizer for color (0-63).
'--minalpha 0', // min quantizer for alpha (0-63).
'--maxalpha 63', // max quantizer for alpha (0-63).
'-a end-usage=q', // rate control mode set to Constant Quality mode.
'-a tune=ssim', // SSIM as tune the encoder for distortion metric.
],
],
/*
* These generators will be used to create an image of media files.
*/
'image_generators' => [
Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class,
Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class,
],
/*
* The path where to store temporary files while performing image conversions.
* If set to null, storage_path('media-library/temp') will be used.
*/
'temporary_directory_path' => null,
/*
* The engine that should perform the image conversions.
* Should be either `gd` or `imagick`.
*/
'image_driver' => env('IMAGE_DRIVER', 'gd'),
/*
* FFMPEG & FFProbe binaries paths, only used if you try to generate video
* thumbnails and have installed the php-ffmpeg/php-ffmpeg composer
* dependency.
*/
'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
/*
* Here you can override the class names of the jobs used by this package. Make sure
* your custom jobs extend the ones provided by the package.
*/
'jobs' => [
'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class,
'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class,
],
/*
* When using the addMediaFromUrl method you may want to replace the default downloader.
* This is particularly useful when the url of the image is behind a firewall and
* need to add additional flags, possibly using curl.
*/
'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class,
'remote' => [
/*
* Any extra headers that should be included when uploading media to
* a remote disk. Even though supported headers may vary between
* different drivers, a sensible default has been provided.
*
* Supported by S3: CacheControl, Expires, StorageClass,
* ServerSideEncryption, Metadata, ACL, ContentEncoding
*/
'extra_headers' => [
'CacheControl' => 'max-age=604800',
],
],
'responsive_images' => [
/*
* This class is responsible for calculating the target widths of the responsive
* images. By default we optimize for filesize and create variations that each are 30%
* smaller than the previous one. More info in the documentation.
*
* https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images
*/
'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class,
/*
* By default rendering media to a responsive image will add some javascript and a tiny placeholder.
* This ensures that the browser can already determine the correct layout.
*/
'use_tiny_placeholders' => true,
/*
* This class will generate the tiny placeholder used for progressive image loading. By default
* the media library will use a tiny blurred jpg image.
*/
'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class,
],
/*
* When enabling this option, a route will be registered that will enable
* the Media Library Pro Vue and React components to move uploaded files
* in a S3 bucket to their right place.
*/
'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
/*
* When converting Media instances to response the media library will add
* a `loading` attribute to the `img` tag. Here you can set the default
* value of that attribute.
*
* Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction.
*
* More info: https://css-tricks.com/native-lazy-loading/
*/
'default_loading_attribute_value' => null,
/*
* You can specify a prefix for that is used for storing all media.
* If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory.
*/
'prefix' => env('MEDIA_PREFIX', ''),
/*
* The temp disk for deferred media data.
* Only used by the medialibrary-helper package.
*/
'temp_disk' => 'temp',
];

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('media', function (Blueprint $table) {
$table->id();
$table->morphs('model');
$table->uuid('uuid')->nullable()->unique();
$table->string('collection_name');
$table->string('name');
$table->string('file_name');
$table->string('mime_type')->nullable();
$table->string('disk');
$table->string('conversions_disk')->nullable();
$table->unsignedBigInteger('size');
$table->json('manipulations');
$table->json('custom_properties');
$table->json('generated_conversions');
$table->json('responsive_images');
$table->unsignedInteger('order_column')->nullable()->index();
$table->nullableTimestamps();
});
}
};

@ -0,0 +1 @@
Subproject commit c4eb67c09f8e310d20ebfdb330b70a265e45b1a4

View File

@ -2,6 +2,8 @@
namespace Tests\Feature\Form; namespace Tests\Feature\Form;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Worksome\RequestFactories\RequestFactory; use Worksome\RequestFactories\RequestFactory;
/** /**
@ -33,6 +35,7 @@ class FormRequest extends RequestFactory
'registration_until' => $this->faker->dateTime()->format('Y-m-d H:i:s'), 'registration_until' => $this->faker->dateTime()->format('Y-m-d H:i:s'),
'mail_top' => $this->faker->text(), 'mail_top' => $this->faker->text(),
'mail_bottom' => $this->faker->text(), 'mail_bottom' => $this->faker->text(),
'header_image' => $this->getHeaderImagePayload(str()->uuid() . '.jpg')
]; ];
} }
@ -51,4 +54,30 @@ class FormRequest extends RequestFactory
{ {
return $this->state([str($method)->snake()->toString() => $args[0]]); return $this->state([str($method)->snake()->toString() => $args[0]]);
} }
public function headerImage(string $fileName): self
{
UploadedFile::fake()->image($fileName, 1000, 1000)->storeAs('media-library', $fileName, 'temp');
Storage::disk('temp')->assertExists('media-library/' . $fileName);
return $this->state([
'header_image' => $this->getHeaderImagePayload($fileName)
]);
}
/**
* @return array<string, mixed>
*/
private function getHeaderImagePayload(string $fileName): array
{
UploadedFile::fake()->image($fileName, 1000, 1000)->storeAs('media-library', $fileName, 'temp');
Storage::disk('temp')->assertExists('media-library/' . $fileName);
return [
'file_name' => $fileName,
'collection_name' => 'headerImage',
];
}
} }

View File

@ -8,12 +8,20 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Tests\TestCase; use Tests\TestCase;
use Generator; use Generator;
use Illuminate\Support\Facades\Storage;
class FormStoreActionTest extends TestCase class FormStoreActionTest extends TestCase
{ {
use DatabaseTransactions; use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
Storage::fake('temp');
}
public function testItStoresForm(): void public function testItStoresForm(): void
{ {
Event::fake([Succeeded::class]); Event::fake([Succeeded::class]);
@ -25,6 +33,7 @@ class FormStoreActionTest extends TestCase
->registrationFrom('2023-05-04 01:00:00')->registrationUntil('2023-07-07 01:00:00')->from('2023-07-07')->to('2023-07-08') ->registrationFrom('2023-05-04 01:00:00')->registrationUntil('2023-07-07 01:00:00')->from('2023-07-07')->to('2023-07-08')
->mailTop('Guten Tag') ->mailTop('Guten Tag')
->mailBottom('Viele Grüße') ->mailBottom('Viele Grüße')
->headerImage('htzz.jpg')
->sections([FormtemplateSectionRequest::new()->name('sname')->fields([FormtemplateFieldRequest::new()])]) ->sections([FormtemplateSectionRequest::new()->name('sname')->fields([FormtemplateFieldRequest::new()])])
->fake(); ->fake();
@ -41,6 +50,8 @@ class FormStoreActionTest extends TestCase
$this->assertEquals('2023-07-07 01:00', $form->registration_until->format('Y-m-d H:i')); $this->assertEquals('2023-07-07 01:00', $form->registration_until->format('Y-m-d H:i'));
$this->assertEquals('2023-07-07', $form->from->format('Y-m-d')); $this->assertEquals('2023-07-07', $form->from->format('Y-m-d'));
$this->assertEquals('2023-07-08', $form->to->format('Y-m-d')); $this->assertEquals('2023-07-08', $form->to->format('Y-m-d'));
$this->assertCount(1, $form->getMedia('headerImage'));
$this->assertEquals('formname.jpg', $form->getMedia('headerImage')->first()->file_name);
Event::assertDispatched(Succeeded::class, fn (Succeeded $event) => $event->message === 'Veranstaltung gespeichert.'); Event::assertDispatched(Succeeded::class, fn (Succeeded $event) => $event->message === 'Veranstaltung gespeichert.');
} }
@ -63,6 +74,7 @@ class FormStoreActionTest extends TestCase
yield [FormRequest::new()->description(''), ['description' => 'Beschreibung ist erforderlich.']]; yield [FormRequest::new()->description(''), ['description' => 'Beschreibung ist erforderlich.']];
yield [FormRequest::new()->state(['from' => null]), ['from' => 'Start ist erforderlich']]; yield [FormRequest::new()->state(['from' => null]), ['from' => 'Start ist erforderlich']];
yield [FormRequest::new()->state(['to' => null]), ['to' => 'Ende ist erforderlich']]; yield [FormRequest::new()->state(['to' => null]), ['to' => 'Ende ist erforderlich']];
yield [FormRequest::new()->state(['header_image' => null]), ['header_image' => 'Bild ist erforderlich']];
} }
/** /**