DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

1

Writing Craft Plugins with Extensible Components

Writing Craft Plugins with Extensible Components

How to write a plu­g­in for Craft CMS that allows oth­ers to extend it in a flex­i­ble, com­po­nent-ized way

Andrew Welch / nystudio107

Puzzle Pieces Components

When writ­ing a plu­g­in for Craft CMS, some­thing that you may encounter is a sit­u­a­tion where your plu­g­in wants to pro­vide func­tion­al­i­ty that could be extend­ed. You might want to extend it your­self in the future, or you might want to allow oth­ers to extend it.

For exam­ple, my Ima­geOp­ti­mize plu­g­in allows you to entire­ly replace what per­forms your image trans­forms, so a ser­vice like Imgix or Thum­bor can be used instead of Craft’s native trans­forms. But how can we write this in an exten­si­ble way so that if any­one want­ed to add anoth­er image trans­form ser­vice, they could?

This arti­cle will dis­cuss spe­cif­ic strate­gies for adding exten­si­ble func­tion­al­i­ty to your plu­g­in. If you want to learn more about Craft CMS plu­g­in devel­op­ment in gen­er­al, check out the So You Wan­na Make a Craft 3 Plu­g­in? article.

Ima­geOp­ti­mize: A Con­crete Example

So let’s use Ima­geOp­ti­mize as a con­crete exam­ple. When I came up with the idea of entire­ly replac­ing what could do the image trans­forms in Craft CMS, I decid­ed that the best way to do it was to write it in a mod­u­lar fashion.

Start­ing with Ima­geOp­ti­mize 1.5.0, I’m using the exact tech­nique out­lined in this arti­cle. So let’s check it out.

Concrete Craft Cms Plugin Example

Focus­ing on a real-world exam­ple can often be more use­ful than using a the­o­ret­i­cal or con­trived exam­ple. So let’s dive in and see how we can imple­ment image trans­forms in Ima­geOp­ti­mize in an exten­si­ble way.

PHP Inter­faces

For­tu­nate­ly, mod­ern PHP pro­vides us with some tools to help us do this. PHP allows you to write an object inter­face that defines the meth­ods that an object must implement.

Why both­er doing this? Well, it essen­tial­ly lets you define an API with the meth­ods that all objects that use that inter­face must imple­ment. So in our case, we have an ImageTransformInterface that looks like this:


<?php
/**
 * ImageOptimize plugin for Craft CMS 3.x
 *
 * Automatically optimize images after they've been transformed
 *
 * @link https://nystudio107.com
 * @copyright Copyright (c) 2018 nystudio107
 */

namespace nystudio107\imageoptimize\imagetransforms;

use craft\base\SavableComponentInterface;
use craft\elements\Asset;
use craft\models\AssetTransform;

/**
 * @author nystudio107
 * @package ImageOptimize
 * @since 1.5.0
 */
interface ImageTransformInterface extends SavableComponentInterface
{
    // Static Methods
    // =========================================================================

    /**
     * Return an array that contains the template root and corresponding file
     * system directory for the Image Transform's templates
     *
     * @return array
     * @throws \ReflectionException
     */
    public static function getTemplatesRoot(): array;

    // Public Methods
    // =========================================================================

    /**
     * Return a URL to a transformed images
     *
     * @param Asset $asset
     * @param AssetTransform|null $transform
     * @param array $params
     *
     * @return string|null
     */
    public function getTransformUrl(Asset $asset, $transform, array $params = []);

    /**
     * Return a URL to the webp version of the transformed image
     *
     * @param string $url
     * @param Asset $asset
     * @param AssetTransform|null $transform
     * @param array $params
     *
     * @return string
     */
    public function getWebPUrl(string $url, Asset $asset, $transform, array $params = []): string;

    /**
     * Return the URL that should be used to purge the Asset
     *
     * @param Asset $asset
     * @param array $params
     *
     * @return mixed
     */
    public function getPurgeUrl(Asset $asset, array $params = []);

