diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 5cb3a86dc..84ab01ad2 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Fix style uses: docker://oskarstark/php-cs-fixer-ga diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6f15f24ad..de79bd175 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,15 +13,13 @@ jobs: fail-fast: false matrix: php: [7.4] - laravel: [8.*, 7.*, 6.*] + laravel: [8.*, 7.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 8.* testbench: 6.* - laravel: 7.* testbench: 5.* - - laravel: 6.* - testbench: 4.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} @@ -63,13 +61,3 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} AWS_BUCKET: ${{ secrets.AWS_BUCKET }} - - - name: Send Slack notification - uses: 8398a7/action-slack@v2 - if: failure() - with: - status: ${{ job.status }} - author_name: ${{ github.actor }} - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c9368c0d1..2f4fdd2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-medialibrary` will be documented in this file +## 9.0.0 - Unreleased + +- names of the generated conversions will now be put in a dedicated `generated_conversions` on media +- add support for media library pro +- responsive image files can now be named using the `file_namer` key in the `media-library` config file (#2114) + ## 8.10.1 - 2020-10-05 - add `queue_conversions_by_default` to config file @@ -86,6 +92,7 @@ All notable changes to `laravel-medialibrary` will be documented in this file - improve handling of file names with special characters (#1937) + ## 8.3.0 - 2020-06-11 - added `Spatie\MediaLibrary\MediaCollections\Models\Collections\MediaCollection` diff --git a/UPGRADING.md b/UPGRADING.md index b5bc6df35..d17f44bb4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,12 @@ Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide. +## From v8 to v9 + +- add a column `generated_conversions` to the `media` table. If you are using Media Library Pro you used copy the values you now have in the `generated_conversions` key of the `custom_properties` column to `generated_conversions` +- rename `conversion_file_namer` key in the `media-library` config to `file_namer`. This will support both the conversions and responsive images from now on. More info [in our docs](https://spatie.be/docs/laravel-medialibrary/v9/advanced-usage/naming-generated-files). +- in several releases of v8 config options were added. We recommend going over your config file in `config/media-library.php` and add any options that are present in the default config file that ships with this package. + ## From v7 to v8 - internally the media library has been restructured and nearly all namespaces have changed. Class names remained the same. In your application code hunt to any usages of classes that start with `Spatie\MediaLibrary`. Take a look in the source code of medialibrary what the new namespace of the class is and use that. diff --git a/composer.json b/composer.json index fdad4d4f3..50cd7eb24 100644 --- a/composer.json +++ b/composer.json @@ -22,14 +22,15 @@ } ], "require": { - "php": "^7.4", + "php": "^7.4|^8.0", + "ext-exif": "*", "ext-fileinfo": "*", "ext-json": "*", - "illuminate/bus": "^6.18|^7.0|^8.0", - "illuminate/console": "^6.18|^7.0|^8.0", - "illuminate/database": "^6.18|^7.0|^8.0", - "illuminate/pipeline": "^6.18|^7.0|^8.0", - "illuminate/support": "^6.18|^7.0|^8.0", + "illuminate/bus": "^7.0|^8.0", + "illuminate/console": "^7.0|^8.0", + "illuminate/database": "^7.0|^8.0", + "illuminate/pipeline": "^7.0|^8.0", + "illuminate/support": "^7.0|^8.0", "league/flysystem": "^1.0.64", "maennchen/zipstream-php": "^1.0|^2.0", "spatie/image": "^1.4.0", @@ -44,7 +45,7 @@ "guzzlehttp/guzzle": "^6.3|^7.0", "league/flysystem-aws-s3-v3": "^1.0.23", "mockery/mockery": "^1.3", - "orchestra/testbench": "^4.0|^5.0|^6.0", + "orchestra/testbench": "^5.0|^6.0", "php-ffmpeg/php-ffmpeg": "^0.16.0", "phpunit/phpunit": "^9.1", "spatie/pdf-to-image": "^2.0", diff --git a/config/media-library.php b/config/media-library.php index 93ade8ebb..3aa8e5ca3 100644 --- a/config/media-library.php +++ b/config/media-library.php @@ -35,60 +35,15 @@ */ 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::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 20% - * smaller than the previous one. More info in the documentation. - * - * https://docs.spatie.be/laravel-medialibrary/v8/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 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/ + * The fully qualified class name of the model used for temporary uploads. */ - 'default_loading_attribute_value' => null, + 'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class, /* - * This is the class that is responsible for naming conversion files. By default, - * it will use the filename of the original and concatenate the conversion name to it. + * This is the class that is responsible for naming generated files. */ - 'conversion_file_namer' => Spatie\MediaLibrary\Conversions\DefaultConversionFileNamer::class, + 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, /* * The class that contains the strategy for determining a media file's path. @@ -145,6 +100,12 @@ 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`. @@ -159,12 +120,6 @@ 'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'), 'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'), - /* - * 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, - /* * 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. @@ -181,9 +136,58 @@ */ 'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class, - 'events' => [ - 'updating' => false, - 'updated' => false, - 'deleted' => false, - ] + '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 20% + * smaller than the previous one. More info in the documentation. + * + * https://docs.spatie.be/laravel-medialibrary/v8/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', true), + + /* + * 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, ]; diff --git a/database/migrations/create_media_table.php.stub b/database/migrations/create_media_table.php.stub index d0ac2719b..4c4e6847f 100644 --- a/database/migrations/create_media_table.php.stub +++ b/database/migrations/create_media_table.php.stub @@ -22,6 +22,7 @@ class CreateMediaTable extends Migration $table->unsignedBigInteger('size'); $table->json('manipulations'); $table->json('custom_properties'); + $table->json('generated_conversions'); $table->json('responsive_images'); $table->unsignedInteger('order_column')->nullable(); diff --git a/docs/_index.md b/docs/_index.md index f45218327..e4ef3c37f 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -1,6 +1,6 @@ --- -title: v8 +title: v9 slogan: Associate files with Eloquent models. githubUrl: https://github.com/spatie/laravel-medialibrary -branch: master +branch: v9 --- diff --git a/docs/advanced-usage/naming-generated-files.md b/docs/advanced-usage/naming-generated-files.md new file mode 100644 index 000000000..dbaf8f676 --- /dev/null +++ b/docs/advanced-usage/naming-generated-files.md @@ -0,0 +1,55 @@ +--- +title: Naming generated files +weight: 11 +--- + +### Naming conversion files + +By default, all conversion files will be named in this format: + +``` +{original-file-name-without-extension}-{name-of-the-conversion}.{extension} +``` + +Should you want to name your conversion file using another format, +then you can specify the class name of your own `FileNamer` in the `file_namer` key +of the `media-library.php` config file. + +The only requirements is that your class extends `Spatie\MediaLibrary\Support\FileNamer`. +In your class you should implement 2 methods: +1. `conversionFileName` should return the media file name combined with the conversion name +2. `responsiveFileName` should return the media file name + +Here is the implementation of `Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer` + +```php +namespace Spatie\MediaLibrary\Support\FileNamer; + +use Spatie\MediaLibrary\Conversions\Conversion; + +class DefaultFileNamer extends FileNamer +{ + public function conversionFileName(string $fileName, Conversion $conversion): string + { + $strippedFileName = pathinfo($fileName, PATHINFO_FILENAME); + + return "{$strippedFileName}-{$conversion->getName()}"; + } + + public function responsiveFileName(string $fileName): string + { + return pathinfo($fileName, PATHINFO_FILENAME); + } +} +``` + +### Naming responsive image files + +By default, all responsive image files will be named in this format: + +``` +{original-file-name-without-extension}___{name-of-the-conversion}_{width}_{height}.{extension} +``` + +Just like the conversion file names, you can use another format for naming your files +by using your own `FileNamer` class. It is only possible to prefix the name, because other parts are needed in processing responsive images. diff --git a/docs/advanced-usage/working-with-multiple-filesystems.md b/docs/advanced-usage/working-with-multiple-filesystems.md index 2f6fd082a..be2e4bfd4 100644 --- a/docs/advanced-usage/working-with-multiple-filesystems.md +++ b/docs/advanced-usage/working-with-multiple-filesystems.md @@ -3,7 +3,7 @@ title: Working with multiple filesystems weight: 1 --- -By default all files are stored on the disk specified as the `disk_name` in the config file. +By default, all files are stored on the disk specified as the `disk_name` in the config file. Files can also be stored [on any filesystem that is configured in your Laravel app](http://laravel.com/docs/7.x/filesystem#configuration). When adding a file to the media library you can choose on which disk the file should be stored. This is useful when you have a combination of small files that should be stored locally and big files that you want to save on S3. @@ -16,7 +16,7 @@ $yourModel->addMedia($pathToAFile)->toMediaCollection('images', 's3'); ## Storing conversions on a separate disk -You can let the media library store [your conversions](/laravel-medialibrary/v8/converting-images/defining-conversions) and [responsive images](/laravel-medialibrary/v8/responsive-images/getting-started-with-responsive-images) on a disk other than the one where you save the original item. Pass the name of the disk where you want conversion to be saved to the `storingConversionsOnDisk` method. +You can let the media library store [your conversions](/v9/converting-images/defining-conversions) and [responsive images](/v9/responsive-images/getting-started-with-responsive-images) on a disk other than the one where you save the original item. Pass the name of the disk where you want conversion to be saved to the `storingConversionsOnDisk` method. Here's an example where the original file is saved on the local disk and the conversions on S3. diff --git a/docs/api/defining-conversions.md b/docs/api/defining-conversions.md index 8e9139a30..c83fcd90d 100644 --- a/docs/api/defining-conversions.md +++ b/docs/api/defining-conversions.md @@ -5,7 +5,7 @@ weight: 2 A media conversion can be added to your model in the `registerMediaConversions`-function. It should start with a call to `addMediaConversion`. From there on you can use any of the methods available in the API. They are all chainable. -Take a look in the [Defining conversions section](/laravel-medialibrary/v8/converting-images/defining-conversions/) +Take a look in the [Defining conversions section](/laravel-medialibrary/v9/converting-images/defining-conversions/) for more details. ## General methods diff --git a/docs/converting-images/defining-conversions.md b/docs/converting-images/defining-conversions.md index b3dd92f7f..655c434c7 100644 --- a/docs/converting-images/defining-conversions.md +++ b/docs/converting-images/defining-conversions.md @@ -81,7 +81,7 @@ $media->getUrl('old-picture') // the url to the sepia, bordered version ## Performing conversions on specific collections -By default a conversion will be performed on all files regardless of which [collection](/laravel-medialibrary/v8/working-with-media-collections/simple-media-collections) is used. Conversions can also be performed on all specific collections by adding a call to `performOnCollections`. +By default a conversion will be performed on all files regardless of which [collection](/laravel-medialibrary/v9/working-with-media-collections/simple-media-collections) is used. Conversions can also be performed on all specific collections by adding a call to `performOnCollections`. This is how that looks like in the model: @@ -109,7 +109,7 @@ $media->getUrl('thumb') // returns '' ## Queuing conversions -By default, a conversion will be added to the queue that you've [specified in the configuration](https://docs.spatie.be/laravel-medialibrary/v8/installation-setup). If you want your image to be created directly (and not on a queue) use `nonQueued` on a conversion. +By default, a conversion will be added to the queue that you've [specified in the configuration](https://docs.spatie.be/laravel-medialibrary/v9/installation-setup). If you want your image to be created directly (and not on a queue) use `nonQueued` on a conversion. ```php // in your model diff --git a/docs/converting-images/naming-conversion-files.md b/docs/converting-images/naming-conversion-files.md deleted file mode 100644 index 19c0ca389..000000000 --- a/docs/converting-images/naming-conversion-files.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Naming conversion files -weight: 4 ---- - -By default, all conversion files will be named in this format: - -``` -{original-file-name-without-extension}-{name-of-the-conversion}.{extension} -``` - -Should you want to name your conversion file using another format, than you can specify the class name of your own `ConversionFileNamer` in the `conversion_file_namer` key of the `media-library.php` config file. - -The only requirement is that your class extends `Spatie\MediaLibrary\Conversion\ConversionFileNamer`. In your class you should implement the `getFileName` method that returns the name of the file without the extension. - -Here the implementation of `Spatie\MediaLibrary\Conversion\DefaultConversionFileNamer` - -```php -namespace Spatie\MediaLibrary\Conversions; - -use Spatie\MediaLibrary\MediaCollections\Models\Media; - -class DefaultConversionFileNamer extends ConversionFileNamer -{ - public function getFileName(Conversion $conversion, Media $media): string - { - $fileName = pathinfo($media->file_name, PATHINFO_FILENAME); - - return "{$fileName}-{$conversion->getName()}"; - } -} -``` diff --git a/docs/converting-images/optimizing-converted-images.md b/docs/converting-images/optimizing-converted-images.md index 8b41d6ddf..0fa7c1d96 100644 --- a/docs/converting-images/optimizing-converted-images.md +++ b/docs/converting-images/optimizing-converted-images.md @@ -26,7 +26,7 @@ The package will use these optimizers if they are present on your system: - [SVGO](https://github.com/svg/svgo) - [Gifsicle](http://www.lcdf.org/gifsicle/) -Head over to [the installation page](https://docs.spatie.be/laravel-medialibrary/v8/installation-setup#optimization-tools) to learn how to install these. +Head over to [the installation page](https://docs.spatie.be/laravel-medialibrary/v9/installation-setup#optimization-tools) to learn how to install these. ## Which tools will do what? diff --git a/docs/converting-other-file-types/creating-a-custom-image-generator.md b/docs/converting-other-file-types/creating-a-custom-image-generator.md index 7e0b0408e..83f3d3f14 100644 --- a/docs/converting-other-file-types/creating-a-custom-image-generator.md +++ b/docs/converting-other-file-types/creating-a-custom-image-generator.md @@ -54,7 +54,7 @@ class PowerPoint extends ImageGenerator ## Registering the custom generator If you want the generator to be applied to all your models, you can override the `Media` class as explained in the -[using your own model](/laravel-medialibrary/v8/advanced-usage/using-your-own-model/) page and modify the +[using your own model](/laravel-medialibrary/v9/advanced-usage/using-your-own-model/) page and modify the `getImageGenerators` method in your own `Media` class. diff --git a/docs/converting-other-file-types/using-image-generators.md b/docs/converting-other-file-types/using-image-generators.md index d58b8f58e..5a79a8bcc 100644 --- a/docs/converting-other-file-types/using-image-generators.md +++ b/docs/converting-other-file-types/using-image-generators.md @@ -16,9 +16,9 @@ $this->addMediaConversion('thumb') The media library includes image generators for the following file types: -- [PDF](/laravel-medialibrary/v8/converting-other-file-types/using-image-generators#pdf) -- [SVG](/laravel-medialibrary/v8/converting-other-file-types/using-image-generators#svg) -- [Video](/laravel-medialibrary/v8/converting-other-file-types/using-image-generators#video) +- [PDF](/laravel-medialibrary/v9/converting-other-file-types/using-image-generators#pdf) +- [SVG](/laravel-medialibrary/v9/converting-other-file-types/using-image-generators#svg) +- [Video](/laravel-medialibrary/v9/converting-other-file-types/using-image-generators#video) ## PDF @@ -41,7 +41,7 @@ The only requirement to perform a conversion of a SVG file is [Imagick](http://p ## Video -The video image generator uses the [PHP-FFMpeg](https://github.com/PHP-FFMpeg/PHP-FFMpeg) package that you can install via composer: +The video image generator uses the [PHP-FFMpeg](https://github.com/PHP-FFMpeg/PHP-FFMpeg) package that you can install via Composer: ```bash composer require php-ffmpeg/php-ffmpeg diff --git a/docs/downloading-media/_index.md b/docs/downloading-media/_index.md index 6d032010d..3e9f1d6d9 100644 --- a/docs/downloading-media/_index.md +++ b/docs/downloading-media/_index.md @@ -1,4 +1,4 @@ --- title: Downloading media -weight: 5 +weight: 6 --- diff --git a/docs/handling-uploads-with-media-library-pro/_index.md b/docs/handling-uploads-with-media-library-pro/_index.md new file mode 100644 index 000000000..cc4743f7e --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/_index.md @@ -0,0 +1,4 @@ +--- +title: Handling uploads with Media Library Pro +weight: 4 +--- diff --git a/docs/handling-uploads-with-media-library-pro/creating-custom-react-components.md b/docs/handling-uploads-with-media-library-pro/creating-custom-react-components.md new file mode 100644 index 000000000..2a1dd123e --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/creating-custom-react-components.md @@ -0,0 +1,114 @@ +--- +title: Creating custom React components +weight: 8 +--- + +Both the Vue and React UI components are built on top of the same core, with a language-specific abstraction layer in between that exposes some helper functions. You can extend the core that the UI components are built on. This allow you to customize the UI. This page will go into detail about these abstraction layers. + +The standard Medialibary UI components are built using helper components. These helper components can be used individually to make custom components. You can read more about the helper components in the [Helper Components](TODO-link) section. + +To create your own UI components that hook into the Media Library Pro JS core, you can use the `useMediaLibrary` hook in a functional component. If you are building a class component, there are several tutorials available online on how to wrap a custom hook in a higher-order component for use in class components. + +For more extensive examples, [see the pre-built UI components on GitHub](TODO-link:#examples) + +### Getting started + +```jsx +import * as React from "react"; +import { useMediaLibrary } from "media-library-pro-react"; + +export default function MediaLibraryAttachment() { + const { + state, + getImgProps, + getFileInputProps, + getDropZoneProps, + removeMedia, + setOrder, + replaceMedia, + getErrors, + clearObjectErrors, + clearInvalidMedia, + isReadyToSubmit, + } = useMediaLibrary({ + initialMedia: initialValue, + validationErrors, + uploadEndpoint, + validationRules, + multiple, + beforeUpload, + afterUpload, + onChange, + }); + + return ( +
+ +
+ ); +} +``` + +You can find a full list of props [at the bottom of this page](TODO-link). + +## Examples + +For extensive examples you can have a look at the source of the pre-built UI components: + +- [React collection component](https://github.com/spatie/laravel-medialibrary-pro/tree/master/resources/js/media-library-pro-react-attachment) +- [React collection component](https://github.com/spatie/laravel-medialibrary-pro/tree/master/resources/js/media-library-pro-react-collection) + +## Helper components + +When building your own UI component using the Media Library Pro, you can also make use of these helper components. + +### DropZone + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-react/src/DropZone.tsx) + +TODO screenshot +TODO description + +### InvalidMedia + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-react/src/InvalidMedia.tsx) + +TODO screenshot +TODO description + +### MediaFormValues + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-react/src/MediaFormValues.tsx) + +TODO screenshot +TODO description + +### PreviewImage + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-react/src/PreviewImage.tsx) + +TODO screenshot +TODO description + +### useDragula (React only) + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-react/src/useDragula.ts) + +TODO description + +## Props + +| prop name (React) | Default value | Description | +| ----------------- | ------------- | ----------- | +| initialMedia | | | +| uploadEndpoint | | | +| validationErrors | | | +| validation | | | +| multiple | | | +| beforeUpload | | | +| afterUpload | | | +| onChange | | | + +TODO @is-ready-to-submit-change and the "is something uploading" listener + +TODO expand on the prop list, look into examples of how other packages do this etc diff --git a/docs/handling-uploads-with-media-library-pro/creating-custom-vue-components.md b/docs/handling-uploads-with-media-library-pro/creating-custom-vue-components.md new file mode 100644 index 000000000..86ee8b89b --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/creating-custom-vue-components.md @@ -0,0 +1,118 @@ +--- +title: Creating custom Vue components +weight: 7 +--- + +Both the Vue and React UI components are built on top of the same core, with a language-specific abstraction layer in between that exposes some helper functions. You can extend the core that the UI components are built on. This allow you to customize the UI. This page will go into detail about these abstraction layers. + +The standard Medialibary UI components are built using helper components. These helper components can be used individually to make custom components. You can read more about the helper components in the [Helper Components](TODO-link) section. + +## Vue + +The Vue implementation uses a renderless component that exposes all the functions and values through a slot scope. + +Note: in a future version if we decide to refactor the Vue implementation to work with the new composition API that was released in Vue 3. + +For more extensive examples, [see the pre-built UI components on GitHub](TODO-link:#examples) + +### Getting started + +```html + + + +``` + +You can find a full list of props [at the bottom of this page](TODO-link). + +## Examples + +For extensive examples you can have a look at the source of the pre-built UI components: + +- [Vue attachment component](https://github.com/spatie/laravel-medialibrary-pro/tree/master/resources/js/media-library-pro-vue-attachment) +- [Vue collection component](https://github.com/spatie/laravel-medialibrary-pro/tree/master/resources/js/media-library-pro-vue-collection) + +## Helper components + +When building your own UI component using the Media Library Pro, you can also make use of these helper components. + +### DropZone + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-vue/src/DropZone.vue) + +TODO screenshot +TODO description + +### InvalidMedia + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-vue/src/InvalidMedia.vue) + +TODO screenshot +TODO description + +### MediaFormValues + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-vue/src/MediaFormValues.vue) + +TODO screenshot +TODO description + +### PreviewImage + +[See code on GitHub](https://github.com/spatie/laravel-medialibrary-pro/blob/master/resources/js/media-library-pro-vue/src/PreviewImage.vue) + +TODO screenshot +TODO description + +## Props + +| prop name (Vue) | Default value | Description | +| ----------------- | ------------- | ----------- | +| initial-media | | | +| upload-endpoint | | | +| validation-errors | | | +| validation | | | +| multiple | | | +| before-upload | | | +| after-upload | | | +| @change | | | + +TODO @is-ready-to-submit-change and the "is something uploading" listener + +TODO expand on the prop list, look into examples of how other packages do this etc diff --git a/docs/handling-uploads-with-media-library-pro/customizing-css.md b/docs/handling-uploads-with-media-library-pro/customizing-css.md new file mode 100644 index 000000000..1fb23e101 --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/customizing-css.md @@ -0,0 +1,79 @@ +--- +title: Customizing CSS +weight: 9 +--- + +If you want to change the look of the Media Library Pro components to match the style of your own app, you have multiple options. + +## 1. Use your own Tailwind CSS configuration + +Instead of importing/linking the pre-built `dist/styles.css` from the package, you can import the `src/styles.css` and run every `@apply` rule through your own `tailwind.config.js`. + +```css +/* app.css */ + +@tailwind base; + +@tailwind components; + +@tailwind utilities; + +@import "src/styles.css"; + +… +``` + +This is exactly what happens in the header of the homepage at [medialibrary.pro](https://medialibrary.pro): the shown component has a slightly blue-ish look, using the color palette of this site. + + +## 2. Override only portions in your CSS + +If you only want to tinker with certain aspects of the component but like to keep the CSS in sync with future package updates, nothing stops you from overriding only certain CSS rules with your own tweaks. Every DOM-element of the component has a class with prefix `media-library`. + +Let's say your thumbs aren't square and you want to show them in their original aspect ratio. +Inspect the component in your browser to find out that the thumbnail is rendered in the DOM element with class `media-library-thumb-img`. Next, write some extra CSS for this class: + +```css +/* app.css */ + +… + +@import "src/styles.css"; + +.media-library-thumb-img { + @apply object-contain; +} + +``` + + +## 3. Copy the CSS to your own project + +If you want to go full-option, you can always copy `src/styles.css` to your own project and go wild. +In this example we renamed the file to `custom/media-library.css`. +Beware: you will have to manually keep this CSS in sync with changes in future package updates. + +```css +/* app.css */ + +… + +@import "custom/media-library.css"; +``` + +One of the many changes we like to do, is detaching the error list at the top and give it rounded corners: + +```css +/* custom/media-library.css */ + +… + +.media-library-listerrors { + … + @apply mb-6; + @apply rounded-lg; + … +} +``` + +This is what we've done on the Customized Collection demo on [medialibrary.pro/demo-customized-collection](http://medialibrary.pro/demo-customized-collection). Pick a file that is too big to see the error list in effect. diff --git a/docs/handling-uploads-with-media-library-pro/handling-uploads-with-blade.md b/docs/handling-uploads-with-media-library-pro/handling-uploads-with-blade.md new file mode 100644 index 000000000..052df0ee7 --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/handling-uploads-with-blade.md @@ -0,0 +1,210 @@ +--- +title: Handling uploads with Blade +weight: 4 +--- + +You can make use of the `x-media-library-attachment` and `x-media-library-collection` Blade components to handle uploads. + +## Getting started + +The Blade components that handle uploads leverage [Livewire](https://laravel-livewire.com) under the hood. That's why +you must follow [Livewire's installation instructions](https://laravel-livewire.com/docs/installation) as well. + +## Handling a single upload + +You can use `x-media-library-attachment` to upload a single file. Here's an example: + +```html +
+ @csrf + + + + + + + +``` + +![Screenshot of the attachment component](/docs/laravel-medialibrary/v9/images/pro/attachment.png) + +The `x-media-library-attachment` will take care of the upload. Under the hood the upload is processed by +a [Livewire](https://laravel-livewire.com) component. + +After a file has been uploaded it will be stored as a temporary upload. In case there are validation errors when +submitting the form, the `x-media-library-attachment` will display the temporary upload when you get redirected back to +the form. There's no need for the user to upload the file again. + +In the controller handling the form submission you should validate the temporary upload and transfer it to an Eloquent +model. You can read more on that [on this page](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/processing-uploads-on-the-server). + +## Handling multiple uploads + +Here's an example of how you can allow multiple uploads + +```html +
+ @csrf + Name: + + + + + +``` + +![Screenshot of the attachment component](/docs/laravel-medialibrary/v9/images/pro/multiple.png) + +After files have been uploaded, they will be stored as a temporary uploads. + +In the controller handling the form submission you should validate the temporary upload and transfer it to an Eloquent +model. You can read more on that [on this page](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/processing-uploads-on-the-server). + +## Setting a maximum amount of uploads + +To set a maximum number of files you can add a `max-items` attribute. Here is an example where users can only upload two +files. + +```html + +``` + +## Validating uploads in real time + +The upload can be validated before the form is submitted by adding a `rules` attribute. In the value of the attribute +you can use any of Laravel's available validation rules that are applicable to file uploads. + +Here's an example where we only accept `png` and `jpg` files that are 1MB or less. + +```html + +``` + +This validation only applies on the creation of the temporary uploads. You should also perform validation +when [processing the upload on the server](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/processing-uploads-on-the-server). + +## Administer the contents of a media library collection + +You can manage the entire contents of a media library collection with `x-media-library-collection` component. This +component is intended to use in admin sections. + +Here is an example where we are going to administer an `images` collection of a `$blogPost` model. We assume that you +already [prepared the model](/docs/laravel-medialibrary/v9/basic-usage/preparing-your-model) to handle uploads. + +```html + +``` + +This component will display the contents of the entire collection. Files can be added, removed, updated and reordered. +New files will be uploaded as temporary uploads. + +The value you pass in `name` of the component will be use as the key name in which the component will send the state of +the collection to the backend. In the controller handling the form submission you should validate the new contents of +the collection and sync it with the collection of the eloquent model. You can read more on that [on this page](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/processing-uploads-on-the-server). + +Like the `x-media-library-attachment` component, the `x-media-library-collection` accepts `max-items` and `rules` props. + +In this example, the collection will be allowed to hold `png` and `jpg` files that are smaller than 1 MB. + +```html + +``` + +### Using custom properties + +The media library supports [custom properties](/docs/laravel-medialibrary/v9/advanced-usage/using-custom-properties) to be saved on a media item. By +default, the `x-media-library-collection` component doesn't show the custom properties. To add them you should create a +blade view that will be used to display all form elements on a row in the component. + +In this example we're going to add a custom property form field called `extra_field`. + +```html +@include('media-library::livewire.partials.collection.fields') + +
+ + customPropertyAttributes('extra_field') }} + /> + + @error($mediaItem->customPropertyErrorName('extra_field')) + + {{ $message }} + + @enderror +
+``` + +You should then pass the path to that view to the `fields-view` prop of the `x-media-library-collection` component. + +```html + +``` + +This is how that will look like. + +![Screenshot of custom property](/docs/laravel-medialibrary/v9/images/pro/extra.png) + + +Custom properties can be validated using [a form request](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/processing-uploads-on-the-server). + +```php +namespace App\Models; + +use Illuminate\Database\Eloquent\Model; +use Spatie\MediaLibrary\HasMedia; +use Spatie\MediaLibrary\InteractsWithMedia; +use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\Image\Manipulations; + +class BlogPost extends Model implements HasMedia +{ + use InteractsWithMedia; + + public function registerMediaConversions(Media $media = null): void + { + $this + ->addMediaConversion('preview') + ->fit(Manipulations::FIT_CROP, 300, 300) + ->nonQueued(); + } +} +``` + +## Uploading directly to S3 + +Under the hood, the `attachment` and `collection` components use Livewire to perform uploads. Currently, Livewire does not support uploading multiple files to S3. That's why only the `attachment` component can be used to upload files to S3. + +To get started with upload files to `s3`, make sure to follow Livewire's instructions on [how to upload directly to S3](https://laravel-livewire.com/docs/2.x/file-uploads#upload-to-s3). + +Next, make sure you configured the media disk that uses the S3 driver. + +With that configuration in place, the `attachment` component will now upload directly to S3. + + diff --git a/docs/handling-uploads-with-media-library-pro/handling-uploads-with-react.md b/docs/handling-uploads-with-media-library-pro/handling-uploads-with-react.md new file mode 100644 index 000000000..91b5c77d8 --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/handling-uploads-with-react.md @@ -0,0 +1,386 @@ +--- +title: Handling uploads with React +weight: 6 +--- + +Media Library Pro provides beautiful UI components for React. They pack a lot of features: temporary uploads, custom property inputs, frontend validation, and robust error handling. + +The `MediaLibraryAttachment` component can upload one or more files with little or no extra information. The attachment component is a lightweight solution for small bits of UI like avatar fields. + +![Screenshot of the MediaLibraryAttachment React component](/docs/laravel-medialibrary/v9/images/pro/attachment.png) + +The `MediaLibraryCollection` component can upload multiple files with custom properties. The collection component shines when you need to manage media, like in backoffices. + +![Screenshot of the MediaLibraryCollection React component](/docs/laravel-medialibrary/v9/images/pro/collection.png) + +If neither of these fit the bill, we've exposed a set of APIs for you to be bold and [roll your own components](#). + +## Basic setup + +First, the server needs to be able to catch your incoming uploads. Use the `temporaryUploads` macro in your routes file. + +```php +// Probably routes/web.php +Route::temporaryUploads(); +``` + +The macro will register a route on `/media-library-uploads`, which is used by the React components by default. If you wish to use a different endpoint, just pass the desired +url to the macro. + +```php +// Probably routes/web.php +Route::temporaryUploads('your-url'); +``` + +### Customizing the upload endpoint + +The React components post data to `/media-library-uploads` by default. If you registered the controller on a different URL, pass it to the `uploadEndpoint` prop of your React components. + +```jsx + +``` + +### Importing the components + +The components aren't available through npm, but are located in `vendor/spatie/laravel-medialibrary-pro/resources/js` when you install the package through Composer. This makes for very long import statements, which you can clean up by adding some configuration to your Webpack/Laravel Mix configuration: + +**laravel-mix >6** + +```js +// webpack.mix.js + +mix.override((webpackConfig) => { + webpackConfig.resolve.modules = [ + "node_modules", + __dirname + "/vendor/spatie/laravel-medialibrary-pro/resources/js", + ]; +} +``` + +**laravel-mix <6** + +```js +// webpack.mix.js + +mix.webpackConfig({ + resolve: { + modules: [ + "node_modules", + __dirname + "/vendor/spatie/laravel-medialibrary-pro/resources/js", + ], + }, +}); +``` + +This will force Webpack to look in `vendor/spatie/laravel-medialibrary-pro/resources/js` when resolving imports, and allows you to shorten your import to this: + +```js +import MediaLibraryAttachment from "media-library-pro-react-attachment"; +``` + +If you're using TypeScript, you will also have have to add this to your tsconfig: + +```json +// tsconfig.json + +{ + "compilerOptions": { + "paths": { + "*": ["vendor/spatie/laravel-medialibrary-pro/resources/js/*"] + } + } +} +``` + +## Your first components + +The most basic components have a `name` prop. This name will be used to identify the media when it's uploaded to the server. + +```jsx +// MyImageUploader.jsx + +import MediaLibraryAttachment from "media-library-pro-react-attachment"; +import MediaLibraryCollection from "media-library-pro-react-collection"; + +export default function MyImageUploader() { + return ( +
+ + + + + + ); +} +``` + +### Passing an initial value + +If your form modifies an existing set of media, you may pass it through in the `initialValue` prop. + +You can retrieve your initial values in Laravel using `$yourModel->getMedia($collectionName);`, this will also take care of any `old` values after an invalid form submit. + +```jsx +
+ + + + + +``` + +Under the hood, these components create hidden `` fields to keep track of the form values on submit. If you would like to submit your values asynchronously, refer to the `Asynchronously submit data` section. + +### Setting validation rules + +You'll probably want to validate what gets uploaded. Use the `validationRules` prop, and don't forget to pass Laravel's validation errors too. The validation errors returned from the server will find errors under the key used in your `name` prop. + +```jsx +
+ + + + + + +``` + +You can also set the maximum amount of images that users can be uploaded using the `max-items` prop. Don't forget to set the `multiple` prop for the attachment component. + +```jsx +
+ + + + + +``` + +See the [Validation rules section](#validation-rules) for a complete list of all possible validation rules. + +### Checking the upload state + +The components keep track of whether they're ready to be submitted, you can use this to disable a submit button while a file is still uploading or when there are frontend validation errors. This value can be tracked by passing a listener method to the `onIsReadyToSubmitChange` prop. If you submit a form while a file is uploading, Laravel will return a HTTP 500 error with an `invalid uuid` message. + +```jsx +import MediaLibraryAttachment from "media-library-pro-react-attachment"; + +function AvatarComponent() { + const [isReadyToSubmit, setIsReadyToSubmit] = useState(true); + + return( + + + + ) +} +``` + +### Using custom properties + +The Media Library supports [custom properties](/docs/laravel-medialibrary/v9/advanced-usage/using-custom-properties) to be saved on a media item. The values for these can be chosen by your users. By default, the `MediaLibraryAttachment` component doesn't show any input fields, and the `MediaLibraryCollection` component only shows a `name` field, with the option to add more fields. + +Use the `fieldsView` render prop to add some fields: + +```jsx + ( +
+
+ + + + {getNameInputErrors().map((error) => ( +

+ {error} +

+ ))} +
+ +
+ + + + {getCustomPropertyInputErrors("extra_field").map((error) => ( +

+ {error} +

+ ))} +
+
+ )} +/> +``` + +When you add an image to your collection, it will look like this. + +![Screenshot of custom property](/docs/laravel-media-library/v9/images/pro/extra.png) + +### Customizing the file properties + +When uploading a file, some properties appear by default: its extension, filesize and a remove or download button (respectively for the attachment or collection component). + +You can customize what is displayed here by using the `propertiesView` scoped slot: + +```jsx + ( +
{object.attributes.name}
+ )} +/> +``` + +### Asynchronously submit data + +If you don't want to use traditional form submits to send your data to the backend, you will have to keep track of the current value of the component using the `onChange` handler. The syntax is the same for all UI components: + +```jsx +import Axios from 'axios'; + +export function AvatarForm({ values }) { + const [media, setMedia] = React.useState(values.media); + const [validationErrors, setValidationErrors] = React.useState({}); + + function submitForm() { + Axios + .post('endpoint', { media }) + .catch(error => setValidationErrors(error.data.errors)); + } + + return ( + <> + + + + + ); +} +``` + +### Using with Laravel Vapor + +If you are planning on deploying your application to AWS using [Laravel Vapor](https://vapor.laravel.com/), you will need to pass `{ enabled: true }` for the optional `vapor` prop to your components so your files can be uploaded to an S3 bucket. + +When uploading a file, the component will first get a signed storage URL from Vapor. By default, its value is `'/vapor/signed-storage-url'`. If you changed it in Laravel, you can override it by setting a value for `vapor.signedStorageUrl`. + +After uploading the file to S3, the component will notify your application of where the file was stored. For this, you need to register the Media Library S3 uploads controller in your routes file: `Route::mediaLibraryS3Uploads('media-library-post-s3');`. The components expect this endpoint to be `/media-library-post-s3`, if you chose a different endpoint, set it in `vapor.postS3Endpoint`. + +```jsx + +``` + +## Validation rules + +There are a couple of different ways to validate files on the frontend. These props are available to you: `validationRules`, `maxItems` and `beforeUpload`. + +**validationRules** + +In the `validationRules` object, we've got the `accept` property, which expects an array of MIME types as strings. Leave it empty to accept all types of files, set its value to `['image/*']` to accept any type of image, or choose your own set of rules using [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types). Remember, the only valid MIME type of a JPEG/JPG is `image/jpeg`! + +The `minSizeInKB` and `maxSizeInKB` properties set the minimum and maximum size of any individual file. + +```jsx + +``` + +**maxItems** + +Set the maximum amount of items in the collection/attachment component at any time. + +```jsx + +``` + +**beforeUpload** + +Pass a method to `beforeUpload` that accepts a [file](https://developer.mozilla.org/en-US/docs/Web/API/File) parameter. Return any value (or resolve a Promise with any value) from this function to upload the file. Throw an Error in this function to cause the file not to be uploaded, and display your error message. + +```jsx +function checkFileValidity(file) { + return new Promise((resolve) => { + if (file.size < 1000) { + return resolve(); + } + + throw new Error("The uploaded file is too big"); + }); +} + +return ( + +); +``` + +## Props + +These props are available on both the `attachment` and the `collection` component. + +| prop name | Default value | Description | +| ------------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | | | +| initialValue | `[]` | | +| uploadEndpoint | `"media-library-uploads"` | | +| validationRules | | Refer to [validation](#validation-rules) section | +| validationErrors | | The standard Laravel validation error object | +| multiple | `false` (always `true` in the `collection` component) | Only exists on the `attachment` components | +| maxItems | `1` when `multiple` = `false`, otherwise `undefined | | +| vapor | | Set to true if you will deploy your application to Vapor. This enables uploading of the files to S3. | +| maxSizeForPreviewInBytes | `5242880` (5 MB) | When an image is added, the component will try to generate a local preview for it. This is done on the main thread, and can freeze the component and/or page for very large files | +| sortable | `true` | Only exists on the `collection` components. Allows the user to drag images to change their order, this will be reflected by a zero-based `order` attribute in the value | +| setMediaLibrary | | Used to set a reference to the MediaLibrary instance, so you can change the internal state of the component. | +| beforeUpload | | A method that is run right before a temporary upload is started. You can throw an `Error` from this function with a custom validation message | +| afterUpload | | A method that is run right after a temporary upload has completed, `{ success: true, uuid }` | +| onChange | | | +| onIsReadyToSubmitChange | | Refer to [Checking the upload state](#checking-the-upload-state) section | diff --git a/docs/handling-uploads-with-media-library-pro/handling-uploads-with-vue.md b/docs/handling-uploads-with-media-library-pro/handling-uploads-with-vue.md new file mode 100644 index 000000000..19f18476c --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/handling-uploads-with-vue.md @@ -0,0 +1,559 @@ +--- +title: Handling uploads with Vue +weight: 5 +--- + +Media Library Pro provides beautiful UI components for Vue 2 and Vue 3. They pack a lot of features: temporary uploads, custom property inputs, frontend validation, and robust error handling. + +The `MediaLibraryAttachment` component can upload one or more files with little or no extra information. The attachment component is a lightweight solution for small bits of UI like avatar fields. + +![Screenshot of the MediaLibraryAttachment Vue component](/docs/laravel-medialibrary/v9/images/pro/attachment.png) + +The `MediaLibraryCollection` component can upload multiple files with custom properties. The collection component shines when you need to manage media, like in backoffices. + +![Screenshot of the MediaLibraryCollection Vue component](/docs/laravel-medialibrary/v9/images/pro/collection.png) + +If neither of these fit the bill, we've exposed a set of APIs for you to be bold and [roll your own components](#). + +## Basic setup + +First, the server needs to be able to catch your incoming uploads. Use the `temporaryUploads` macro in your routes file. + +```php +// Probably routes/web.php +Route::temporaryUploads(); +``` + +The macro will register a route on `/media-library-uploads`, which is used by the Vue components by default. If you wish to use a different endpoint, just pass the desired +URL to the macro. + +```php +// Probably routes/web.php +Route::temporaryUploads('your-url'); +``` + +### Customizing the upload endpoint + +The Vue components post data to `/media-library-uploads` by default. If you registered the controller on a different URL, pass it to the `upload-endpoint` prop of your Vue components. + +```html + +``` + +### Importing the components + +The components aren't available through npm, but are located in `vendor/spatie/laravel-medialibrary-pro/resources/js` when you install the package through Composer. This makes for very long import statements, which you can clean up by adding some configuration to your Webpack/Laravel Mix configuration: + +**laravel-mix >6** + +```js +// webpack.mix.js + +mix.override((webpackConfig) => { + webpackConfig.resolve.modules = [ + "node_modules", + __dirname + "/vendor/spatie/laravel-medialibrary-pro/resources/js", + ]; +} +``` + +**laravel-mix <6** + +```js +// webpack.mix.js + +mix.webpackConfig({ + resolve: { + modules: [ + "node_modules", + __dirname + "/vendor/spatie/laravel-medialibrary-pro/resources/js", + ], + }, +}); +``` + +This will force Webpack to look in `vendor/spatie/laravel-medialibrary-pro/resources/js` when resolving imports, and allows you to shorten your import. Notice that the Vue 2 and Vue 3 components are separate components. + +```js +import MediaLibraryAttachment from "media-library-pro-vue2-attachment"; +// or +import MediaLibraryAttachment from "media-library-pro-vue3-attachment"; +``` + +If you're using TypeScript, you will also have have to add this to your tsconfig: + +```json +// tsconfig.json + +{ + "compilerOptions": { + "paths": { + "*": ["vendor/spatie/laravel-medialibrary-pro/resources/js/*"] + } + } +} +``` + +To use a component in your Blade templates, import the components you plan to use in your `app.js` file, and add them to your main Vue app's `components` object. + +**Vue 2** + +```js +import Vue from "vue"; +import MediaLibraryAttachment from "media-library-pro-vue2-attachment"; +import MediaLibraryCollection from "media-library-pro-vue2-collection"; + +new Vue({ + el: "#app", + + components: { + MediaLibraryAttachment, + MediaLibraryCollection, + }, +}); +``` + +**Vue 3** + +```js +import { createApp } from "vue"; +import MediaLibraryAttachment from "media-library-pro-vue3-attachment"; +import MediaLibraryCollection from "media-library-pro-vue3-collection"; + +createApp({ + components: { + MediaLibraryAttachment, + MediaLibraryCollection, + }, +}).mount("#app"); +``` + +You can now use them in any `.blade.php` file in your application. + +```html + + +
+
+ + + + +
+``` + +You may also choose to import the components on the fly in a `.vue` file. + +```html + + + + + +``` + +## Your first components + +The most basic components have a `name` prop. This name will be used to identify the media when it's uploaded to the server. + +```html + + + + + +``` + +### Passing an initial value + +If your form modifies an existing set of media, you may pass it through in the `initial-value` prop. + +You can retrieve your initial values in Laravel using `$yourModel->getMedia($collectionName)`. This will also take care of any `old` values after an invalid form submit. You can also use this straight in your blade file: + +```html +
+ + + + + + +``` + +Under the hood, these components create hidden `` fields to keep track of the form values on submit. If you would like to submit your values asynchronously, refer to [the `Asynchronously submit data` section](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/handling-uploads-with-vue#asynchronously-submit-data). + +### Setting validation rules + +You'll probably want to validate what gets uploaded. Use the `validation-rules` prop, and don't forget to pass Laravel's validation errors too. The validation errors returned from the server will find errors under the key used in your `name` prop. + +```html +
+ + + + + + +``` + +You can also set the maximum amount of images that users can be uploaded using the `max-items` prop. Don't forget to set the `multiple` prop for the attachment component. + +```html +
+ + + + + + +``` + +See the [Validation rules section](#validation-rules) for a complete list of all possible validation rules. + +### Checking the upload state + +The components keep track of whether they're ready to be submitted, you can use this to disable a submit button while a file is still uploading or when there are frontend validation errors. This value can be tracked by listening to a `is-ready-to-submit-change` event on the components. If you submit a form while a file is uploading, Laravel will return a HTTP 500 error with an `invalid uuid` message. + +```html + + + +``` + +### Using custom properties + +The Media Library supports [custom properties](/docs/laravel-medialibrary/v9/advanced-usage/using-custom-properties) to be saved on a media item. The values for these can be chosen by your users. By default, the `MediaLibraryAttachment` component doesn't show any input fields, and the `MediaLibraryCollection` component only shows a `name` field, with the option to add more fields. + +Use the `fields` scoped slot to add some fields: + +**Vue 2** + +```html + + + +``` + +**Vue 3** + +```html + + + +``` + +When you add an image to your collection, it will look like this. + +![Screenshot of custom property](/docs/laravel-medialibrary/v9/images/pro/extra.png) + +### Customizing the file properties + +When uploading a file, some properties appear by default: its extension, filesize and a remove or download button (respectively for the attachment or collection component). + +You can customize what is displayed here by using the `properties` scoped slot: + +**Vue 2** + +```html + + + +``` + +**Vue 3** + +```html + + +
+``` + +### Asynchronously submit data + +If you don't want to use traditional form submits to send your data to the backend, you will have to keep track of the current value of the component using the `onChange` handler. The syntax is the same for all UI components: + +```html + + + +``` + +### Using with Laravel Vapor + +If you are planning on deploying your application to AWS using [Laravel Vapor](https://vapor.laravel.com/), you will need to pass `{ enabled: true }` for the optional `vapor` prop to your components so your files can be uploaded to an S3 bucket. + +When uploading a file, the component will first get a signed storage URL from Vapor. By default, its value is `'/vapor/signed-storage-url'`. If you changed it in Laravel, you can override it by setting a value for `vapor.signedStorageUrl` (leave empty to use the default). + +After uploading the file to S3, the component will notify your application of where the file was stored. For this, you need to register the Media Library S3 uploads controller in your routes file: `Route::mediaLibraryS3Uploads('media-library-post-s3');`. The components expect this endpoint to be `/media-library-post-s3`, if you chose a different endpoint, set it in `vapor.postS3Endpoint` (leave empty to use the default). + +```html + +``` + +## Validation rules + +There are a couple of different ways to validate files on the frontend. These props are available to you: `validationRules`, `maxItems` and `beforeUpload`. + +**validationRules** + +In the `validationRules` object, we've got the `accept` property, which expects an array of MIME types as strings. Leave it empty to accept all types of files, set its value to `['image/*']` to accept any type of image, or choose your own set of rules using [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types). Remember, the only valid MIME type of a JPEG/JPG is `image/jpeg`! + +The `minSizeInKB` and `maxSizeInKB` properties set the minimum and maximum size of any individual file. + +```html + +``` + +**maxItems** + +Set the maximum amount of items in the collection/attachment component at any time. + +```html + +``` + +**beforeUpload** + +Pass a method to `before-upload` that accepts a [file](https://developer.mozilla.org/en-US/docs/Web/API/File) parameter. Return any value (or resolve a Promise with any value) from this function to upload the file. Throw an Error in this function to cause the file not to be uploaded, and display your error message. + +```html + + + +``` + +## Props + +These props are available on both the `attachment` and the `collection` component. + +| prop name | Default value | Description | +| ----------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | | | +| initial-value | `[]` | | +| upload-endpoint | `"media-library-uploads"` | | +| validation-rules | | Refer to [validation rules](#validation-rules) section | +| validation-errors | | The standard Laravel validation error object | +| multiple | `false` (always `true` in the `collection` component) | Only exists on the `attachment` components | +| max-items | `1` when `multiple` = `false`, otherwise `undefined | | +| vapor | | Set to true if you will deploy your application to Vapor. This enables uploading of the files to S3. | +| max-size-for-preview-in-bytes | `5242880` (5 MB) | When an image is added, the component will try to generate a local preview for it. This is done on the main thread, and can freeze the component and/or page for very large files | +| sortable | `true` | Only exists on the `collection` components. Allows the user to drag images to change their order, this will be reflected by a zero-based `order` attribute in the value | +| ref | | Used to set a reference to the MediaLibrary instance, so you can change the internal state of the component. | +| before-upload | | A method that is run right before a temporary upload is started. You can throw an `Error` from this function with a custom validation message | +| after-upload | | A method that is run right after a temporary upload has completed, `{ success: true, uuid }` | +| @changed | | | +| @is-ready-to-submit-change | | Refer to [Checking the upload state](#checking-the-upload-state) section | diff --git a/docs/handling-uploads-with-media-library-pro/installation.md b/docs/handling-uploads-with-media-library-pro/installation.md new file mode 100644 index 000000000..91e769145 --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/installation.md @@ -0,0 +1,132 @@ +--- +title: Installation +weight: 2 +--- + +[Media Library Pro](medialibrary.pro) is a paid add-on package for Laravel Media Library. In order to use it, you must have the base version of media library installed in your project. Here are [the installation instructions for the base version](/docs/laravel-medialibrary/v9/installation-setup). + +## Installing the base package + +## Getting a license + +You must buy a license on [the Media Library Pro product page](https://spatie.be/products/media-library-pro) at spatie.be + +Single application licenses maybe installed in a single Laravel app. In case you bought the unlimited application license, there are no restrictions. A license comes with one year of upgrades. If a license expires, you are still allowed to use Media Library Pro, but you won't get any updates anymore. + +## Requiring Media Library Pro + +After you've purchased a license, add the `satis.spatie.be` repository in your `composer.json`. + +```php +"repositories": [ + { + "type": "composer", + "url": "https://satis.spatie.be" + } +], +``` + +Next, you need to create a file called `auth.json` and place it either next to the `composer.json` file in your project, or in the Composer home directory. You can determine the Composer home directory on \*nix machines by using this command. + +```bash +composer config --list --global | grep home +``` + +This is the content you should put in `auth.json`: + +```php +{ + "http-basic": { + "satis.spatie.be": { + "username": "", + "password": "" + } + } +} +``` + +With the configuration above in place, you'll be able to install the Media Library Pro into your project using this command: + +```bash +composer require "spatie/laravel-medialibrary-pro:^1.0.0" +``` + +## Prepare the database + +Media Library Pro tracks all temporary uploads in a table called `temporary_uploads`. + +To create the table you need to publish and run the migration: + +```bash +php artisan vendor:publish --provider="Spatie\MediaLibraryPro\MediaLibraryProServiceProvider" --tag="migrations" +php artisan migrate +``` + +## Automatically removing temporary uploads + +If you are using Media Library Pro, you must schedule this artisan command in `app/Console/Kernel` to automatically delete temporary uploads + +```php +// in app/Console/Kernel.php + +protected function schedule(Schedule $schedule) +{ + $schedule->command('media-library:delete-old-temporary-uploads')->daily(); +} +``` + +## Add the route macro + +To accept temporary uploads, you must add this macro to your routes file. + +```php +// Probably routes/web.php + +Route::temporaryUploads('temporary-uploads'); +``` + +## Using the CSS + +You have a couple of options for how you can use the UI components' CSS, depending on your and your project's needs: + +### Using Laravel Mix or Webpack with css-loader + +You can import the built CSS in your own CSS files using `@import(vendor/spatie/laravel-medialibrary-pro/resources/js/media-library-pro-styles)`. + +This isn't a very pretty import, but you can make it cleaner by adding this configuration to your Webpack config: + +**laravel-mix >6** + +```js +mix.override((webpackConfig) => { + webpackConfig.resolve.modules = [ + "node_modules", + __dirname + "/vendor/spatie/laravel-media-library-pro/resources/js", + ]; +} +``` + +**laravel-mix <6** + +```js +mix.webpackConfig({ + resolve: { + modules: [ + "node_modules", + __dirname + "/vendor/spatie/laravel-media-library-pro/resources/js", + ], + }, +}); +``` + +This will force Webpack to look in `vendor/spatie/laravel-medialibrary-pro/resources/js` when resolving imports, and allows you to shorten your import to this: + +```css +@import "media-library-pro-styles"; +``` + +### Directly in Blade/HTML + +You should copy the built CSS from `vendor/spatie/laravel-medialibrary-pro/resources/js/media-library-pro-styles/dist/styles.css` into your `public` folder, and then use a `link` tag in your blade/html to get it: ``. + +If you would like to customize the CSS we provide, head over to [the section on Customizing CSS](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/customizing-css). diff --git a/docs/handling-uploads-with-media-library-pro/introduction.md b/docs/handling-uploads-with-media-library-pro/introduction.md new file mode 100644 index 000000000..92a8f799b --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/introduction.md @@ -0,0 +1,42 @@ +--- +title: Introduction +weight: 1 +--- + +[Media Library Pro](http://medialibrary.pro) is a paid add-on package that offers Blade, Vue, and React components to upload files to your application. + +**Media Library Pro hasn't launched yet. We will release the package mid-november 2020. Subscribe at [Media Library Pro](http://medialibrary.pro) to get notified the moment it is available.** + +Media Library Pro ships with two components for every environment: an attachment component, and a collection component. + +The attachment component can upload one or more files with little or no extra information. It's is a lightweight solution for small bits of UI like avatar fields or message attachments. + +![Screenshot of the attachment component](/docs/laravel-medialibrary/v9/images/pro/attachment.png) + +The collection component can upload multiple files with custom properties. Use the collection component shines when you need to manage media, for example in admin panels. + +![Screenshot of the attachment component](/docs/laravel-medialibrary/v9/images/pro/collection.png) + +If none of those fit the bill, Media Library Pro supplies you with a number helpers to build your own components. + +## Dive in + +All components upload media to the server with the same API. Before you dive into the frontend, read our server guide. + +[Processing uploads on the server](processing-uploads-on-the-server) + +Next, choose your own journey. We have written extensive guides for all three flavours. Be sure to first follow [the base installation instructions](/docs/laravel-medialibrary/v9/installation-setup) and [pro installation instructions](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/installation). + +### Blade + +[Handling uploads with Blade](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/handling-uploads-with-blade) + +### Vue + +[Handling uploads with Vue](handling-uploads-with-vue)
+[Creating custom Vue components](creating-custom-vue-components) + +### React + +[Handling uploads with React](handling-uploads-with-react)
+[Creating custom React components](creating-custom-react-components) diff --git a/docs/handling-uploads-with-media-library-pro/processing-uploads-on-the-server.md b/docs/handling-uploads-with-media-library-pro/processing-uploads-on-the-server.md new file mode 100644 index 000000000..7048eb931 --- /dev/null +++ b/docs/handling-uploads-with-media-library-pro/processing-uploads-on-the-server.md @@ -0,0 +1,341 @@ +--- +title: Processing uploads on the server +weight: 3 +--- + +All Blade, Vue and React components communicate with the server in the same way. After a user selects one or more files, they're immediate sent to the server and stored as temporary uploads. When the parent form is submitted, the media items can be attached to a model. + +## Enabling temporary uploads + +Plain HTML file ``s have two major shortcomings: they only upload the file when the form is submitted, and they're unable to remember the file when a form fails to submit. Temporary uploads solve both these problems. + +When a user selects or drops a file in one of the Media Library components, it gets uploaded to the server immediately. Problem number 1 solved! + +If the form submission fails later on, Media Library will pass down the previously added temporary upload objects so it can prefill the component with the previously uploaded files. Problem number 2 solved too! + +To set up temporary uploads, register the temporary uploads route with our handy macro. + +```php +// Probably routes/web.php + +Route::mediaLibrary(); +``` + +This will registered a route at `/media-library-pro/uploads` + + +### Enabling Vapor support + +If you will use React or Vue components to handle uploads you must set the `enable_vapor_uploads` key in the `media-library` config file to `true`. 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 an S3 bucket to their right place. + +With the config option enable, the `Route::mediaLibrary();` will register a route at `/media-library-pro/post-s3 + instead of `/media-library-pro/uploads`. + +### Customizing the upload URL + +You can customize the upload url by passing a base url to the macro. + +```php +// Probably routes/web.php + +Route::mediaLibrary('my-custom-url'); +``` + +This will registered a route at `/my-custom-url/uploads` + +## Setting up the view & controller + +After a user has added files and they've been stored as temporary uploads, the user will submit the form. At this point the form request will hit one of your application's controllers. This is where you can permanently attach the file to your models. + +To illustrate, we'll set up a little profile screen where a user may upload their avatar. + +```php +// Back in routes/web.php +use App\Http\Controllers\ProfileController; + +Route::get('profile', [ProfileController::class, 'edit']); +Route::post('profile', [ProfileController::class, 'store']); + +Route::temporaryUploads(); +``` + +The profile controller has a simple form that uses the Blade attachment component. + +```blade +{{-- resources/views/profile.blade.php --}} + + +``` + +And, assuming you're familiar with the [basic usage](../basic-usage) of the Media Library, this is how we'd store the uploaded avatar on the user. + +```php +namespace App\Http\Controllers; + +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; + +class ProfileController +{ + public function edit() + { + return view('profile', [$user => Auth::user()]); + } + + public function store(Request $request) + { + $user = Auth::user(); + + $user + ->addMediaFromMediaLibraryRequest($request, 'avatar') + ->toMediaCollection('avatar'); + } +} +``` + +## Validation + +The `ProfileController` we built assumes users will only upload the exact file types we're looking for. Of course they won't! We need to validate the incoming media before attaching them to our models. + +The Media Library components provide instant client-side validation. You'll read more about that in the component docs. First, we'll set up server-side validation. + +To validate uploaded media, we'll use create a custom form request. + +```diff +- public function store(Request $request) ++ public function store(ProfileRequest $request) +``` +```php +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Spatie\MediaLibraryPro\Rules\Concerns\ValidatesMedia; + +class ProfileRequest extends FormRequest +{ + use ValidatesMedia; + + public function rules() + { + return [ + 'images' => $this + ->validateMultipleMedia() + ->minItems(1) + ->maxItems(5) + ->extension('png') + ->maxItemSizeInKb(1024) + ->attribute('name', 'required') + ]; + } +} +``` + +--- + +Every component will pass data in a key of a request. The name of that key is the name you passed to the `name` prop of any of the components. + +```html +// data will get passed via the `avatar` key of the request. + + +``` + +The content of that request key will be an array. For each file uploaded that array will hold an array with these keys. + +- `name`: the name of the uploaded file +- `uuid`: the UUID of a `Media` model. For newly uploaded files that have not been associated to a model yet, the `Media` model will be associated with a `TemporaryUpload` model +- `order`: the order in which this item should be stored in a media collection. + +## Validating responses + +Even though the upload components do some validation of their own, we highly recommend always validating responses on the server as well. + +You should handle validation in a form request. On the form request you should use the `Spatie\MediaLibraryPro\Rules\Concerns\ValidatesMedia` trait. This will give you access to the `validateSingleMedia` and `validateMultipleMedia` methods. + +In this example we assume that a component was configured to use the `images` key of the request. We validate that there was at least one item uploaded, but no more than 5. Only `png`s that are up to 1MB in size are allowed. All images should have a name. + +```php +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Spatie\MediaLibraryPro\Rules\Concerns\ValidatesMedia; + +class MyRequest extends FormRequest +{ + use ValidatesMedia; + + public function rules() + { + return [ + 'images' => $this + ->validateMultipleMedia() + ->minItems(1) + ->maxItems(5) + ->extension('png') + ->maxItemSizeInKb(1024) + ->attribute('name', 'required') + ]; + } +} +``` + +If you are only allowing one uploaded file, you can use the `validateSingleMedia` in much the same way. + +```php +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Spatie\MediaLibraryPro\Rules\Concerns\ValidatesMedia; + +class MyRequest extends FormRequest +{ + use ValidatesMedia; + + public function rules() + { + return [ + 'avatar' => $this + ->validateSingleMedia() + ->extension('png') + ->maxItemSizeInKb(1024) + ]; + } +} +``` + +These are the available validation methods on `validateSingleMedia() ` and`validateMultipleMedia` + +- `minSizeInKb($minSizeInKb)`: validates that a single upload is not smaller than the `$minSizeInKb` given +- `maxSizeInKb($maxSizeInKb)`: validates that a single upload is not greater than the `$minSizeInKb` given +- `extension($extension)`: this rule expects a single extension as a string or multiple extensions as an array. Under the hood, the rule will validate if the value has the mime type that corresponds with the given extension. +- `mime($mime)`: this rule expects a single mime type as a string or multiple mime types as an array. +- `itemName($rules)`: This rule accepts rules that should be used to validate the name of a media item. +- `customProperty($name, $rules)`: this rule accepts a custom property name and rules that should be used to validate the attribute + +These rules can be used on `validateMultipleMedia`; + +- `minTotalSizeInKb($maxTotalSizeInKb)`: validates that the combined size of uploads is not smaller than the `$minTotalSizeInKb` given +- `maxTotalSizeInKb($maxTotalSizeInKb)`: validates that the combined size of uploads is not greater than the `$maxTotalSizeInKb` given + +### Validating attributes and custom properties + +If you're [using custom properties](/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/handling-uploads-with-blade#using-custom-properties), you can validate them with the `customProperty` function. The first argument should be the name of the custom property you are validating. The second argument should be a string or an array with rules you'd like to use. + +Here's an example where we validate `extra_propery` and `another_extra_property`. + +```php +use Illuminate\Foundation\Http\FormRequest; +use Spatie\MediaLibraryPro\Rules\Concerns\ValidatesMedia; + +class StoreLivewireCollectionCustomPropertyRequest extends FormRequest +{ + use ValidatesMedia; + + public function rules() + { + return [ + 'name' => 'required', + 'images' => $this->validateMultipleMedia() + ->customProperty('extra_field', 'required|max:50') + ->customProperty('another_extra_property', ['required', 'max:50']) + ]; + } +} +``` + +## Processing responses + +After you've validated the response, you should persist the changes to the media library. The media library provides two methods for that: `syncFromMediaLibraryRequest` and `addFromMediaLibraryRequest`. Both these methods are available on all [models that handle media](/docs/laravel-medialibrary/v9/basic-usage/preparing-your-model). + +### `addFromMediaLibraryRequest` + +This method will add all media whose `uuid` is in the response to a media collection of a model. Existing media associated on the model will remain untouched. + +You should probably use this method when only accepting new uploads. + +```php +// in a controller + +public function yourMethod(YourFormRequest $request) +{ + // retrieve model + + $yourModel + ->addFromMediaLibraryRequest($request->get('images')) + ->toMediaCollection('images'); + + flash()->success('Your model has been saved.') + + return back(); +} +``` + +### `syncFromMediaLibraryRequest` + +You should use this method when you are using the `x-media-library-collection` Blade component (or equivalent Vue or React component). + +Here is an example where we are going to sync that the contents of the `images` key in the request to the media library. +In this example we use the `images` key, but of course you should use the name that you used. + +All media associated with `$yourModel` whose `uuid` is not present in the `images` array of the request will be deleted. + +```php +// in a controller + +public function yourMethod(YourFormRequest $request) +{ + // retrieve model + + $yourModel + ->syncFromMediaLibraryRequest($request->images) + ->toMediaCollection('images'); + + flash()->success('Your model has been saved.') + + return back(); +} +``` + +After this code has been executed, the media, whose `uuid` is present in the `images` array of request, will be in the `images collection of `$yourModel`. + +```php +$yourModel->getMedia('images'); // the media that we just synced will be returned. +``` + +### Handling custom properties + +If you are using properties for your media items you should pass the names of the custom properties you expect to the `withCustomProperties` method. Only these custom properties will be accepted. + +```php +$yourModel + ->syncFromMediaLibraryRequest($request->images) + ->withCustomProperties('extra_field', 'another_extra_field') + ->toMediaCollection('images'); +``` + +## Customizing the preview images + +All Blade, Vue and React components will display previews images that are generated by the `preview` conversion of +the `TemporaryUpload` model. This conversion will create a 500x500 representation of the image. + +You can customize this by registering a preview manipulation. Typically, this would be done in a service provider. +Here's an example where we will create 300x300 previews + +```php +use Spatie\MediaLibraryPro\Models\TemporaryUpload; +use Spatie\MediaLibrary\Conversions\Conversion; +use Spatie\Image\Manipulations; + +// in a service provider +TemporaryUpload::manipulatePreview(function(Conversion $conversion) { + $conversion->fit(Manipulations::FIT_CROP, 300, 300); +}); +``` + +The components will use the `preview` conversion of models that have made associated to them. For example, if you have +a `$blogPost` model, and you use the components to display the media associated to that model, the components will +use `preview` conversion on the `BlogPost` model. + +Make sure such an `preview` conversion exists for each model that handles media. We highly recommend to use `nonQueued` +so the image is immediately available. diff --git a/docs/images/pro/attachment-filled.png b/docs/images/pro/attachment-filled.png new file mode 100644 index 000000000..b66c373d6 Binary files /dev/null and b/docs/images/pro/attachment-filled.png differ diff --git a/docs/images/pro/attachment.png b/docs/images/pro/attachment.png new file mode 100644 index 000000000..4109baf46 Binary files /dev/null and b/docs/images/pro/attachment.png differ diff --git a/docs/images/pro/collection.png b/docs/images/pro/collection.png new file mode 100644 index 000000000..7e24ca814 Binary files /dev/null and b/docs/images/pro/collection.png differ diff --git a/docs/images/pro/extra.png b/docs/images/pro/extra.png new file mode 100644 index 000000000..7d1504960 Binary files /dev/null and b/docs/images/pro/extra.png differ diff --git a/docs/images/pro/multiple.png b/docs/images/pro/multiple.png new file mode 100644 index 000000000..14d84387d Binary files /dev/null and b/docs/images/pro/multiple.png differ diff --git a/docs/installation-setup.md b/docs/installation-setup.md index 3096d3c1e..305f9ebfb 100644 --- a/docs/installation-setup.md +++ b/docs/installation-setup.md @@ -1,35 +1,38 @@ --- -title: Installation & setup +title: Base installation weight: 4 --- -MediaLibrary can be installed via composer: +Media library can be installed via Composer: + +If you only use the base package issue this command: ```bash composer require "spatie/laravel-medialibrary:^8.0.0" ``` -The package will automatically register a service provider. +If you have a license for media library pro, you should use `laravel-media-library-pro` + +```bash +composer require "spatie/laravel-medialibrary-pro:^8.0.0" +``` + +## Preparing the database -You need to publish and run the migration: +You need to publish the migration to create the `media` table: ```bash php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations" -php artisan migrate ``` -If you use Artisan's `migrate:rollback` or `migrate:refresh` commands you should edit the migration to include a down method: +After that, you need to run migrations. -```php -/** - * Reverse the migrations. - */ -public function down() -{ - Schema::dropIfExists('media'); -} +```bash +php artisan migrations ``` +## Publishing the config file + Publishing the config file is optional: ```bash @@ -60,8 +63,7 @@ return [ 'queue_name' => '', /* - * Choose if your conversions should be generated in queue by default. - * Set `false` to have them generated synchronously. + * By default all conversions will be performed on a queue. */ 'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true), @@ -70,68 +72,23 @@ return [ */ 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::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 20% - * smaller than the previous one. More info in the documentation. - * - * https://docs.spatie.be/laravel-medialibrary/v8/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 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/ + * The fully qualified class name of the model used for temporary uploads. */ - 'default_loading_attribute_value' => null, + 'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class, /* - * This is the class that is responsible for naming conversion files. By default, - * it will use the filename of the original and concatenate the conversion name to it. + * This is the class that is responsible for naming generated files. */ - 'conversion_file_namer' => \Spatie\MediaLibrary\Conversions\DefaultConversionFileNamer::class, - + '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, /* - * When urls to files get generated, this class will be called. Leave empty + * 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, @@ -143,7 +100,7 @@ return [ 'version_urls' => false, /* - * MediaLibrary will try to optimize all converted images by removing + * 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. */ @@ -180,11 +137,17 @@ return [ 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' => 'gd', + 'image_driver' => env('IMAGE_DRIVER', 'gd'), /* * FFMPEG & FFProbe binaries paths, only used if you try to generate video @@ -194,23 +157,81 @@ return [ 'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'), 'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'), - /* - * 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, - /* * 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, + '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 20% + * smaller than the previous one. More info in the documentation. + * + * https://docs.spatie.be/laravel-medialibrary/v8/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', true), + + /* + * 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, ]; ``` +## Adding a media disk + By default, the media library will store its files on Laravel's `public` disk. If you want a dedicated disk you should add a disk to `config/filesystems.php`. This would be a typical configuration: ```php @@ -225,15 +246,29 @@ By default, the media library will store its files on Laravel's `public` disk. I ... ``` -Don't forget to ignore the directory of your configured disk so the files won't end up in your git repo. +Don't forget to ignore the directory of your configured disk, so the files won't end up in your git repo. -If you are planning on working with image manipulations it's recommended to configure a queue on your server and specify it in the config file. +To store all media on that disk by default, you should set the `disk_name` config value in the `media-library` config file to the name of the disk you added. + +```php +// config/media-library.php + +return [ + 'disk_name' => 'media', + + // ... +]; +``` Want to use S3? Then follow Laravel's instructions on [how to add the S3 Flysystem driver](https://laravel.com/docs/filesystem#configuration). -### Optimization tools +## Setting up a queue + +If you are planning on working with image manipulations it's recommended to configure a queue on your server and specify it in the config file. + +### Setting up optimization tools -Media library will use these tools to [optimize converted images](https://docs.spatie.be/laravel-medialibrary/v8/converting-images/optimizing-converted-images) if they are present on your system: +Media library will use these tools to [optimize converted images](https://docs.spatie.be/laravel-medialibrary/v9/converting-images/optimizing-converted-images) if they are present on your system: - [JpegOptim](http://freecode.com/projects/jpegoptim) - [Optipng](http://optipng.sourceforge.net/) @@ -248,7 +283,7 @@ sudo apt install jpegoptim optipng pngquant gifsicle npm install -g svgo ``` -And here's how to install the binaries on MacOS (using [Homebrew](https://brew.sh/)): +Here's how to install the binaries on MacOS (using [Homebrew](https://brew.sh/)): ```bash brew install jpegoptim @@ -257,3 +292,11 @@ brew install pngquant brew install svgo brew install gifsicle ``` + +## Installing Media Library Pro + +[Media Library Pro](http://medialibrary.pro) is an optional add-on package that offers Blade, Vue, and React components to upload files to your application. It [integrates](https://spatie.be/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/general) beautifully with the laravel-medialibrary. + +You can buy a license for Media Library Pro on [the product page](https://spatie.be/products/media-library-pro) at spatie.be. + +To install Media Library Pro, you should follow [these instructions](https://spatie.be/docs/laravel-medialibrary/v9/handling-uploads-with-media-library-pro/installing-media-library-pro). diff --git a/docs/introduction.md b/docs/introduction.md index b053ef36b..e0ebdaea8 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -3,7 +3,7 @@ title: Introduction weight: 1 --- -This package can associate all sorts of files with Eloquent models. It provides a simple, fluent API to work with. +This package can associate all sorts of files with Eloquent models. It provides a simple, fluent API to work with. The [Pro version of the package](https://medialibrary.pro) offers Blade, Vue and React components to handle uploads to the media library and to adminster the content of a medialibrary collection. Here are some quick code examples: @@ -27,7 +27,7 @@ $yourModel->addMedia($bigFile)->toMediaCollection('downloads', 's3'); The storage of the files is handled by [Laravel's Filesystem](http://laravel.com/docs/5.6/filesystem), so you can plug in any compatible filesystem. -The package can also generate derived images such as thumbnails for images, video's and pdf's. Once you've [set up your model](/laravel-medialibrary/v8/basic-usage/preparing-your-model), they're easily accessible: +The package can also generate derived images such as thumbnails for images, video's and pdf's. Once you've [set up your model](/laravel-medialibrary/v9/basic-usage/preparing-your-model), they're easily accessible: ```php $yourModel->getMedia('images')->first()->getUrl('thumb'); @@ -37,7 +37,7 @@ $yourModel->getMedia('images')->first()->getUrl('thumb'); We've recorded [a video course](https://spatie.be/videos/discovering-laravel-media-library) on how to use this package. It's the best way to get started using media library -[![video course](/docs/laravel-medialibrary/v8/images/video-course.jpg)](https://spatie.be/videos/discovering-laravel-media-library/intro) +[![video course](/docs/laravel-medialibrary/v9/images/video-course.jpg)](https://spatie.be/videos/discovering-laravel-media-library/intro) ## We have badges! diff --git a/docs/requirements.md b/docs/requirements.md index a32e1f118..d5da39c9a 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -3,7 +3,7 @@ title: Requirements weight: 3 --- -The MediaLibrary package requires **PHP 7.4+** and **Laravel 6+**. +Laravel Media Library requires **PHP 7.4+** and **Laravel 8+**. This package uses `json` columns. MySQL 5.7 or higher is required. @@ -16,14 +16,5 @@ If you're running into problems with Ghostscript and/or PDF to image generation ## Older versions -We only support the latest version. If you do not meet the requirements, you can opt to use an older version of the package. - -Laravel 5.5 and PHP 7.0 users can use [V6 of this package](https://docs.spatie.be/laravel-medialibrary/v6/introduction). - -Laravel 5.4 users can use [V5 of this package](https://docs.spatie.be/laravel-medialibrary/v5/introduction). - -Using Laravel version 5.1, 5.2 or 5.3? Head over to [V4 of this package](https://docs.spatie.be/laravel-medialibrary/v4/introduction). - -If you're stuck on PHP 5 take a look at [V3 of this package](https://docs.spatie.be/laravel-medialibrary/v3/introduction). - +If you do not meet the requirements, you can opt to use an older version of the package. We only actively maintain the latest version of the media library. diff --git a/docs/responsive-images/_index.md b/docs/responsive-images/_index.md index 2672150a1..c8a72c6a2 100644 --- a/docs/responsive-images/_index.md +++ b/docs/responsive-images/_index.md @@ -1,4 +1,4 @@ --- title: Responsive images -weight: 4 +weight: 5 --- diff --git a/docs/responsive-images/demo.md b/docs/responsive-images/demo.md index 758a8fd2f..3c290078a 100644 --- a/docs/responsive-images/demo.md +++ b/docs/responsive-images/demo.md @@ -19,7 +19,7 @@ weight: 5

- The example above demonstrates the responsive images technique used in the Laravel MediaLibrary. + The example above demonstrates the responsive images technique used in the Laravel MediaLibrary.

Try this:

    @@ -30,7 +30,7 @@ weight: 5

    What happens?

    • - We start with an image that has srcset values, rendered by the Laravel MediaLibrary + We start with an image that has srcset values, rendered by the Laravel MediaLibrary
    • An initial sizes="1px" attribute is used to render an inline base64-encoded SVG placeholder first diff --git a/docs/responsive-images/generating-your-own-tiny-placeholder.md b/docs/responsive-images/generating-your-own-tiny-placeholder.md index 4b8f9915b..36c49a21e 100644 --- a/docs/responsive-images/generating-your-own-tiny-placeholder.md +++ b/docs/responsive-images/generating-your-own-tiny-placeholder.md @@ -3,7 +3,7 @@ title: Generating your own tiny placeholder weight: 4 --- -When generating responsive images, the media library will generate a tiny version of your image which will be used for [progressive image loading](/laravel-medialibrary/v8/responsive-images/getting-started-with-responsive-images#progressive-image-loading). By default this tiny version will be blurred version of the original. +When generating responsive images, the media library will generate a tiny version of your image which will be used for [progressive image loading](/laravel-medialibrary/v9/responsive-images/getting-started-with-responsive-images#progressive-image-loading). By default this tiny version will be blurred version of the original. You can customize how the tiny version of the image should be generated. Maybe you want a to just use the dominant color instead of blur. In the `responsive_images.tiny_placeholder_generator` of the `media-library` config file you can specify a class that implements `Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator`. This interface only requires you to implement one function: diff --git a/docs/responsive-images/getting-started-with-responsive-images.md b/docs/responsive-images/getting-started-with-responsive-images.md index ff830fa51..78eca6462 100644 --- a/docs/responsive-images/getting-started-with-responsive-images.md +++ b/docs/responsive-images/getting-started-with-responsive-images.md @@ -7,7 +7,7 @@ Websites are viewed on various devices with widely differing screen sizes and co The media library has support for generating the necessary files and html markup for responsive images. In addition the medialibrary also has support for progressive image loading. -Try a [demo of the concept](/laravel-medialibrary/v8/responsive-images/demo) here. +Try a [demo of the concept](/laravel-medialibrary/v9/responsive-images/demo) here. ### Introducing the srcset attribute @@ -76,7 +76,7 @@ $yourModel ->toMediaCollection(); ``` -Behind the scenes, the media library will generate multiple size variations of your image. To learn which variations are generated and how to customize head over [here](/laravel-medialibrary/v8/responsive-images/using-your-own-width-calculator). +Behind the scenes, the media library will generate multiple size variations of your image. To learn which variations are generated and how to customize head over [here](/laravel-medialibrary/v9/responsive-images/using-your-own-width-calculator). ### Displaying responsive images @@ -96,7 +96,7 @@ Per image attached to your model the resulting html will look more or less like ### Generating responsive images for conversions -You can also generate responsive images for any [image conversions](https://docs.spatie.be/laravel-medialibrary/v8/converting-images/defining-conversions) you define. Simply use `withResponsiveImages` when defining a conversion. +You can also generate responsive images for any [image conversions](https://docs.spatie.be/laravel-medialibrary/v9/converting-images/defining-conversions) you define. Simply use `withResponsiveImages` when defining a conversion. Here's an example where we define a conversion to make a greyscale version and generate responsive, greyscaled images. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 000000000..5f567aeb9 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,27 @@ +--- +title: Troubleshooting +weight: 8 +--- + + +Here are some common problems and how to solve them. + +## Cannot find module '@babel/compat-data/corejs3-shipped-proposals' + +This is caused by a compatability issue between a version of `@babel/preset-env` that an older release of Laravel Mix required, and certain versions of node. + +### Fix + +``` +yarn upgrade laravel-mix +``` + +This will upgrade your installation of `laravel-mix` to a version that requires `@babel/preset-env: ^7.9`. You shouldn't have to change anything else, since this upgrade is a non-breaking change. + +## Cannot find name 'describe' / Cannot find name 'test' + +This is your TypeScript checker finding methods that it does not have types for. + +### Fix + +You should add `"include": ["resources/**/*"]` to your tsconfig.json (edit the path to where you manage your JS), or add `"exclude": ["vendor"]`. This doesn't normally happen with other libraries because `node_modules` is excluded from type checking by default. diff --git a/docs/working-with-media-collections/defining-media-collections.md b/docs/working-with-media-collections/defining-media-collections.md index 816005332..65fd0eb99 100644 --- a/docs/working-with-media-collections/defining-media-collections.md +++ b/docs/working-with-media-collections/defining-media-collections.md @@ -3,9 +3,9 @@ title: Defining media collections weight: 2 --- -A media collection can be more than [just a name to group files](/laravel-medialibrary/v8/working-with-media-collections/simple-media-collections). By defining a media collection in your model you can add certain behaviour collections. +A media collection can be more than [just a name to group files](/laravel-medialibrary/v9/working-with-media-collections/simple-media-collections). By defining a media collection in your model you can add certain behaviour collections. -To get started with media collections add a function called `registerMediaCollections` to [your prepared model](/laravel-medialibrary/v8/basic-usage/preparing-your-model). Inside that function you can use `addMediaCollection` to start a media collection. +To get started with media collections add a function called `registerMediaCollections` to [your prepared model](/laravel-medialibrary/v9/basic-usage/preparing-your-model). Inside that function you can use `addMediaCollection` to start a media collection. ```php // in your model @@ -197,7 +197,7 @@ $yourModel->getFirstMediaUrl('avatar'); // will return an url to the `$secondFil ## Registering media conversions -It's recommended that your first read the section on [converting images](/laravel-medialibrary/v8/converting-images/defining-conversions) before reading the following paragraphs. +It's recommended that your first read the section on [converting images](/laravel-medialibrary/v9/converting-images/defining-conversions) before reading the following paragraphs. Normally image conversions are registered inside the `registerMediaConversions` function on your model. However, images conversions can also be registered inside media collections. @@ -227,7 +227,7 @@ $yourModel->addMedia($pathToImage)->toMediaCollection('my-collection'); $yourModel->getFirstMediaUrl('thumb') // returns an url to a 100x100 version of the added image. ``` -Take a look at the [defining conversions section](/laravel-medialibrary/v8/converting-images/defining-conversions) to learn all the functions you can tack on to `addMediaConversion`. +Take a look at the [defining conversions section](/laravel-medialibrary/v9/converting-images/defining-conversions) to learn all the functions you can tack on to `addMediaConversion`. ## Generating responsive images diff --git a/resources/views/responsiveImage.blade.php b/resources/views/responsiveImage.blade.php index 4f90ec397..d98852b20 100644 --- a/resources/views/responsiveImage.blade.php +++ b/resources/views/responsiveImage.blade.php @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/views/responsiveImageWithPlaceholder.blade.php b/resources/views/responsiveImageWithPlaceholder.blade.php index 8b0d2e3b7..00ec43194 100644 --- a/resources/views/responsiveImageWithPlaceholder.blade.php +++ b/resources/views/responsiveImageWithPlaceholder.blade.php @@ -1 +1 @@ - + diff --git a/src/Conversions/Actions/PerformConversionAction.php b/src/Conversions/Actions/PerformConversionAction.php new file mode 100644 index 000000000..e8efaa2c2 --- /dev/null +++ b/src/Conversions/Actions/PerformConversionAction.php @@ -0,0 +1,60 @@ +convert($copiedOriginalFile, $conversion); + + $manipulationResult = (new PerformManipulationsAction)->execute($media, $conversion, $copiedOriginalFile); + + $newFileName = $conversion->getConversionFile($media); + + $renamedFile = $this->renameInLocalDirectory($manipulationResult, $newFileName); + + if ($conversion->shouldGenerateResponsiveImages()) { + /** @var ResponsiveImageGenerator $responsiveImageGenerator */ + $responsiveImageGenerator = app(ResponsiveImageGenerator::class); + + $responsiveImageGenerator->generateResponsiveImagesForConversion( + $media, + $conversion, + $renamedFile + ); + } + + app(Filesystem::class)->copyToMediaLibrary($renamedFile, $media, 'conversions'); + + $media->markAsConversionGenerated($conversion->getName()); + + event(new ConversionHasBeenCompleted($media, $conversion)); + } + + protected function renameInLocalDirectory( + string $fileNameWithDirectory, + string $newFileNameWithoutDirectory + ): string { + $targetFile = pathinfo($fileNameWithDirectory, PATHINFO_DIRNAME).'/'.$newFileNameWithoutDirectory; + + rename($fileNameWithDirectory, $targetFile); + + return $targetFile; + } +} diff --git a/src/Conversions/Actions/PerformManipulationsAction.php b/src/Conversions/Actions/PerformManipulationsAction.php new file mode 100644 index 000000000..eaa2b3ef2 --- /dev/null +++ b/src/Conversions/Actions/PerformManipulationsAction.php @@ -0,0 +1,49 @@ +getManipulations()->isEmpty()) { + return $imageFile; + } + + $conversionTempFile = $this->getConversionTempFileName($media, $conversion, $imageFile); + + File::copy($imageFile, $conversionTempFile); + + $supportedFormats = ['jpg', 'pjpg', 'png', 'gif']; + if ($conversion->shouldKeepOriginalImageFormat() && in_array($media->extension, $supportedFormats)) { + $conversion->format($media->extension); + } + + ImageFactory::load($conversionTempFile) + ->manipulate($conversion->getManipulations()) + ->save(); + + return $conversionTempFile; + } + + protected function getConversionTempFileName( + Media $media, + Conversion $conversion, + string $imageFile + ): string { + $directory = pathinfo($imageFile, PATHINFO_DIRNAME); + + $fileName = Str::random(32)."{$conversion->getName()}.{$media->extension}"; + + return "{$directory}/{$fileName}"; + } +} diff --git a/src/Conversions/Conversion.php b/src/Conversions/Conversion.php index 4d03e2302..32480f07f 100644 --- a/src/Conversions/Conversion.php +++ b/src/Conversions/Conversion.php @@ -5,13 +5,14 @@ use BadMethodCallException; use Spatie\Image\Manipulations; use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\MediaLibrary\Support\FileNamer\FileNamer; /** @mixin \Spatie\Image\Manipulations */ class Conversion { protected string $name = ''; - protected ConversionFileNamer $conversionFileNamer; + protected FileNamer $fileNamer; protected float $extractVideoFrameAtSecond = 0; @@ -37,7 +38,7 @@ public function __construct(string $name) ->optimize(config('media-library.image_optimizers')) ->format(Manipulations::FORMAT_JPG); - $this->conversionFileNamer = app(config('media-library.conversion_file_namer')); + $this->fileNamer = app(config('media-library.file_namer')); $this->loadingAttributeValue = config('media-library.default_loading_attribute_value'); @@ -217,8 +218,10 @@ public function getResultExtension(string $originalFileExtension = ''): string public function getConversionFile(Media $media): string { - $fileName = $this->conversionFileNamer->getFileName($this, $media); - $extension = $this->conversionFileNamer->getExtension($this, $media); + $fileName = $this->fileNamer->conversionFileName($media->file_name, $this); + + $fileExtension = $this->fileNamer->extensionFromBaseImage($media->file_name); + $extension = $this->getResultExtension($fileExtension) ?: $fileExtension; return "{$fileName}.{$extension}"; } diff --git a/src/Conversions/ConversionCollection.php b/src/Conversions/ConversionCollection.php index 9788298f7..931ccdee1 100644 --- a/src/Conversions/ConversionCollection.php +++ b/src/Conversions/ConversionCollection.php @@ -83,13 +83,6 @@ public function getConversions(string $collectionName = ''): self return $this->filter(fn (Conversion $conversion) => $conversion->shouldBePerformedOn($collectionName)); } - public function getQueuedConversions(string $collectionName = ''): self - { - return $this - ->getConversions($collectionName) - ->filter(fn (Conversion $conversion) => $conversion->shouldBeQueued()); - } - protected function addManipulationToConversion(Manipulations $manipulations, string $conversionName) { /** @var \Spatie\MediaLibrary\Conversions\Conversion|null $conversion */ @@ -116,13 +109,6 @@ protected function addManipulationToConversion(Manipulations $manipulations, str } } - public function getNonQueuedConversions(string $collectionName = ''): self - { - return $this - ->getConversions($collectionName) - ->reject(fn (Conversion $conversion) => $conversion->shouldBeQueued()); - } - public function getConversionsFiles(string $collectionName = ''): self { return $this diff --git a/src/Conversions/ConversionFileNamer.php b/src/Conversions/ConversionFileNamer.php deleted file mode 100644 index 45badd9a7..000000000 --- a/src/Conversions/ConversionFileNamer.php +++ /dev/null @@ -1,17 +0,0 @@ -file_name, PATHINFO_EXTENSION); - - return $conversion->getResultExtension($fileExtension) ?: $fileExtension; - } -} diff --git a/src/Conversions/DefaultConversionFileNamer.php b/src/Conversions/DefaultConversionFileNamer.php deleted file mode 100644 index 34bb1bfce..000000000 --- a/src/Conversions/DefaultConversionFileNamer.php +++ /dev/null @@ -1,15 +0,0 @@ -file_name, PATHINFO_FILENAME); - - return "{$fileName}-{$conversion->getName()}"; - } -} diff --git a/src/Conversions/FileManipulator.php b/src/Conversions/FileManipulator.php index 93eeeeeac..0a8b06b17 100644 --- a/src/Conversions/FileManipulator.php +++ b/src/Conversions/FileManipulator.php @@ -2,164 +2,104 @@ namespace Spatie\MediaLibrary\Conversions; -use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use Spatie\MediaLibrary\Conversions\Events\ConversionHasBeenCompleted; -use Spatie\MediaLibrary\Conversions\Events\ConversionWillStart; +use Spatie\MediaLibrary\Conversions\Actions\PerformConversionAction; use Spatie\MediaLibrary\Conversions\ImageGenerators\ImageGeneratorFactory; use Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob; use Spatie\MediaLibrary\MediaCollections\Filesystem; use Spatie\MediaLibrary\MediaCollections\Models\Media; -use Spatie\MediaLibrary\ResponsiveImages\ResponsiveImageGenerator; -use Spatie\MediaLibrary\Support\ImageFactory; use Spatie\MediaLibrary\Support\TemporaryDirectory; class FileManipulator { - public function createDerivedFiles(Media $media, array $only = [], bool $onlyMissing = false): void - { - $profileCollection = ConversionCollection::createForMedia($media); - - if (! empty($only)) { - $profileCollection = $profileCollection->filter( - fn (Conversion $conversion) => in_array($conversion->getName(), $only) - ); + public function createDerivedFiles( + Media $media, + array $onlyConversionNames = [], + bool $onlyMissing = false + ): void { + if (!$this->canConvertMedia($media)) { + return; } - $this->performConversions( - $profileCollection->getNonQueuedConversions($media->collection_name), - $media, - $onlyMissing - ); + [$queuedConversions, $conversions] = ConversionCollection::createForMedia($media) + ->filter(function (Conversion $conversion) use ($onlyConversionNames) { + if (count($onlyConversionNames) === 0) { + return true; + } - $queuedConversions = $profileCollection->getQueuedConversions($media->collection_name); + return in_array($conversion->getName(), $onlyConversionNames); + }) + ->filter(fn (Conversion $conversion) => $conversion->shouldBePerformedOn($media->collection_name)) + ->partition(fn (Conversion $conversion) => $conversion->shouldBeQueued()); - if ($queuedConversions->isNotEmpty()) { - $this->dispatchQueuedConversions($media, $queuedConversions, $onlyMissing); - } + $this + ->dispatchQueuedConversions($media, $queuedConversions, $onlyMissing) + ->performConversions($conversions, $media, $onlyMissing); } - public function performConversions(ConversionCollection $conversions, Media $media, bool $onlyMissing = false) - { + public function performConversions( + ConversionCollection $conversions, + Media $media, + bool $onlyMissing = false + ): self { if ($conversions->isEmpty()) { - return; - } - - $imageGenerator = ImageGeneratorFactory::forMedia($media); - - if (! $imageGenerator) { - return; + return $this; } $temporaryDirectory = TemporaryDirectory::create(); - $copiedOriginalFile = $this->filesystem()->copyFromMediaLibrary( + $copiedOriginalFile = app(Filesystem::class)->copyFromMediaLibrary( $media, - $temporaryDirectory->path(Str::random(16).'.'.$media->extension) + $temporaryDirectory->path(Str::random(32) . '.' . $media->extension) ); $conversions ->reject(function (Conversion $conversion) use ($onlyMissing, $media) { $relativePath = $media->getPath($conversion->getName()); - $rootPath = config("filesystems.disks.{$media->disk}.root"); - - if ($rootPath) { + if ($rootPath = config("filesystems.disks.{$media->disk}.root")) { $relativePath = str_replace($rootPath, '', $relativePath); } return $onlyMissing && Storage::disk($media->disk)->exists($relativePath); }) - ->each(function (Conversion $conversion) use ($media, $imageGenerator, $copiedOriginalFile) { - event(new ConversionWillStart($media, $conversion, $copiedOriginalFile)); - - $copiedOriginalFile = $imageGenerator->convert($copiedOriginalFile, $conversion); - - $manipulationResult = $this->performManipulations($media, $conversion, $copiedOriginalFile); - - $newFileName = $conversion->getConversionFile($media); - - $renamedFile = $this->renameInLocalDirectory($manipulationResult, $newFileName); - - if ($conversion->shouldGenerateResponsiveImages()) { - /** @var ResponsiveImageGenerator $responsiveImageGenerator */ - $responsiveImageGenerator = app(ResponsiveImageGenerator::class); - - $responsiveImageGenerator->generateResponsiveImagesForConversion( - $media, - $conversion, - $renamedFile - ); - } - - $this->filesystem()->copyToMediaLibrary($renamedFile, $media, 'conversions'); - - $media->markAsConversionGenerated($conversion->getName(), true); - - event(new ConversionHasBeenCompleted($media, $conversion)); + ->each(function (Conversion $conversion) use ($media, $copiedOriginalFile) { + (new PerformConversionAction)->execute($conversion, $media, $copiedOriginalFile); }); $temporaryDirectory->delete(); - } - - public function performManipulations(Media $media, Conversion $conversion, string $imageFile): string - { - if ($conversion->getManipulations()->isEmpty()) { - return $imageFile; - } - $conversionTempFile = $this->getConversionTempFileName($media, $conversion, $imageFile); - - File::copy($imageFile, $conversionTempFile); - - $supportedFormats = ['jpg', 'pjpg', 'png', 'gif']; - if ($conversion->shouldKeepOriginalImageFormat() && in_array($media->extension, $supportedFormats)) { - $conversion->format($media->extension); - } - - ImageFactory::load($conversionTempFile) - ->manipulate($conversion->getManipulations()) - ->save(); - - return $conversionTempFile; + return $this; } - protected function dispatchQueuedConversions(Media $media, ConversionCollection $queuedConversions, bool $onlyMissing = false) - { - $performConversionsJobClass = config('media-library.jobs.perform_conversions', PerformConversionsJob::class); - - $job = new $performConversionsJobClass($queuedConversions, $media, $onlyMissing); - - if ($customQueue = config('media-library.queue_name')) { - $job->onQueue($customQueue); + protected function dispatchQueuedConversions( + Media $media, + ConversionCollection $conversions, + bool $onlyMissing = false + ): self { + if ($conversions->isEmpty()) { + return $this; } - dispatch($job); - } - - protected function renameInLocalDirectory( - string $fileNameWithDirectory, - string $newFileNameWithoutDirectory - ): string { - $targetFile = pathinfo($fileNameWithDirectory, PATHINFO_DIRNAME).'/'.$newFileNameWithoutDirectory; + $performConversionsJobClass = config( + 'media-library.jobs.perform_conversions', + PerformConversionsJob::class + ); - rename($fileNameWithDirectory, $targetFile); + /** @var PerformConversionsJob $job */ + $job = (new $performConversionsJobClass($conversions, $media, $onlyMissing)) + ->onQueue(config('media-library.queue_name')); - return $targetFile; - } + dispatch($job); - protected function filesystem(): Filesystem - { - return app(Filesystem::class); + return $this; } - protected function getConversionTempFileName(Media $media, Conversion $conversion, string $imageFile): string + protected function canConvertMedia(Media $media): bool { - $directory = pathinfo($imageFile, PATHINFO_DIRNAME); - - $fileName = Str::random(16)."{$conversion->getName()}.{$media->extension}"; + $imageGenerator = ImageGeneratorFactory::forMedia($media); - return "{$directory}/{$fileName}"; + return $imageGenerator ? true : false; } } diff --git a/src/HasMedia.php b/src/HasMedia.php index a1bc154c2..5e1343f5f 100644 --- a/src/HasMedia.php +++ b/src/HasMedia.php @@ -67,6 +67,7 @@ public function shouldDeletePreservingMedia(): bool; * @param string $collectionName * * @return mixed + * */ public function loadMedia(string $collectionName); diff --git a/src/InteractsWithMedia.php b/src/InteractsWithMedia.php index 03f41d37a..77115617b 100644 --- a/src/InteractsWithMedia.php +++ b/src/InteractsWithMedia.php @@ -5,6 +5,7 @@ use DateTimeInterface; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Http\File; +use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Validator; @@ -22,6 +23,8 @@ use Spatie\MediaLibrary\MediaCollections\MediaCollection; use Spatie\MediaLibrary\MediaCollections\MediaRepository; use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\MediaLibrary\Support\MediaLibraryPro; +use Spatie\MediaLibraryPro\PendingMediaLibraryRequestHandler; trait InteractsWithMedia { @@ -74,6 +77,11 @@ public function addMedia($file): FileAdder return app(FileAdderFactory::class)->create($this, $file); } + public function addMediaFromRequest(string $key): FileAdder + { + return app(FileAdderFactory::class)->createFromRequest($this, $key); + } + /** * Add a file from the given disk. * @@ -87,9 +95,26 @@ public function addMediaFromDisk(string $key, string $disk = null): FileAdder return app(FileAdderFactory::class)->createFromDisk($this, $key, $disk ?: config('filesystems.default')); } - public function addMediaFromRequest(string $key): FileAdder + public function addFromMediaLibraryRequest(?array $mediaLibraryRequestItems): PendingMediaLibraryRequestHandler { - return app(FileAdderFactory::class)->createFromRequest($this, $key); + MediaLibraryPro::ensureInstalled(); + + return new PendingMediaLibraryRequestHandler( + $mediaLibraryRequestItems ?? [], + $this, + $preserveExisting = true + ); + } + + public function syncFromMediaLibraryRequest(?array $mediaLibraryRequestItems): PendingMediaLibraryRequestHandler + { + MediaLibraryPro::ensureInstalled(); + + return new PendingMediaLibraryRequestHandler( + $mediaLibraryRequestItems ?? [], + $this, + $preserveExisting = false + ); } /** @@ -242,11 +267,13 @@ public function hasMedia(string $collectionName = 'default'): bool * @param string $collectionName * @param array|callable $filters * - * @return \Illuminate\Support\Collection + * @return MediaCollections\Models\Collections\MediaCollection */ - public function getMedia(string $collectionName = 'default', $filters = []): Collection + public function getMedia(string $collectionName = 'default', $filters = []): MediaCollections\Models\Collections\MediaCollection { - return app(MediaRepository::class)->getCollection($this, $collectionName, $filters); + return app(MediaRepository::class) + ->getCollection($this, $collectionName, $filters) + ->collectionName($collectionName); } public function getFirstMedia(string $collectionName = 'default', $filters = []): ?Media @@ -503,6 +530,8 @@ public function loadMedia(string $collectionName): Collection ? $this->media : collect($this->unAttachedMediaLibraryItems)->pluck('media'); + $collection = new MediaCollections\Models\Collections\MediaCollection($collection); + return $collection ->filter(fn (Media $mediaItem) => $mediaItem->collection_name === $collectionName) ->sortBy('order_column') diff --git a/src/MediaCollections/Commands/CleanCommand.php b/src/MediaCollections/Commands/CleanCommand.php index f2c28f375..d597c71b5 100644 --- a/src/MediaCollections/Commands/CleanCommand.php +++ b/src/MediaCollections/Commands/CleanCommand.php @@ -175,10 +175,12 @@ protected function markConversionAsRemoved(Media $media, string $conversionPath) $generatedConversionName = null; $media->getGeneratedConversions() - ->filter(fn (bool $isGenerated, string $generatedConversionName) => Str::contains($conversionFile, $generatedConversionName)) - ->each(function (bool $isGenerated, string $generatedConversionName) use ($media) { - $media->markAsConversionGenerated($generatedConversionName, false); - }); + ->filter( + fn (bool $isGenerated, string $generatedConversionName) => Str::contains($conversionFile, $generatedConversionName) + ) + ->each( + fn (bool $isGenerated, string $conversionName) => $media->markAsConversionNotGenerated($conversionName) + ); $media->save(); } diff --git a/src/MediaCollections/Contracts/MediaLibraryRequest.php b/src/MediaCollections/Contracts/MediaLibraryRequest.php new file mode 100644 index 000000000..3595addec --- /dev/null +++ b/src/MediaCollections/Contracts/MediaLibraryRequest.php @@ -0,0 +1,10 @@ +filesystem = $fileSystem; @@ -126,6 +129,10 @@ public function setFile($file): self return $this; } + if ($file instanceof TemporaryUpload) { + return $this; + } + throw UnknownType::create(); } @@ -148,6 +155,13 @@ public function setName(string $name): self return $this; } + public function setOrder(int $order): self + { + $this->order = $order; + + return $this; + } + public function usingFileName(string $fileName): self { return $this->setFileName($fileName); @@ -254,6 +268,7 @@ public function toMediaCollectionFromRemote(string $collectionName = 'default', $media->size = $storage->size($this->pathToFile); $media->custom_properties = $this->customProperties; + $media->generated_conversions = []; $media->responsive_images = []; $media->manipulations = $this->manipulations; @@ -275,6 +290,10 @@ public function toMediaCollection(string $collectionName = 'default', string $di return $this->toMediaCollectionFromRemote($collectionName, $diskName); } + if ($this->file instanceof TemporaryUpload) { + return $this->toMediaCollectionFromTemporaryUpload($collectionName, $diskName); + } + if (! is_file($this->pathToFile)) { throw FileDoesNotExist::create($this->pathToFile); } @@ -308,8 +327,14 @@ public function toMediaCollection(string $collectionName = 'default', string $di $media->mime_type = File::getMimeType($this->pathToFile); $media->size = filesize($this->pathToFile); + + if (! is_null($this->order)) { + $media->order_column = $this->order; + } + $media->custom_properties = $this->customProperties; + $media->generated_conversions = []; $media->responsive_images = []; $media->manipulations = $this->manipulations; @@ -477,4 +502,23 @@ protected function checkGenerateResponsiveImages(Media $media) $this->withResponsiveImages(); } } + + protected function toMediaCollectionFromTemporaryUpload(string $collectionName, string $diskName): Media + { + /** @var TemporaryUpload $temporaryUpload */ + $temporaryUpload = $this->file; + + $media = $temporaryUpload->getFirstMedia(); + + $media->name = $this->mediaName; + $media->custom_properties = $this->customProperties; + + if (! is_null($this->order)) { + $media->order_column = $this->order; + } + + $media->save(); + + return $temporaryUpload->moveMedia($this->subject, $collectionName, $diskName); + } } diff --git a/src/MediaCollections/FileAdderFactory.php b/src/MediaCollections/FileAdderFactory.php index 26241c26e..7d196548d 100644 --- a/src/MediaCollections/FileAdderFactory.php +++ b/src/MediaCollections/FileAdderFactory.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Spatie\MediaLibrary\MediaCollections\Exceptions\RequestDoesNotHaveFile; use Spatie\MediaLibrary\Support\RemoteFile; +use Spatie\MediaLibraryPro\Dto\PendingMediaItem; class FileAdderFactory { @@ -69,4 +70,16 @@ public static function createAllFromRequest(Model $subject): Collection return static::createMultipleFromRequest($subject, $fileKeys); } + + public static function createForPendingMedia(Model $subject, PendingMediaItem $pendingMedia): FileAdder + { + /** @var \Spatie\MediaLibrary\MediaCollections\FileAdder $fileAdder */ + $fileAdder = app(FileAdder::class); + + return $fileAdder + ->setSubject($subject) + ->setFile($pendingMedia->temporaryUpload) + ->setName($pendingMedia->name) + ->setOrder($pendingMedia->order); + } } diff --git a/src/MediaCollections/HtmlableMedia.php b/src/MediaCollections/HtmlableMedia.php index c59a526a2..ddfd7eb6e 100644 --- a/src/MediaCollections/HtmlableMedia.php +++ b/src/MediaCollections/HtmlableMedia.php @@ -70,13 +70,17 @@ public function toHtml() $viewName = 'image'; $width = ''; + $height = ''; if ($this->media->hasResponsiveImages($this->conversionName)) { $viewName = config('media-library.responsive_images.use_tiny_placeholders') ? 'responsiveImageWithPlaceholder' : 'responsiveImage'; - $width = $this->media->responsiveImages($this->conversionName)->files->first()->width(); + $responsiveImage = $this->media->responsiveImages($this->conversionName)->files->first(); + + $width = $responsiveImage->width(); + $height = $responsiveImage->height(); } $media = $this->media; @@ -88,6 +92,7 @@ public function toHtml() 'attributeString', 'loadingAttributeValue', 'width', + 'height', ))->render(); } diff --git a/src/MediaCollections/Models/Collections/MediaCollection.php b/src/MediaCollections/Models/Collections/MediaCollection.php index 9aa9cd168..a549d03f5 100644 --- a/src/MediaCollections/Models/Collections/MediaCollection.php +++ b/src/MediaCollections/Models/Collections/MediaCollection.php @@ -2,12 +2,71 @@ namespace Spatie\MediaLibrary\MediaCollections\Models\Collections; +use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Collection; +use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\MediaLibrary\Support\MediaLibraryPro; -class MediaCollection extends Collection +class MediaCollection extends Collection implements Htmlable { + public ?string $collectionName = null; + + public ?string $formFieldName = null; + + public function collectionName(string $collectionName): self + { + $this->collectionName = $collectionName; + + return $this; + } + + public function formFieldName(string $formFieldName): self + { + $this->formFieldName = $formFieldName; + + return $this; + } + public function totalSizeInBytes(): int { return $this->sum('size'); } + + public function toHtml() + { + MediaLibraryPro::ensureInstalled(); + + return e(json_encode(old($this->formFieldName ?? $this->collectionName) ?? $this->map(function (Media $media) { + return [ + 'name' => $media->name, + 'file_name' => $media->file_name, + 'uuid' => $media->uuid, + 'preview_url' => $media->hasGeneratedConversion('preview') ? $media->getUrl('preview') : '', + 'order' => $media->order_column, + 'custom_properties' => $media->custom_properties, + 'extension' => $media->extension, + 'size' => $media->size, + ]; + })->keyBy('uuid'))); + } + + public function jsonSerialize() + { + if (!($this->formFieldName ?? $this->collectionName)) { + return []; + } + + return old($this->formFieldName ?? $this->collectionName) ?? $this->map(function (Media $media) { + return [ + 'name' => $media->name, + 'file_name' => $media->file_name, + 'uuid' => $media->uuid, + 'preview_url' => $media->hasGeneratedConversion('preview') ? $media->getUrl('preview') : '', + 'order' => $media->order_column, + 'custom_properties' => $media->custom_properties, + 'extension' => $media->extension, + 'size' => $media->size, + ]; + })->keyBy('uuid'); + } } diff --git a/src/MediaCollections/Models/Media.php b/src/MediaCollections/Models/Media.php index 8c337808e..8b00dabcc 100644 --- a/src/MediaCollections/Models/Media.php +++ b/src/MediaCollections/Models/Media.php @@ -5,8 +5,10 @@ use DateTimeInterface; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Responsable; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\MorphToMany; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Spatie\MediaLibrary\Conversions\Conversion; @@ -21,8 +23,10 @@ use Spatie\MediaLibrary\MediaCollections\Models\Concerns\IsSorted; use Spatie\MediaLibrary\ResponsiveImages\RegisteredResponsiveImages; use Spatie\MediaLibrary\Support\File; +use Spatie\MediaLibrary\Support\MediaLibraryPro; use Spatie\MediaLibrary\Support\TemporaryDirectory; use Spatie\MediaLibrary\Support\UrlGenerator\UrlGeneratorFactory; +use Spatie\MediaLibraryPro\Models\TemporaryUpload; class Media extends Model implements Responsable, Htmlable { @@ -32,10 +36,6 @@ class Media extends Model implements Responsable, Htmlable protected $table = 'media'; - // private $attachableModuleTypes = []; - - // private $attachableModuleTypes = []; - const TYPE_OTHER = 'other'; protected $guarded = []; @@ -43,27 +43,20 @@ class Media extends Model implements Responsable, Htmlable protected $casts = [ 'manipulations' => 'array', 'custom_properties' => 'array', + 'generated_conversions' => 'array', 'responsive_images' => 'array', ]; + public function newCollection(array $models = []) { return new MediaCollection($models); } - // public function modules(): Array - // { - // $arr = []; - // foreach ($this->attachableModuleTypes as $type => $val) { - // $arr[$type] = $this->morphedByMany('App\Models\\'.$type, 'model', 'model_has_media', 'media_id', 'model_id')->withPivot('model_story_id')->get(); - // } - // return $arr; - // } - public function modules($type): MorphToMany - { - return $this->morphedByMany('App\Models\\'.$type, 'model', 'model_has_media', 'media_id', 'model_id')->withPivot('model_story_id'); - } - /* - * Get the full url to a original media file. - */ + + public function model(): MorphTo + { + return $this->morphTo(); + } + public function getFullUrl(string $conversionName = ''): string { return url($this->getUrl($conversionName)); @@ -194,34 +187,53 @@ public function getMediaConversionNames(): array return $conversions->map(fn (Conversion $conversion) => $conversion->getName())->toArray(); } - public function hasGeneratedConversion(string $conversionName): bool + public function getGeneratedConversions(): Collection { - $generatedConversions = $this->getGeneratedConversions(); + return collect($this->generated_conversions ?? []); + } - return $generatedConversions[$conversionName] ?? false; + + public function markAsConversionGenerated(string $conversionName): self + { + $generatedConversions = $this->generated_conversions; + + Arr::set($generatedConversions, $conversionName, true); + + $this->generated_conversions = $generatedConversions; + + $this->save(); + + return $this; } - public function markAsConversionGenerated(string $conversionName, bool $generated): self + public function markAsConversionNotGenerated(string $conversionName): self { - $this->setCustomProperty("generated_conversions.{$conversionName}", $generated); + $generatedConversions = $this->generated_conversions; + + Arr::set($generatedConversions, $conversionName, false); + + $this->generated_conversions = $generatedConversions; $this->save(); return $this; } - public function getGeneratedConversions(): Collection + public function hasGeneratedConversion(string $conversionName): bool { - return collect($this->getCustomProperty('generated_conversions', [])); + $generatedConversions = $this->getGeneratedConversions(); + + return $generatedConversions[$conversionName] ?? false; } + public function toResponse($request) { $downloadHeaders = [ 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 'Content-Type' => $this->mime_type, 'Content-Length' => $this->size, - 'Content-Disposition' => 'attachment; filename="'.$this->file_name.'"', + 'Content-Disposition' => 'attachment; filename="' . $this->file_name . '"', 'Pragma' => 'public', ]; @@ -264,7 +276,7 @@ public function copy(HasMedia $model, $collectionName = 'default', string $diskN { $temporaryDirectory = TemporaryDirectory::create(); - $temporaryFile = $temporaryDirectory->path('/').DIRECTORY_SEPARATOR.$this->file_name; + $temporaryFile = $temporaryDirectory->path('/') . DIRECTORY_SEPARATOR . $this->file_name; /** @var \Spatie\MediaLibrary\MediaCollections\Filesystem $filesystem */ $filesystem = app(Filesystem::class); @@ -274,6 +286,7 @@ public function copy(HasMedia $model, $collectionName = 'default', string $diskN $newMedia = $model ->addMedia($temporaryFile) ->usingName($this->name) + ->setOrder($this->order_column) ->withCustomProperties($this->custom_properties) ->toMediaCollection($collectionName, $diskName); @@ -311,4 +324,25 @@ public function __invoke(...$arguments): HtmlableMedia { return $this->img(...$arguments); } + + public function temporaryUpload(): BelongsTo + { + MediaLibraryPro::ensureInstalled(); + + return $this->belongsTo(TemporaryUpload::class); + } + + public static function findWithTemporaryUploadInCurrentSession(array $uuids) + { + MediaLibraryPro::ensureInstalled(); + + return static::query() + ->whereIn('uuid', $uuids) + ->whereHasMorph( + 'model', + [TemporaryUpload::class], + fn (Builder $builder) => $builder->where('session_id', session()->getId()) + ) + ->get(); + } } diff --git a/src/MediaCollections/Models/Observers/MediaObserver.php b/src/MediaCollections/Models/Observers/MediaObserver.php index 65881d992..83698e1e9 100644 --- a/src/MediaCollections/Models/Observers/MediaObserver.php +++ b/src/MediaCollections/Models/Observers/MediaObserver.php @@ -13,7 +13,9 @@ class MediaObserver public function creating(Media $media) { if ($media->shouldSortWhenCreating()) { - $media->setHighestOrderNumber(); + if (is_null($media->order_column)) { + $media->setHighestOrderNumber(); + } } } @@ -35,7 +37,7 @@ public function updated(Media $media) $original = $media->getOriginal('manipulations'); - if (! $this->isLaravel7orHigher()) { + if (!$this->isLaravel7orHigher()) { $original = json_decode($original, true); } @@ -55,7 +57,7 @@ public function updated(Media $media) public function deleted(Media $media) { if (in_array(SoftDeletes::class, class_uses_recursive($media))) { - if (! $media->isForceDeleting()) { + if (!$media->isForceDeleting()) { return; } } diff --git a/src/ResponsiveImages/ResponsiveImage.php b/src/ResponsiveImages/ResponsiveImage.php index 7516f5f2a..83c6634e0 100644 --- a/src/ResponsiveImages/ResponsiveImage.php +++ b/src/ResponsiveImages/ResponsiveImage.php @@ -91,7 +91,9 @@ protected function getPropertyParts(): array protected function stringBetween(string $subject, string $startCharacter, string $endCharacter): string { - $between = strstr($subject, $startCharacter); + $lastPos = strrpos($subject, $startCharacter); + + $between = substr($subject, $lastPos); $between = str_replace('___', '', $between); diff --git a/src/ResponsiveImages/ResponsiveImageGenerator.php b/src/ResponsiveImages/ResponsiveImageGenerator.php index 39e89ccb5..4935da9f2 100644 --- a/src/ResponsiveImages/ResponsiveImageGenerator.php +++ b/src/ResponsiveImages/ResponsiveImageGenerator.php @@ -11,6 +11,7 @@ use Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\TinyPlaceholderGenerator; use Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\WidthCalculator; use Spatie\MediaLibrary\Support\File; +use Spatie\MediaLibrary\Support\FileNamer\FileNamer; use Spatie\MediaLibrary\Support\ImageFactory; use Spatie\MediaLibrary\Support\TemporaryDirectory; use Spatie\TemporaryDirectory\TemporaryDirectory as BaseTemporaryDirectory; @@ -25,6 +26,8 @@ class ResponsiveImageGenerator protected const DEFAULT_CONVERSION_QUALITY = 90; + protected FileNamer $fileNamer; + public function __construct( Filesystem $filesystem, WidthCalculator $widthCalculator, @@ -35,6 +38,8 @@ public function __construct( $this->widthCalculator = $widthCalculator; $this->tinyPlaceholderGenerator = $tinyPlaceholderGenerator; + + $this->fileNamer = app(config('media-library.file_namer')); } public function generateResponsiveImages(Media $media): void @@ -87,11 +92,8 @@ public function generateResponsiveImage( BaseTemporaryDirectory $temporaryDirectory, int $conversionQuality = self::DEFAULT_CONVERSION_QUALITY ): void { - $responsiveImagePath = $this->appendToFileName( - $media->file_name, - "___{$conversionName}_{$targetWidth}", - $baseImage - ); + $extension = $this->fileNamer->extensionFromBaseImage($baseImage); + $responsiveImagePath = $this->fileNamer->temporaryFileName($media, $extension); $tempDestination = $temporaryDirectory->path($responsiveImagePath); @@ -103,15 +105,22 @@ public function generateResponsiveImage( $responsiveImageHeight = ImageFactory::load($tempDestination)->getHeight(); - $finalImageFileName = $this->appendToFileName($responsiveImagePath, "_{$responsiveImageHeight}"); + // Users can customize the name like they want, but we expect the last part in a certain format + $fileName = $this->addPropertiesToFileName( + $responsiveImagePath, + $conversionName, + $targetWidth, + $responsiveImageHeight, + $extension + ); - $finalResponsiveImagePath = $temporaryDirectory->path($finalImageFileName); + $responsiveImagePath = $temporaryDirectory->path($fileName); - rename($tempDestination, $finalResponsiveImagePath); + rename($tempDestination, $responsiveImagePath); - $this->filesystem->copyToMediaLibrary($finalResponsiveImagePath, $media, 'responsiveImages'); + $this->filesystem->copyToMediaLibrary($responsiveImagePath, $media, 'responsiveImages'); - ResponsiveImage::register($media, $finalImageFileName, $conversionName); + ResponsiveImage::register($media, $fileName, $conversionName); } public function generateTinyJpg( @@ -177,4 +186,10 @@ protected function cleanResponsiveImages(Media $media, string $conversionName = return $media; } + + protected function addPropertiesToFileName(string $fileName, string $conversionName, int $width, int $height, string $extension): string + { + $fileName = pathinfo($fileName, PATHINFO_FILENAME); + return "{$fileName}___{$conversionName}_{$width}_{$height}.{$extension}"; + } } diff --git a/src/Support/Factories/TemporaryUploadFactory.php b/src/Support/Factories/TemporaryUploadFactory.php new file mode 100644 index 000000000..652f60262 --- /dev/null +++ b/src/Support/Factories/TemporaryUploadFactory.php @@ -0,0 +1,7 @@ +getName()}"; + } + + public function responsiveFileName(string $fileName): string + { + return pathinfo($fileName, PATHINFO_FILENAME); + } +} diff --git a/src/Support/FileNamer/FileNamer.php b/src/Support/FileNamer/FileNamer.php new file mode 100644 index 000000000..25d520488 --- /dev/null +++ b/src/Support/FileNamer/FileNamer.php @@ -0,0 +1,23 @@ +responsiveFileName($media->file_name)}.{$extension}"; + } + + public function extensionFromBaseImage(string $baseImage): string + { + return pathinfo($baseImage, PATHINFO_EXTENSION); + } +} diff --git a/src/Support/MediaLibraryPro.php b/src/Support/MediaLibraryPro.php new file mode 100644 index 000000000..f50da93ff --- /dev/null +++ b/src/Support/MediaLibraryPro.php @@ -0,0 +1,21 @@ +', $media->id)->delete(); - $media->markAsConversionGenerated('test-deprecated', true); + $media->markAsConversionGenerated('test-deprecated'); $media->save(); diff --git a/tests/Conversions/ConversionFileNamerTest.php b/tests/Conversions/ConversionFileNamerTest.php index 91649cb46..6d2d1c27a 100644 --- a/tests/Conversions/ConversionFileNamerTest.php +++ b/tests/Conversions/ConversionFileNamerTest.php @@ -3,25 +3,27 @@ namespace Spatie\MediaLibrary\Tests\Conversions; use Spatie\MediaLibrary\Tests\TestCase; -use Spatie\MediaLibrary\Tests\TestSupport\testfiles\TestConversionFileNamer; +use Spatie\MediaLibrary\Tests\TestSupport\TestFileNamer; class ConversionFileNamerTest extends TestCase { + public string $fileName = "prefix_test_suffix"; + /** @test */ - public function it_can_use_a_custom_conversion_file_namer() + public function it_can_use_a_custom_file_namer() { - config()->set('media-library.conversion_file_namer', TestConversionFileNamer::class); + config()->set("media-library.file_namer", TestFileNamer::class); $this ->testModelWithConversion ->addMedia($this->getTestJpg()) ->toMediaCollection(); - $path = $this->testModelWithConversion->refresh()->getFirstMediaPath('default', 'thumb'); + $path = $this->testModelWithConversion->refresh()->getFirstMediaPath("default", "thumb"); - $this->assertStringEndsWith('test---thumb.jpg', $path); + $this->assertStringEndsWith("{$this->fileName}---thumb.jpg", $path); $this->assertFileExists($path); - $this->assertEquals('/media/1/conversions/test---thumb.jpg', $this->testModelWithConversion->getFirstMediaUrl('default', 'thumb')); + $this->assertEquals("/media/1/conversions/{$this->fileName}---thumb.jpg", $this->testModelWithConversion->getFirstMediaUrl("default", "thumb")); } } diff --git a/tests/Conversions/FileManipulatorTest.php b/tests/Conversions/FileManipulatorTest.php index 098e78ecf..277b5e2d5 100644 --- a/tests/Conversions/FileManipulatorTest.php +++ b/tests/Conversions/FileManipulatorTest.php @@ -2,8 +2,8 @@ namespace Spatie\MediaLibrary\Tests\Conversions; +use Spatie\MediaLibrary\Conversions\Actions\PerformManipulationsAction; use Spatie\MediaLibrary\Conversions\Conversion; -use Spatie\MediaLibrary\Conversions\FileManipulator; use Spatie\MediaLibrary\Tests\TestCase; class FileManipulatorTest extends TestCase @@ -25,7 +25,7 @@ public function it_does_not_perform_manipulations_if_not_necessary() $imageFile = $this->getTestJpg(); $media = $this->testModelWithoutMediaConversions->addMedia($this->getTestJpg())->toMediaCollection(); - $conversionTempFile = (new FileManipulator)->performManipulations( + $conversionTempFile = (new PerformManipulationsAction())->execute( $media, $this->conversion->withoutManipulations(), $imageFile diff --git a/tests/Feature/FileAdder/IntegrationTest.php b/tests/Feature/FileAdder/IntegrationTest.php index 987158ca4..b5ca9e2b4 100644 --- a/tests/Feature/FileAdder/IntegrationTest.php +++ b/tests/Feature/FileAdder/IntegrationTest.php @@ -174,6 +174,8 @@ public function it_can_add_an_upload_to_the_media_library_from_the_current_reque filesize($this->getTestFilesDirectory('test.jpg')) ); + $this->withoutExceptionHandling(); + $result = $this->call('get', 'upload', [], [], ['file' => $fileUpload]); $this->assertEquals(200, $result->getStatusCode()); diff --git a/tests/Feature/Media/ToHtmlTest.php b/tests/Feature/Media/ToHtmlTest.php index aa869505d..17501aa94 100644 --- a/tests/Feature/Media/ToHtmlTest.php +++ b/tests/Feature/Media/ToHtmlTest.php @@ -111,7 +111,7 @@ public function it_will_not_rendering_extra_javascript_or_including_base64_svg_w $imgTag = $media->refresh()->img(); - $this->assertEquals('', $imgTag); + $this->assertEquals('', $imgTag); } /** @test */ diff --git a/tests/Feature/S3Integration/S3IntegrationTest.php b/tests/Feature/S3Integration/S3IntegrationTest.php index 9f347de37..55e188707 100644 --- a/tests/Feature/S3Integration/S3IntegrationTest.php +++ b/tests/Feature/S3Integration/S3IntegrationTest.php @@ -40,13 +40,17 @@ public function tearDown(): void /** @test */ public function it_can_add_media_from_a_disk_to_s3() { - Storage::disk('s3_disk')->put('tmp/test.jpg', file_get_contents($this->getTestJpg())); + $randomNumber = rand(); + + $fileName = "test{$randomNumber}.jpg"; + + Storage::disk('s3_disk')->put("tmp/{$fileName}", file_get_contents($this->getTestJpg())); $media = $this->testModel - ->addMediaFromDisk('tmp/test.jpg', 's3_disk') + ->addMediaFromDisk("tmp/{$fileName}", 's3_disk') ->toMediaCollection('default', 's3_disk'); - $this->assertS3FileExists("{$this->s3BaseDirectory}/{$media->id}/test.jpg"); + $this->assertS3FileExists("{$this->s3BaseDirectory}/{$media->id}/{$fileName}"); } /** @test */ diff --git a/tests/ResponsiveImages/ResponsiveImageFileNamerTest.php b/tests/ResponsiveImages/ResponsiveImageFileNamerTest.php new file mode 100644 index 000000000..f118b1b17 --- /dev/null +++ b/tests/ResponsiveImages/ResponsiveImageFileNamerTest.php @@ -0,0 +1,18 @@ +set("media-library.file_namer", TestFileNamer::class); + + $this->fileName = "prefix_test_suffix"; + $this->fileNameWithUnderscore = "prefix_test__suffix"; + } +} diff --git a/tests/ResponsiveImages/ResponsiveImageGeneratorFileNamerTest.php b/tests/ResponsiveImages/ResponsiveImageGeneratorFileNamerTest.php new file mode 100644 index 000000000..d89dce928 --- /dev/null +++ b/tests/ResponsiveImages/ResponsiveImageGeneratorFileNamerTest.php @@ -0,0 +1,17 @@ +set("media-library.file_namer", TestFileNamer::class); + + $this->fileName = "prefix_test_suffix"; + } +} diff --git a/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php b/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php index 71820aeeb..9505ef1f7 100644 --- a/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php +++ b/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php @@ -8,6 +8,8 @@ class ResponsiveImageGeneratorTest extends TestCase { + public string $fileName = "test"; + /** @test */ public function it_can_generate_responsive_images() { @@ -16,9 +18,9 @@ public function it_can_generate_responsive_images() ->withResponsiveImages() ->toMediaCollection(); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___media_library_original_237_195.jpg')); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___media_library_original_284_233.jpg')); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___media_library_original_340_280.jpg')); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___media_library_original_237_195.jpg")); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___media_library_original_284_233.jpg")); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___media_library_original_340_280.jpg")); } /** @test */ @@ -29,11 +31,11 @@ public function it_will_generate_responsive_images_if_withResponsiveImagesIf_ret ->withResponsiveImagesIf(fn () => true) ->toMediaCollection(); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___media_library_original_237_195.jpg')); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___media_library_original_284_233.jpg')); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___media_library_original_340_280.jpg')); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___media_library_original_237_195.jpg")); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___media_library_original_284_233.jpg")); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___media_library_original_340_280.jpg")); } - + /** @test */ public function it_will_not_generate_responsive_images_if_withResponsiveImagesIf_returns_false() { @@ -42,7 +44,7 @@ public function it_will_not_generate_responsive_images_if_withResponsiveImagesIf ->withResponsiveImagesIf(fn () => false) ->toMediaCollection(); - $this->assertFileDoesNotExist($this->getTempDirectory('media/1/responsive-images/test___media_library_original_237_195.jpg')); + $this->assertFileDoesNotExist($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___media_library_original_237_195.jpg")); } /** @test */ @@ -53,7 +55,7 @@ public function its_conversions_can_have_responsive_images() ->withResponsiveImages() ->toMediaCollection(); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___thumb_50_41.jpg')); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___thumb_50_41.jpg")); } /** @test */ @@ -64,7 +66,7 @@ public function its_conversions_can_have_responsive_images_and_change_format() ->withResponsiveImages() ->toMediaCollection(); - $this->assertFileExists($this->getTempDirectory('media/1/responsive-images/test___pngtojpg_700_883.jpg')); + $this->assertFileExists($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___pngtojpg_700_883.jpg")); } /** @test */ @@ -84,32 +86,32 @@ public function it_triggers_an_event_when_the_responsive_images_are_generated() public function it_cleans_the_responsive_images_urls_from_the_db_before_regeneration() { $media = $this->testModelWithResponsiveImages - ->addMedia($this->getTestFilesDirectory('test.jpg')) + ->addMedia($this->getTestFilesDirectory("test.jpg")) ->withResponsiveImages() ->toMediaCollection(); - $this->assertCount(1, $media->fresh()->responsive_images['thumb']['urls']); + $this->assertCount(1, $media->fresh()->responsive_images["thumb"]["urls"]); - $this->artisan('media-library:regenerate'); - $this->assertCount(1, $media->fresh()->responsive_images['thumb']['urls']); + $this->artisan("media-library:regenerate"); + $this->assertCount(1, $media->fresh()->responsive_images["thumb"]["urls"]); } /** @test */ public function it_will_add_responsive_image_entries_when_there_were_none_when_regenerating() { $media = $this->testModelWithResponsiveImages - ->addMedia($this->getTestFilesDirectory('test.jpg')) + ->addMedia($this->getTestFilesDirectory("test.jpg")) ->withResponsiveImages() ->toMediaCollection(); // remove all responsive image db entries $responsiveImages = $media->responsive_images; - $responsiveImages['thumb']['urls'] = []; + $responsiveImages["thumb"]["urls"] = []; $media->responsive_images = $responsiveImages; $media->save(); - $this->assertCount(0, $media->fresh()->responsive_images['thumb']['urls']); + $this->assertCount(0, $media->fresh()->responsive_images["thumb"]["urls"]); - $this->artisan('media-library:regenerate'); - $this->assertCount(1, $media->fresh()->responsive_images['thumb']['urls']); + $this->artisan("media-library:regenerate"); + $this->assertCount(1, $media->fresh()->responsive_images["thumb"]["urls"]); } } diff --git a/tests/ResponsiveImages/ResponsiveImageTest.php b/tests/ResponsiveImages/ResponsiveImageTest.php index 9a0a11d46..8f9628945 100644 --- a/tests/ResponsiveImages/ResponsiveImageTest.php +++ b/tests/ResponsiveImages/ResponsiveImageTest.php @@ -6,6 +6,9 @@ class ResponsiveImageTest extends TestCase { + public string $fileName = 'test'; + public string $fileNameWithUnderscore = 'test_'; + /** @test */ public function a_media_instance_can_get_responsive_image_urls() { @@ -18,16 +21,16 @@ public function a_media_instance_can_get_responsive_image_urls() $media = $this->testModelWithResponsiveImages->getFirstMedia(); $this->assertEquals([ - 'http://localhost/media/1/responsive-images/test___media_library_original_340_280.jpg', - 'http://localhost/media/1/responsive-images/test___media_library_original_284_233.jpg', - 'http://localhost/media/1/responsive-images/test___media_library_original_237_195.jpg', + "http://localhost/media/1/responsive-images/{$this->fileName}___media_library_original_340_280.jpg", + "http://localhost/media/1/responsive-images/{$this->fileName}___media_library_original_284_233.jpg", + "http://localhost/media/1/responsive-images/{$this->fileName}___media_library_original_237_195.jpg", ], $media->getResponsiveImageUrls()); $this->assertEquals([ - 'http://localhost/media/1/responsive-images/test___thumb_50_41.jpg', - ], $media->getResponsiveImageUrls('thumb')); + "http://localhost/media/1/responsive-images/{$this->fileName}___thumb_50_41.jpg", + ], $media->getResponsiveImageUrls("thumb")); - $this->assertEquals([], $media->getResponsiveImageUrls('non-existing-conversion')); + $this->assertEquals([], $media->getResponsiveImageUrls("non-existing-conversion")); } /** @test */ @@ -41,16 +44,16 @@ public function a_media_instance_can_generate_the_contents_of_scrset() $media = $this->testModelWithResponsiveImages->getFirstMedia(); $this->assertStringContainsString( - 'http://localhost/media/1/responsive-images/test___media_library_original_340_280.jpg 340w, http://localhost/media/1/responsive-images/test___media_library_original_284_233.jpg 284w, http://localhost/media/1/responsive-images/test___media_library_original_237_195.jpg 237w', + "http://localhost/media/1/responsive-images/{$this->fileName}___media_library_original_340_280.jpg 340w, http://localhost/media/1/responsive-images/{$this->fileName}___media_library_original_284_233.jpg 284w, http://localhost/media/1/responsive-images/{$this->fileName}___media_library_original_237_195.jpg 237w", $media->getSrcset() ); - $this->assertStringContainsString('data:image/svg+xml;base64', $media->getSrcset()); + $this->assertStringContainsString("data:image/svg+xml;base64", $media->getSrcset()); $this->assertStringContainsString( - 'http://localhost/media/1/responsive-images/test___thumb_50_41.jpg 50w', - $media->getSrcset('thumb') + "http://localhost/media/1/responsive-images/{$this->fileName}___thumb_50_41.jpg 50w", + $media->getSrcset("thumb") ); - $this->assertStringContainsString('data:image/svg+xml;base64,', $media->getSrcset('thumb')); + $this->assertStringContainsString("data:image/svg+xml;base64,", $media->getSrcset("thumb")); } /** @test */ @@ -65,7 +68,7 @@ public function a_responsive_image_can_return_some_properties() $responsiveImage = $media->responsiveImages()->files->first(); - $this->assertEquals('media_library_original', $responsiveImage->generatedFor()); + $this->assertEquals("media_library_original", $responsiveImage->generatedFor()); $this->assertEquals(340, $responsiveImage->width()); @@ -78,10 +81,10 @@ public function responsive_image_generation_respects_the_conversion_quality_sett $this->testModelWithResponsiveImages ->addMedia($this->getTestJpg()) ->preservingOriginal() - ->toMediaCollection('default'); + ->toMediaCollection("default"); - $standardQualityResponsiveConversion = $this->getTempDirectory('media/1/responsive-images/test___standardQuality_340_280.jpg'); - $lowerQualityResponsiveConversion = $this->getTempDirectory('media/1/responsive-images/test___lowerQuality_340_280.jpg'); + $standardQualityResponsiveConversion = $this->getTempDirectory("media/1/responsive-images/{$this->fileName}___standardQuality_340_280.jpg"); + $lowerQualityResponsiveConversion = $this->getTempDirectory("media/1/responsive-images/{$this->fileName}___lowerQuality_340_280.jpg"); $this->assertLessThan(filesize($standardQualityResponsiveConversion), filesize($lowerQualityResponsiveConversion)); } @@ -92,13 +95,37 @@ public function a_media_instance_can_get_responsive_image_urls_with_conversions_ $this->testModelWithResponsiveImages ->addMedia($this->getTestJpg()) ->withResponsiveImages() - ->storingConversionsOnDisk('secondMediaDisk') + ->storingConversionsOnDisk("secondMediaDisk") ->toMediaCollection(); $media = $this->testModelWithResponsiveImages->getFirstMedia(); $this->assertEquals([ - 'http://localhost/media2/1/responsive-images/test___thumb_50_41.jpg', - ], $media->getResponsiveImageUrls('thumb')); + "http://localhost/media2/1/responsive-images/{$this->fileName}___thumb_50_41.jpg", + ], $media->getResponsiveImageUrls("thumb")); + } + + /** @test */ + public function it_can_handle_file_names_with_underscore() + { + $this + ->testModelWithResponsiveImages + ->addMedia($this->getTestImageEndingWithUnderscore()) + ->withResponsiveImages() + ->toMediaCollection(); + + $media = $this->testModelWithResponsiveImages->getFirstMedia(); + + $this->assertSame([ + "http://localhost/media/1/responsive-images/{$this->fileNameWithUnderscore}___media_library_original_340_280.jpg", + "http://localhost/media/1/responsive-images/{$this->fileNameWithUnderscore}___media_library_original_284_233.jpg", + "http://localhost/media/1/responsive-images/{$this->fileNameWithUnderscore}___media_library_original_237_195.jpg", + ], $media->getResponsiveImageUrls()); + + $this->assertSame([ + "http://localhost/media/1/responsive-images/{$this->fileNameWithUnderscore}___thumb_50_41.jpg", + ], $media->getResponsiveImageUrls("thumb")); + + $this->assertSame([], $media->getResponsiveImageUrls("non-existing-conversion")); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index fb809b3cd..80132a5ab 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,12 +3,14 @@ namespace Spatie\MediaLibrary\Tests; use CreateMediaTable; +use CreateTemporaryUploadsTable; use Dotenv\Dotenv; use File; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Schema\Blueprint; use Orchestra\Testbench\TestCase as Orchestra; use Spatie\MediaLibrary\MediaLibraryServiceProvider; +use Spatie\MediaLibrary\Support\MediaLibraryPro; use Spatie\MediaLibrary\Tests\TestSupport\TestModels\TestModel; use Spatie\MediaLibrary\Tests\TestSupport\TestModels\TestModelWithConversion; use Spatie\MediaLibrary\Tests\TestSupport\TestModels\TestModelWithConversionQueued; @@ -74,9 +76,11 @@ protected function loadEnvironmentVariables() */ protected function getPackageProviders($app) { - return [ - MediaLibraryServiceProvider::class, + $serviceProviders = [ + MedialibraryServiceProvider::class, ]; + + return $serviceProviders; } /** @@ -129,7 +133,14 @@ protected function setUpDatabase($app) TestModel::create(['name' => 'test']); - include_once __DIR__.'/../database/migrations/create_media_table.php.stub'; + + if (MediaLibraryPro::isInstalled()) { + include_once __DIR__ . '/../vendor/spatie/laravel-medialibrary-pro/database/migrations/create_temporary_uploads_table.stub'; + (new CreateTemporaryUploadsTable())->up(); + } + + include_once(__DIR__ . '/../database/migrations/create_media_table.php.stub'); + (new CreateMediaTable())->up(); } @@ -216,6 +227,11 @@ public function getTestImageWithoutExtension(): string return $this->getTestFilesDirectory('image'); } + public function getTestImageEndingWithUnderscore(): string + { + return $this->getTestFilesDirectory('test_.jpg'); + } + public function getAntaresThumbJpgWithAccent(): string { return $this->getTestFilesDirectory('antarèsthumb.jpg'); diff --git a/tests/TestSupport/TestFileNamer.php b/tests/TestSupport/TestFileNamer.php new file mode 100644 index 000000000..45e91b2be --- /dev/null +++ b/tests/TestSupport/TestFileNamer.php @@ -0,0 +1,23 @@ +getName()}"; + } +} diff --git a/tests/TestSupport/testfiles/TestConversionFileNamer.php b/tests/TestSupport/testfiles/TestConversionFileNamer.php deleted file mode 100644 index c22190f67..000000000 --- a/tests/TestSupport/testfiles/TestConversionFileNamer.php +++ /dev/null @@ -1,17 +0,0 @@ -file_name, PATHINFO_FILENAME); - - return "{$fileName}---{$conversion->getName()}"; - } -} diff --git a/tests/TestSupport/testfiles/test_.jpg b/tests/TestSupport/testfiles/test_.jpg new file mode 100644 index 000000000..ba70fe087 Binary files /dev/null and b/tests/TestSupport/testfiles/test_.jpg differ