Skip to content
Open
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
24 changes: 5 additions & 19 deletions .github/workflows/test-measure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:

[[ "${{ steps.determine-file-counts.outputs.css-count }}" -le 0 ]] && SKIPPED+=("lint-css")
[[ "${{ steps.determine-file-counts.outputs.js-count }}" -le 0 ]] && SKIPPED+=("lint-js" "unit-tests-js" "build-prod")
[[ "${{ steps.determine-file-counts.outputs.php-count }}" -le 0 ]] && SKIPPED+=("lint-php" "unit-test-php")
[[ "${{ steps.determine-file-counts.outputs.php-count }}" -le 0 ]] && SKIPPED+=("lint-php")

if [[ ${#SKIPPED[@]} -gt 0 ]]; then
echo "The following jobs will be skipped as no relevant files were changed:"
Expand Down Expand Up @@ -161,6 +161,7 @@ jobs:
php-version: '8.2'
coverage: none
tools: cs2pr
github-token: ${{ secrets.WP_FRAMEWORK_REPO_TEMP_TOKEN }}

- name: Get Composer Cache Directory
id: composer-cache
Expand All @@ -174,6 +175,9 @@ jobs:
restore-keys: |
${{ runner.os }}-composer-

- name: Ensure git is installed
run: which git || (sudo apt-get update && sudo apt-get install -y git)

- name: Install Composer dependencies
run: composer install --prefer-dist --optimize-autoloader --no-progress --no-interaction --no-scripts

Expand All @@ -183,24 +187,6 @@ jobs:
- name: Detect coding standard violations (PHPCS)
run: vendor/bin/phpcs -q --report=checkstyle --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 | cs2pr --graceful-warnings

unit-test-php:
needs: pre-run
if: needs.pre-run.outputs.changed-php-count > 0
name: "PHP Unit test"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Node with cache
uses: ./.github/actions/setup-node-with-cache

- name: Start WP environment
run: npm run wp-env start

- name: Run tests
run: npm run test:php

build-prod:
needs: pre-run
if: needs.pre-run.outputs.changed-js-count > 0 || needs.pre-run.outputs.changed-css-count > 0
Expand Down
151 changes: 99 additions & 52 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,86 +1,133 @@
# Development Guide

## PSR-4 Namespace Convention
## Architecture overview

All namespaced PHP classes live under `inc/` using the `rtCamp\Theme\Elementary\` root namespace.
The file path must map to the namespace and class name.
The theme is split into two layers:

Examples:
- **`vendor/rtcamp/wp-framework/`** — The upstream framework, installed as a Composer dependency. Provides reusable scaffolding (`Singleton`, `Loader`, `Container`, `AssetLoaderTrait`, `TemplateLoaderTrait`) and abstract base classes (`AbstractSettingsPage`, `AbstractPostType`, etc.). **Do not modify.** Changes belong in the framework repository.
- **`inc/`** — All theme-specific code. Extends framework abstracts, registers theme services, and bootstraps the theme.

- `inc/Main.php` → `rtCamp\Theme\Elementary\Main`
- `inc/Core/Assets.php` → `rtCamp\Theme\Elementary\Core\Assets`
- `inc/BlockExtensions/MediaTextInteractive.php` → `rtCamp\Theme\Elementary\Modules\BlockExtensions\MediaTextInteractive`
- `inc/Framework/Traits/Singleton.php` → `rtCamp\Theme\Elementary\Framework\Traits\Singleton`
The `vendor/` boundary enforces the rule by convention: editing files there gets blown away on every `composer install`.

## Directory tree
## PSR-4 namespace convention

The `inc/` directory follows a PSR-4 structure and is split into meaningful areas:
Single PSR-4 root, declared in `composer.json`:

- `inc/Framework/`
- Upstream-owned framework code.
- Base traits, contracts, and utilities.
- Do not modify in downstream themes unless there is no other option.
- `inc/Main.php`
- Theme bootstrap entry point.
- Defines the primary theme class under `rtCamp\Theme\Elementary`.
- `inc/Core/`
- Project-specific core classes.
- Example: asset loading, theme setup, shared services.
- `inc/BlockExtensions/`
- Project-specific block extension classes.
- Example: block render filters, block-specific integrations.
- `inc/helpers/`
- Non-namespaced helper files.
- Loaded via Composer `files` autoload and not subject to PSR-4 class name rules.
```json
"autoload": {
"psr-4": {
"rtCamp\\Theme\\Elementary\\": "inc/"
}
}
```

Directory segments map 1:1 to namespace segments. Files are PascalCase.

## Two-layer model
| Namespace | File |
|------------------------------------------------------------------------|-------------------------------------------------------|
| `rtCamp\Theme\Elementary\Main` | `inc/Main.php` |
| `rtCamp\Theme\Elementary\Autoloader` | `inc/Autoloader.php` |
| `rtCamp\Theme\Elementary\Core\Assets` | `inc/Core/Assets.php` |
| `rtCamp\Theme\Elementary\Modules\BlockExtensions\MediaTextInteractive` | `inc/Modules/BlockExtensions/MediaTextInteractive.php`|
| `rtCamp\Theme\Elementary\Modules\Settings\ThemeOptions` | `inc/Modules/Settings/ThemeOptions.php` |
| `rtCamp\Theme\Elementary\Helpers\Util` | `inc/Helpers/Util.php` |

The repository is split into two layers:
## Directory layout

- `inc/Framework/` — upstream-owned framework code. Do not modify in downstream projects.
- `inc/` (outside `Framework/`) — project-specific implementation and customizations.
```
inc/
├── Autoloader.php # Wraps vendor/autoload.php with graceful failure
├── Main.php # Theme bootstrap — loads services
├── Helpers/ # Stateless static utility classes (final, private __construct)
│ └── Util.php # General-purpose helpers (add static methods as needed)
├── Core/ # Theme-wide infrastructure
│ └── Assets.php # Asset registration (uses AssetLoaderTrait)
└── Modules/ # Feature areas
├── BlockExtensions/ # Block render filters and integrations
│ └── MediaTextInteractive.php
└── Settings/ # Admin settings pages (extend AbstractSettingsPage)
└── ThemeOptions.php
```

### `inc/Framework/`
## Helpers

This is the base layer. It should contain only reusable traits, interfaces, and low-level utilities.
Treat it like vendored code. If you need to change behavior, extend the framework class in `inc/` instead.
`inc/Helpers/` is the home for stateless utility classes — `final`, `private __construct()`, static methods only. Today it holds one class, `Util`, kept as a placeholder for theme-wide helpers that don't earn their own dedicated class. Add siblings (e.g. `Str`, `Cache`, `Url`) as cross-cutting helpers accumulate, rather than letting `Util` grow into a grab-bag.

### `inc/`
## Picking a base

This is the application layer for this theme. Add new feature classes, hooks, and theme-specific behavior here.
| Feature | Extends / implements |
|------------------------------------------|-------------------------------------|
| Settings page | `AbstractSettingsPage` |
| Admin (non-settings) page | `AbstractAdminPage` |
| Dynamic block (server-side render) | `AbstractBlock` |
| REST controller | `AbstractRESTController` |
| Shortcode | `AbstractShortcode` |
| Anything else that just wires hooks | `Registrable` interface |
| Same, but registration is conditional | `ConditionallyRegistrable` interface|

## Adding a new class

1. Create a new PHP file under `inc/` using PascalCase file names.
2. Use the matching namespace path.
3. Define the class name to match the filename exactly.
4. If the class belongs to a feature group, create a subdirectory and namespace that group.
1. Pick the right abstract or interface from the table above.
2. Drop the file in the matching `inc/Modules/<Area>/` directory (or `inc/Core/` if it's theme-wide infrastructure).
3. Register it in `Main::__construct()`'s `$this->load( [ … ] )` call.
4. Run `composer dump-autoload`.

Example:

`inc/Example/Feature.php`

```php
namespace rtCamp\Theme\Elementary\Example;
// inc/Modules/Example/Feature.php
namespace rtCamp\Theme\Elementary\Modules\Example;

use rtCamp\WPFramework\Contracts\Interfaces\Registrable;

class Feature {
// ...
final class Feature implements Registrable {
public function register_hooks(): void {
add_action( 'init', [ $this, 'do_something' ] );
}

public function do_something(): void {
// ...
}
}
```

## Existing non-namespaced file
Then in `Main::__construct()`:

```php
$this->load( [
Assets::class,
MediaTextInteractive::class,
ThemeOptions::class,
\rtCamp\Theme\Elementary\Modules\Example\Feature::class,
] );
```

## Conditional registration

`inc/helpers/custom-functions.php` is intentionally not namespaced and remains loaded through Composer `files` autoload.
A class can opt out of registration at runtime by implementing `ConditionallyRegistrable` instead of `Registrable`:

## Running autoload generation
```php
final class DevToolbarExtension implements ConditionallyRegistrable {
public function can_register(): bool {
return defined( 'WP_DEBUG' ) && WP_DEBUG;
}

public function register_hooks(): void {
// Wire dev-only hooks here.
}
}
```

The `Loader` calls `can_register()` first and skips `register_hooks()` when it returns false.

After moving or adding namespaced classes, regenerate Composer autoload files:
## Running Composer

```bash
# First-time setup
composer install

# After adding, renaming, or moving a class
composer dump-autoload
```

## Notes

- Do not use `classmap` autoloading for namespaced classes.
- Keep the `rtCamp\Theme\Elementary\` PSR-4 root aligned with `inc/`.
If `vendor/autoload.php` is missing at runtime, the theme shows an admin notice instead of fataling — see `inc/Autoloader.php` and `AutoloaderTrait` in the framework.
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

A starter theme that facilitates a quick head start for developing new [block-based themes](https://developer.wordpress.org/block-editor/how-to-guides/themes/block-theme-overview/) along with a bunch of developer-friendly features.

- [Understand the Folder Structure](https://github.com/rtCamp/theme-elementary#understand-the-folder-structure-open_file_folder)
- [Get Started](https://github.com/rtCamp/theme-elementary#get-started-rocket)
- [Development](https://github.com/rtCamp/theme-elementary#development-computer)
Reusable scaffolding (singleton, autoloader, asset loader, template loader, and abstract base classes) ships separately as the `rtcamp/wp-framework` Composer package and is loaded from `vendor/`.

> **Working on this theme?** See [DEVELOPMENT.md](DEVELOPMENT.md) for the architecture overview, the module pattern, and how to add new classes.

- [Understand the Folder Structure](#understand-the-folder-structure-open_file_folder)
- [Get Started](#get-started-rocket)
- [Development](#development-computer)

## Understand the Folder Structure :open_file_folder:
```
.
.
├── src (Frontend source)
│ ├── css
│ │ ├── frontend/
Expand All @@ -30,20 +34,25 @@ A starter theme that facilitates a quick head start for developing new [block-ba
│ └── build (Compiled output)
├── bin (Holds scripts)
├── functions.php (PHP entry point)
├── inc
│ ├── Core/ (Project-specific core classes)
│ ├── BlockExtensions/ (Block extension classes)
│ ├── Framework/ (Upstream framework code)
│ └── helpers/ (Non-namespaced helpers)
├── inc (All project-specific PHP — PSR-4 root)
│ ├── Autoloader.php (Wraps vendor/autoload.php with graceful failure)
│ ├── Main.php (Theme bootstrap — loads services)
│ ├── Helpers/ (Stateless static utility classes)
│ ├── Core/ (Theme-wide infrastructure — assets, etc.)
│ └── Modules/ (Feature areas)
│ ├── BlockExtensions/ (Block render filters and integrations)
│ └── Settings/ (Admin settings pages — extend AbstractSettingsPage)
├── parts (Block Template Parts)
├── patterns (Block Patterns)
├── style.css
├── templates (Block Templates)
├── tests (Holds JS & PHP tests)
│ ├── js/
│ └── php/
├── vendor
│ └── rtcamp/wp-framework/ (Framework — do not modify; Composer-managed)
├── DEVELOPMENT.md
└── theme.json

```

## Get Started :rocket:
Expand Down
15 changes: 10 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@
"type": "wordpress-theme",
"homepage": "https://rtcamp.com/",
"license": "GPL-2.0-or-later",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/rtCamp/wp-framework.git",
"no-api": true
}
],
"require": {
"php": ">=8.2"
"php": ">=8.2",
"rtcamp/wp-framework": "dev-framework"
},
"require-dev": {
"wp-coding-standards/wpcs": "^2.3",
Expand Down Expand Up @@ -34,10 +42,7 @@
"autoload": {
"psr-4": {
"rtCamp\\Theme\\Elementary\\": "inc/"
},
"files": [
"inc/helpers/custom-functions.php"
]
}
},
"autoload-dev": {
"psr-4": {
Expand Down
Loading