优雅/laravel-media

一个灵活的Laravel媒体库

v2.2.4 2024-09-11 21:09 UTC

README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

本包提供极其灵活的媒体库,允许您存储任何类型的文件及其转换(支持嵌套转换)。它旨在与任何文件系统解决方案(本地或云)无缝工作,例如Bunny.net、AWS S3/MediaConvert、Transloadit等。

本包的灵感来源于出色的 spatie/laravel-media-library 包(请务必查看Spatie的包,它们都是一流的)。然而,它不是分支,因为内部架构不同,为您提供更多功能。从 spatie/laravel-media-library 迁移是可行的,但如果您希望保留转换文件,可能会具有挑战性。

动机

Spatie团队开发了一个出色的包 spatie/laravel-media-library,它非常适合大多数常见场景。然而,我发现自己在自己的项目中受到他们架构的限制。为了解决这个问题,我需要以下功能

  • 文件转换
  • 高级媒体转换
  • 嵌套媒体转换

因此,我开发了这款具有最高灵活性的包。我已经在生产环境中使用它近一年,每月处理数TB的文件。

完整示例

以下示例将更好地向您展示包的功能。

我们将创建一个类似于YouTube的服务,有一个名为 Channel 的模型。这个 Channel 将有两种类型的媒体:avatarvideos。我们将在 registerMediaCollections 方法中定义这些媒体类型。

我们希望将头像存储为方形格式,尺寸不超过500px,并以WebP格式存储。我们将在 registerMediaTransformations 方法中完成这项工作。

对于每种媒体类型,我们都需要一组转换,如下所示树形图所示

/avatar
  /avatar-360
/video
  /poster
    /poster-360
    /poster-720
  /360
  /720
  /1080
  /hls

我们将在 registerMediaConversions 方法中定义这些转换。

以下是我们的 Channel 类的定义

namespace App\Models;

use Elegantly\Media\Traits\HasMedia;
use Elegantly\Media\MediaCollection;
use Elegantly\Media\MediaConversion;
use Elegantly\Media\Enums\MediaType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Spatie\Image\Enums\Fit;
use \App\Jobs\Media\OptimizedImageConversionJob;
use Elegantly\Media\Models\Media;
use Elegantly\Media\Contracts\InteractWithMedia;
use Illuminate\Contracts\Support\Arrayable;
use Elegantly\Media\Support\ResponsiveImagesConversionsPreset;

class Channel extends Model implements InteractWithMedia
{
    use HasMedia;

    public function registerMediaCollections(): Arrayable|iterable|null;
    {
       return [
            new MediaCollection(
                name: 'avatar',
                acceptedMimeTypes: [
                    'image/jpeg', 'image/png', 'image/gif', 'image/webp',
                ],
            )
            new MediaCollection(
                name: 'videos',
                acceptedMimeTypes: [
                    'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime',
                ],
            )
       ];
    }

    public function registerMediaTransformations($media, UploadedFile|File $file): UploadedFile|File
    {
        if($media->collection_name === "avatar"){
            Image::load($file->getRealPath())
                ->fit(Fit::Crop, 500, 500)
                ->optimize()
                ->save();
        }

        return $file;
    }

    public function registerMediaConversions($media): Arrayable|iterable|null;
    {

        if($media->collection_name === 'avatar'){
            return [
                new MediaConversion(
                    conversionName: '360',
                    job: new OptimizedImageConversionJob(
                        media: $media,
                        width: 360,
                        fileName: "{$media->name}-360.jpg"
                    ),
                )
            ]
        }elseif($media->collection_name === 'videos'){
            return [
                new MediaConversion(
                    conversionName: 'poster',
                    sync: true,// The conversion will not be queued, you will have access to it immediatly
                    job: new VideoPosterConversionJob(
                        media: $media,
                        seconds: 1,
                        fileName: "{$media->name}-poster.jpg"
                    ),
                    conversions: function(GeneratedConversion $generatedConversion) use ($media){
                        return ResponsiveImagesConversionsPreset::make(
                            media: $media,
                            generatedConversion: $generatedConversion
                            widths: [360, 720]
                        )
                    }
                ),
                ...ResponsiveVideosConversionsPreset::make(
                    media: $media,
                    widths: [360, 720, 1080],
                )
            ]
        }

        return null;
    }
}

从现在开始,我们可以以最简单的方式存储文件

从控制器存储文件

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ChannelAvatarController extends Controller
{
    function function store(Request $request, Channel $channel)
    {
        $channel->addMedia(
            file: $file->file('avatar'),
            collection_name: 'avatar',
            name: "{$channel->name}-avatar",
        )
    }
}

从Livewire组件存储文件

namespace App\Livewire;

use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;
use Livewire\Component;

class ImageUploader extends Component
{
    use WithFileUploads;

    function function save()
    {
        /** @var TemporaryUploadedFile */
        $file = $this->avatar;

        $this->channel->addMedia(
            file: $file->getRealPath(),
            collection_name: 'avatar',
            name: "{$channel->name}-avatar",
        )
    }
}

