Within the WordPress ecosystem, adopting a freemium fashion is a prevalent manner for selling and monetizing industrial plugins. This method involves freeing a elementary model of the plugin at no cost—in most cases in the course of the WordPress plugin listing—and providing enhanced options via a PRO model or add-ons, in most cases offered at the plugin’s web site.

There are 3 other ways to combine industrial options inside a freemium fashion:

  1. Send those industrial options throughout the unfastened plugin, and turn on them simplest when the industrial model is put in at the web site or a industrial license key’s equipped.
  2. Create the unfastened and PRO variations as unbiased plugins, with the PRO model designed to interchange the unfastened model, making sure that just one model is put in at any given time.
  3. Set up the PRO model along the unfastened plugin, extending its capability. This calls for each variations to be provide.

Then again, the primary method is incompatible with the tips for plugins allotted by means of the WordPress plugin listing, as those regulations restrict the inclusion of options which can be limited or locked till a cost or improve is made.

This leaves us with the remaining two choices, which provide benefits and downsides. The sections beneath provide an explanation for why the latter technique, “PRO on most sensible of unfastened,” is our top choice.

Let’s delve into the second one choice, “PRO as alternative of unfastened,” its defects, and why it’s in the end no longer advisable.

Due to this fact, we discover extensive the “PRO on most sensible of unfastened,” highlighting why it sticks out as the most well liked selection.

Benefits of the “PRO as alternative of unfastened” technique

The “PRO as alternative of unfastened” technique is moderately simple to enforce for the reason that builders can use a unmarried codebase for each plugins (unfastened and PRO) and create two outputs from it, with the unfastened (or “typical”) model merely together with a subset of the code, and the PRO model together with all of the code.

For instance, the venture’s codebase may well be break up into typical/ and professional/ directories. The plugin would at all times load the usual code, with the PRO code being loaded conditionally, in response to the presence of the respective listing:

// Major plugin record: myplugin.php

// At all times load the usual plugin's code
require_once __DIR__ . '/typical/load.php';

