Coding Guidelines

A central principle of the Sprout Plugin Suite is to create an experience – for both users and developers – that looks and feels like the native experience with Craft CMS. Toward that end, wherever possible, Sprout adopts conventions set forth by Craft CMS. General conventions include:

Convention Notes
Craft CMS codebase (opens new window) Use Craft's APIs in the same way that Craft uses them
Coding Guidelines (opens new window) Follow conventions outlined in Craft's Coding Guidelines
PhpStorm Settings (opens new window) Use the 'Barrel Strength - Craft CMS Project' Code Styles and Inspections. These were initially inspired by the infrequently-updated Craft Code Style (opens new window) and Craft Inspections (opens new window) with some additional customization.
Php Inspections (EA Extended) (opens new window) More code conventions for PhpStorm
Yii2 and Craft CMS 3 Inspections (opens new window) Yii 2 and Craft-specific inspections in PhpStorm
Sass Mixins for Craft CMS (opens new window) Use Craft's Control Panel style conventions (reference)
Craft UI (opens new window) Vue.js components and styles for Craft CMS apps (reference)

Where there are no clear Craft conventions for our codebase or workflows we endeavor to establish our own conventions that align with the Craft User Experience and are consistent with cultural conventions in the Craft community. An incomplete list of these conventions are outlined below.

# Git Workflow

We use a git workflow in the spirit of the Git branching model (opens new window) with a few updates to better fit our workflow.

As we have to maintain multiple master copies of our plugins, instead of a master branch, we maintain multiple master version branches. These branches are named using the format v# where v2 would represent all releases for the v2.x branch of the software. These version numbers correlate with the plugin version numbers, not the Craft version number, as we may have multiple major releases of a plugin for a single release of Craft.

# Example plugin branches
- develop
- feature/feature-name
- bugfix/bugfix-name
- v1
- v2
- v3

# Local Development

Create a symlink to the plugin or module within the Craft project that will be used for development:

$ ln -s /path/to/cloned/repo /path/to/symlink/in/project/vendor/barrelstrength/folder

In the case you will be editing front-end assets, you'll need to configure npm and process the scss and js files you edit in a give asset bundle's src/web/[assetbundle]/src folder into the src/web/[assetbundle]/dist folder:

npm install
npm run watch

# Naming

The name of a plugin will be used in several different contexts. We use the following conventions:

Context Naming Convention
GitHub repo barrelstrength/craft-sprout-forms
Composer/Packagist barrelstrength/sprout-forms
Plugin folder barrelstrength/sprout-forms
Namespace barrelstrength/sproutforms

# Folder Structure

When possible, we follow conventions in Craft's folder architecture in our plugins.

# Root directory and key src files

├── .github
├── .gitignore
├── CHANGELOG.md
├── composer.json
├── lib
├── LICENSE.md
├── README.md
└── src
    ├── Plugin.php
    └── translations
        └── en
└── webpack.mix.js

References within README.md and any other general information files should be kept to a minimum and point users toward our docs, where we maintain more comprehensive documentation. As we maintain several plugins, it gets tedious to update references in numerous general information files and our documentation serves as a centralized place for this type of information about our plugins.

Similarly, we aim to keep the composer.json file as simple as possible. Don't add schemaVersion, hasCpSection, or hasCpSettings to this file. They should go in the primary plugin module class to more easily toggle the settings without running into issues with cached values in Craft's plugins.php file.

We rename the primary module class Plugin.php to use the name of the plugin (i.e. SproutForms.php). This update requires that we set the composer.json extra->class setting to define the Plugin.php as a file with the name of the plugin itself.

Front-end packages are managed in package.json. Third-party libraries that must be included in the plugin are are added to the lib folder. CSS (SCSS) and Javascript (ES6) assets are managed with Webpack via Laravel Mix. Each Asset Source is configured in webpack.mix.js to process files from the src/web/[assetbundle]/src folder and compile them to the src/web/[assetbundle]/dist folder.

Four npm scripts are available during development:

  • watch - Watches for changes in resources and builds production dist/ files
  • build - Builds production dist/ files once
  • debug - Watches for changes in resources and builds dev dist/ files
  • dev - Builds dev dist/ files once

The dist/ files built for production and dev have the same filenames, so it is a convention that we only commit production files to the repository. The debug and dev commands can be used when debugging.

# Resources and templating

└── src
    └── web
        └── assets
            └── [assetbundle]
                ├── dist
                ├── src
                └── [CustomAssetBundle].php    
        └── twig
            └── variables
                └── [CustomVariable.php]

All asset bundles are managed in the src/web/assets folder and all things Twig (variables, filters, nodes, etc.) are managed in the src/web/twig folder.

# Components and Integrations

└── src
    └── base
        └── [BaseCustomComponentType].php
    └── [customcomponenttype]
        ├── [CustomComponentType1].php
        └── [CustomComponentType2].php
    └── fields
        ├── [CustomField1].php
        └── [CustomField2].php
    └── integrations
        └── [pluginname]
            └── [integrationcustomcomponenttype]
                ├── [IntegrationCustomComponentType1].php
                └── [IntegrationCustomComponentType2].php
    └── templates 
        └── _components
            └── [customcomponenttype]
                ├── [customcomponenttype1]
                └── [customcomponenttype2]
            └── fields
                ├── [customfield1]
                └── [customfield2]
        └── _integrations
            └── [pluginname]
                └── [integrationcustomcomponenttype]
                    ├── [integrationcustomcomponenttype1]
                    └── [integrationcustomcomponenttype2]

We manage various types of components within our plugins. The structure above illustrates how we organize three classes of components. We use the example of a Custom Field as how we would organize extending Craft component within our plugins.

