Andrew Welch · Insights · #plugin #craftcms #webdev

Published , updated · 5 min read ·


For more tools, technologies, and techniques, check out the devMode.fm podcast!

Cutting the Cord: Removing Craft CMS 3 Plugins

Plu­g­ins are a fan­tas­tic way to add func­tion­al­i­ty to Craft CMS 3. How­ev­er many times, you don’t need them at all. Let’s unplug!

Plug-wall-outlets-plugin

Craft CMS has a Plu­g­in Store that allows you to extend the func­tion­al­i­ty of Craft CMS with a sim­ple click.

While there are some fan­tas­tic plu­g­ins out there, this arti­cle is going to focus on unplug­ging: get­ting rid of plu­g­ins you don’t actu­al­ly need.

This may seem strange com­ing from me, giv­en that I cre­at­ed plug​in​fac​to​ry​.io and a num­ber of plu­g­ins such as SEO­mat­ic, Retour, Webperf, and Ima­geOp­ti­mize.

How­ev­er real­ly, it’s a tes­ta­ment to how much func­tion­al­i­ty Pix­el & Ton­ic has exposed in Twig via craft.app.

Link Unplugging

I have no bias against plu­g­ins, in fact I have a great affin­i­ty for them. Many of the plu­g­ins I wrote, I cre­at­ed only after get­ting tired of wast­ing many hours try­ing to do it manually.”

So don’t take any of this as an indict­ment of plu­g­ins in gen­er­al. They can be fan­tas­tic, save you time, and allow you to pro­duce a bet­ter qual­i­ty prod­uct for your client on time, and on budget.

But there’s noth­ing wrong with rec­og­niz­ing cas­es where we might not need a plu­g­in at all. And we’ll learn a whole lot more about Craft CMS in the process, too.

And that’s the real goal here: exploring the power Craft CMS makes available to you.

The Craft Plu­g­in Store recent­ly passed the 500 plu­g­in mark, and there are many plu­g­ins there that are absolute­ly worth the price in terms of the time they save, and the val­ue they add.

How­ev­er, we’re going to focus on some plu­g­ins you just plain don’t need. Start­ing with a few plu­g­ins that I wrote!

Fried-outlet

The rea­son you don’t need these plu­g­ins is because you can accom­plish what they do native­ly. The real point of this arti­cle is an explo­ration into the untapped poten­tial that you may not be aware is wait­ing for you.

So let’s get to it, and see how we can get rid of some plu­g­ins entire­ly! I’m focus­ing on 3 plu­g­ins that I wrote, so that no one gets mad at me for say­ing their plu­g­in does­n’t need to be used.

But there are def­i­nite­ly plen­ty of oth­er plu­g­ins around that could be added to this list as well. So here we go!

Link Plugin #1 - Eager Beaver

When I orig­i­nal­ly wrote the Eager Beaver plu­g­in for Craft 2.x, there was no native way to eager load rela­tions for Entry and oth­er ele­ments that were auto-inject­ed into templates.

For more infor­ma­tion on eager load­ing, check out the Speed up your Craft CMS Tem­plates with Eager Load­ing article.

But with Craft CMS 3, the craft.app. vari­able has pret­ty much every ser­vice that’s avail­able in PHP also avail­able in Twig. So we can access the elements ser­vice like this:

{% do craft.app.elements.eagerLoadElements(
    className(entry),
    [entry],
    ['assetsField', 'categoriesField.assetField', 'matrixField.blockType:assetsField']
) %}

This calls the eagerLoadElements method in the craft\services\Elements.php ser­vice class, which looks like this:

/**
 * Eager-loads additional elements onto a given set of elements.
 *
 * @param string $elementType The root element type class
 * @param ElementInterface[] $elements The root element models that should be updated with the eager-loaded elements
 * @param string|array $with Dot-delimited paths of the elements that should be eager-loaded into the root elements
 */
public function eagerLoadElements(string $elementType, array $elements, $with)

Now, if this all just seems like too much work, or you have a Craft 2.x code base that used Eager Beaver, sure. By all means, use the plugin.

But you don’t have to!