// Load the PRO plugin's code provided that the folder exists
$proFolder = __DIR__ . '/professional';
if (file_exists($proFolder)) {
  require_once $proFolder . '/load.php';

Then, when producing the plugin by means of a Steady Integration device, we will be able to create the 2 belongings myplugin-standard.zip and myplugin-pro.zip from the similar supply code.

If webhosting the venture on GitHub and producing the belongings by means of GitHub Movements, the next workflow does the activity:

title: Generate the usual and PRO plugins
    sorts: [published]

    title: Generate plugins
    runs-on: ubuntu-latest
      - title: Checkout code
        makes use of: movements/checkout@v3

      - title: Set up zip
        makes use of: montudor/action-zip@v1.0.0

      - title: Create the usual plugin .zip record (except all PRO code)
        run: zip -X -r myplugin-standard.zip . -x **/src/professional/*

      - title: Create the PRO plugin .zip record
        run: zip -X -r myplugin-pro.zip . -x myplugin-standard.zip

      - title: Add each plugins to the discharge web page
        makes use of: softprops/action-gh-release@v1
          information: |
          GITHUB_TOKEN: ${{ secrets and techniques.GITHUB_TOKEN }}

Issues of the “PRO as alternative of unfastened” technique

The “PRO as alternative of unfastened” technique calls for changing the unfastened plugin with the PRO model. As a end result, if the unfastened plugin is sent by means of the WordPress plugin listing, its “lively set up” rely will move down (because it simplest tracks the unfastened plugin, no longer the PRO model), giving the impact that the plugin is much less fashionable than it in reality is.

This consequence would defeat the aim of the use of the WordPress plugin listing within the first position: As a plugin discovery channel the place customers can know about our plugin, obtain it, and set up it. (After that, as soon as it’s been put in, the unfastened plugin can invite customers to improve to the PRO model).

If the lively set up rely isn’t prime, customers will not be satisfied to put in our plugin. For instance of ways issues can move improper, the homeowners of the E-newsletter Glue plugin made up our minds to take away the plugin from the WordPress plugin listing, because the low activation rely was once hurting the possibilities of the plugin.

Because the “PRO as alternative of unfastened” technique isn’t viable, that leaves us with just one selection: the “PRO on most sensible of unfastened” technique.

Let’s discover the bits and bobs of this technique.

Conceptualizing the “PRO on most sensible of unfastened” technique

The theory is that the unfastened plugin is put in at the website, and its capability may also be prolonged via putting in further plugins or addons. This may well be by means of a unmarried PRO plugin or by means of a number of PRO extensions or addons, with every of them offering some particular capability.

On this state of affairs, the unfastened plugin does no longer care what different plugins are put in at the website. All it does is to supply further capability. This fashion is flexible, taking into account enlargement via each the unique builders and third-party creators, fostering an ecosystem the place a plugin can evolve in unexpected instructions.

Please understand the way it doesn’t subject if the PRO extensions shall be produced via us (i.e. the similar builders construction the usual plugin), or via someone else: The code to care for each is identical. As such, this can be a just right concept to create a basis that doesn’t limit how the plugin may also be prolonged. This may occasionally make it conceivable for Third-party builders to increase our plugin in tactics we hadn’t conceived of.

Design approaches: hooks and repair packing containers

There are two major approaches to creating PHP code extensible:

  1. By means of the WordPress motion and filter out hooks
  2. By means of a carrier container

The primary method is the most typical one in every of WordPress builders, whilst the latter one is most popular via the wider PHP neighborhood.

Let’s see examples of each.

Making code extensible by means of motion and filter out hooks

WordPress gives hooks (filters and movements) as a mechanism to change conduct. Filter out hooks are used to override values, and motion hooks to execute customized capability.

Our major plugin can then be “littered” with hooks all over its codebase, permitting builders to change its conduct.

A just right instance of that is WooCommerce, which has spanned an enormous ecosystem of add-ons, with the vast majority of them being owned via Third-party suppliers. That is conceivable because of the in depth choice of hooks presented via this plugin.

The builders of WooCommerce have purposefully added hooks, even if they themselves do not have them. It’s for any person else to make use of. Understand the good choice of “sooner than” and “after” motion hooks:

  • woocommerce_after_account_downloads
  • woocommerce_after_account_navigation
  • woocommerce_after_account_orders
  • woocommerce_after_account_payment_methods
  • woocommerce_after_available_downloads
  • woocommerce_after_cart
  • woocommerce_after_cart_contents
  • woocommerce_after_cart_item_name
  • woocommerce_after_cart_table
  • woocommerce_after_cart_totals
  • woocommerce_before_account_downloads
  • woocommerce_before_account_navigation
  • woocommerce_before_account_orders
  • woocommerce_before_account_orders_pagination
  • woocommerce_before_account_payment_methods
  • woocommerce_before_available_downloads
  • woocommerce_before_cart
  • woocommerce_before_cart_collaterals
  • woocommerce_before_cart_contents
  • woocommerce_before_cart_table
  • woocommerce_before_cart_totals

For instance, record downloads.php accommodates a number of movements to inject additional capability, and the store URL may also be overridden by means of a filter out:

$has_downloads = (bool) $downloads;

do_action( 'woocommerce_before_account_downloads', $has_downloads ); ?>




  ' . esc_html__( 'Browse merchandise', 'woocommerce' ) . '', 'understand' );

Making code extensible by means of carrier packing containers

A carrier container is a PHP object that is helping us set up the instantiation of all categories within the venture, usually presented as a part of a “dependency injection” library.

Dependency injection is a technique that permits gluing all portions of the appliance in combination in a decentralized method: PHP categories are injected into the appliance by means of configuration, and the appliance retrieves cases of those PHP categories by means of the carrier container.

There are many dependency injection libraries to be had. The next are fashionable ones and are interchangeable as all of them fulfill PSR-11 ( PHP typical advice) that describes dependency injection packing containers:

Laravel additionally accommodates a carrier container this is already baked into the appliance.

The usage of dependency injection, the unfastened plugin does no longer want to know prematurely what PHP categories are provide on runtime: It merely requests cases of all categories to the carrier container. Whilst many PHP categories are equipped via the unfastened plugin itself to fulfill its capability, others are equipped via whichever addons are put in at the website to increase capability.

A just right instance of the use of a carrier container is Gato GraphQL, which depends upon Symfony’s DependencyInjection library.

That is how the carrier container is instantiated:

cacheContainerConfiguration = $cacheContainerConfiguration;

    if ($this->cacheContainerConfiguration) {
      if (!$listing) {
        $listing = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'container-cache';
      $listing .= DIRECTORY_SEPARATOR . $namespace;
      if (!is_dir($listing)) {
        @mkdir($listing, 0777, true);
      // Retailer the cache beneath this record
      $this->cacheFile = $listing . 'container.php';

      $containerConfigCache = new ConfigCache($this->cacheFile, false);
      $this->cached = $containerConfigCache->isFresh();
    } else {
      $this->cached = false;

    // If no longer cached, then create the brand new example
    if (!$this->cached) {
      $this->example = new ContainerBuilder();
    } else {
      require_once $this->cacheFile;
      /** @var class-string */
      $containerFullyQuantifiedClass = "GatoGraphQLServiceContainer";
      $this->example = new $containerFullyQuantifiedClass();

  public serve as getInstance(): ContainerInterface
    go back $this->example;

   * If the container isn't cached, then collect it and cache it
   * @param CompilerPassInterface[] $compilerPasses Compiler Go gadgets to check in at the container
  public serve as maybeCompileAndCacheContainer(
    array $compilerPasses = []
  ): void {
     * Collect Symfony's DependencyInjection Container Builder.
     * After compiling, cache it in disk for efficiency.
     * This occurs simplest the primary time the website is accessed
     * at the present server.
    if ($this->cached) {
      go back;

    /** @var ContainerBuilder */
    $containerBuilder = $this->getInstance();
    foreach ($compilerPasses as $compilerPass) {

    // Collect the container.

    // Cache the container
    if (!$this->cacheContainerConfiguration) {
      go back;
    // Create the folder if it does not exist, and test it was once a success
    $dir = dirname($this->cacheFile);
    $folderExists = file_exists($dir);
    if (!$folderExists) {
      $folderExists = @mkdir($dir, 0777, true);
      if (!$folderExists) {
        go back;

    // Save the container to disk
    $dumper = new PhpDumper($containerBuilder);
      $dumper->sell off(
          'class' => 'ServiceContainer',
          'namespace' => 'GatoGraphQL',

    // Trade the permissions so it may be changed via exterior processes
    chmod($this->cacheFile, 0777);

Please understand that the carrier container (available beneath PHP object with category GatoGraphQLServiceContainer) is generated the primary time that the plugin is done after which cached to disk (as record container.php in a machine temp folder). It’s because producing the carrier container is a dear procedure that might probably take a number of seconds to finish.

Then, each the principle plugin and all its extensions outline what products and services to inject into the container by means of a configuration record:

products and services:
    public: true
    autowire: true
    autoconfigure: true

    category: GatoGraphQLGatoGraphQLRegistriesModuleTypeRegistry

    category: GatoGraphQLGatoGraphQLLogLogger

    useful resource: ../src/Products and services/*

    useful resource: '../src/State/*'

Understand that we will be able to instantiate gadgets for particular categories (similar to GatoGraphQLGatoGraphQLLogLogger, accessed by means of its contract interface GatoGraphQLGatoGraphQLLogLoggerInterface), and we will be able to additionally point out “instantiate all categories beneath some listing” (similar to all products and services beneath ../src/Products and services).

In the end, we inject the configuration into the carrier container:

isCached()) {
      go back;

    // Initialize the ContainerBuilder with this module's carrier implementations
    /** @var ContainerBuilder */
    $containerBuilder = App::getContainer();
    $loader = new YamlFileLoader($containerBuilder, new FileLocator($dir));

Products and services injected into the container may also be configured to be initialized at all times or simplest when asked (lazy mode).

For example, to constitute a customized submit form, the plugin has category AbstractCustomPostType, whose initialize manner executes the common sense to initialize it in line with WordPress:


   * Check in the submit form
  public serve as initCustomPostType(): void
    register_post_type($this->getCustomPostType(), $this->getCustomPostTypeArgs());

  summary public serve as getCustomPostType(): string;

   * Arguments for registering the submit form
   * @go back array
  secure serve as getCustomPostTypeArgs(): array
    /** @var array */
    $postTypeArgs = [
      'public' => $this->isPublic(),
      'publicly_queryable' => $this->isPubliclyQueryable(),
      'label' => $this->getCustomPostTypeName(),
      'labels' => $this->getCustomPostTypeLabels($this->getCustomPostTypeName(), $this->getCustomPostTypePluralNames(true), $this->getCustomPostTypePluralNames(false)),
      'capability_type' => 'post',
      'hierarchical' => $this->isAPIHierarchyModuleEnabled() && $this->isHierarchical(),
      'exclude_from_search' => true,
      'show_in_admin_bar' => $this->showInAdminBar(),
      'show_in_nav_menus' => true,
      'show_ui' => true,
      'show_in_menu' => true,
      'show_in_rest' => true,
    go back $postTypeArgs;

   * Labels for registering the submit form
   * @param string $name_uc Singular title uppercase
   * @param string $names_uc Plural title uppercase
   * @param string $names_lc Plural title lowercase
   * @go back array
  secure serve as getCustomPostTypeLabels(string $name_uc, string $names_uc, string $names_lc): array
    go back array(
      'title'         => $names_uc,
      'singular_name'    => $name_uc,
      'add_new'      => sprintf(__('Upload New %s', 'gatographql'), $name_uc),
      'add_new_item'     => sprintf(__('Upload New %s', 'gatographql'), $name_uc),
      'edit_item'      => sprintf(__('Edit %s', 'gatographql'), $name_uc),
      'new_item'       => sprintf(__('New %s', 'gatographql'), $name_uc),
      'all_items'      => $names_uc,//sprintf(__('All %s', 'gatographql'), $names_uc),
      'view_item'      => sprintf(__('View %s', 'gatographql'), $name_uc),
      'search_items'     => sprintf(__('Seek %s', 'gatographql'), $names_uc),
      'not_found'      => sprintf(__('No %s discovered', 'gatographql'), $names_lc),
      'not_found_in_trash' => sprintf(__('No %s present in Trash', 'gatographql'), $names_lc),
      'parent_item_colon'  => sprintf(__('Father or mother %s:', 'gatographql'), $name_uc),

Then, the category GraphQLCustomEndpointCustomPostType.php is an implementation of a customized submit form. Upon being injected as a carrier into the container, it’s instantiated and registered into WordPress:

This category is provide at the unfastened plugin and different customized post-type categories, in a similar way extending from AbstractCustomPostType, are equipped via PRO extensions.

Evaluating hooks and repair packing containers

Let’s evaluate the 2 design approaches.

At the plus aspect for motion and filter out hooks, it's the more effective manner, with its capability being a part of WordPress core. And any developer operating with WordPress already is aware of how one can take care of hooks, therefore the training curve is low.

Then again, its common sense is hooked up to a hook title, which is a string, and, as such, might result in insects: If the hook title is changed, it breaks the common sense of the extension. Then again, the developer would possibly not understand that there's a drawback for the reason that PHP code nonetheless compiles.

In consequence, deprecated hooks have a tendency to be stored for a long time within the codebase, perhaps even eternally. The venture then accumulates stale code that may’t be got rid of for concern of breaking extensions.

Again to WooCommerce, this example is evidenced on record dashboard.php (understand how the deprecated hooks are stored since model 2.6, while the present recent model is 8.5):

The usage of a carrier container has the downside that it calls for an exterior library, which additional provides complexity. Much more, this library will have to be scoped (the use of PHP-Scoper or Strauss) for concern {that a} other model of the similar library is put in via some other plugin at the identical website, which might produce conflicts.

The usage of a carrier container is no doubt harder to enforce and takes longer construction time.

At the plus aspect, carrier packing containers care for PHP categories with no need to couple common sense to a few string. This leads to the venture the use of extra PHP perfect practices, resulting in a codebase this is more straightforward to handle in the longer term.


When making a plugin for WordPress, it’s a good suggestion for it to strengthen extensions to permit us (the creators of the plugin) to provide industrial options and likewise somebody else so as to add additional capability and, expectantly, span an ecosystem targeted across the plugin.

On this article, we explored what are the issues in regards to the structure of the PHP venture to make the plugin extensible. As we realized, we will be able to make a choice from two design approaches: the use of hooks or the use of a carrier container. And we when put next each approaches, figuring out the virtues and weaknesses of every.

Do you propose to make your WordPress plugin extensible? Tell us within the feedback phase.

The submit Architecting a WordPress plugin to strengthen extensions gave the impression first on Kinsta®.

WP Hosting

[ continue ]