Design Patterns: Using The Decorator Pattern To Refactor Out Duplication In Code Enter a brief summary for this post

PHP Design Patterns: Using The Decorator Pattern To Refactor Out Duplication In Code

I had thrown together the following (below) in haste and I knew it was an implementation that would need refactoring at some point. I found some time recently and the refactoring cleaned up nicely when I used the Decorator design pattern ― one of my favourite patterns actually. The Composite and Visitor patterns are two other favourites, especially when working with a parent <> child relational data structure.

The following implementation will resize an original image and also add a watermark to this large image.

// a Laravel controller...
public function store(Request $request) : JsonResponse
    {   
        /**
         * @note    need to install Amazon S3 package, and Intervention Image package, use:
         * 
         *          - composer require --with-all-dependencies league/flysystem-aws-s3-v3 "^1.0"
         *          - composer require intervention/image
         * 
         * @see     https://image.intervention.io/v2/introduction/installation#integration-in-laravel
         * 
         */

        try
        {
            set_time_limit(0);
            ini_set('memory_limit', '128M');
            ini_set('upload_max_filesize', '128M');

            $filename=time().'.jpg';
            $sourceOriginal=Image::make($request->file('file'))
                ->resize(Photo::DEFAULT_WIDTH, null, function($constraint) 
                {
                    $constraint->aspectRatio();
                    $constraint->upsize();
                })
                ->encode('jpg', Photo::QUALITY);

            /**
             * @note    step one: upload, and move resized image to ./original directory, to 
             *          preserve JPEG in event require a different watermark
             */

            $originalTarget='original/'.$filename;
            Storage::disk('s3')->put($originalTarget, $sourceOriginal->stream(), 'public');
            
            /**
             * @note    step two: upload, and move resized image to ./dev directory, to 
             *          ready JPEG for watermark
             */

            $devTarget='dev/'.$filename;
            Storage::disk('s3')->put($devTarget, $sourceOriginal->stream(), 'public');
            
            if($request->thumbnail == 'on')
            {
                /**
                 * @note    create a thumbnail, if required to do so, of the original image
                 */

                $sourceThumbnail=Image::make($request->file('file'))
                    ->resize(Photo::THUMBNAIL_WIDTH, null, function($constraint) 
                    {
                        $constraint->aspectRatio();
                        $constraint->upsize();
                    })
                    ->encode('jpg', Photo::QUALITY);

                $thumbTarget='dev/thumb/'.$filename;
                Storage::disk('s3')->put($thumbTarget, $sourceThumbnail->stream(), 'public');
            }
            else
            {
                /**
                 * @note    create a watermark on the image but only if it isn't used as a banner 
                 *          for the top of the page, this being the case, as there is no thumbnail, and
                 *          the minimum width is at least defined pixels
                 */

                $tmpSource=Image::make($request->file('file'));
                if($tmpSource->width() >= Photo::DEFAULT_WIDTH) 
                {
                    $imageWatermark=Image::make(Storage::disk('s3')->url($devTarget));
                    $y_position=$imageWatermark->height()/2;

                    $imageWatermark
                        ->text(config('app.name').' '.config('app.name').' '.config('app.name').' '.config('app.name'), 960, $y_position, function($font) 
                        {
                            $font->file(public_path('/fonts/roboto.ttf'));
                            $font->size(Photo::WATERMARK_SIZE);
                            $font->color(array(255, 255, 255, 0.1));
                            $font->align('center');
                            $font->valign('middle');
                        });

                    Storage::disk('s3')->put($devTarget, $imageWatermark->stream(), 'public'); 
                }
            }

            set_time_limit(60); 
            // ... etc ...

A thumbnail will also be created if required, separately. The refactoring is an improvement because not only is the new implementation cleaner, it's possible to resize multiple images all in one go with minimal restrictions ― including adding a watermark to any of the resized images independently. Let's look at this refactoring.

The Decorator Design Pattern

As you can see from the original implementation it is fragile and untestable. And there is duplication which is an unnecessary evil and yet you go with the first (usually easiest) solution due to time constraints. Leaving a refactoring to rectify concerns at a later date when time permits. The Decorator pattern is perfect for being able to repeat an operation over and over again to a given interface.

And in this implementation I've used a Decorator within a Decorator. First of all the interfaces.

namespace App\Classes\Image\Interfaces;

interface DecoratorInterface {
	public function use(DecoratorInterface $decorator) : void;
	public function process(string $path, string $source) : void;
	
}

namespace App\Classes\Image\Interfaces;

interface ResizerInterface {
	public function resize(string $path);

}

I'm terrible at naming class names and I'm not alone. Why so many are thankful for the saving grace of namespaces in modern PHP. Next the concrete classes. There are three of them: one for the resizing and one for adding the watermark and the third class is what holds it all together (the decoration).

namespace App\Classes\Image;

use App\Models\Photo;
use Intervention\Image\ImageManagerStatic as Image;
use App\Classes\Image\Interfaces\ResizerInterface;

class Resizer implements ResizerInterface {
	private int $width;
	
	public function __construct(int $width)
	{
		$this->width=$width;
	}

	public function resize(string $path) 
	{
		$image = Image::make($path)
			->resize($this->width, null, function($constraint) 
			{
				$constraint->aspectRatio();
				$constraint->upsize();
			});

		$image->encode(Photo::DEFAULT_FORMAT, Photo::QUALITY);
		
		return $image;
	}
}

namespace App\Classes\Image;

use App\Models\Photo;
use App\Classes\Dev\Resizer;
use App\Classes\Image\Interfaces\ResizerInterface;

class Watermark implements ResizerInterface {
	private int $width;
	private $resizer;
	private string $watermark;

	public function __construct(ResizerInterface $resizer, int $width, string $watermark)
	{
		$this->resizer=$resizer;
		$this->watermark=$watermark;
		$this->width=$width;
	}

	public function resize(string $path) 
	{
		$image = $this->resizer->resize($path); 
		
		$y_position=$image->height() / 2; 
		$x_position=$this->width / 2;

		$image
                ->text($this->watermark, $x_position, $y_position, function($font) 
                    {
                        $font->file(public_path('/fonts/roboto.ttf'));
                        $font->size(Photo::WATERMARK_SIZE);
                        $font->color(array(255, 255, 255, 0.2));
                        $font->align('center');
                        $font->valign('middle');
                    });

		$image->encode(Photo::DEFAULT_FORMAT, Photo::QUALITY);
		return $image;
	}
}

namespace App\Classes\Image;

use App\Classes\Image\Resizer;
use App\Classes\Image\Interfaces\ResizerInterface;
use App\Classes\Image\Interfaces\DecoratorInterface;

class Decorator implements DecoratorInterface 
{
	private int $width;
	private string $destination;
	private $decorator;
	private $resizer;

	public function __construct(ResizerInterface $resizer, string $destination) 
	{
		$this->destination=$destination;
		$this->resizer=$resizer;
	}

	public function use(DecoratorInterface $decorator) : void
	{
		$this->decorator=$decorator;
	}

	public function process(string $path, string $source) : void
	{	
		$image=$this->resizer->resize($path.'/'.$source);
		\Storage::disk('public')->put($this->destination.'/'.$source, (string) $image);

		if(!is_null($this->decorator))
		{
			$this->decorator->process($path, $source);
		}
	}
}

One thing I'm not so happy about is how I've left in Laravel's Storage facade in the decorator. Yes, Laravel's file and disk layer is very flexible but it's something I feel should be external. A refactoring for another time because improving code is incremental and not one time only.

In the controller you have this is how you'd use the implementation (below).

// ... etc ...
        $path=public_path('/storage/tmp'); 

        $large=new Decorator(
            new Watermark(
                new Resizer(Photo::DEFAULT_LARGE_WIDTH), Photo::DEFAULT_LARGE_WIDTH, config('app.name')
            ), '/dev/large'
        );
        $large->use(
            $medium=new Decorator(
                new Watermark(
                    new Resizer(Photo::DEFAULT_MEDIUM_WIDTH), Photo::DEFAULT_MEDIUM_WIDTH, config('app.name')
                ), '/dev/medium'
            )
        );
        $medium->use(
            $small=new Decorator(
                new Resizer(Photo::DEFAULT_SMALL_WIDTH), '/dev/small'
            )
        );

        $large->process($path, '1672301193.jpg');

You can see I've wanted to add a watermark on the large and medium sized images but not the smallest of the three. The implementation can now resize any image to specification and any number of times. With or without a watermark. I use a text based watermark with this implementation but I could so easily swap this watermark out for another which uses a PNG image (for example).

Without too much trouble and without breaking anything.

Should I use Classes or Library for my Class Directory?

A lot of Laravel developers would opt for naming their directory Library but I would argue it depends. In fact you may need both because each serves a different purpose:

  • the Library structure is used for (in my opinion) those classes that are reusable from one application to another
  • the Classes structure (or other name if you prefer) is used for those classes applicable to that one application only

In large applications it's accepted the Library may actually be installable as a Composer package that is constantly maintained. I have a lot of useful code in my own framework I designed and developed prior to using Laravel. I'd like to one day canobalise that code for continued use with my Laravel projects.