    /**
     * Purge the URL from the service's cache
     *
     * @param string $url
     * @param array $params
     *
     * @return bool
     */
    public function purgeUrl(string $url, array $params = []): bool;

    /**
     * Return the URI to the asset
     *
     * @param Asset $asset
     *
     * @return mixed
     */
    public function getAssetUri(Asset $asset);

    /**
     * Prefetch the remote file to prime the cache
     *
     * @param string $url
     */
    public function prefetchRemoteFile($url);

    /**
     * Get the parameters needed for this transform
     *
     * @return array
     */
    public function getTransformParams(): array;
}

Note that we’re not writ­ing any actu­al code here; we’re just defin­ing the meth­ods that an object that wants to do image trans­forms needs to imple­ment. We define the method names, and the para­me­ters that must be passed into this method (this is often called the method­’s sig­na­ture).

Think of it like writ­ing a stan­dards doc­u­ment for your classes.

This forces us to think about the prob­lem of an image trans­form in an abstract way; what would a gener­ic inter­face to image trans­forms look like?

Note that our ImageTransformInterface extends anoth­er inter­face: Sav­able­Com­po­nentIn­ter­face. This is a Craft pro­vid­ed inter­face that defines the meth­ods that a com­po­nent that has sav­able set­tings must implement.

This is great, we can lever­age the work that the fine folks at Pix­el & Ton­ic have done, because we want to be able to have sav­able set­tings too! Many com­po­nents in Craft like Fields, Wid­gets, etc. all use the SavableComponentInterface so we can, too!

Also note that there are no prop­er­ties defined in an inter­face; just methods.

PHP Traits

PHP also imple­ments the idea of traits. They are sim­i­lar to a PHP class, but they are designed to side-step the lim­i­ta­tion that PHP has from a lack of mul­ti­ple inher­i­tance. In PHP, an object can only inher­it (extends, in PHP par­lance) from one object.

Oth­er lan­guages allow for mul­ti­ple inher­i­tance; instead in PHP we can define a trait, and our objects can use that trait. Think of it as a way to pro­vide the prop­er­ties & meth­ods like a class would, but in a way that mul­ti­ple objects of dif­fer­ent types can use it.

Here’s what the ImageTransformTrait looks like in ImageOptimize:


<?php
/**
 * ImageOptimize plugin for Craft CMS 3.x
 *
 * Automatically optimize images after they've been transformed
 *
 * @link https://nystudio107.com
 * @copyright Copyright (c) 2018 nystudio107
 */

namespace nystudio107\imageoptimize\imagetransforms;

/**
 * @author nystudio107
 * @package ImageOptimize
 * @since 1.5.0
 */
trait ImageTransformTrait
{
    // Public Properties
    // =========================================================================
}

Seems kin­da use­less, right? That’s because cur­rent­ly, it is! I’m not defin­ing any prop­er­ties or any meth­ods in the ImageTransformTrait, I’m just imple­ment­ing it for future expan­sion purposes.

In fact, although you can have both prop­er­ties and meth­ods in a trait, I tend to use them only for defin­ing properties.

The rea­son I do this is that it’s very com­mon to want to over­ride an objec­t’s meth­ods with your own code, and then call the par­ent method, e.g.: parent::init(). This gets pret­ty awk­ward to do with traits. So instead, we use base abstract classes.

PHP Base Abstract Classes

Final­ly, the last bit of PHP we’ll take advan­tage of is abstract class­es. Abstract class­es are just PHP class­es that imple­ment some meth­ods but are nev­er instan­ti­at­ed on their own. They exist sim­ply so that oth­er class­es can extend them.

So they pro­vide some base func­tion­al­i­ty, but you’d nev­er actu­al­ly cre­ate one. Instead, you’d write anoth­er class that extends a base abstract class, and over­ride the meth­ods you want to over­ride, call­ing the parent::method as appropriate.

