diff --git a/README.md b/README.md index 7ddd82e..937f119 100644 --- a/README.md +++ b/README.md @@ -240,27 +240,63 @@ public function messages() ### Image Field : `UploadableImage` Trait -If you use Image CRUD Field, you can implement this Trait on your Model to automatically upload / delete image(s) on server. +If you use the [Image CRUD Field](https://laravel-backpack.readme.io/docs/crud-fields#section-image), you can implement this trait on your model to automatically manage saving and deleting the image on the server. Example: ```php -// Article Model +namespace App\Models; -class Article extends \Backpack\NewsCRUD\app\Models\Article +use Backpack\CRUD\CrudTrait; +use Illuminate\Database\Eloquent\Model; +use Novius\Backpack\CRUD\ModelTraits\UploadableImage; + +class Example extends Model { - use Sluggable, SluggableScopeHelpers; - use HasTranslations; + use CrudTrait; use UploadableImage; - protected $fillable = ['slug', 'title', 'content', 'image', 'status', 'category_id', 'featured', 'date', 'thumbnail']; - protected $translatable = ['slug', 'title', 'content']; + protected $fillable = ['title', 'image', 'thumbnail']; + + public function uploadableImages() + { + return [ + [ + 'name' => 'image', // The attribute name where to store the image path + 'slug' => 'title', // The attribute name from which to generate the image file name (optionnal) + ], + [ + 'name' => 'thumbnail', + ], + ]; + } +} +``` + +If you want to perform some custom actions on your image after saving or deleting it : + +```php +namespace App\Models; + +use Backpack\CRUD\CrudTrait; +use Illuminate\Database\Eloquent\Model; +use Novius\Backpack\CRUD\ModelTraits\UploadableImage; + +class Example extends Model +{ + use CrudTrait; + use UploadableImage { + imagePathSaved as imagePathSavedNative; + imagePathDeleted as imagePathDeletedNative; + } + + protected $fillable = ['title', 'image', 'thumbnail']; public function uploadableImages() { return [ [ - 'name' => 'image', // Attribute name where to stock image path - 'slug' => 'title', // Attribute name to generate image file name (optionnal) + 'name' => 'image', // The attribute name where to store the image path + 'slug' => 'title', // The attribute name from which to generate the image file name (optionnal) ], [ 'name' => 'thumbnail', @@ -269,70 +305,48 @@ class Article extends \Backpack\NewsCRUD\app\Models\Article } /** - * You might like to perform some custom actions on your image after saving it. + * Callback triggered after image saved on disk */ public function imagePathSaved(string $imagePath, string $imageAttributeName = null, string $diskName = null) { - //perfoms some custom actions here - $this->addMedia($imagePath) - ->preservingOriginal() - ->toMediaCollection(); + if (!$this->imagePathSavedNative()) { + return false; + } + + // Do what you want here return true; } /** - * You might like to perform some custom actions after deleting the image. + * Callback triggered after image deleted on disk */ public function imagePathDeleted(string $imagePath, string $imageAttributeName = null, string $diskName = null) { - $this->clearMediaCollection(); + if (!$this->imagePathDeletedNative()) { + return false; + } + + // Do what you want here return true; } } +``` +#### MediaLibrary -``` +If you want to store the images in the [MediaLibrary](https://github.com/spatie/laravel-medialibrary) provided by Spatie, use the trait `SpatieMediaLibrary\UploadableImage` instead of `UploadableImage`. -If you like to use imagePathSaved and the medialibrary of Spatie, you will need : +For example with the MediaLibrary you can easily manage conversions (crop, resize, ...). -1. Override this method and adds whatever actions you prefer. -2. The configuration file _medialibrary.php_ should define an existing file system and image driver: -``` -'defaultFilesystem' => 'public', -'image_driver' => 'imagick', -``` -3. Your _composer.json_ should include: -``` -"spatie/laravel-medialibrary": "your.version.here" -``` -___ - +#### Translations -```php -// ArticleCrudController +Both traits `UploadableImage` and `SpatieMediaLibrary\UploadableImage` are compatible with the [translation package](https://github.com/spatie/laravel-translatable) provided by Spatie. -$this->crud->addField([ - 'label' => 'Image', - 'name' => 'image', - 'type' => 'image', - 'upload' => true, - 'crop' => true, // set to true to allow cropping, false to disable - 'aspect_ratio' => 0, // ommit or set to 0 to allow any aspect ratio - 'prefix' => '/storage/', -]); +In the case of translatable images with `SpatieMediaLibrary\UploadableImage`, the name of the collection where the image is stored is composed of the name of the attribute and the locale, separated by a dash (eg. `image-en`, `image-fr`, ...). -$this->crud->addField([ - 'label' => 'Image', - 'name' => 'thumbnail', - 'type' => 'image', - 'upload' => true, - 'crop' => true, // set to true to allow cropping, false to disable - 'aspect_ratio' => 0, // ommit or set to 0 to allow any aspect ratio - 'prefix' => '/storage/', -]); -``` +___ ### CRUD : custom routes diff --git a/src/ModelTraits/SpatieMediaLibrary/UploadableImage.php b/src/ModelTraits/SpatieMediaLibrary/UploadableImage.php new file mode 100644 index 0000000..c2b1f05 --- /dev/null +++ b/src/ModelTraits/SpatieMediaLibrary/UploadableImage.php @@ -0,0 +1,75 @@ +setUploadedImage($value) in your model attribute mutator + * + * @package Novius\Backpack\CRUD\ModelTraits\SpatieMediaLibrary + */ +trait UploadableImage +{ + use UploadableImageOriginal { + imagePathSaved as imagePathSavedOriginal; + imagePathDeleted as imagePathDeletedOriginal; + } + + /** + * Callback triggered after image saved on disk + * + * @param string $imageAttributeName + * @param string|null $imagePath + * @param string|null $diskName + * @return bool + */ + public function imagePathSaved(string $imagePath, string $imageAttributeName = null, string $diskName = null) : bool + { + // Adds the image to the medialibrary + $this->addMedia($imagePath) + ->preservingOriginal() + ->toMediaCollection($this->getImageCollectionName($imageAttributeName)); + + return $this->imagePathSavedOriginal($imagePath, $imageAttributeName, $diskName); + } + + /** + * Callback triggered after image deleted on disk + * + * @param string $imagePath + * @param string|null $imageAttributeName + * @param string|null $diskName + * @return bool + */ + public function imagePathDeleted(string $imagePath, string $imageAttributeName = null, string $diskName = null) : bool + { + // Removes the image from the medialibrary + $this->clearMediaCollection($this->getImageCollectionName($imageAttributeName)); + + return $this->imagePathDeletedOriginal($imagePath, $imageAttributeName, $diskName); + } + + /** + * Gets the localized image attribute name + * + * @param string $imageAttributeName + * @param string|null $locale + * @return string + */ + public function getImageCollectionName(string $imageAttributeName, string $locale = null) + { + $collectionName = $imageAttributeName; + + // Appends the locale if translatable + if ($this->isTranslatableImageAttribute($imageAttributeName)) { + $collectionName .= '-'.($locale ?? $this->getLocale()); + } + + return $collectionName; + } +} diff --git a/src/ModelTraits/UploadableImage.php b/src/ModelTraits/UploadableImage.php index 3886936..08fb45f 100644 --- a/src/ModelTraits/UploadableImage.php +++ b/src/ModelTraits/UploadableImage.php @@ -35,31 +35,22 @@ public static function bootUploadableImage() */ public function fillUploadedImageAttributeValue(string $imageAttributeName, string $path) { - if (method_exists($this, 'isTranslatableAttribute') - && $this->isTranslatableAttribute($imageAttributeName) - ) { - $this->setTranslation($imageAttributeName, (string) request('locale', $this->getLocale()), $path); // Default value is relevant when using seeders or any environment where we dont have acces to "request". + // Generates a unique URI path (for cache bursting) + $uniquePath = $this->generateImageUniqueUriPath($path); - if (!empty($path)) { - $path = preg_replace('/\?v=.*/', '', $path); - $this->setTranslation($imageAttributeName, (string) request('locale', $this->getLocale()), $path.'?v='.uniqid()); - } + if ($this->isTranslatableImageAttribute($imageAttributeName)) { + $this->setTranslation($imageAttributeName, $this->getLocale(), $uniquePath); } else { - $this->{$imageAttributeName} = $path; - - if (!empty($path)) { - $path = preg_replace('/\?v=.*/', '', $path); - $this->{$imageAttributeName} = $path.'?v='.uniqid(); - } + $this->{$imageAttributeName} = $uniquePath; } } /** - * Called after image saved on disk + * Callback triggered after image saved on disk * * @param string $imageAttributeName - * @param string $imagePath - * @param string $diskName + * @param string|null $imagePath + * @param string|null $diskName * @return bool */ public function imagePathSaved(string $imagePath, string $imageAttributeName = null, string $diskName = null) : bool @@ -68,7 +59,7 @@ public function imagePathSaved(string $imagePath, string $imageAttributeName = n } /** - * Called after image deleted on disk + * Callback triggered after image deleted on disk * * @param string $imagePath * @param string|null $imageAttributeName @@ -80,6 +71,43 @@ public function imagePathDeleted(string $imagePath, string $imageAttributeName = return true; } + /** + * Generates a unique image URI path + * + * @param string $path + * @return string + */ + public function generateImageUniqueUriPath(string $path) + { + $path = preg_replace('/\?v=.*/i', '', $path); + if (!empty($path)) { + $path .= '?v='.uniqid(); + } + + return $path; + } + + /** + * Checks if the given image attribute name is translatable + * + * @param string $imageAttributeName + * @return bool + */ + public function isTranslatableImageAttribute(string $imageAttributeName) + { + return $this->canTranslateImage() && $this->isTranslatableAttribute($imageAttributeName); + } + + /** + * Checks if the model can translate an image + * + * @return bool + */ + public function canTranslateImage() + { + return method_exists($this, 'isTranslatableAttribute') && method_exists($this, 'setTranslation'); + } + /** * Get model attributes name for image upload * Simple example: return ['name' => 'image', slug' => 'title'];