palgoal / media-library
Standalone Laravel media library package (upload, browse, single/multiple picker). Model + Controller + Migration + Routes + Views + JS, ready to drop into any Laravel 10/11/12 project.
Requires
- php: ^8.1
- illuminate/auth: ^10.0|^11.0|^12.0
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/filesystem: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- illuminate/validation: ^10.0|^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0
README
مكتبة وسائط (Media Library) مستقلة لـ Laravel: رفع، تصفح، بحث/فلترة، وتعديل ميتاداتا للملفات، بالإضافة إلى منتقي وسائط (Media Picker) قابل لإعادة الاستخدام في أي فورم — Single أو Multiple.
هذا المستودع مستقل بالكامل ولا يعتمد على أي مشروع Laravel بعينه. لا يفترض وجود App\Models\User، ولا أي View أو Route أو ملف CSS/JS خارج هذه الحزمة — كل ما هو خاص بالتطبيق المضيف يمرّ عبر config/media-library.php.
المحتوى
- Model:
Palgoal\MediaLibrary\Models\Media(جدولmedia) - Controller:
Palgoal\MediaLibrary\Http\Controllers\MediaController(index/store/show/edit/update/destroy/bulkDestroy) - Resource:
Palgoal\MediaLibrary\Http\Resources\MediaResource - Policy:
Palgoal\MediaLibrary\Policies\MediaPolicy(قابلة للاستبدال بالكامل) - Support:
Palgoal\MediaLibrary\Support\MediaPathNormalizer(أداة مساعدة لتحويل مسارات نصية قديمة إلىmedia_idعند الترحيل من نظام آخر) - Migration: إنشاء جدول
media - Concerns:
Palgoal\MediaLibrary\Concerns\HasMedia— Trait اختياري لربط الوسائط بأي Model (منتج، مستخدم، شركة...) عبر جدول pivot عامmediables، بدون أي تغيير على سلوك الحزمة الحالي. التفاصيل الكاملة والأمثلة فيdocs/HAS-MEDIA.md - Routes: صفحة المكتبة الكاملة + JSON API، تحت بادئة قابلة للتهيئة (افتراضياً
/media-library) - Views: صفحة المكتبة (بـ Layout قابل للاستبدال ديناميكياً عبر config، افتراضياً Layout بسيطة مستقلة Tailwind عبر CDN)، مكوّن الاختيار
<x-media-library::picker>، مودال الاختيار العام - JS:
media-library.js(صفحة المكتبة) وmedia-picker.js(المودال القابل لإعادة الاستخدام) — كلاهما يعتمد فقط علىwindow.MEDIA_CONFIG، بلا أي مسار مربوط بمشروع بعينه
المتطلبات
| Laravel | PHP |
|---|---|
| 10.x | 8.1+ |
| 11.x | 8.2+ |
| 12.x | 8.2+ |
قاعدة البيانات: MySQL / MariaDB / PostgreSQL / SQLite (انظر ملاحظات التوافق أدناه).
Quick Start
لتجربة الحزمة خلال دقيقة واحدة:
composer require palgoal/media-library php artisan vendor:publish --tag=media-library-config php artisan vendor:publish --tag=media-library-assets php artisan migrate php artisan storage:link
ثم افتح:
/media-library
التثبيت
من Packagist (بعد النشر)
composer require palgoal/media-library
ملاحظة: هذه الحزمة لم تُنشر على Packagist بعد وقت كتابة هذا الملف. إلى أن يتم النشر، استخدم أحد الخيارين التاليين.
كمستودع VCS مباشرة من GitHub
أضف الحزمة إلى composer.json الخاص بمشروعك:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/palgooal/Palgoal-Laravel-Media-Library"
}
],
"require": {
"palgoal/media-library": "dev-main"
}
}
ثم:
composer update palgoal/media-library
كـ path repository (تطوير محلي / مونوريبو)
{
"repositories": [
{
"type": "path",
"url": "../path/to/Palgoal-Laravel-Media-Library",
"options": {
"symlink": true
}
}
],
"require": {
"palgoal/media-library": "dev-main"
}
}
الإعداد بعد التثبيت
الحزمة تُسجَّل تلقائياً عبر Laravel Package Discovery (extra.laravel.providers في composer.json). لا حاجة لإضافة الـ Service Provider يدوياً.
١. انشر الإعدادات والأصول والـ Views حسب الحاجة
php artisan vendor:publish --tag=media-library-config # config/media-library.php php artisan vendor:publish --tag=media-library-assets # public/vendor/media-library/js/* php artisan vendor:publish --tag=media-library-views # اختياري، لتخصيص الشكل php artisan vendor:publish --tag=media-library-migrations # اختياري، إذا أردت تعديل الـ migration نفسها php artisan vendor:publish --tag=media-library-relations-migration # اختياري، فقط إذا ستستخدم HasMedia — انظر docs/HAS-MEDIA.md
إذا لم تنشر media-library-migrations، ستُحمَّل الـ migration تلقائياً من داخل الحزمة عند تشغيل migrate — لا حاجة لنسخها يدوياً في الحالة العادية.
ملاحظة: بعكس migration جدول media (تلقائية دائماً)، وسم media-library-relations-migration هو الوحيد الذي لا يُحمَّل تلقائياً إطلاقاً — يجب نشره صراحةً قبل php artisan migrate إن أردت استخدام ربط الوسائط بالموديلات (Concerns\HasMedia). راجع docs/HAS-MEDIA.md.
٢. شغّل الـ migration
php artisan migrate
٣. اربط التخزين إذا كنت تستخدم disk "public" (الافتراضي)
php artisan storage:link
٤. اضبط config/media-library.php حسب مشروعك
أهم المفاتيح:
route_prefix— بادئة الروابط (افتراضياًmedia-library؛ يمكن أن تكون مساراً متعدد الأجزاء مثلdashboard/media-library— انظر دمج المكتبة داخل لوحة تحكم).middleware— أضف الـ guard/الصلاحيات الخاصة بلوحتك (مثال:['web', 'auth', 'can:access-admin']).disk/directory— أين تُخزَّن الملفات فعلياً.max_upload_size_kb,allowed_mimes,allowed_mimetypes— قيود الرفع (SVG غير مسموح افتراضياً، انظر مخاطر SVG).allowed_types— القيم المسموحة لفلترtypeفي الـ API (image|video|audio|document|other).user_model— كلاس المستخدم المستخدم في علاقةuploader()(انظر تخصيص User model).policy/register_policy— انظر تخصيص Policy.layout/breadcrumb— انظر دمج المكتبة داخل لوحة تحكم.
استخدام صفحة المكتبة
الرابط الافتراضي: /media-library (اسم الـ route: media-library.page).
الـ API: /media-library/media/* (كل أسماء الـ routes تبدأ بـ media-library.media.).
الصفحة الكاملة (Blade + JS) جاهزة للاستخدام مباشرة بتصميم Tailwind مستقل، ويمكنك تخصيصها بنشر الـ views وتعديلها لتستخدم Layout مشروعك الخاص، أو (أسهل، بلا نشر أي ملف) عبر layout/breadcrumb في config — انظر القسم التالي.
دمج المكتبة داخل لوحة تحكم (Dashboard)
المكتبة تدعم التركيب داخل أي لوحة تحكم موجودة بالإعدادات فقط — بدون إضافة أي route، وبدون أي اعتماد على بنية مشروعك (لا App\...، ولا لوحة جاهزة كـ Filament/Nova/Voyager، ولا افتراض وجود routes/dashboard.php).
١. بادئة الرابط (Route Prefix)
// config/media-library.php 'route_prefix' => 'dashboard/media-library',
هذا وحده كافٍ لتسجيل كل الروابط تحت /dashboard/media-library بدل /media-library — الحزمة تستخدم Route::prefix() القياسية في Laravel والتي تدعم مسارات متعددة الأجزاء أصلاً؛ لا تحتاج أي route file إضافي في مشروعك. أسماء الـ routes (media-library.page, media-library.media.index, ...) لا تتغيّر أبداً بتغيير هذا الإعداد، فأي route('media-library.media.index') في كودك يستمر بالعمل دون تعديل.
٢. الـ Layout
الصفحة الكاملة تُغلَّف بمكوّن ديناميكي (<x-dynamic-component>)، فيمكنك استبداله بأي Layout Component عندك يقبل $slot افتراضياً:
// config/media-library.php 'layout' => 'dashboard-layout',
-
القيمة الافتراضية
nullتعني استخدام Layout المستقل المدمج في الحزمة (Tailwind عبر CDN)، بلا أي إعداد إضافي. -
أي قيمة أخرى تُمرَّر مباشرة إلى
<x-dynamic-component :component="...">، أي أنها تخضع لنفس قواعد Laravel المعتادة لتسمية الـ Components — اسم بدونnamespace::يُبحث عنه كمكوّن anonymous تحتresources/views/components/(تماماً كأي<x-...>عادي في Laravel، وليس قاعدة خاصة بهذه الحزمة):'layout' => 'dashboard-layout', // resources/views/components/dashboard-layout.blade.php 'layout' => 'layouts.admin', // resources/views/components/layouts/admin.blade.php
-
الشرط الوحيد: أن يستقبل الـ Component سلوت افتراضي (
{{ $slot }}) — نفس عقد أي<x-app-layout>معتاد في Laravel. الحزمة لا تكتب أو تعتمد على أي ملف من هذا الـ Layout؛ فقط تشير إليه بالاسم عبر config.
٣. الـ Breadcrumb
// config/media-library.php 'breadcrumb' => [ ['label' => 'لوحة التحكم', 'url' => '/dashboard'], ['label' => 'مكتبة الوسائط', 'url' => null], ],
القيمة الافتراضية null تعني عدم عرض أي breadcrumb (كما كانت الحزمة سابقاً). عند ضبطها، تُعرض شريحة breadcrumb أعلى رأس الصفحة — آخر عنصر (أو أي عنصر بلا url) يُعرض كنص عادي بدل رابط.
مثال متكامل
// config/media-library.php return [ 'route_prefix' => 'dashboard/media-library', 'middleware' => ['web', 'auth', 'can:access-admin'], 'layout' => 'dashboard-layout', 'breadcrumb' => [ ['label' => 'لوحة التحكم', 'url' => '/dashboard'], ['label' => 'مكتبة الوسائط', 'url' => null], ], // ... باقي المفاتيح كما هي ];
بهذا الإعداد وحده، صفحة المكتبة تظهر على /dashboard/media-library، ملفوفة بـ Layout لوحتك الخاصة، مع breadcrumb يعكس مكانها داخل التنقل — دون تعديل أي ملف داخل الحزمة نفسها.
مثال: مشروع يستخدم Guard مختلفاً (Multi Guard)
مشاريع كثيرة لا تستخدم guard web الافتراضي للوحة التحكم، بل guard مخصصاً مثل admin. اضبط middleware وuser_model ليطابقا نفس الـ guard الذي تسجّل الدخول عبره لوحتك:
// config/media-library.php 'route_prefix' => 'dashboard/media-library', 'middleware' => [ 'web', 'auth:admin', ], 'user_model' => \App\Models\Admin::class, 'layout' => 'dashboard-layout',
استخدم نفس Guard الذي تستخدمه لوحة التحكم لديك. استخدام
authفقط يعني استخدام الـ Guard الافتراضي (غالباًweb).
استخدام منتقي الوسائط (Media Picker)
أضف المودال العام مرة واحدة فقط في نهاية الـ layout الرئيسي لمشروعك:
@include('media-library::partials.picker-modal')
Single Picker
<x-media-library::picker name="logo_media_id" label="الشعار" />
يُنشئ حقل <input type="hidden" name="logo_media_id"> يحمل id العنصر المختار من جدول media، بالإضافة إلى زر يفتح المودال ومعاينة مصغّرة.
Multiple Picker
<x-media-library::picker name="gallery_media_ids" :multiple="true" label="معرض الصور" />
القيمة المخزَّنة في الحقل المخفي تكون IDs مفصولة بفواصل (1,5,9).
تخصيص القيمة المخزَّنة
storeValue يتحكم فيما يُكتب داخل الحقل المخفي: id (افتراضي)، path، أو url.
<x-media-library::picker name="cover_url" :multiple="false" store-value="url" />
يوصى باستخدام
storeValue="id"وربط الوسائط باستخدامmedia.idأو TraitHasMedia. لا يُنصح بحفظ URL أوfile_pathكمراجع دائمة داخل قاعدة البيانات لأنهما قد يتغيران عند تغيير الـ Storage أو الـ Disk.
الأحداث القابلة للربط في JS
بعد التأكيد يُطلق المودال حدثين على window:
media-picker-confirmed— الحمولة الكاملة{ items, ids, values, storeValue, targetInputId }.media-selected(للتوافق مع إصدارات سابقة) — أول عنصر مختار فقط.
window.addEventListener('media-picker-confirmed', (e) => { console.log(e.detail.items); });
ربط الوسائط بأي Model (اختياري)
Palgoal\MediaLibrary\Concerns\HasMedia يتيح لأي Model في مشروعك (منتج، مستخدم، شركة...) أن يمتلك وسائط منظّمة في Collections مسمّاة (logo, cover, gallery, documents, ...) عبر جدول pivot عام واحد (mediables)، دون أي جدول أو migration خاص بكل Model، ودون أي تغيير في سلوك الحزمة أو الـ Picker الحاليين.
يجب أولاً نشر وتشغيل Migration الخاصة بـ
mediables، وإلا ستفشل العلاقة بخطأ SQL:php artisan vendor:publish --tag=media-library-relations-migration php artisan migrate
use Palgoal\MediaLibrary\Concerns\HasMedia; class Product extends Model { use HasMedia; } $product->attachMedia($mediaId, 'gallery'); $product->firstMediaUrl('cover', asset('images/placeholder.png'));
يتطلب migration اختيارية منفصلة (php artisan vendor:publish --tag=media-library-relations-migration && php artisan migrate). الدليل الكامل بكل الأمثلة (صورة مفردة، معرض صور، شعار شركة، صورة مستخدم، مستندات، Soft Delete، Morph Map، الترتيب، وأكثر) في docs/HAS-MEDIA.md.
دورة حياة العلاقات
- حذف عنصر Media يحذف تلقائياً كل روابطه في
mediables(لا يترك رابطاً يتيماً). - حذف الرابط (
detachMedia/syncMedia) لا يحذف ملف Media نفسه أو سجله من جدولmedia. clearMediaCollection()يحذف روابط الـ Collection فقط، وليس أي ملف.- Soft Delete على الموديل المضيف يحتفظ بروابطه (قابلة للعودة عند
restore()). - Force Delete ينظّف روابط الموديل المضيف فعلياً.
مثال: ربط الـ Picker مباشرة بـ HasMedia
<x-media-library::picker name="gallery_media_ids" label="معرض الصور" :multiple="true" :value="$product->mediaCollection('gallery')->pluck('id')->all()" :preview-urls="$product->mediaCollection('gallery')->pluck('url')->filter()->all()" />
use Palgoal\MediaLibrary\Support\MediaSelection; $mediaIds = MediaSelection::parse( $request->input('gallery_media_ids') ); validator( ['gallery_media_ids' => $mediaIds], [ 'gallery_media_ids' => ['array'], 'gallery_media_ids.*' => [ 'integer', Rule::exists('media', 'id'), ], ] )->validate(); $product->syncMedia($mediaIds, 'gallery');
مثال مختصر فقط — التفاصيل الكاملة والأمثلة الإضافية في docs/HAS-MEDIA.md.
تخصيص Policy
الـ Policy الافتراضية (Palgoal\MediaLibrary\Policies\MediaPolicy) تسمح لأي مستخدم مسجّل دخول بعرض/تعديل/حذف أي عنصر وسائط، بغض النظر عمّن رفعه. هذا قرار افتراضي متعمَّد يناسب لوحة تحكم بمستخدم واحد أو فريق موثوق — وليس افتراضاً آمناً لتطبيق متعدد المستخدمين غير الموثوقين. راجع SECURITY.md لتفاصيل أوسع.
لتخصيصها:
// config/media-library.php بعد النشر 'policy' => \App\Policies\MyMediaPolicy::class,
أو عطّل التسجيل التلقائي للـ Policy وسجّلها بنفسك:
// config/media-library.php 'register_policy' => false,
// أحد Service Providers الخاصة بتطبيقك Gate::policy(\Palgoal\MediaLibrary\Models\Media::class, \App\Policies\MyMediaPolicy::class);
مثال على Policy تقيّد الحذف/التعديل بصاحب الملف فقط:
class MyMediaPolicy { public function viewAny($user): bool { return (bool) $user; } public function view($user, Media $media): bool { return (bool) $user; } public function create($user): bool { return (bool) $user; } public function update($user, Media $media): bool { return $media->uploader_id === $user->id; } public function delete($user, Media $media): bool { return $media->uploader_id === $user->id; } }
تخصيص User model
علاقة Media::uploader() تستخدم — بالترتيب — config('media-library.user_model') ثم config('auth.providers.users.model'). إن لم يُحدَّد أيّهما، أو كان الكلاس المحدَّد غير موجود فعلياً، تُرمى RuntimeException واضحة بدل افتراض App\Models\User (الحزمة لا تفترض بنية تطبيق بعينه).
// config/media-library.php 'user_model' => \App\Models\Admin::class,
تخصيص disk وdirectory
// config/media-library.php 'disk' => 's3', // أي disk معرَّف في config/filesystems.php 'directory' => 'uploads', // الملفات تُخزَّن تحت {disk}/{directory}/{Year}/{Month}/{file}
إذا كان الـ disk المستخدم لا يدعم توليد روابط (url())، فإن Media::url (accessor) تعيد null بدل رمي استثناء — تحقق من القيمة قبل استخدامها إن كنت تستخدم disk غير قياسي.
مخاطر SVG
SVG غير مسموح افتراضياً في allowed_mimes / allowed_mimetypes. ملف SVG يمكن أن يحتوي <script> أو خصائص أحداث (onload, onerror) تُنفَّذ عند فتح الملف مباشرة أو تضمينه — وهي ثغرة Stored XSS معروفة وشائعة في أنظمة رفع الملفات. هذه الحزمة لا تحتوي أداة Sanitization لملفات SVG.
لا تُفعّل SVG إلا بعد إضافة تعقيم فعلي (مثل enshrined/svg-sanitize) يُطبَّق على كل ملف قبل حفظه. التفاصيل الكاملة في SECURITY.md.
توافق جدول media الموجود مسبقاً
الـ migration تتحقق عبر Schema::hasTable('media') وتتجاهل الإنشاء إن كان الجدول موجوداً مسبقاً — لكنها لا تتحقق من تطابق الأعمدة. إن كان لديك جدول media سابق (من حزمة أخرى أو كودك الخاص)، لن تفشل الـ migration، لكنك قد تواجه أخطاء SQL لاحقاً (عمود غير موجود) عند استخدام الـ Model/Controller إن كان المخطط غير متوافق. تحقّق من أعمدة الجدول الموجود فعلياً قبل التثبيت فوقه؛ لا تفترض عدم وجود أي تعارض لمجرد نجاح الـ migration بصمت. راجع تعليقات ملف الـ migration نفسه لمزيد من التفاصيل.
Upgrade Guide
هذا أول تدقيق (audit) رسمي للحزمة بعد استقلالها في مستودعها الخاص؛ لا يوجد إصدار موسوم سابق. عند الترقية من نسخة محلية أقدم غير موسومة (منسوخة يدوياً من PalgooalWeb مثلاً)، انتبه للتغييرات التالية:
- SVG لم يعد مسموحاً افتراضياً. إن كنت تعتمد عليه، أضفه صراحة إلى
allowed_mimes/allowed_mimetypesبعد التأكد من وجود تعقيم فعلي. - فلتر
typeأصبح مُتحقَّقاً منه. قيمة خارجimage|video|audio|document|otherتُعيد الآن422بدل نتيجة فارغة بصمت. Media::uploader()يرمي استثناءً بدل الرجوع الصامت إلىApp\Models\Userإن لم تضبطuser_model/auth.providers.users.model.- إضافة (غير كاسرة):
layoutوbreadcrumbمفتاحان جديدان في config، وكلاهماnullافتراضياً — أي تطبيق موجود يستمر بالعمل بلا أي تغيير في السلوك أو الشكل. إن كان لديك ملفconfig/media-library.phpمنشور من نسخة أقدم لا يحتوي هذين المفتاحين، ستُدمَج القيم الافتراضية تلقائياً من الحزمة (mergeConfigFrom) بلا حاجة لإعادة النشر. - راجع
CHANGELOG.mdللقائمة الكاملة.
Troubleshooting
الصور لا تظهر بعد الرفع (404 على الرابط)
تأكد من تشغيل php artisan storage:link إن كنت تستخدم disk public، ومن أن APP_URL مضبوط بشكل صحيح.
419 Page Expired عند الرفع من صفحة المكتبة
تأكد من أن window.MEDIA_CONFIG.csrfToken يصل فعلياً (راجع <meta name="csrf-token"> في الـ layout المنشور)، ومن أن الجلسة (session) تعمل بشكل صحيح خلف أي بروكسي/CDN.
403 عند محاولة الحذف رغم تسجيل الدخول
هذا متوقَّع إن استبدلت الـ Policy الافتراضية بأخرى تقيّد الحذف على صاحب الملف أو دور معيّن — راجع تخصيص Policy.
خطأ SQL يخص عمود غير موجود في جدول media
غالباً يعني وجود جدول media سابق بمخطط مختلف — راجع توافق جدول media الموجود مسبقاً.
رفع الملفات يفشل بصمت أو يظهر خطأ عام
شغّل الطلب مع Accept: application/json وراجع نص الخطأ في الاستجابة (رسائل التحقق mimes/mimetypes/max تُعاد كما هي من Laravel Validation).
Security considerations
راجع SECURITY.md للتفاصيل الكاملة، وأبرزها: SVG معطّل افتراضياً، الـ Policy الافتراضية غير مخصَّصة للمستخدمين غير الموثوقين، ولا يوجد Foreign Key على uploader_id لأسباب تتعلق بقابلية النقل بين قواعد البيانات.
تعتمد الحزمة على آليات Laravel للتحقق من نوع الملف (MIME detection)، لكنها لا تقوم بتعقيم محتوى الملفات، لذلك يبقى SVG معطلاً افتراضياً حتى إضافة Sanitizer مناسب.
الاختبارات
الحزمة تستخدم Orchestra Testbench:
composer install composer test # vendor/bin/phpunit composer format-test # vendor/bin/pint --test
نقل الباكدج أو المساهمة
راجع CONTRIBUTING.md. هذا مستودع مستقل تماماً — لا تفترض وجود أي ملف من مشروع PalgooalWeb أو أي مشروع آخر عند العمل عليه أو نشره.