Component Type Name Notes
Craft Components fields Craft component classes get placed in a folder named after the component type in the src folder
Custom Components [customcomponenttype] Custom Component classes get placed in a folder named after the component type in the src folder
Integration Components [integrationcustomcomponenttype] Integration Component classes get placed in a folder named after the component type in the integrations/pluginname folder

# Github community templates

└── .github
    ├── CODE_OF_CONDUCT.md
    ├── CONTRIBUTING.md
    └── ISSUE_TEMPLATE
        bug-report.md
        feature-request.md
    ├── SECURITY.md
    └── SUPPORT.md

Community templates should link to our pages in our documentation with more comprehensive information wherever possible.

# Common Modules

Many Sprout plugins share functionality and this code is managed in shared Yii Modules.

Module Description
barrelstrength/sprout-base Common settings and UI components
barrelstrength/sprout-base-email Common email functionality
barrelstrength/sprout-base-fields Common field functionality
barrelstrength/sprout-base-reports Common reporting functionality
barrelstrength/sprout-base-redirects Common sitemap functionality
barrelstrength/sprout-base-sent-email Common sent email functionality
barrelstrength/sprout-base-sitemaps Common redirects functionality
barrelstrength/sprout-base-uris Common URL-enabled Section functionality

# Uninstall Migrations

As the Sprout Plugins depend on several shared modules, we need to make sure no shared data is removed when uninstalling a plugin. To do this, plugins using shared modules should implement SproutDependencyInterface and SproutDependencyTrait.

use barrelstrength\sproutbase\base\SproutDependencyInterface;
use barrelstrength\sproutbase\base\SproutDependencyTrait;

class SproutForms extends Plugin implements SproutDependencyInterface
{
    use SproutDependencyTrait;

    public function getSproutDependencies(): array
    {
        return [
            SproutDependencyInterface::SPROUT_BASE,
            SproutDependencyInterface::SPROUT_BASE_REPORTS
        ];
    }
}

And then check for any shared dependencies before performing uninstall actions.

use barrelstrength\sproutbase\base\SproutDependencyInterface;
use barrelstrength\sproutbasereports\migrations\Install as SproutBaseReportsInstall;
use barrelstrength\sproutreports\SproutReports;
use craft\db\Migration;

class Install extends Migration
{
    public function safeDown(): bool
    {
        $sproutBaseReportsInUse = SproutReports::getInstance()->dependencyInUse(SproutDependencyInterface::SPROUT_BASE_REPORTS);

        if (!$sproutBaseReportsInUse) {
            $migration = new SproutBaseReportsInstall();

            ob_start();
            $migration->safeDown();
            ob_end_clean();
        }

        return true;
    }
}

# Translations

Use the default Craft conventions for translations. This allows us to benefit from the Yii Inspections that allow us to bulk add and remove translations.

Context Translation Convention
PHP Craft::t('sprout-forms', 'Message');
Twig {{ "Message"|t('sprout-forms') }}
CP Javascript Craft:t('sprout-forms');

Each plugin or module maintains it's own translation file. As some plugins depend on multiple modules for their functionality, this may mean that someone translating a plugin will also have to translate translation files in other modules. For example, to completely translate Sprout Forms one would need to translate the files in Sprout Forms, Sprout Base Fields, and Sprout Base.

# Exceptions

Exceptions are for developers, not for users. Exception messages should not be translated:

BAD:
throw new Exception(Craft::t('sprout-forms, 'Something happened'));

GOOD:
throw new Exception('Something happened');

# Migrations

Due to our application structure using shared modules, in some cases migrations may need to be run by multiple plugins and we cannot know which order they will get run in. To address this, we use the following conventions:

  • Make sure every migration can be run twice, without throwing errors if it has already been run once.
  • All migrations that affect a plugin with a shared module should be placed in the base module and instances of those migrations should be created in each respective plugin where they are needed.

# Naming migrations

Order

Migration naming will use the date in the first segment and the second segment will just represent the order that they should be run in for a particular release. The following migrations are all be part of a release on the same day, and are ordered 1, 2, 3 in the order they should run:

m190101_000001_migration_description.php
m190101_000002_migration_description.php
m190101_000003_migration_description.php

Migrations used by multiple plugins

Any migration instance that is just running a migration in base module should use the same name as the base migration and append the plugin name that it is being run from.

m190101_000001_migration_description.php              // Sprout Base Email
m190101_000001_migration_description_sproutemail.php  // Sprout Email
m190101_000001_migration_description_sproutforms.php  // Sprout Forms

# Testing Prior to Release

To test one or more plugins and modules under development on real websites before releases, changes can be pushed to a development branch and pulled into any appropriate project for testing.

In this example, we grab the latest on the develop branch for the Sprout SEO plugin and the Sprout Base Redirects module. To ensure composer things it's working with the release numbers we're using in our composer.json we can tell composer what version number to use for the code we are testing:

composer require barrelstrength/sprout-seo:"dev-develop as 4.2.0" barrelstrength/sprout-base-redirects:"dev-develop as 1.1.1"

Requiring dev-develop should pull in the latest commit on the develop branch. For more specific tests, you can also target a specific commit hash:

composer require barrelstrength/sprout-seo:"dev-develop as 4.2.0" barrelstrength/sprout-base-redirects:"dev-develop#dfae1a922cdb5dd32fd8a813839fddc26ff412b0 as 1.1.1"

For additional troubleshooting, consider some of the following steps:

rm composer.lock      # Remove the composer.lock file
composer clear-cache  # Clear composers cache
composer remove ...   # Try removing the package you are testing before installing it
rm -r ./vendor        # Remove the entire ./vendor directory and rebuild it with `composer update`
Last Updated: 11/23/2023, 10:32:53 PM