Here’s what the base abstract Image­Trans­form class looks like in ImageOptimize:


<?php
/**
 * ImageOptimize plugin for Craft CMS 3.x
 *
 * Automatically optimize images after they've been transformed
 *
 * @link https://nystudio107.com
 * @copyright Copyright (c) 2018 nystudio107
 */

namespace nystudio107\imageoptimize\imagetransforms;

use nystudio107\imageoptimize\helpers\UrlHelper;

use craft\base\SavableComponent;
use craft\elements\Asset;
use craft\helpers\FileHelper;
use craft\helpers\StringHelper;
use craft\models\AssetTransform;

/**
 * @author nystudio107
 * @package ImageOptimize
 * @since 1.5.0
 */
abstract class ImageTransform extends SavableComponent implements ImageTransformInterface
{
    // Traits
    // =========================================================================

    use ImageTransformTrait;

    // Static Methods
    // =========================================================================

    /**
     * @inheritdoc
     */
    public static function displayName(): string
    {
        return Craft::t('image-optimize', 'Generic Transform');
    }

    /**
     * @inheritdoc
     */
    public static function getTemplatesRoot(): array
    {
        $reflect = new \ReflectionClass(static::class);
        $classPath = FileHelper::normalizePath(
            dirname($reflect->getFileName())
            . '/../templates'
        )
        . DIRECTORY_SEPARATOR;
        $id = StringHelper::toKebabCase($reflect->getShortName());

        return [$id, $classPath];
    }

    // Public Methods
    // =========================================================================

    /**
     * @inheritdoc
     */
    public function getTransformUrl(Asset $asset, $transform, array $params = [])
    {
        $url = null;

        return $url;
    }

    /**
     * @inheritdoc
     */
    public function getWebPUrl(string $url, Asset $asset, $transform, array $params = []): string
    {
        return $url;
    }

    /**
     * @inheritdoc
     */
    public function getPurgeUrl(Asset $asset, array $params = [])
    {
        $url = null;

        return $url;
    }

    /**
     * @inheritdoc
     */
    public function purgeUrl(string $url, array $params = []): bool
    {
        return true;
    }

    /**
     * @inheritdoc
     */
    public function getAssetUri(Asset $asset)
    {
        $volume = $asset->getVolume();
        $assetPath = $asset->getPath();

        // Account for volume types with a subfolder setting
        // e.g. craftcms/aws-s3, craftcms/google-cloud
        if ($volume->subfolder ?? null) {
            return rtrim($volume->subfolder, '/').'/'.$assetPath;
        }

        return $assetPath;
    }

    /**
     * @param string $url
     */
    public function prefetchRemoteFile($url)
    {
        // Get an absolute URL with protocol that curl will be happy with
        $url = UrlHelper::absoluteUrlWithProtocol($url);
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_FOLLOWLOCATION => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_NOBODY => 1,
        ]);
        curl_exec($ch);
        curl_close($ch);
    }

    /**
     * @inheritdoc
     */
    public function getTransformParams(): array
    {
        $params = [
        ];

        return $params;
    }

    /**
     * Append an extension a passed url or path
     *
     * @param $pathOrUrl
     * @param $extension
     *
     * @return string
     */
    public function appendExtension($pathOrUrl, $extension): string
    {
        $path = $this->decomposeUrl($pathOrUrl);
        $path_parts = pathinfo($path['path']);
        $new_path = $path_parts['filename'] . '.' . $path_parts['extension'] . $extension;
        if (!empty($path_parts['dirname']) && $path_parts['dirname'] !== '.') {
            $new_path = $path_parts['dirname'] . DIRECTORY_SEPARATOR . $new_path;
            $new_path = preg_replace('/([^:])(\/{2,})/', '$1/', $new_path);
        }
        $output = $path['prefix'] . $new_path . $path['suffix'];

        return $output;
    }

    // Protected Methods
    // =========================================================================

    /**
     * Decompose a url into a prefix, path, and suffix
     *
     * @param $pathOrUrl
     *
     * @return array
     */
    protected function decomposeUrl($pathOrUrl): array
    {
        $result = array();

        if (filter_var($pathOrUrl, FILTER_VALIDATE_URL)) {
            $url_parts = parse_url($pathOrUrl);
            $result['prefix'] = $url_parts['scheme'] . '://' . $url_parts['host'];
            $result['path'] = $url_parts['path'];
            $result['suffix'] = '';
            $result['suffix'] .= empty($url_parts['query']) ? '' : '?' . $url_parts['query'];
            $result['suffix'] .= empty($url_parts['fragment']) ? '' : '#' . $url_parts['fragment'];
        } else {
            $result['prefix'] = '';
            $result['path'] = $pathOrUrl;
            $result['suffix'] = '';
        }

        return $result;
    }
}