Link Plugin #2 - Cookies

Cook­ies is anoth­er plu­g­in I wrote for Craft 2.x that I port­ed over to Craft CMS 3. There is a native way to access cook­ies via Twig, by get­ting a Cook­ieCol­lec­tion object from either the request or the response.

From the Yii2 docs:

Yii rep­re­sents each cook­ie as an object of yii\web\Cookie. Both yii\web\Request and yii\web\Response main­tain a col­lec­tion of cook­ies via the prop­er­ty named cookies. The cook­ie col­lec­tion in the for­mer rep­re­sents the cook­ies sub­mit­ted in a request, while the cook­ie col­lec­tion in the lat­ter rep­re­sents the cook­ies that are to be sent to the user.

If this seems a lit­tle con­fus­ing, remem­ber that there are both client-side cook­ies (set via JavaScript), and serv­er-side cook­ies (set via PHP). So let’s have a look:

{% set requestCookies = craft.app.request.cookies %}
{% set responseCookies = craft.app.response.cookies %}

{{ dump(requestCookies) }}
{{ dump(responseCookies) }}

This will out­put some­thing like the fol­low­ing (assum­ing devMode is on). First the requestCookies:

object(yii\web\CookieCollection)[3073]
  public 'readOnly' => boolean true
  private '_cookies' => 
    array (size=3)
      '1031b8c41dfff97a311a7ac99863bdc5_username' => 
        object(yii\web\Cookie)[2797]
          public 'name' => string '1031b8c41dfff97a311a7ac99863bdc5_username' (length=41)
          public 'value' => string 'andrew@nystudio107.com' (length=22)
          public 'domain' => string '' (length=0)
          public 'expire' => null
          public 'path' => string '/' (length=1)
          public 'secure' => boolean false
          public 'httpOnly' => boolean true
      '1031b8c41dfff97a311a7ac99863bdc5_identity' => 
        object(yii\web\Cookie)[3819]
          public 'name' => string '1031b8c41dfff97a311a7ac99863bdc5_identity' (length=41)
          public 'value' => string '["1","[\"6cNs0MuhrKlBTKLp73-ouXSLN2QAiYRucOPWX0M_mG90O7VrflwZu5bKPzNMjxlt8ifXWQW0s7k4VyPyForuCmEDx44BRIetqa9m\",null,\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36\"]",3600]' (length=250)
          public 'domain' => string '' (length=0)
          public 'expire' => null
          public 'path' => string '/' (length=1)
          public 'secure' => boolean false
          public 'httpOnly' => boolean true
      'CRAFT_CSRF_TOKEN' => 
        object(yii\web\Cookie)[2810]
          public 'name' => string 'CRAFT_CSRF_TOKEN' (length=16)
          public 'value' => string 'G4vpaSZSS_t5Qb_fhHZpouhulokN6E9zBPFUgFRk|2743f0ddc0782509eb23bb440949233522120c5f1dea511adc984e6eae3c763aG4vpaSZSS_t5Qb_fhHZpouhulokN6E9zBPFUgFRk|1|$2y$13$PPuGgVpBHBXmfnttZNiFjOaB/ij7PA/EzLtiv69LdOLKkFvScE6ja' (length=208)
          public 'domain' => string '' (length=0)
          public 'expire' => null
          public 'path' => string '/' (length=1)
          public 'secure' => boolean false
          public 'httpOnly' => boolean true

Next the responseCookies:

object(yii\web\CookieCollection)[2794]
  public 'readOnly' => boolean false
  private '_cookies' => 
    array (size=1)
      '1031b8c41dfff97a311a7ac99863bdc5_identity' => 
        object(yii\web\Cookie)[2796]
          public 'name' => string '1031b8c41dfff97a311a7ac99863bdc5_identity' (length=41)
          public 'value' => string '["1","[\"6cNs0MuhrKlBTKLp73-ouXSLN2QAiYRucOPWX0M_mG90O7VrflwZu5bKPzNMjxlt8ifXWQW0s7k4VyPyForuCmEDx44BRIetqa9m\",null,\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36\"]",3600]' (length=250)
          public 'domain' => string '' (length=0)
          public 'expire' => int 1552795358
          public 'path' => string '/' (length=1)
          public 'secure' => boolean false
          public 'httpOnly' => boolean true

