Displaying the various sizes of image within early builds of Willow CMS was getting cumbersome and repetive. In this post, I’ll share how I used PHP traits and magic methods to create a clean, maintainable solution for image URL generation.

Understanding PHP Traits

Traits provide a mechanism for code reuse in single inheritance languages like PHP. They allow you to encapsulate methods to be included in multiple classes, and classes can have multiple traits. Instead of duplicating code or creating complex inheritance hierarchies, traits let us “mix in” functionality where needed using the use keyword. This approach helps maintain DRY (Don’t Repeat Yourself) principles.

Magic Methods in PHP

Magic methods in PHP are special methods that start with a double underscore (__). They’re automatically called in specific situations, allowing us to define custom behavior for common operations. For example, when you try to access a property that’s not directly accessible, PHP calls the __get() magic method if you there is code to ‘magically’ make that attribute exist.

Introducing ImageUrlTrait

The ImageUrlTrait combines the power of traits and magic methods to simplify image URL generation. Instead of explicitly defining methods or attributes for each image size (micro, tiny, small, medium, large, extra-large, massive), this trait dynamically handles URL generation based on the desired size. This means we can get image URLs for a class using the trait with $image->smallImageUrl or $image->largeImageUrl without writing specific methods/attributes for each size.

How ImageUrlTrait Works

Take a look at the code with comments. Let’s break down the key components of the trait:

protected function _getImageUrl(): string
{
    return str_replace('webroot/', '', $this->dir . $this->image);
}

This method provides a way to get the URL for the original image file using an attribute like $image->imageUrl or $user->imageUrl. The method generates the URL for the original image by combining the directory and filename and removing the ‘webroot/’ prefix (since the dir column in the database holds the relative filesystem directory path following the CakePHP project folder structure).

public function &__get(string $attribute): mixed
{
    if (preg_match('/^(.+)ImageUrl$/', $attribute, $matches)) {
        $size = lcfirst($matches[1]);
        $imageSizes = SettingsManager::read('ImageSizes');
        if (isset($imageSizes[$size])) {
            $url = $this->getImageUrlBySize($size);

            return $url;
        }
    }
    return parent::__get($attribute);
}

The magic happens here. When accessing a property like $iamge->smallImageUrl or $image->massiveImageUrl, this method:

  1. Checks if the property name ends with “ImageUrl” using a regular expression and preg_match
  2. Extracts the size prefix (e.g., “small”) into $size
  3. Verifies the size exists in our configuration
  4. Calls a method in the trait to fetch the URL for the given size
  5. Returns the URL
  6. If a match is not found, calls the parent __get() method (because CakePHP also uses magic methods and we don’t want to break the method call chain)

The Old Way: Verbose and Error-Prone

Before implementing the trait, displaying images with previews required verbose code:

$this->Html->image(SettingsManager::read('ImageSizes.small', '200') . '/' . $user->picture, 
    ['pathPrefix' => 'files/Users/picture/', 
    'alt' => $user->alt_text, 
    'class' => 'img-thumbnail', 
    'width' => '50',
    'data-bs-toggle' => 'popover',
    'data-bs-trigger' => 'hover',
    'data-bs-html' => 'true',
    'data-bs-content' => $this->Html->image(SettingsManager::read('ImageSizes.large', '400') . '/' . $user->picture, 
        ['pathPrefix' => 'files/Users/picture/', 
        'alt' => $user->alt_text, 
        'class' => 'img-fluid', 
        'style' => 'max-width: 300px; max-height: 300px;'])
    ]) 

Eugh! This approach had several issues:

  • Repeated calls to SettingsManager::read() with fiddly dot notation to choose an image size
  • Complex nested HTML helper calls
  • Hard-to-read code
  • Increased chance of typos in size names or paths when copy/pasting this throughout the views

The New Way: Clean and Maintainable

Now, with ImageUrlTrait and a simple element, we can write much cleaner code:

    $this->element('image/icon', [
        'model' => $article, 
        'icon' => $article->smallImageUrl, 
        'preview' => $article->largeImageUrl
    ]); 

The benefits are clear:

  • More readable code
  • Less repetition
  • Reduced chance of errors
  • Consistent image handling across the application
  • Easy to modify image sizes (if I want the preview image to be larger I just do 'preview' => $article->massiveImageUrl)

Tags

Refactoring Code CakePHP CodeQuality Features