安装

您可以通过composer安装此包

composer require elegantly/laravel-media

您必须使用以下命令发布并运行迁移

php artisan vendor:publish --tag="laravel-media-migrations"
php artisan migrate

您可以使用以下命令发布配置文件

php artisan vendor:publish --tag="laravel-media-config"

这是已发布配置文件的内容

use Elegantly\Media\Jobs\DeleteModelMediaJob;
use Elegantly\Media\Models\Media;

return [
    /**
     * The media model
     * Define your own model here by extending \Elegantly\Media\Models\Media::class
     */
    'model' => Media::class,

    /**
     * The default disk used for storing files
     */
    'disk' => env('MEDIA_DISK', env('FILESYSTEM_DISK', 'local')),

    /**
     * Determine if media should be deleted with the model
     * when using the HasMedia Trait
     */
    'delete_media_with_model' => true,

    /**
     * Determine if media should be deleted with the model
     * when it is soft deleted
     */
    'delete_media_with_trashed_model' => false,

    /**
     * Deleting a large number of media attached to a model can be time-consuming
     * or even fail (e.g., cloud API error, permissions, etc.)
     * For performance and monitoring, when a model with the HasMedia trait is deleted,
     * each media is individually deleted inside a job.
     */
    'delete_media_with_model_job' => DeleteModelMediaJob::class,

    /**
     * The default collection name
     */
    'default_collection_name' => 'default',

    /**
     * Prefix for the generated path of files
     * Set to null if you do not want any prefix
     * To fully customize the generated default path, extend the Media model and override the generateBasePath method
     */
    'generated_path_prefix' => null,

    /**
     * Customize the queue connection used when dispatching conversion jobs
     */
    'queue_connection' => env('QUEUE_CONNECTION', 'sync'),

    /**
     * Customize the queue used when dispatching conversion jobs
     * null will fall back to the default Laravel queue
     */
    'queue' => null,

    /**
     * Customize WithoutOverlapping middleware settings
     */
    'queue_overlapping' => [
        /**
         * The release value should be longer than the longest conversion job that might run
         * Default is: 1 minute. Increase it if your jobs are longer.
         */
        'release_after' => 60,
        /**
         * The expire value allows you to forget a lock in case of an unexpected job failure
         *
         * @see https://laravel.net.cn/docs/10.x/queues#preventing-job-overlaps
         */
        'expire_after' => 60 * 60,
    ],

];

可选地,您可以使用以下命令发布视图

php artisan vendor:publish --tag="laravel-media-views"

概念介绍

有两个基本概念需要理解,它们都与模型及其媒体相关

  • 媒体集合:这定义了一组具有特定设置的媒体(该组只能包含一个媒体项)。例如,头像、缩略图和上传都是媒体集合。

  • 媒体转换:这定义了特定媒体项的文件转换。例如,720p版本的更大1440p视频,WebP或PNG格式的图像转换,都是媒体转换的例子。值得注意的是,媒体转换也可以有它自己的媒体转换。

用法

准备您的模型

本包旨在将媒体与模型相关联,但也可以在不关联任何模型的情况下使用。

注册媒体集合

首先,您需要将 HasMedia 特性和 InteractWithMedia 接口添加到您的模型中

namespace App\Models;

use Elegantly\Media\Traits\HasMedia;
use Illuminate\Database\Eloquent\Model;
use Elegantly\Media\Contracts\InteractWithMedia;

class Channel extends Model implements InteractWithMedia
{
    use HasMedia;

}

然后,您可以在 registerMediaCollections 方法中定义您的媒体集合

namespace App\Models;

use Elegantly\Media\Traits\HasMedia;
use Elegantly\Media\MediaCollection;
use Illuminate\Database\Eloquent\Model;
use Elegantly\Media\Contracts\InteractWithMedia;
use Illuminate\Contracts\Support\Arrayable;

class Channel extends Model implements InteractWithMedia
{
    use HasMedia;

    public function registerMediaCollections(): Arrayable|iterable|null;
    {
       return [
            new MediaCollection(
                name: 'avatar',
                acceptedMimeTypes: [
                    'image/jpeg', 'image/png', 'image/gif', 'image/webp',
                ],
            )
            new MediaCollection(
                name: 'videos',
                acceptedMimeTypes: [
                    'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime',
                ],
            )
       ];
    }
}

注册媒体转换

此包为您的转换提供了常见任务,以简化您的工作

  • VideoPosterConversionJob:此任务使用 pbmedia/laravel-ffmpeg 提取海报。
  • OptimizedVideoConversionJob:此任务使用 pbmedia/laravel-ffmpeg 优化、调整大小或转换任何视频。
  • OptimizedImageConversionJob:此任务使用 spatie/image 优化、调整大小或转换任何图像。
  • ResponsiveImagesConversionsPreset:此预设创建了一组不同尺寸的优化图像。
  • ResponsiveVideosConversionsPreset:此预设创建了一组不同尺寸的优化视频。
