Getting started
This guide shows the smallest complete integration: install the package, boot it from a plugin or theme, group services in a module, and retrieve a deliberately shared service.
Requirements
- PHP 8.2 or newer
- WordPress 6.5 or newer
- Composer
The package has no Composer runtime dependencies beyond PHP. It is nevertheless
a WordPress library: its registration classes and most utilities call WordPress
APIs. Encryptor additionally requires the OpenSSL PHP extension when used.
Install
The package is not published on public Packagist. Add the repository to the
consuming project's composer.json first, then require it:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/rtCamp/wp-framework"
}
]
}
composer require rtcamp/wp-framework:^1.0
If the project is already wired to an rtCamp-hosted Composer registry that
serves this package, the repositories entry is unnecessary and
composer require rtcamp/wp-framework:^1.0 is enough on its own.
Pin with a caret constraint. inc/Contracts/ is the public API and the project
follows Semantic Versioning, so ^1.0
accepts additive releases and refuses the next major. Read
upgrading.md before moving across a major.
Composer exposes framework classes through the rtCamp\WPFramework\ namespace.
The consuming plugin or theme remains responsible for requiring its own Composer
autoload file and starting its entry class.
1. Create a service
Use the narrowest contract that describes the service. Most hook-driven classes
only need Registrable:
<?php
declare( strict_types = 1 );
namespace Acme\Example;
use rtCamp\WPFramework\Contracts\Interfaces\Registrable;
final class ContentFilters implements Registrable {
public function register_hooks(): void {
add_filter( 'the_content', [ $this, 'append_notice' ] );
}
public function append_notice( string $content ): string {
return $content . '<p>' . esc_html__( 'Example is active.', 'acme-example' ) . '</p>';
}
}
For post types, taxonomies, blocks, shortcodes, REST controllers, settings pages,
admin pages, and user roles, extend the matching Abstract* class instead of
repeating its registration plumbing. See abstracts.md.
2. Group services in a module
An AbstractModule is a loader for a related set of classes. Every listed class
must be constructible without required constructor arguments.
<?php
declare( strict_types = 1 );
namespace Acme\Example;
use rtCamp\WPFramework\Contracts\Abstracts\AbstractModule;
final class ContentModule extends AbstractModule {
protected function get_classes(): array {
return [
ContentFilters::class,
];
}
}
3. Bootstrap the loader
The framework deliberately does not ship a Main class. A plugin or theme owns
its entry point and decides when the first load begins.
Plugin entry point
<?php
/**
* Plugin Name: Acme Example
*/
declare( strict_types = 1 );
namespace Acme\Example;
use rtCamp\WPFramework\Contracts\Traits\Loader;
use rtCamp\WPFramework\Contracts\Traits\Singleton;
require_once __DIR__ . '/vendor/autoload.php';
final class Main {
use Loader;
use Singleton;
protected function __construct() {
// Publish before loading: constructors reached during load may call Main::get_instance().
static::$instance = $this;
$this->load( [ ContentModule::class ] );
}
}
Main::get_instance();
Theme entry point
The same Main shape works in a theme. Require the theme's Composer autoloader
from functions.php and attach boot() to after_setup_theme instead of
plugins_loaded.
The early static::$instance = $this assignment is important when construction
can re-enter get_instance(). A class using Singleton and its subclasses also
share one storage slot; do not call get_instance() on a subclass of a singleton.
See contracts.md.
Sharing a service intentionally
Classes are not retained by default. Add the Shareable marker only when another
class must retrieve the exact instance that was loaded:
use rtCamp\WPFramework\Contracts\Interfaces\Shareable;
use rtCamp\WPFramework\Utils\Cache;
final class PluginCache extends Cache implements Shareable {
public function __construct() {
parent::__construct( 'acme-example' );
}
}
final class InfrastructureModule extends AbstractModule {
protected function get_classes(): array {
return [ PluginCache::class ];
}
public function cache(): PluginCache {
return $this->get_shared( PluginCache::class );
}
}
get_shared() belongs to the loader that loaded the class. In this example the
cache is retrieved from InfrastructureModule, not from Main. If an outer
loader must retrieve the module itself, the module must also implement
Shareable.
Prefer constructor injection when objects can be assembled directly. Shareable
is for loader-created objects that genuinely need later retrieval; it should not
be the default for every service.
What happens during load
For each unique class name, the loader:
- constructs one instance;
- calls
can_register()for aConditionallyRegistrable; - calls
register_hooks()when registration is allowed; - stores the instance when it is
Shareable.
Duplicate class names in one list are ignored. Each call to load() creates a
fresh container, so a later call on the same loader replaces the previously
shared set. Prefer one load per loader with the complete class list.
Continue with architecture.md for the lifecycle model and abstracts.md for implementation recipes. When something doesn't register, start at troubleshooting.md.