Пишем свой behavior-комбаин

Общие вопросы по использованию второй версии фреймворка. Если не знаете как что-то сделать и это про Yii 2, вам сюда.
Ответить
godzie
Сообщения: 62
Зарегистрирован: 2016.04.03, 00:38

Пишем свой behavior-комбаин

Сообщение godzie »

Ввиду часто повторяющегося кейса: "загрузить картинку -> сохранить исходную копию + обрезанную + с другим масштабом + etc", решил написать свой многофункциональный behavior. Настройка примерно примерно такая

Код: Выделить всё

[
                'class' => ...
                'imageAttr' => 'avatar', // аттрибут ар в котором будет лежать FileUploaded

                'additionalImages' => [ //дополнительные изображение
                  'small' => ['resize' => [300,400]], //дополнительное изображение 1, постфикс small, применяем модификатор Resize 300x400
                  'medium-rotate' => ['resize' => [500,700], 'rotate' => [40]], //по аналогии выше
                ],

                'postfix' => 'avatar', //постфикс для оригинального изображения
                'saveDirectory' => static::IMG_DIR, //папка для сохрания
            ],
 
Сам behavior

Код: Выделить всё

public function beforeUpdate($event)
    {
        if ($this->owner->validate()) {
            $this->initBehavior();
            $this->imageList->save($this->saveDirectory);
        }
    }
    
    public function initBehavior(){
        if ($this->owner->{$this->imageAttr} instanceof \yii\web\UploadedFile) {
            $this->uploadImage = $this->owner->{$this->imageAttr};
        } else {
            throw new Exception('file object must be instance of UploadFile');
        }

        $fileName = $this->uploadImage->tempName; 
        
        $imageFactory = new helpers\ImageFactory(); //фабрика изображений
        $this->imageList = new helpers\ImageList(); //контейнер изображений

        $this->imageList->add(
            $imageFactory->getImage($fileName, $this->postfix, []) // добавляем в контейнер оригинальное изображение
        );

        foreach ($this->additionalImages as $imagePostfix=>$imageOptions){
            $this->imageList->add(
                $imageFactory->getImage($fileName, $imagePostfix, $imageOptions) //добавляем в контейнер модифицированные изображения
            );
        }
    }
 
Фабрика

Код: Выделить всё

class ImageFactory
{
    /**
     * @param $fileName
     * @param $postfix
     * @return ImageInterface
     */
    private function getOriginalImage($fileName, $postfix)
    {
        return new ImageFile($fileName, $postfix);
    }

    /**
     * @param string $prefix decorator class prefix
     * @param ImageInterface $object
     * @param array $params
     * @return ImageInterface
     * @throws \yii\base\InvalidConfigException
     */
    private function addDecorator($prefix, $object, $params)
    {
        $className = $this->decorators[ucfirst($prefix)] ;
        return \Yii::createObject($className, [$object, $params]);
    }

    /**
     * @param string $fileName full path to file
     * @param string $postfix
     * @param array $imageOptions
     * @return ImageInterface
     */
    public function getImage($fileName, $postfix, array $imageOptions)
    {
        $image = $this->getOriginalImage($fileName, $postfix);
        foreach ($imageOptions as $option => $value) {
            $image = $this->addDecorator($option, $image, $value);
        }
        return $image;
    }

    public function __construct()
    {
        $this->decorators = [
            'Resize' => decorators\ResizeDecorator::className(),
            'Rotate' => decorators\RotateDecorator::className()
        ];
    }

    private $decorators;
} 
Оригинальная картинка:

Код: Выделить всё

class ImageFile implements ImageInterface{
    /**
     * @property mixed $image
     * @property string $postfix
     */
    private $image;
    private $postfix;

    public function __construct($filePath, $postfix){
        $driver = new ImageDriver;
        $driver->driver = 'GD';
        $this->image = $driver->load($filePath);
        $this->postfix = $postfix;
    }

    /**
     * @inheritdoc
     */
    public function save($path)
    {
        $fileName = $path . ??? .  $this->postfix; 
        $this->image->save($fileName, $quality = 90);
    }

    /**
     * @return mixed
     */
    public function getSource(){
        return $this->image;
    }

} 
И пример декоратора

Код: Выделить всё

class RotateDecorator extends Object implements ImageInterface
{
    /**
     * @property ImageInterface
     */
    private $imageFile;

    /**
     * @property int $degrees 
     */
    private $degrees;

    /**
     * @param ImageInterface $imageFile
     * @param integer[] $config
     */
    public function __construct(ImageInterface $imageFile, array $config)
    {
        $this->imageFile = $imageFile;

        $this->degrees = $config[0];
    }

    /**
     * @inheritdoc
     */
    public function save($path)
    {
        $this->imageFile->getSource()->rotate($this->degrees);

        $this->imageFile->save($path);
    }
    
    /**
     * @return mixed
     */
    public function getSource()
    {
        return $this->imageFile->getSource();
    }
} 
Основная идея в этом всем добавлять новый функционал (типа blur, flip) добавлением классов декораторов. Но я столкнулся с вопросами которые не совсем понимаю как решить в "правильном" подходе:

1) Должен ли behavior предоставлять get методы для каждой из сохраненных картинок, единственный вариант реализации __get ?
2) в какой момент лучше сохранять изображения beforeU, AfterU имеет ли вообще смысл проверка if ($this->owner->validate()) в событии beforeUpdate или это событие генерятся когда валидация уже прошла?
2) Как удобнее всего генерировать имя картинки? Использовать time-stamp или id записи или..?
3) Попробовал использовать timestamp, очевидно получилось что у каждого сохраненного изображения полностью оригинальное название и понять что является модифицированной копией какого не возможно. Если использовать id то придется параметризировать save дополнительным параметром и тащить этот параметр через все декораторы. Возможно стоит добавить объект который бы решал вопрос генерации имен, но как он должен выглядеть и куда инкапсулироваться?

Буду рад помощи, если у кого то найдется время все это читать :)
Ответить