namespace App\Models;

use Elegantly\Media\Traits\HasMedia;
use Elegantly\Media\MediaCollection;
use Elegantly\Media\MediaConversion;
use Elegantly\Media\Enums\MediaType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Spatie\Image\Enums\Fit;
use \App\Jobs\Media\OptimizedImageConversionJob;
use Elegantly\Media\Models\Media;
use Elegantly\Media\Contracts\InteractWithMedia;
use Illuminate\Contracts\Support\Arrayable;
use Elegantly\Media\Support\ResponsiveImagesConversionsPreset;

class Channel extends Model implements InteractWithMedia
{
    use HasMedia;

    // ...

    public function registerMediaConversions($media): Arrayable|iterable|null;
    {

        if($media->collection_name === 'avatar'){
            return [
                new MediaConversion(
                    conversionName: '360',
                    job: new OptimizedImageConversionJob(
                        media: $media,
                        width: 360,
                        fileName: "{$media->name}-360.jpg"
                    ),
                )
            ]
        }elseif($media->collection_name === 'videos'){
            return [
                new MediaConversion(
                    conversionName: 'poster',
                    sync: true, // The conversion will not be queued, you will have access to it immediatly
                    job: new VideoPosterConversionJob(
                        media: $media,
                        seconds: 1,
                        fileName: "{$media->name}-poster.jpg"
                    ),
                    conversions: function(GeneratedConversion $generatedConversion) use ($media){
                        return ResponsiveImagesConversionsPreset::make(
                            media: $media,
                            generatedConversion: $generatedConversion
                            widths: [360, 720]
                        )
                    }
                ),
                ...ResponsiveVideosConversionsPreset::make(
                    media: $media,
                    widths: [360, 720, 1080],
                )
            ]
        }

        return null;
    }
}

定义您自己的媒体转换

您可以通过在您的应用程序中创建一个新的类(例如,App\Support\MediaConversions)并扩展 MediaConversionJob 来创建自己的转换。

媒体转换是通过 Laravel 任务执行的。您可以在任务中执行任何任务,只要

  • 您的任务扩展了 Elegantly\Media\Jobs\MediaConversion
  • 您的任务定义了一个 run 方法。
  • 您的任务调用 $this->media->storeConversion(...)

让我们考虑一个常见的媒体转换任务:优化图像。这是您如何在您的应用程序中实现它的方法

注意:以下任务是此包提供的,但它是概念的一个极好介绍。

namespace App\Support\MediaConversions;

use Elegantly\Media\Models\Media;
use Illuminate\Support\Facades\File;
use Spatie\Image\Enums\Fit;
use Spatie\Image\Image;
use Spatie\ImageOptimizer\OptimizerChain;
use Elegantly\Media\Jobs\MediaConversionJob;

class OptimizedImageConversionJob extends MediaConversionJob
{
    public string $fileName;

    public function __construct(
        public Media $media,
        ?string $queue = null,
        public ?int $width = null,
        public ?int $height = null,
        public Fit $fit = Fit::Contain,
        public ?OptimizerChain $optimizerChain = null,
        ?string $fileName = null,
    ) {
        parent::__construct($media, $queue);

        $this->fileName = $fileName ?? $this->media->file_name;
    }

    public function run(): void
    {
        $temporaryDisk = $this->getTemporaryDisk();
        $path = $this->makeTemporaryFileCopy();

        $newPath = $temporaryDisk->path($this->fileName);

        Image::load($path)
            ->fit($this->fit, $this->width, $this->height)
            ->optimize($this->optimizerChain)
            ->save($newPath);

        $this->media->storeConversion(
            file: $newPath,
            conversion: $this->conversionName,
            name: File::name($this->fileName)
        );
    }
}

使用您自己的媒体模型

您可以使用自己的媒体模型与库一起使用。

首先,创建您自己的模型类

namespace App\Models;

use Elegantly\Media\Models\Media as ElegantlyMedia;

class Media extends ElegantlyMedia
{
    // ...
}

然后,更新 config 文件

use App\Models\Media;

return [

    'model' => Media::class,

    // ...

];

库使用泛型进行类型化,因此您可以无缝使用自己的媒体模型

namespace App\Models;

use App\Models\Media;
use Elegantly\Media\Traits\HasMedia;
use Elegantly\Media\Contracts\InteractWithMedia;

/**
 * @implements InteractWithMedia<Media>
 */
class Post extends Model implements InteractWithMedia
{
    /** @use HasMedia<Media> **/
    use HasMedia;

    // ...
}

测试

composer test

变更日志

请参阅 变更日志 了解最近更改的更多信息。

贡献

请随意打开一个问题或讨论。

安全漏洞

请联系 报告安全漏洞。

鸣谢

许可

MIT 许可证(MIT)。请参阅 许可文件 了解更多信息。