优雅 / laravel-media
一个灵活的Laravel媒体库
Requires
- php: ^8.1
- illuminate/contracts: ^10.0|^11.0
- maennchen/zipstream-php: ^3.1
- pbmedia/laravel-ffmpeg: ^8.3
- spatie/image: ^3.0.0
- spatie/laravel-package-tools: ^1.14.0
- spatie/temporary-directory: ^2.2
Requires (Dev)
- larastan/larastan: ^2.0.1
- laravel/pint: ^1.0
- nunomaduro/collision: ^7.8|^8.1
- orchestra/testbench: ^8.8|^9.0
- pestphp/pest: ^2.0
- pestphp/pest-plugin-arch: ^2.0
- pestphp/pest-plugin-laravel: ^2.0
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^1.0
- phpstan/phpstan-phpunit: ^1.0
This package is auto-updated.
Last update: 2024-09-11 21:09:48 UTC
README
本包提供极其灵活的媒体库,允许您存储任何类型的文件及其转换(支持嵌套转换)。它旨在与任何文件系统解决方案(本地或云)无缝工作,例如Bunny.net、AWS S3/MediaConvert、Transloadit等。
本包的灵感来源于出色的 spatie/laravel-media-library
包(请务必查看Spatie的包,它们都是一流的)。然而,它不是分支,因为内部架构不同,为您提供更多功能。从 spatie/laravel-media-library
迁移是可行的,但如果您希望保留转换文件,可能会具有挑战性。
动机
Spatie团队开发了一个出色的包 spatie/laravel-media-library
,它非常适合大多数常见场景。然而,我发现自己在自己的项目中受到他们架构的限制。为了解决这个问题,我需要以下功能
- 文件转换
- 高级媒体转换
- 嵌套媒体转换
因此,我开发了这款具有最高灵活性的包。我已经在生产环境中使用它近一年,每月处理数TB的文件。
完整示例
以下示例将更好地向您展示包的功能。
我们将创建一个类似于YouTube的服务,有一个名为 Channel
的模型。这个 Channel
将有两种类型的媒体:avatar
和 videos
。我们将在 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)。请参阅 许可文件 了解更多信息。