As you can see, it pro­vides a bit of base func­tion­al­i­ty that may be fine for a par­tic­u­lar image trans­form, but any of these meth­ods can be over­rid­den as need­ed. It’s also pret­ty com­mon to pro­vide some gener­ic util­i­tar­i­an func­tion­al­i­ty in a base abstract class.

Note that the ImageTransform class also extends the Craft base class SavableComponent so that we get the func­tion­al­i­ty of a sav­able component!

Tying it all together

So what we end up with is a hier­ar­chy that looks like this:

Image Transform Class Hierarchy

Things pro­vid­ed by Craft are col­ored red; things that we nev­er instan­ti­ate are col­ored grey, and then the actu­al objects that we use in our plu­g­in are col­ored aqua.

We have:

  • an inter­face that defines our Image Trans­form meth­ods (our API)
  • a trait that (could) define any prop­er­ties we want all of our Image Trans­forms to have
  • a base abstract class that defines our core func­tion­al­i­ty that oth­er class­es extend
  • …and then mul­ti­ple class­es that extends our base abstract ImageTransform class to imple­ment the functionality

While this might seem at first blush to be com­pli­cat­ed, actu­al­ly what we’ve done is moved var­i­ous bits around to their own self-con­tained files, with a defined set of func­tion­al­i­ty. This will result is clear­er, more eas­i­ly main­tain­able & exten­si­ble code.

Mix­ing Our Com­po­nents In

So this is great! We’ve got a nice defined inter­face & base abstract class­es for our Image Trans­form. This will make our life eas­i­er when we’re writ­ing the code to imple­ment our Image Transforms.

It also gives oth­er devel­op­ers a clear­ly defined way to write their own Image Trans­forms, just like Craft gives you a PluginInterface.php inter­face and base abstract Plugin.php classes.

But how do we mix our Image Trans­forms into our plugin?

Mixing In Plugin Components

The first thing we do is we have a prop­er­ty in our plug­in’s Settings mod­el that holds the ful­ly qual­i­fied class name of what­ev­er the cur­rent­ly select­ed Image Trans­form is:


/**
 * @var string The image transform class to use for image transforms
 */
public $transformClass = CraftImageTransform::class;

We default this to CraftImageTransform::class, but it can end up being any class that imple­ments our ImageTransformInterface.

Next we take advan­tage of the fact that our plu­g­in is actu­al­ly a Yii2 Mod­ule… and all Yii2 Mod­ules can have Yii2 Com­po­nents.

In fact, any ser­vice class­es you define in your plu­g­in are just added as com­po­nents of your plug­in’s Mod­ule. See the Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule arti­cle for more details on Craft CMS modules.

So we can set our transformMethod com­po­nent dynam­i­cal­ly in our plu­g­in by call­ing this method in our plug­in’s init() method:


/**
 * Set the transformMethod component
 */
