Работая над новым проектом, для администрирования которого используется Laravel Nova, я его проверил в инструментах для веб-мастеров от Google. Оказалось что некоторые фотографии на ресурсе были не оптимизированными — размер их можно было существенно сократить. Те, что идут в проекте, можно обработать при сборке проекта используя node.js. Для этого существует множество готовых пакетов. Остаются ещё изображения, загружаемые пользователем непосредственно из панели администрирования сайтом. Можно конечно оптимизировать каждое изображение перед загрузкой на сайт, но почему бы не делать эту процедуру автоматической. Так и родилась идея сделать пакет для Laravel Nova: OptimalImage.
Пакет довольно простой, по-этому и статься не претендует на полноценное руководство по созданию пакетов для Laravel Nova. Здесь не рассматривается довольно обширная тема по работе с визуальной составляющей компонентов. Если появится интересная идея компонента, с визуальной составляющей — я напишу статью и на эту тему.
Постановка задачи
Требуется создать пакет, который добавит новое поле. Это поле будет отличаться от
обычного поля с загрузкой изображения тем, что для загруженного изображения будет производиться оптимизация.
Установка и настройка окружения.
Чтобы повторить все шаги данного руководства, вам понадобится проект c Laravel и Laravel Nova. Laravel Nova является платным инструментом, но по-моему мнению цена на него достаточно демократичная.
Я использовал в работе:
- PHP 7.3.16
- Laravel 7
- Laravel Nova 3.0
Создание скелета.
Ну что приступим. Для начала добавим новый пакет — как это описано в документации:
cd /path/to/nova/project
php artisan nova:field yarbala/optimal-image
На все вопросы отвечаем утвердительно. В директории nova-components
появился компонент OptimalImage.
Добавим nova-components
в .gitignore и инициализируем новый репозиторий командой git init
cd /path/to/nova/project/nova-components/OptimalImage
git init
git add .
git commit -m "First commit"
git remote add origin git@github.com:yarbala/nova-optimal-image-field.git
git push -u origin master
Код компонента
Для оптимизации решил попробовать библиотеку spatie/laravel-image-optimizer
, для этого в секцию require
файла composer.json
нашего компонента добавим
"spatie/laravel-image-optimizer": "^1.6"
и в корне нашего Laravel проекта вызываем
composer update
Так как мы не будем менять визуальную составляющую компонента удалим все каталоги связанные с отображением компонента.
Директории:
nova-components/OptimalImage/dist
nova-components/OptimalImage/resources
Файлы:
nova-components/OptimalImage/webpack.mix.js
nova-components/OptimalImage/package.json
nova-components/OptimalImage/mix-manifest.json
Модифицируем класс OptimalImage следующим образом:
namespace Yarbala\OptimalImage;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Laravel\Nova\Fields\Image;
/**
* Данный класс унаследован от стандартного класса Image. Его предназначение, это переопределить процесс записи
* изображения таким образом, чтобы обработать изображение после его загрузки.
*
* Class OptimalImage
* @package Yarbala\OptimalImage
*/
class OptimalImage extends Image
{
/**
* Функция переопределяет базовую из родительского класса для Image 'Fields/File.php'. Код взят из базовой функции
* и добавлен вызов optimizeImage после загрузки.
*
* @param \Illuminate\Http\Request $request
* @param string $requestAttribute
* @return string
*/
protected function storeFile($request, $requestAttribute)
{
if (! $this->storeAsCallback) {
$fileName = $request->file($requestAttribute)->store($this->getStorageDir(), $this->getStorageDisk());
if ($fileName) {
try {
$this->optimizeImage($fileName);
} catch (FileNotFoundException $e) {
}
}
return $fileName;
}
$fileName = $request->file($requestAttribute)->storeAs(
$this->getStorageDir(), call_user_func($this->storeAsCallback, $request), $this->getStorageDisk()
);
if ($fileName) {
try {
$this->optimizeImage($fileName);
} catch (FileNotFoundException $e) {
//
}
}
return $fileName;
}
/**
* Функция производящая оптимизацию изображения
*
* @param $fileName
* @throws FileNotFoundException
*/
protected function optimizeImage($fileName)
{
// ...
}
}
Унаследовали класс от Laravel\Nova\Fields\Image
. Удалили
public $component = 'optimal-image';
И переопределил функцию storeFile
из Laravel\Nova\Fields\File
, который является базовым для Laravel\Nova\Fields\Image
. Именно эта функция производит сохранение картинки. Оставим алгоритм практически таким же как он был, единственное добавим вызов optimizeImage
после загрузки.
Реализаци optimizeImage:
namespace Yarbala\OptimalImage;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Laravel\Nova\Fields\Image;
use Spatie\ImageOptimizer\OptimizerChain as ImageOptimizer;
use Storage;
/**
* Данный класс унаследован от стандартного класса Image. Его предназначение, это переопредилить процесс записи
* изображения таким образом, чтобы обработать изображение после его загрузки.
*
* Class OptimalImage
* @package Yarbala\OptimalImage
*/
class OptimalImage extends Image
{
// ...
/**
* Функция производящая оптимизацию изображения
*
* @param $fileName
* @throws FileNotFoundException
*/
protected function optimizeImage($fileName)
{
$needsUploadBack = false;
$localDisk = 'local';
$disk = $this->getStorageDisk();
/* Так как базовый компонент работает с классом Storage, а Spatie\ImageOptimizer работает с локальными файлами
мы выгрузим файл в локальное хранилище в случае надобности
*/
if (!Storage::disk($localDisk)->exists($fileName)) {
Storage::disk($localDisk)->put($fileName, Storage::disk($disk)->get($fileName));
$needsUploadBack = true;
}
// Получаем путь к изображению в локальном хранилище
$path = Storage::disk($localDisk)->path($fileName);
// Оптимизация изображения
app(ImageOptimizer::class)->optimize($path);
// Если мы вгружали файл, загрузим его обратно
if ($needsUploadBack) {
Storage::disk($disk)->put($fileName, Storage::disk($localDisk)->get($fileName));
Storage::disk($localDisk)->delete($fileName);
}
}
}
Так как базовый компонент работает с классом Storage, а Spatie\ImageOptimizer работает с локальными файлами мы выгрузим файл в локальное хранилище в случае надобности. По-умолчанию название локального хранилища local
.
Для изменения названия локального хранилища добавим методы:
class OptimalImage extends Image
{
// ....
public function localDisk(string $disk)
{
return $this->withMeta(['localDisk' => $disk]);
}
protected function getLocalDisk()
{
return $this->meta['localDisk'] ?? 'local';
}
}
Далее для полноты картины привожу исходный код класса OptimalImage полностью:
<?php
namespace Yarbala\OptimalImage;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Laravel\Nova\Fields\Image;
use Spatie\ImageOptimizer\OptimizerChain as ImageOptimizer;
use Illuminate\Support\Facades\Storage;
/**
* Данный класс унаследован от стандартного класса Image. Его предназначение, это переопределить процесс записи
* изображения таким образом, чтобы обработать изображение после его загрузки.
*
* Class OptimalImage
* @package Yarbala\OptimalImage
*/
class OptimalImage extends Image
{
/**
* Функция переопределяет базовую из родительского класса для Image 'Fields/File.php'. Код взят из базовой функции
* и ы него добавлен вызов optimizeImage после загрузки.
*
* @param \Illuminate\Http\Request $request
* @param string $requestAttribute
* @return string
*/
protected function storeFile($request, $requestAttribute)
{
if (! $this->storeAsCallback) {
$fileName = $request->file($requestAttribute)->store($this->getStorageDir(), $this->getStorageDisk());
if ($fileName) {
try {
$this->optimizeImage($fileName);
} catch (FileNotFoundException $e) {
}
}
return $fileName;
}
$fileName = $request->file($requestAttribute)->storeAs(
$this->getStorageDir(), call_user_func($this->storeAsCallback, $request), $this->getStorageDisk()
);
if ($fileName) {
try {
$this->optimizeImage($fileName);
} catch (FileNotFoundException $e) {
//
}
}
return $fileName;
}
/**
* Функция производящая оптимизацию изображения
*
* @param $fileName
* @throws FileNotFoundException
*/
protected function optimizeImage($fileName)
{
$needsUploadBack = false;
$localDisk = $this->getLocalDisk();
$disk = $this->getStorageDisk();
/* Так как базовый компонент работает с классом Storage, а Spatie\ImageOptimizer работает с локальными файлами
мы выгрузим файл в локальное хранилище в случае надобности
*/
if (!Storage::disk($localDisk)->exists($fileName)) {
Storage::disk($localDisk)->put($fileName, Storage::disk($disk)->get($fileName));
$needsUploadBack = true;
}
// Получаем путь к изображению в локальном хранилище
$path = Storage::disk($localDisk)->path($fileName);
// Оптимизация изображения
app(ImageOptimizer::class)->optimize($path);
// Если мы выгружали файл, загрузим его обратно
if ($needsUploadBack) {
Storage::disk($disk)->put($fileName, Storage::disk($localDisk)->get($fileName));
Storage::disk($localDisk)->delete($fileName);
}
}
/**
* Установка локального хранилища для обработки изображений
*
* @param string $disk
* @return OptimalImage
*/
public function localDisk(string $disk)
{
return $this->withMeta(['localDisk' => $disk]);
}
/**
* Получение локального хранилища
*
* @return string
*/
protected function getLocalDisk()
{
return $this->meta['localDisk'] ?? 'local';
}
}
Можно использовать параметры по умолчанию для оптимизации. Для изменения параметров оптимизации, надо опубликовать файл конфигурации config/image-optimizer.php в котором можно настроить параметры оптимизаторов:
php artisan vendor:publish --provider="Spatie\LaravelImageOptimizer\ImageOptimizerServiceProvider"
Библиотека для оптимизации изображений пользуется различными оптимизаторами, установка которых описана в документации.
Далее остается добавить файл лицензии, написать readme и опубликовать пакет.
Получилось довольно простое решение, но по-моему мнению довольно полезное. Надеюсь данный пакет пригодиться в вашей работе. В любом случае данный текст может служить руководством для начинающих разработчиков по созданию простых пакетов.
Буду рад конструктивным комментариям и пожеланиям по оптимизации данного руководства или пакета.
Исходный код проекта доступен по адресу и распространяется по лицензии MIT.
MrMYSTIC
Пожалуйста, не надо так! Это же все зависимости обновит. Вообще редактировать composer.json руками не очень хорошая идея.
А в целом статья интересная.
yarbala Автор
Спасибо за комментарий.
В данном случае речь идёт не о добавлении зависимости в проект, а идёт о добавлении зависимости в пакет. По-этому composer require — тут не подойдёт. Нам необходимо, чтобы пакет при установке — подтянул зависимости.
Если создать проект с 0 как описано в статье, то composer update на проекте — будет самым простым способом подтянуть зависимости именно пакета.