Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Cutting the Cord: Removing Craft CMS 3 Plugins
Plugins are a fantastic way to add functionality to Craft CMS 3. However many times, you don’t need them at all. Let’s unplug!
Craft CMS has a Plugin Store that allows you to extend the functionality of Craft CMS with a simple click.
While there are some fantastic plugins out there, this article is going to focus on unplugging: getting rid of plugins you don’t actually need.
This may seem strange coming from me, given that I created pluginfactory.io and a number of plugins such as SEOmatic, Retour, Webperf, and ImageOptimize.
However really, it’s a testament to how much functionality Pixel & Tonic has exposed in Twig via craft.app.
Link Unplugging
I have no bias against plugins, in fact I have a great affinity for them. Many of the plugins I wrote, I created only after getting tired of wasting many hours trying to “do it manually.”
So don’t take any of this as an indictment of plugins in general. They can be fantastic, save you time, and allow you to produce a better quality product for your client on time, and on budget.
But there’s nothing wrong with recognizing cases where we might not need a plugin 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 Plugin Store recently passed the 500 plugin mark, and there are many plugins there that are absolutely worth the price in terms of the time they save, and the value they add.
However, we’re going to focus on some plugins you just plain don’t need. Starting with a few plugins that I wrote!
The reason you don’t need these plugins is because you can accomplish what they do natively. The real point of this article is an exploration into the untapped potential that you may not be aware is waiting for you.
So let’s get to it, and see how we can get rid of some plugins entirely! I’m focusing on 3 plugins that I wrote, so that no one gets mad at me for saying their plugin doesn’t need to be used.
But there are definitely plenty of other plugins around that could be added to this list as well. So here we go!
Link Plugin #1 - Eager Beaver
When I originally wrote the Eager Beaver plugin for Craft 2.x, there was no native way to eager load relations for Entry and other elements that were auto-injected into templates.
For more information on eager loading, check out the Speed up your Craft CMS Templates with Eager Loading article.
But with Craft CMS 3, the craft.app. variable has pretty much every service that’s available in PHP also available in Twig. So we can access the elements service 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 service 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
Cookies is another plugin I wrote for Craft 2.x that I ported over to Craft CMS 3. There is a native way to access cookies via Twig, by getting a CookieCollection object from either the request or the response.
From the Yii2 docs:
Yii represents each cookie as an object of yii\web\Cookie. Both yii\web\Request and yii\web\Response maintain a collection of cookies via the property named cookies. The cookie collection in the former represents the cookies submitted in a request, while the cookie collection in the latter represents the cookies that are to be sent to the user.
If this seems a little confusing, remember that there are both client-side cookies (set via JavaScript), and server-side cookies (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 output something like the following (assuming 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 wanted to read the value of a the cookie named CRAFT_CSRF_TOKEN we could do something 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 Cookie 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 existing cookies without a plugin. But what if we want to modify cookie values, or set new cookies?
Notice that the request’s CookieCollection is public 'readOnly' => boolean true. This makes sense, we can’t modify client-side cookies that have already been sent in the request. But we can modify any cookies that are set in the response just by finding the cookie, and changing the value.
But what about setting new server-side cookies in the response? Until Craft CMS 3.1.16 we were out of luck, because while a CookieCollection object does have a .add() method, the first parameter has to be a yii\web\Cookie object, and we had no way to create one!
Now in Craft CMS 3.1.16 or later, we have a new create() Twig function that allows us to create any PHP object we want right in our Twig templates. It ends up calling 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 overly complicated, but the long and short of it is that we can pass in the class name of the object we want to create, as well as any key/value pairs that define the default properties of the object. So to make a new cookie, 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 cookie 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 similar technique to modify an existing cookie, just create a new Cookie object with the name property set to the cookie we want to modify, and add() it in. It’ll replace the existing cookie.
So there you go, you can check the value of cookies in Twig, and you can add or modify cookies in Twig too.
If this all seems like a bit too much work, then I’m not going to fault you for using my Cookies plugin. But again, you don’t have to.
For a full macro you can just {% include '_macros/cookies.twig' %}, check out _macros/cookies.twig.
{#
# Cookies
#
# Macros for getting and setting cookies natively with Craft CMS 3.1.17 or later
#
# Usage:
# {% import "_macros/cookies.twig" as cookies %}
# Get a cookie:
# {% set woof = cookies.get('woof') %}
# Set a cookie:
# {% do cookies.set('woof', 'bark', now | date_modify('+1 hour').timestamp) %}
#
# @package nystudio107\craft
# @since 1.0.0
# @author nystudio107
# @link https://nystudio107.com
# @copyright Copyright (c) 2019 nystudio107
# @license MIT
# @license https://opensource.org/licenses/MIT MIT Licensed
#}
{#
# Return the value the cookie specified in `name`. The return value will be an
# empty string if the cookie doesn't exist.
#
# @param string name
# @param yii\web\CookieCollection cookieJar
#
# @return string className
#}
{%- macro get(name, cookieJar = null) -%}
{% spaceless %}
{% set cookieJar = cookieJar ?? craft.app.request.cookies %}
{% set cookie = cookieJar.get(name) %}
{{ cookie.value ?? '' }}
{% endspaceless %}
{%- endmacro -%}
{#
# Set the cookie specified in `name` to the value specified in `value`, with an
# expiration set to the number of seconds in `expires`. You can use an
# expression like `now | date_modify("+1 hour").timestamp` to set `expires`.
# c.f.: https://www.php.net/manual/en/function.setcookie.php
#
# @param string name
# @param string value
# @param int expires
# @param string path
# @param string domain
# @param bool secure
# @param bool httpOnly
# @param yii\web\CookieCollection cookieJar
#}
{%- macro set(name, value = '', expires = 0, path = '/', domain = '', secure = false, httpOnly = false, cookieJar = null) -%}
{% spaceless %}
{% set cookieJar = cookieJar ?? craft.app.response.cookies %}
{% set domain = domain | length ?: craft.app.config.general.defaultCookieDomain %}
{% set cookie = create({
'class': 'yii\\web\\Cookie',
'name': name,
'value': value,
'path': path,
'domain': domain,
'secure': secure,
'httpOnly': httpOnly,
'expire': expires
}) %}
{% do cookieJar.add(cookie) %}
{% endspaceless %}
{%- endmacro -%}
Link Plugin #3 - Typogrify
Another plugin I wrote called Typogrify is very similar to the WordSmith plugin (both are free) that allows you to do some very cool things such as automatic hyphenation, widow prevention, automatic “smart” printer’s quotes, and a bunch more.
One feature they both also offer is the ability to do more interesting string manipulation and truncation. That aspect of both plugins, we can replace with native Twig!
Craft CMS 3 includes the PHP package Stringy which adds some wonderful string manipulation functions. 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 function that we saw in the Cookies example, but this time instead of passing in a configuration array as the first parameter, we’re passing in a string with the class name.
Then for the second parameter, we’re passing in an array of values to pass into the object’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 variable stringy that we can do whatever we want with!
Safely truncating on a string is a super useful thing to be able to do, but Stringy also offers a massive number of other string manipulation functions too:
And now you have them all in Twig!
Granted, both plugins do more than just string manipulation, but this may be all you need!
Pro tip: This same technique works for any of Craft’s “helper” classes, too. So if you needed 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
Connect is a plugin I wrote to allow you to connect to external/remote databases from your Twig templates. I thought it was pretty cool, until Brad Bell noted that you can do this without a plugin entirely.
In your config/app.php you can put something 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 something like:
{% set dbCmd = craft.app.otherDb.createCommand() %}
{% do dbCmd.update('someTable', someData).execute() %}
The createCommand() method returns a Command object, and from there, you can do pretty much anything you want.
You can also use craft.query() to return a new craft\db\Query object, and you can pass your custom db connection in to the .all() (or whatever) method:
{% set results = craft.query()
.from('table')
.where({
'column': 'value'
})
.limit(10)
.all(craft.app.otherDb)
%}
I think this is a particularly nifty example, in that it shows how customizable Craft CMS is. In config/app.php we have access to the configuration 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 component, configured it to connect to our remote database, and now that component is available system-wide (and in Twig, too). Oh yeah!
Then it also demonstrates how just by exposing these APIs via craft.app. into our templates, we’ve been given enormous power 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 power can be exposed in Twig, and a mess of complicated things can be done via Twig without any plugins.
However, remember that Twig is a templating language. I’m going to say that again:
Remember that Twig is a templating language.
Yes, you can do all of these things, and more! However I think we should adopt some balance in our approach.
No one wins a consolation prize for creating a website entirely without plugins. In fact, it can be a detriment to you in terms of your time, and a detriment to your client in terms of costs if you do everything bespoke.
There’s also a vast difference between the price of something, and its cost. A free but unsupported plugin can cost far more than a paid but maintained & supported plugin.
It’s also fairly obvious that if you bill out at $100/hr and it’ll take you 3 hours to implement something custom in Twig that a $59 plugin will do just as well… you’re costing yourself money.
Put value not just on dollars, 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 unlikely that you’ll go back to fix or enhance your bespoke work for clients, unless you’re being paid to do so. But a plugin developer often will.
So adopt some balance in terms of what you implement “natively” and what you use a plugin for.
Typically it’s considered to be good practice to keep business logic out of our templates, and instead use Twig as a convenient way to render that data. Check out the article Handling Errors Gracefully in Craft CMS article 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 plugins where they save you significant time or offer significant functionality. And consider looking into “going native” in cases where a plugin provides relatively trivial functionality.
Also avoid putting too much “business logic” into your Twig templates. Just because you can doesn’t mean you should.
Link Use a Module!
If for whatever reason your custom functionality isn’t available via the Craft APIs (more on that below), you could also consider putting that business logic code into a module.
For every Craft 3 website I create, I have a site module built into it. It’s pretty empty initially, just providing some custom CSS to allow me to customize the client’s login screen.
But the main reason the site module is there is for situations like this. If I need to offload a bit of core functionality to the module, it’s already there waiting for me.
You can read all about how to do this in the Enhancing a Craft CMS 3 Website with a Custom Module article.
Link Unplugged Nirvana
I’ve provided a few concrete examples of ways you can use Craft CMS in a native way instead of using plugins. But how do we get to reach native nirvana and discover what we can do ourselves?
As a plugin developer, I’m aware of many “under the hood” Craft APIs that if I did only frontend work, it’s unlikely I’d ever real encounter.
This is excellent design, by the way. Good software is deep, not broad. Craft CMS lets you use it at whatever layers are most appropriate for a given project.
From traditional websites to headless API providers to custom 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 interested in uncovering other native functionality, where can we look? A good place to start is the Craft 3 Class Reference.
This is a list of every Service that Craft makes available to your Twig templates off of the craft.app. variable.
So for instance the Entries Service is available at craft.app.entries. — wait, you already knew that! You’ve been using Craft’s services directly all along, whether you knew it or not.
And there are many other Services that are available, just waiting for you to uncover them.
Another place to look is the Craft CMS source code! For example, we can take a look at the default config/app.php that Craft uses to configure itself to uncover things we can customize in new and cool ways.
In fact the config/app.php is so powerful, it’s probably something we should devote an entire future article to…
If you use PhpStorm, check out the Auto-Complete Craft CMS 3 APIs in Twig with PhpStorm article for a pretty fantastic way to explore the Craft CMS APIs via Twig interactively.
Happy exploring!