protected function setImageTransformComponent()
{
    $settings = $this->getSettings();
    $definition = array_merge(
        $settings->imageTransformTypeSettings[$settings->transformClass] ?? [],
        ['class' => $settings->transformClass]
    );
    try {
        $this->set('transformMethod', $definition);
    } catch (InvalidConfigException $e) {
        Craft::error($e->getMessage(), __METHOD__ );
    }
    self::$transformParams = ImageOptimize::$plugin->transformMethod->getTransformParams();
}

All this is doing is call­ing the set() method that our plu­g­in inher­its from Yii2’s Ser­vice­Lo­ca­tor class. You pass in alias that you want to refer to the com­po­nent as (in this case transformMethod), along with a con­fig­u­ra­tion array that con­tains a class key with the ful­ly qual­i­fied class name to instan­ti­ate, along with any oth­er key/​value pairs of prop­er­ties that the class should be ini­tial­ized with.

In our case, we pass along any set­tings that our Image Trans­form might have, so that it’ll be con­fig­ured with any user-defin­able settings.

The mag­ic is that after we set() our com­po­nent on our plug­in’s class, we can then access it like any oth­er ser­vice: ImageOptimize::$plugin->transformMethod-> to call any of the meth­ods we defined in our ImageTransformInterface.

The plu­g­in does­n’t know, and does­n’t care exact­ly what class is pro­vid­ing the com­po­nent, so in this way we can swap in any class that imple­ments our ImageTransformInterface and away we go!

Under the hood, this all uses Yii2’s Depen­den­cy Injec­tion Con­tain­er (DI) to work its magic.

You’ve prob­a­bly seen this in action before, with­out even real­iz­ing it. When you adjust set­tings in your config/app.php to, say, add Redis as a caching method, the array you’re pro­vid­ing is just a con­fig­u­ra­tion for DI so it can find and instan­ti­ate the cache class to use!

Mak­ing Com­po­nents Discoverable

So it’s great that we can lever­age all of this Yii2 good­ness to make our lives eas­i­er, but we still have the prob­lem of how to let our plu­g­in know about our com­po­nent class­es to begin with. We can take a 3‑pronged approach to this.

First, we sim­ply define an array of built-in class­es in our plu­g­in that come baked in:


const DEFAULT_IMAGE_TRANSFORM_TYPES = [
        CraftImageTransform::class,
        ImgixImageTransform::class,
        ThumborImageTransform::class,
    ];

Then we want to be able to let peo­ple just composer require an arbi­trary Com­pos­er pack­age that imple­ments our ImageTransformInterface, and let Ima­geOp­ti­mize know about it with­out any addi­tion­al code. As an exam­ple, check out the craft-ima­geop­ti­mize-imgix and craft-ima­geop­ti­mize-thum­bor packages.

We can do that by hav­ing a prop­er­ty in our Set­tings mod­el (and thus also in the mul­ti-envi­ron­ment config/image-optimize.php):


// The default Image Transform type classes
'defaultImageTransformTypes' => [
],

This allows peo­ple to just add the appro­pri­ate class to their config/image-optimize.php mul­ti-envi­ron­ment con­fig, which we merge into the built-in Image Transforms:


$imageTransformTypes = array_unique(array_merge(
        ImageOptimize::$plugin->getSettings()->defaultImageTransformTypes ?? [],
        self::DEFAULT_IMAGE_TRANSFORM_TYPES
    ), SORT_REGULAR);

So this is awe­some, peo­ple can add their own Image Trans­form to our plu­g­in with­out writ­ing a cus­tom mod­ule or plu­g­in to pro­vide it.

But peo­ple might also want to wrap their Image Trans­form in a plu­g­in (to make it eas­i­ly user-instal­lable from the Craft Plu­g­in Store) or cus­tom site mod­ule. We can do this by trig­ger­ing an event that modules/​plugins can lis­ten to in order to reg­is­ter the Image Trans­forms that they provide:


use craft\events\RegisterComponentTypesEvent;

...

    const EVENT_REGISTER_IMAGE_TRANSFORM_TYPES = 'registerImageTransformTypes';