So if we want­ed to read the val­ue of a the cook­ie named CRAFT_CSRF_TOKEN we could do some­thing like this:

{% set requestCookies = craft.app.request.cookies %}
{% set csrfCookie = requestCookies.get('CRAFT_CSRF_TOKEN') %}
{% if csrfCookie and csrfCookie.value != '' %}
    {{ dump(csrfCookie) }}
{% endif %}

This will return a Cook­ie object to us:

object(yii\web\Cookie)[2873]
  public 'name' => string 'CRAFT_CSRF_TOKEN' (length=16)
  public 'value' => string 'G4vpaSZSS_t5Qb_fhHZpouhulokN6E9zBPFUgFRk|2743f0ddc0782509eb23bb440949233522120c5f1dea511adc984e6eae3c763aG4vpaSZSS_t5Qb_fhHZpouhulokN6E9zBPFUgFRk|1|$2y$13$PPuGgVpBHBXmfnttZNiFjOaB/ij7PA/EzLtiv69LdOLKkFvScE6ja' (length=208)
  public 'domain' => string '' (length=0)
  public 'expire' => null
  public 'path' => string '/' (length=1)
  public 'secure' => boolean false
  public 'httpOnly' => boolean true

So this is great, we can use Twig to look at exist­ing cook­ies with­out a plu­g­in. But what if we want to mod­i­fy cook­ie val­ues, or set new cookies?

Notice that the requests Cook­ieCol­lec­tion is public 'readOnly' => boolean true. This makes sense, we can’t mod­i­fy client-side cook­ies that have already been sent in the request. But we can mod­i­fy any cook­ies that are set in the response just by find­ing the cook­ie, and chang­ing the value.

But what about set­ting new serv­er-side cook­ies in the response? Until Craft CMS 3.1.16 we were out of luck, because while a Cook­ieCol­lec­tion object does have a .add() method, the first para­me­ter has to be a yii\web\Cookie object, and we had no way to cre­ate one!

Now in Craft CMS 3.1.16 or lat­er, we have a new cre­ate() Twig func­tion that allows us to cre­ate any PHP object we want right in our Twig tem­plates. It ends up call­ing Craft::createObject() which looks like this:

/**
 * Creates a new object using the given configuration.
 *
 * You may view this method as an enhanced version of the `new` operator.
 * The method supports creating an object based on a class name, a configuration array or
 * an anonymous function.
 *
 * Below are some usage examples:
 *
 * ```php
 * // create an object using a class name
 * $object = Yii::createObject('yii\db\Connection');
 *
 * // create an object using a configuration array
 * $object = Yii::createObject([
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
 *     'username' => 'root',
 *     'password' => '',
 *     'charset' => 'utf8',
 * ]);
 *
 * // create an object with two constructor parameters
 * $object = \Yii::createObject('MyClass', [$param1, $param2]);
 * ```
 *
 * Using [[\yii\di\Container|dependency injection container]], this method can also identify
 * dependent objects, instantiate them and inject them into the newly created object.
 *
 * @param string|array|callable $type the object type. This can be specified in one of the following forms:
 *
 * - a string: representing the class name of the object to be created
 * - a configuration array: the array must contain a `class` element which is treated as the object class,
 *   and the rest of the name-value pairs will be used to initialize the corresponding object properties
 * - a PHP callable: either an anonymous function or an array representing a class method (`[$class or $object, $method]`).
 *   The callable should return a new instance of the object being created.
 *
 * @param array $params the constructor parameters
 * @return object the created object
 * @throws InvalidConfigException if the configuration is invalid.
 * @see \yii\di\Container
 */
public static function createObject($type, array $params = [])

This may seem over­ly com­pli­cat­ed, but the long and short of it is that we can pass in the class name of the object we want to cre­ate, as well as any key/​value pairs that define the default prop­er­ties of the object. So to make a new cook­ie, we can do:

{% set responseCookies = craft.app.response.cookies %}
{% set cookie = create({
    'class': 'yii\\web\\Cookie',
    'name': 'cookie-monster',
    'value': 'coooooooookkkiiiiiiieeeeee!',
    'expire': now | date_modify("+1 hour").timestamp
}) %}
{% do responseCookies.add(cookie) %}
{{ dump(responseCookies) }}

Boom, we just added a cook­ie to the response that will be sent back to the client:

object(yii\web\CookieCollection)[2994]
  public 'readOnly' => boolean false
  private '_cookies' => 
    array (size=2)
      '1031b8c41dfff97a311a7ac99863bdc5_identity' => 
        object(yii\web\Cookie)[2996]
          public 'name' => string '1031b8c41dfff97a311a7ac99863bdc5_identity' (length=41)
          public 'value' => string '["1","[\"6cNs0MuhrKlBTKLp73-ouXSLN2QAiYRucOPWX0M_mG90O7VrflwZu5bKPzNMjxlt8ifXWQW0s7k4VyPyForuCmEDx44BRIetqa9m\",null,\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36\"]",3600]' (length=250)
          public 'domain' => string '' (length=0)
          public 'expire' => int 1552798667
          public 'path' => string '/' (length=1)
          public 'secure' => boolean false
          public 'httpOnly' => boolean true
      'cookie-monster' => 
        object(yii\web\Cookie)[4352]
          public 'name' => string 'cookie-monster' (length=14)
          public 'value' => string 'coooooooookkkiiiiiiieeeeee!' (length=27)
          public 'domain' => string '' (length=0)
          public 'expire' => int 1552798667
          public 'path' => string '/' (length=1)
          public 'secure' => boolean false
          public 'httpOnly' => boolean true

We can use a sim­i­lar tech­nique to mod­i­fy an exist­ing cook­ie, just cre­ate a new Cook­ie object with the name prop­er­ty set to the cook­ie we want to mod­i­fy, and add() it in. It’ll replace the exist­ing cookie.

So there you go, you can check the val­ue of cook­ies in Twig, and you can add or mod­i­fy cook­ies in Twig too.

If this all seems like a bit too much work, then I’m not going to fault you for using my Cook­ies plu­g­in. But again, you don’t have to.

For a full macro you can just {% include '_macros/cookies.twig' %}, check out _macros/cookies.twig.

Link Plugin #3 - Typogrify

Anoth­er plu­g­in I wrote called Typogri­fy is very sim­i­lar to the Word­Smith plu­g­in (both are free) that allows you to do some very cool things such as auto­mat­ic hyphen­ation, wid­ow pre­ven­tion, auto­mat­ic smart” print­er’s quotes, and a bunch more.

One fea­ture they both also offer is the abil­i­ty to do more inter­est­ing string manip­u­la­tion and trun­ca­tion. That aspect of both plu­g­ins, we can replace with native Twig!

Craft CMS 3 includes the PHP pack­age Stringy which adds some won­der­ful string manip­u­la­tion func­tions. Since Craft includes it, we can get at it:

{# This outputs "The quick…" #}
{% set stringy = create(
    "Stringy\\Stringy",
    ["The quick brown fox jumps over the lazy dog"]
) %}
{{ stringy.safeTruncate(10, "…") }}

This will output:

The quick…

We’re using the same create() Twig func­tion that we saw in the Cook­ies exam­ple, but this time instead of pass­ing in a con­fig­u­ra­tion array as the first para­me­ter, we’re pass­ing in a string with the class name.

Then for the sec­ond para­me­ter, we’re pass­ing in an array of val­ues to pass into the objec­t’s constructor: 

/**
 * Initializes a Stringy object and assigns both str and encoding properties
 * the supplied values. $str is cast to a string prior to assignment, and if
 * $encoding is not specified, it defaults to mb_internal_encoding(). Throws
 * an InvalidArgumentException if the first argument is an array or object
 * without a __toString method.
 *
 * @param  mixed  $str      Value to modify, after being cast to string
 * @param  string $encoding The character encoding
 * @throws \InvalidArgumentException if an array or object without a
 *         __toString method is passed as the first argument
 */
public function __construct($str = '', $encoding = null)
{

This gives us a Stringy object in the Twig vari­able stringy that we can do what­ev­er we want with!

Safe­ly trun­cat­ing on a string is a super use­ful thing to be able to do, but Stringy also offers a mas­sive num­ber of oth­er string manip­u­la­tion func­tions too:

Stringy string manipulation functions

Stringy string manip­u­la­tion functions

And now you have them all in Twig!

Grant­ed, both plu­g­ins do more than just string manip­u­la­tion, but this may be all you need!

Pro tip: This same tech­nique works for any of Craft’s helper” class­es, too. So if you need­ed the UrlHelper class, you could do:

{# Use any Craft "helper" classes in your Twig templates by creating them #}
{% set urlHelper = create(
    "craft\\helpers\\UrlHelper"
) %}
{# You can then call any of their methods, e.g.: #}
{{ urlHelper.rootRelativeUrl(craft.app.config.general.resourceBaseUrl) }}

Link Plugin #4 - Connect

Con­nect is a plu­g­in I wrote to allow you to con­nect to external/​remote data­bas­es from your Twig tem­plates. I thought it was pret­ty cool, until Brad Bell not­ed that you can do this with­out a plu­g­in entirely.

In your config/app.php you can put some­thing like:

return [
    'components' => [
        'otherDb' => [
            'class' => craft\db\Connection::class,
            'driverName' => 'mysql',
            'dsn' => 'mysql:host=localhost;dbname=mydb;port=3306;',
            'username' => getenv('OTHER_DB_USER'),
            'password' => getenv('OTHER_DB_PASSWORD'),
            'charset' => 'utf8',
            'tablePrefix' => '',
            'enableSchemaCache' => !YII_DEBUG,
        ],
    ],
]

…and then in your .env you’d need to define both OTHER_DB_USER & OTHER_DB_PASSWORD.

Then via Twig you can do some­thing like:

{% set dbCmd = craft.app.otherDb.createCommand() %}
{% do dbCmd.update('someTable', someData).execute() %}

The createCommand() method returns a Com­mand object, and from there, you can do pret­ty much any­thing you want.

You can also use craft.query() to return a new craft\db\Query object, and you can pass your cus­tom db con­nec­tion in to the .all() (or what­ev­er) method:

{% set results = craft.query()
    .from('table')
    .where({
        'column': 'value'
    })
    .limit(10)
    .all(craft.app.otherDb)
%}

I think this is a par­tic­u­lar­ly nifty exam­ple, in that it shows how cus­tomiz­able Craft CMS is. In config/app.php we have access to the con­fig­u­ra­tion of the entire Yii2 web app that is Craft CMS.

Think about that. Via a simple config file, we can radically customize how Craft CMS works.

We added a com­po­nent, con­fig­ured it to con­nect to our remote data­base, and now that com­po­nent is avail­able sys­tem-wide (and in Twig, too). Oh yeah!

Then it also demon­strates how just by expos­ing these APIs via craft.app. into our tem­plates, we’ve been giv­en enor­mous pow­er to do a whole lot of things via Twig.

But should we?

Link How far should we unplug?

I think it’s super cool that all of this pow­er can be exposed in Twig, and a mess of com­pli­cat­ed things can be done via Twig with­out any plugins.

How­ev­er, remem­ber that Twig is a tem­plat­ing lan­guage. I’m going to say that again:

Remember that Twig is a templating language.

Yes, you can do all of these things, and more! How­ev­er I think we should adopt some bal­ance in our approach.

Thanos-balance-universe

No one wins a con­so­la­tion prize for cre­at­ing a web­site entire­ly with­out plu­g­ins. In fact, it can be a detri­ment to you in terms of your time, and a detri­ment to your client in terms of costs if you do every­thing bespoke.

There’s also a vast dif­fer­ence between the price of some­thing, and its cost. A free but unsup­port­ed plu­g­in can cost far more than a paid but main­tained & sup­port­ed plugin.

It’s also fair­ly obvi­ous that if you bill out at $100/​hr and it’ll take you 3 hours to imple­ment some­thing cus­tom in Twig that a $59 plu­g­in will do just as well… you’re cost­ing your­self money.

Put val­ue not just on dol­lars, but on your time and effort.

Every line of code we write is tech debt; it’s just a matter of who, if anyone, is servicing that debt.

It’s also very unlike­ly that you’ll go back to fix or enhance your bespoke work for clients, unless you’re being paid to do so. But a plu­g­in devel­op­er often will.

So adopt some bal­ance in terms of what you imple­ment native­ly” and what you use a plu­g­in for.

Typ­i­cal­ly it’s con­sid­ered to be good prac­tice to keep busi­ness log­ic out of our tem­plates, and instead use Twig as a con­ve­nient way to ren­der that data. Check out the arti­cle Han­dling Errors Grace­ful­ly in Craft CMS arti­cle for more on that distinction.

A nice rule of thumb is that if more than half of the page in your editor is Twig code, you’ve probably put too much business logic in your templates.

So, use plu­g­ins where they save you sig­nif­i­cant time or offer sig­nif­i­cant func­tion­al­i­ty. And con­sid­er look­ing into going native” in cas­es where a plu­g­in pro­vides rel­a­tive­ly triv­ial functionality.

Also avoid putting too much busi­ness log­ic” into your Twig tem­plates. Just because you can does­n’t mean you should.

Link Use a Module!

If for what­ev­er rea­son your cus­tom func­tion­al­i­ty isn’t avail­able via the Craft APIs (more on that below), you could also con­sid­er putting that busi­ness log­ic code into a module.

For every Craft 3 web­site I cre­ate, I have a site mod­ule built into it. It’s pret­ty emp­ty ini­tial­ly, just pro­vid­ing some cus­tom CSS to allow me to cus­tomize the clien­t’s login screen.

But the main rea­son the site mod­ule is there is for sit­u­a­tions like this. If I need to offload a bit of core func­tion­al­i­ty to the mod­ule, it’s already there wait­ing for me.

You can read all about how to do this in the Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule article.

Link Unplugged Nirvana

I’ve pro­vid­ed a few con­crete exam­ples of ways you can use Craft CMS in a native way instead of using plu­g­ins. But how do we get to reach native nir­vana and dis­cov­er what we can do ourselves?

Craft-cms-unpluged-nirvana

As a plu­g­in devel­op­er, I’m aware of many under the hood” Craft APIs that if I did only fron­tend work, it’s unlike­ly I’d ever real encounter.

This is excel­lent design, by the way. Good soft­ware is deep, not broad. Craft CMS lets you use it at what­ev­er lay­ers are most appro­pri­ate for a giv­en project.

From tra­di­tion­al web­sites to head­less API providers to cus­tom apps, Craft CMS allows you do all of these things.

You get to choose how deeply down the rabbit hole you want to go.

But let’s assume we are inter­est­ed in uncov­er­ing oth­er native func­tion­al­i­ty, where can we look? A good place to start is the Craft 3 Class Ref­er­ence.

This is a list of every Ser­vice that Craft makes avail­able to your Twig tem­plates off of the craft.app. variable.

So for instance the Entries Ser­vice is avail­able at craft.app.entries. — wait, you already knew that! You’ve been using Craft’s ser­vices direct­ly all along, whether you knew it or not.

And there are many oth­er Ser­vices that are avail­able, just wait­ing for you to uncov­er them.

Turn-on-tune-in-drop-out

Turn on, tune in, unplug

Anoth­er place to look is the Craft CMS source code! For exam­ple, we can take a look at the default config/app.php that Craft uses to con­fig­ure itself to uncov­er things we can cus­tomize in new and cool ways.

In fact the config/app.php is so pow­er­ful, it’s prob­a­bly some­thing we should devote an entire future arti­cle to…

If you use Php­Storm, check out the Auto-Com­plete Craft CMS 3 APIs in Twig with Php­Storm arti­cle for a pret­ty fan­tas­tic way to explore the Craft CMS APIs via Twig interactively.

Hap­py exploring!