gebruederheitz/wp-easy-customizer

A simplified, object-oriented interface for the Wordpress Customizer.

v3.0.0 2024-03-06 16:10 UTC

README

A simplified, object-oriented interface for the Wordpress Customizer.

Installation

via composer:

> composer require gebruederheitz/wp-easy-customizer

Make sure you have Composer autoload or an alternative class loader present.

Usage

For information on migrating from v1.x or v2.x see UPGRADING.md

You will need at least one CustomizerPanel object to start with, optionally passing a label (which defaults to "Theme Settings"). Then you can start adding sections to your new panel.

# functions.php (or controller class)
use Gebruederheitz\Wordpress\Customizer\CustomizerPanel;

// If your settings handler implement their getters as static methods, you will
// only need to instantiate the whole bunch on the customizer page:
if (is_customize_preview()) {
    // Set up a new customizer panel with the title 'My Settings'
    // It will also 'clean up' the Customizer by removing some panels (see below)
    // The ID is automatically prefixed with 'ghwp_customizer_panel_'.
    $panel = new CustomizerPanel('my_main_panel', 'My Settings');

Adding Sections to a panel

A section is sub-panel or sub-menu which contains the actual settings. There are three basic ways to add sections to a panel. Which one you use is mostly a matter of taste and code organisation. The main difference is that sections added via the filter hook (variants (a) & (c), using $panel->addNewSection() or $section->setPanel()) are added to the rendered panel after the ones directly added (variant (b), using $panel->addSections()). So if you require a specific order of sections, you'll need to make certain these are registered through the same mechanism.

(a) Directly from the panel using the automatic filter hook

use Gebruederheitz\Wordpress\Customizer\CustomizerPanel;

$panel = new CustomizerPanel('my_main_panel')
$panel->addNewSection(
    // A unique ID for the section
    'my_general_settings',
    // The title of the section / panel
    'General Settings',
    // An optional description shown on the top of the open panel
    null,
    // Some settings, more information below
    [
        CompanyInformation::get(),
    ]
)->addNewSection(/* This method can be chained */);

This allows for a very compact Customizer setup and is particularly useful if you only have a handful of fairly standard settings. The disadvantage is that you can not use any custom section classes, as addNewSection() will always instantiate a new vanilla CustomizerSection. And since we don't actually receive the created section object, we have to add all settings right away.

(b) Instance for instance - directly with objects

use Gebruederheitz\Wordpress\Customizer\CustomizerPanel;
use Gebruederheitz\Wordpress\Customizer\CustomizerSection;

$panel = new CustomizerPanel('my_main_panel')
$panel->addsSections(
    new CustomizerSection(
        'my_general_settings',
        'General Settings',
        null,
        [ CompanyInformation::get() ]
    ),
    // We could add more sections here if we wanted
);

(c) Instance for instance – indirectly via automatic hooks

use Gebruederheitz\Wordpress\Customizer\CustomizerSection;

$panelId = $panel->getId();

$section = new CustomizerSection(
    'ghwp_general_settings',
    'General Settings',
    null,
    [ CompanyInformation::get() ]
);

// We can associate the section with a panel in two ways:
// Using the actual panel instance directly...
$section->setPanel($panel);
// ...or using its ID.
$section->setPanel($panelId);

In all these examples, we've added our settings right when constructing the sections. If we need some advanced logic, they can be added separately:

// You can also create a section without any handlers and then add them 
// later:
$consentManagementSection = new CustomizerSection(
    'ghwp_consent_management_settings',
    'Consent Management'
);

// Associate with the panel one way...
$consentManagementSection->setPanel($panel);
// ...or another
$panel->addSections($consentManagementSection);
    
// Add hanlders retroactively
$consentManagementSection->addSettings(ConsentManagementEnabled::get());
$consentManagementSection->addSettings(
    OtherConsentManagementSetting::get(),
    ExtendedConsentManagementSetting::get(),
);

Defining Settings and adding them to a section

Now you will have to define your settings. Each setting is a class implementing CustomizerSetting. You can extend BasicCustomizerSetting for convenience:

use \Gebruederheitz\Wordpress\Customizer\BasicCustomizerSetting;

class TelephoneNo extends BasicCustomizerSetting 
{
    // These two methods are abstract in BasicCustomizerSetting, so your IDE 
    // will conveniently create stubs for you
    public function getKey(): string
    {
        // A unique key for the option's database entry
        return 'prefix_company_info_telephone';
    }
    
    public function getLabel(): string
    {
        // The input label shown to the user
        return 'Telephone No';
    }
}

BasicCustomizerSettings are singleton objects, so instead of constructing them you retrieve an instance with the static get() method:

$section = new CustomizerSection(
    $slug,
    $label,
    $description,
    [
        TelephoneNo::get(),
        /* ... more settings, if you like ... */
    ]
);

$section->addSettings(TelephoneNo::get(), OtherSetting::get());

Retrieving the value

# Somewhere in your code, like templates, action handlers, controllers, hook
# callbacks etc.
use My\TelephoneNo;

$phoneNumber = TelephoneNo::get()->getValue();
// or you can use our little shortcut with any BasicCustomizerSetting:
$phoneNumber = TelephoneNo::value();

Settings: Advanced

You can do more with settings than the basic example above. Here are some more detailed usages:

use Gebruederheitz\Wordpress\Customizer\BasicCustomizerSetting;

/**
 * A setting with an alternative input type and explicit default value. This 
 * example would of course be simpler to implement using the 
 * CheckboxCustomizerSetting, as described below.
 */
class ShowAddress extends BasicCustomizerSetting
{
    public function getKey() {
        return 'prefix_company_info_show_address';
    }
    
    public function getLabel() {
        return 'Show address in footer';
    }
    
    // Optional: default value, defaults to ''
    protected $default = false;
    
    // Optional: input type
    protected ?string $inputType = 'checkbox';
}

/**
 * A setting with an alternative sanitizer
 */
class StreetAddress extends BasicCustomizerSetting
{
    public function getKey() {
        return 'prefix_company_info_street_address';
    }
    
    public function getLabel() {
        return 'Street address';
    }
    
    // A "callable-string" for a function that will receive the raw value and
    // return a sanitized value.
    protected ?string $sanitizer = 'sanitize_text_field';
}

/**
 * A setting using a select field
 */
class SupportIcon extends BasicCustomizerSetting
{
    /* ... key and label */
    
    protected ?string $sanitizer = 'sanitize_text_field';
    
    protected ?string $inputType = 'select';
    
    public function getOptions: ?array
    {
        return [
            ExampleSelectValues::FIRST => 'Label for first option',
            ExampleSelectValues::SECOND => 'Label for the second option',
        ];
    }

/**
 * A setting for selecting a page from the current site, which is only visible
 * if the switch "ShowAddress" is active.
 */
class ContactPage extends BasicCustomizerSetting
{
    // ... key and label ...
    
    protected static $inputType = 'dropdown-pages';
   
    public function getActiveCallback() : ?callable
    {
        // Using an anonymous callback 
        return function () {
            $showAddress = ShowAddress::get();
            return CustomizerSettings::getValue(
                $showAddress->getKey(),
                $showAddress->getDefault()
            );
        }
        
        // Using a class method
        return [ShowAddress::get(), 'getValue'] // getValue() has a default implementation in BasicCustomizerSetting
    }
}

Some of these examples can lead to a lot of repetition. A URL input is always going to have the type url and should ideally always have a sanitizer sanitize_url. For these cases, some specialized input classes are available at Gebruederheitz\Wordpress\Customizer\InputTypes:

class MyCheckbox extends \Gebruederheitz\Wordpress\Customizer\InputTypes\CheckboxCustomizerSetting
{
    public function getKey(): string {
        return 'my-checkbox';
    }
    
    public function getLabel(): string {
        return 'My Checkbox';
    }
    
    /* The values below are already set on CheckboxCustomizerSetting: */
    // protected ?string $inputType = 'checkbox';
    // protected $default = false;
}

class MyUrlField extends \Gebruederheitz\Wordpress\Customizer\InputTypes\UrlCustomizerSetting
{
    public function getKey(): string {
        return 'my-url-field';
    }
    
    public function getLabel(): string {
        return 'My URL';
    }
    
    /* Already set: */
    // protected ?string $inputType = 'url';
    // protected ?string $sanitizer = 'sanitize_url';
}

class MyTextField extends \Gebruederheitz\Wordpress\Customizer\InputTypes\TextCustomizerSetting
{
    public function getKey(): string {
        return 'my-text';
    }
        
    public function getLabel(): string {
        return 'My Text';
    }
    
    /* Already set: */
    // protected ?string $sanitizer = 'sanitize_text_field';
}

Customizing which panels are removed

By default, CustomizerSettings "cleans up" the Customizer, removing some panels that are rarely used. You can use a filter hook to control which of the default panels are removed:

use Gebruederheitz\Wordpress\Customizer\CustomizerSettings;

add_filter(
    CustomizerSettings::HOOK_FILTER_DECLUTTER_ITEMS, 
    function ($items) {
        unset($items['panels']['themes']);
        unset($items['sections']['static_front_page']);
        unset($items['sections']['custom_css']);
        unset($items['controls']['custom_logo']);
        unset($items['controls']['site_icon']);
        
        return $items;
        
        // To not "declutter" at all simply
        return [];
    }
);

Using Custom Controls

You can use custom controls if they extend the default WP_Customize_Control by returning the FQCN from CustomizerSetting::getInputType():

class MyCustomizeControl extends WP_Customize_Control {
    /* ... */
}

class MyCustomSetting implements CustomizerSetting
{
    /* ... */
    
    protected ?string $inputType = MyCustomizeControl::class;
}

Using the Separator field

The separator custom field allows inserting static separations between settings in the shape of a <hr>. Optionally you can specify a label that will render an <h2> underneath the horizontal rule. The easiest way is to use the SeparatorSetting class:

use Gebruederheitz\Wordpress\Customizer\InputTypes\SeparatorSetting;

$settings = [
    // A plain horizontal rule with 2em vertical margin
    SeparatorSetting::factory('some-unique-id-for-this-separator'),
    TelephoneNo::get(),
    // With custom margin of 3em
    SeparatorSetting::factory(
        'sep-with-custom-margin',
        null,
        [
            'margin' => 3,
        ]
    ),
    // with a heading in the default color #08d
    SeparatorSetting::factory(
        'sep-general-settings',
        __('General Settings', 'namespace')
    ),
    // with heading in a custom color
    SeparatorSetting::factory(
        'some-unique-id-for-this-separator',
        'Heading',
        [
            'color' => 'hotpink',
        ]       
    ),
    // with heading in a custom color and custom margin
    // hr bottom margin is calc(${customMargin}em + 2em) to compensate for
    // the heading's margin collapsing
    SeparatorSetting::factory(
        'some-unique-id-for-this-separator',
        'Heading'
        [
            'color' => 'hotpink',
            'margin' => 1,
        ]       
    ),
];

Development

Dependencies

  • asdf tool version manager
  • nodeJS LTS (v18.x) via asdf
  • PHP >= 7.4 (via asdf)
  • Composer 2.x (via asdf)
  • Nice to have: GNU Make (or drop-in alternative)