...

        $event = new RegisterComponentTypesEvent([
            'types' => $imageTransformTypes
        ]);
        $this->trigger(self::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES, $event);

Obser­vant read­ers will note that this is the exact method that Craft uses to allow plu­g­ins to reg­is­ter addi­tion­al Field types, and oth­er func­tion­al­i­ty. On the module/​plugin side of things, the code they’d have to imple­ment would just look like this:


use vendor\package\imagetransforms\MyImageTransform;

use nystudio107\imageoptimize\services\Optimize;
use craft\events\RegisterComponentTypesEvent;
use yii\base\Event;

Event::on(Optimize::class,
     Optimize::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES,
     function(RegisterComponentTypesEvent $event) {
         $event->types[] = MyImageTransform::class;
     }
);

Beau­ti­ful! Now we can write our our Image Trans­forms eas­i­ly, and oth­er devel­op­ers can add their own Image Trans­forms how­ev­er they want.

We do still have one oth­er sub­ject to cov­er, which is how exact­ly do we allow Image Trans­forms to present their own GUI for set­tings, and save those settings?

Edit­ing and Stor­ing Com­po­nent Settings

Since our Image Trans­form com­po­nents extend from SavableComponent, we get the scaf­fold­ing we need in order to dis­play a GUI for edit­ing plu­g­in set­tings, as well as sav­ing our settings!

To present a GUI, we just need to imple­ment the getSettingsHtml() method; here’s an exam­ple from ImgixImageTransform:


/**
 * @inheritdoc
 */
public function getSettingsHtml()
{
    return Craft::$app->getView()->renderTemplate('imgix-image-transform/settings/image-transforms/imgix.twig', [
        'imageTransform' => $this,
    ]);
}

We’re just pass­ing in our com­po­nent in imageTransform and ren­der­ing a Twig template:


{% from 'image-optimize/_includes/macros' import configWarning %}

{% import "_includes/forms" as forms %}

<!-- imgixDomain -->
{{ forms.textField({
    label: 'Imgix Source Domain',
    instructions: "The source domain to use for the Imgix transforms."|t('image-optimize'),
    id: 'domain',
    name: 'domain',
    value: imageTransform.domain,
    warning: configWarning('imageTransformTypeSettings', 'image-optimize'),
}) }}
<!-- imgixApiKey -->
{{ forms.textField({
    label: 'Imgix API Key',
    instructions: "The API key to use for the Imgix transforms (needed for auto-purging changed assets)."|t('image-optimize'),
    id: 'apiKey',
    name: 'apiKey',
    value: imageTransform.apiKey,
    warning: configWarning('imageTransformTypeSettings', 'image-optimize'),
}) }}
<!-- imgixSecurityToken -->
{{ forms.textField({
    label: 'Imgix Security Token',
    instructions: "The optional [security token](https://docs.imgix.com/setup/securing-images) used to sign image URLs from Imgix."|t('image-optimize'),
    id: 'securityToken',
    name: 'securityToken',
    value: imageTransform.securityToken,
    warning: configWarning('imageTransformTypeSettings', 'image-optimize'),
}) }}

Since the prop­er­ties on our Image Trans­form class are used as the sav­able set­tings (just like for Craft Fields and Wid­gets), we just have those prop­er­ties in ImgixImageTransform.php:


// Public Properties
// =========================================================================

/**
 * @var string
 */
public $domain;

/**
 * @var string
 */
public $apiKey;

/**
 * @var string
 */
public $securityToken;

To actu­al­ly ren­der the Image Trans­for­m’s set­tings, we just do this in Ima­geOp­ti­mize’s _settings.twig:


<!-- transformClass -->
{{ forms.selectField({
    label: "Transform Method"|t('image-optimize'),
    instructions: "Choose from Craft native transforms or an image transform service to handle your image transforms site-wide."|t('image-optimize'),
    id: 'transformClass',
    name: 'transformClass',
    value: settings.transformClass,
    options: imageTransformTypeOptions,
    class: 'io-transform-method',
    warning: configWarning('transformClass', 'image-optimize'),
}) }}

