Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
/infection.json.dist
/package.json
/package-lock.json
/phpstan-bootstrap.php
/phpstan.neon.dist
/phpunit.xml.dist
/rector.php
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
/infection.json.dist export-ignore
/package-lock.json export-ignore
/package.json export-ignore
/phpstan-bootstrap.php export-ignore
/phpstan.neon.dist export-ignore
/phpunit.xml.dist export-ignore
/rector.php export-ignore
Expand Down
17 changes: 17 additions & 0 deletions phpstan-bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/**
* PHPStan bootstrap
*
* Declares the plugin constants for static analysis. They are defined at
* runtime in plugin-slug.php, but PHPStan does not evaluate that define(),
* so it is told about them here via the config's bootstrapFiles.
*
* @package Gamajo\PluginSlug
* @author Gary Jones
* @copyright 2024-2026 Gary Jones
* @license GPL-2.0-or-later
*/

declare( strict_types = 1 );

define( 'PLUGIN_SLUG_FILE', __FILE__ );
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ parameters:
paths:
- plugin-slug.php
- src
# Declares the plugin constants, which are defined at runtime.
bootstrapFiles:
- phpstan-bootstrap.php
27 changes: 5 additions & 22 deletions plugin-slug.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,18 @@
die;
}

// The one constant the plugin needs: every path is derived from it at runtime.
define( 'PLUGIN_SLUG_FILE', __FILE__ );

// Load Composer autoloader.
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
require_once __DIR__ . '/vendor/autoload.php';
}

// Initialize the plugin on a hook, rather than at file include time.
// Wire the plugin's features to WordPress on a hook, rather than at include time.
add_action(
'plugins_loaded',
static function (): void {
plugin_slug()->run();
new Bootstrapper()->init();
}
);

/**
* Get the plugin instance.
*
* Builds and caches the Plugin object on first call, so no work happens
* when this file is merely included.
*
* @since 0.1.0
*
* @return Plugin Plugin instance.
*/
function plugin_slug(): Plugin {
static $plugin = null;

if ( ! $plugin instanceof Plugin ) {
$plugin = new Plugin( __FILE__ );
}

return $plugin;
}
36 changes: 36 additions & 0 deletions src/Bootstrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Plugin bootstrapper
*
* @package Gamajo\PluginSlug
* @author Gary Jones
* @copyright 2024-2026 Gary Jones
* @license GPL-2.0-or-later
*/

declare( strict_types = 1 );

namespace Gamajo\PluginSlug;

/**
* The composition root.
*
* Instantiates the plugin's features and registers them with WordPress. This
* is the single place where features are wired up; add new ones to init().
*
* @since 0.2.0
*
* @package Gamajo\PluginSlug
* @author Gary Jones
*/
final class Bootstrapper {

/**
* Initialise the plugin.
*
* @since 0.2.0
*/
public function init(): void {
new SettingsPage()->register();
}
}
27 changes: 12 additions & 15 deletions src/Plugin.php → src/SettingsPage.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Main plugin file
* Settings page feature
*
* @package Gamajo\PluginSlug
* @author Gary Jones
Expand All @@ -13,18 +13,17 @@
namespace Gamajo\PluginSlug;

/**
* Main plugin class.
*
* Registers an example settings page directly against the WordPress API.
*
* Swap these registrations for your own; the boilerplate keeps them here,
* fully typed, rather than behind a configuration abstraction.
*
* @since 0.1.0
* @since 0.2.0
*
* @package Gamajo\PluginSlug
* @author Gary Jones
*/
class Plugin {
final class SettingsPage {

/**
* Settings page slug, and the page the settings are shown on.
Expand Down Expand Up @@ -83,23 +82,21 @@ class Plugin {
private string $hook_suffix = '';

/**
* Instantiate a Plugin object.
* Derive the plugin paths from the one plugin-file constant.
*
* @since 0.2.0
*
* @param string $file Absolute path to the main plugin file.
*/
public function __construct( string $file ) {
$this->dir = plugin_dir_path( $file );
$this->url = plugin_dir_url( $file );
public function __construct() {
$this->dir = plugin_dir_path( PLUGIN_SLUG_FILE );
$this->url = plugin_dir_url( PLUGIN_SLUG_FILE );
}

/**
* Launch the initialization process.
* Register the feature's hooks.
*
* @since 0.1.0
* @since 0.2.0
*/
public function run(): void {
public function register(): void {
add_action( 'admin_menu', $this->register_settings_page( ... ) );
add_action( 'admin_init', $this->register_settings( ... ) );
add_action( 'admin_enqueue_scripts', $this->enqueue_admin_assets( ... ) );
Expand Down Expand Up @@ -141,7 +138,7 @@ private function render_settings_page(): void {
* The field passes a label_for argument matching the id of the input in
* its view, so the rendered field title labels that control.
*
* @since 0.1.0
* @since 0.2.0
*/
public function register_settings(): void {
register_setting(
Expand Down
55 changes: 55 additions & 0 deletions tests/Unit/BootstrapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
/**
* Unit tests for Bootstrapper
*
* @package Gamajo\PluginSlug\Tests\Unit
* @author Gary Jones
* @copyright 2026 Gary Jones
* @license GPL-2.0-or-later
*/

declare( strict_types = 1 );

namespace Gamajo\PluginSlug\Tests\Unit;

use Brain\Monkey\Functions;
use Closure;
use Gamajo\PluginSlug\Bootstrapper as Testee;
use Gamajo\PluginSlug\Tests\TestCase;
use Mockery;

/**
* Bootstrapper test case.
*
* @covers \Gamajo\PluginSlug\Bootstrapper
* @uses \Gamajo\PluginSlug\SettingsPage
*/
class BootstrapperTest extends TestCase {

/**
* Prepares the test environment before each test.
*/
protected function setUp(): void {
parent::setUp();

if ( ! defined( 'PLUGIN_SLUG_FILE' ) ) {
define( 'PLUGIN_SLUG_FILE', '/plugin/plugin-slug.php' );
}

// SettingsPage derives its paths from these in its constructor.
Functions\when( 'plugin_dir_path' )->justReturn( '/plugin/' );
Functions\when( 'plugin_dir_url' )->justReturn( 'https://example.test/' );
}

/**
* The init() method registers the settings page feature with WordPress.
*/
public function test_init_registers_the_settings_page(): void {
// SettingsPage::register() adds exactly these three hooks.
Functions\expect( 'add_action' )->once()->with( 'admin_menu', Mockery::type( Closure::class ) );
Functions\expect( 'add_action' )->once()->with( 'admin_init', Mockery::type( Closure::class ) );
Functions\expect( 'add_action' )->once()->with( 'admin_enqueue_scripts', Mockery::type( Closure::class ) );

new Testee()->init();
}
}
Loading