{% for type in allImageTransformTypes %}
    {% set isCurrent = (type == className(imageTransform)) %}
    <div id="{{ type|id }}" class="io-method-settings {{ 'io-' ~ type|id ~ '-method' }}" {% if not isCurrent %} style="display: none;"{% endif %}>
        {% namespace 'imageTransformTypeSettings['~type~']' %}
            {% set _imageTransform = isCurrent ? imageTransform : craft.imageOptimize.createImageTransformType(type) %}
            {{ _imageTransform.getSettingsHtml()|raw }}
        {% endnamespace %}
    </div>
{% endfor %}

This just presents a Drop­down for select­ing the Trans­form Method to use, and then loops through the avail­able Image Trans­form com­po­nents, and ren­ders their set­tings HTML.

We then store the result in our Set­tings mod­el (so that it works nice­ly with Craft CMS 3.1’s Project Con­fig) in the imageTransformTypeSettings prop­er­ty as an array. The key is the ful­ly qual­i­fied class name of the Image Trans­form, and the val­ue is an array that con­tains what­ev­er set­tings that Image Trans­form provides.

This is what the ImageOptimize.php main plu­g­in class’s settingsHtml() method looks like:


/**
 * @inheritdoc
 */
public function settingsHtml()
{
    // Get only the user-editable settings
    $settings = $this->getSettings();

    // Get the image transform types
    $allImageTransformTypes = ImageOptimize::$plugin->optimize->getAllImageTransformTypes();
    $imageTransformTypeOptions = [];
    /** @var ImageTransformInterface $class */
    foreach ($allImageTransformTypes as $class) {
        if ($class::isSelectable()) {
            $imageTransformTypeOptions[] = [
                'value' => $class,
                'label' => $class::displayName(),
            ];
        }
    }
    // Sort them by name
    ArrayHelper::multisort($imageTransformTypeOptions, 'label');

    // Render the settings template
    try {
        return Craft::$app->getView()->renderTemplate(
            'image-optimize/settings/_settings.twig',
            [
                'settings' => $settings,
                'gdInstalled' => \function_exists('imagecreatefromjpeg'),
                'imageTransformTypeOptions' => $imageTransformTypeOptions,
                'allImageTransformTypes' => $allImageTransformTypes,
                'imageTransform' => ImageOptimize::$plugin->transformMethod,
            ]
        );
    } catch (\Twig_Error_Loader $e) {
        Craft::error($e->getMessage(), __METHOD__ );
    } catch (Exception $e) {
        Craft::error($e->getMessage(), __METHOD__ );
    }

    return '';
}

Go Forth and Component-ize!

This is quite a bit to digest, but I think it’ll help with cre­at­ing plu­g­ins that offer exten­si­bil­i­ty in a very Yii2/Craft-like man­ner. Armed with this knowl­edge, you can go forth and make awe­some, exten­si­ble plugins!

Go Forth And Explore

It’s cer­tain­ly friend­lier long-term than hard-cod­ing it all into your plu­g­in (with all of the asso­ci­at­ed PRs from peo­ple want­i­ng to add func­tion­al­i­ty), and it’s more man­age­able than requir­ing addi­tion­al plu­g­ins being installed.

It pro­vides the struc­ture that will help you archi­tect your plu­g­in well, and also allows for great flex­i­bil­i­ty in allow­ing oth­ers to extend it with addi­tion­al functionality.

This tech­nique could also eas­i­ly be used for a suite of plu­g­ins that rely on the same core func­tion­al­i­ty. Instead of requir­ing a shared plu­g­in be installed, sim­ply com­po­nent-ize the need­ed func­tion­al­i­ty, and add it in as a com­pos­er pack­age dependency.

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Instrument, monitor, fix: a hands-on debugging session

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️