Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
62d504c
Add wiki docs
darylldoyle Aug 14, 2025
446b739
Remove get_post_types from readme as it’s confusing
darylldoyle Aug 14, 2025
3e85b2c
Update docs
darylldoyle Aug 14, 2025
d09fca1
Add headings
darylldoyle Aug 14, 2025
2ecfa59
Merge pull request #19 from 10up/feature/docs
darylldoyle Aug 14, 2025
01ae2ee
Fix a typo
szepeviktor Aug 14, 2025
937e083
Merge pull request #20 from szepeviktor/typos
fabiankaegy Aug 18, 2025
78df870
Add Rector for upgrades
darylldoyle Sep 11, 2025
c70f6cd
Add types
darylldoyle Sep 11, 2025
8bb4ca4
Merge pull request #21 from 10up/feature/typing
darylldoyle Sep 16, 2025
29f5268
Allow caching to be skipped
darylldoyle Nov 13, 2025
95a2986
Test cache skipping is working
darylldoyle Nov 13, 2025
e96edde
Update docs
darylldoyle Nov 13, 2025
d612e51
Linting fixes
darylldoyle Nov 13, 2025
ea9349f
Update docs
darylldoyle Nov 13, 2025
3db410b
Revert "Feature/typing"
darylldoyle Nov 13, 2025
9ba6771
Merge pull request #28 from 10up/revert-21-feature/typing
darylldoyle Nov 13, 2025
5e40a1a
Merge branch 'develop' into feature/allow-skipping-caching
darylldoyle Nov 13, 2025
95837f9
Merge pull request #26 from 10up/feature/allow-skipping-caching
darylldoyle Nov 13, 2025
8779ff3
Disable chains
darylldoyle Nov 18, 2025
dc9091d
Merge pull request #31 from 10up/feature/remove-chaining
darylldoyle Nov 25, 2025
0bbb517
Bump phpunit/phpunit in the composer group across 1 directory
dependabot[bot] Feb 4, 2026
85c548f
Merge pull request #34 from 10up/dependabot/composer/composer-73a4f1667a
darylldoyle Feb 5, 2026
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
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@ class Demo extends AbstractTaxonomy {
public function get_plural_label() {
return esc_html__( 'Categories', 'tenup-plugin' );
}

public function get_post_types() {
return [ 'tenup-demo' ];
}
}
```

Expand Down
162 changes: 109 additions & 53 deletions composer.lock

Large diffs are not rendered by default.

114 changes: 114 additions & 0 deletions docs/Asset-Loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Asset Loading

## Overview
Use the `TenupFramework\Assets\GetAssetInfo` trait to read dependency and version metadata generated by your build (the `.asset.php` sidecar files). The trait looks for files in:
- `dist/js/{slug}.asset.php`
- `dist/css/{slug}.asset.php`
- `dist/blocks/{slug}.asset.php`

If no sidecar is found, it falls back to the version you provide and returns an empty dependency list, allowing safe enqueues during development or when assets are missing.

## Setup
Prerequisites: define common constants and a dist/ directory for built assets.

```php
// Plugin main file or bootstrap
define( 'YOUR_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'YOUR_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'YOUR_PLUGIN_INC', YOUR_PLUGIN_PATH . 'inc/' );
define( 'YOUR_PLUGIN_VERSION', '1.0.0' );
```

Typical dist/ layout (your build may vary):
```
dist/
├─ js/
│ ├─ admin.js
│ └─ admin.asset.php
├─ css/
│ └─ admin.css
└─ blocks/
├─ my-block.js
└─ my-block.asset.php
```

Include the trait in any class that enqueues assets and set the dist path and fallback version once (e.g., in the constructor or on first use):

```php
use TenupFramework\Assets\GetAssetInfo;

class AssetsModule {
use GetAssetInfo;

public function __construct() {
$this->setup_asset_vars(
dist_path: YOUR_PLUGIN_PATH . 'dist/',
fallback_version: YOUR_PLUGIN_VERSION
);
}
}
```

Notes:
- If you call `get_asset_info()` before `setup_asset_vars()`, a RuntimeException will be thrown.
- During local development, sidecar files (e.g., `admin.asset.php`) may be missing. The trait safely falls back to your provided fallback_version and an empty dependency list, so your enqueues still work.
- If your build produces multiple variants (e.g., `admin.js` vs `admin.min.js`), you can conditionally enqueue based on `SCRIPT_DEBUG` or `wp_get_environment_type() === 'development'`.

## Enqueuing scripts
```php
wp_enqueue_script(
'tenup_plugin_admin',
YOUR_PLUGIN_URL . 'dist/js/admin.js',
$this->get_asset_info( 'admin', 'dependencies' ),
$this->get_asset_info( 'admin', 'version' ),
true
);
```
- dependencies: array of script handles from `admin.asset.php`
- version: string used for cache busting

## Enqueuing styles
```php
wp_enqueue_style(
'tenup_plugin_admin',
YOUR_PLUGIN_URL . 'dist/css/admin.css',
[], // CSS dependencies are uncommon; pass [] unless needed
$this->get_asset_info( 'admin', 'version' )
);
```

## Working with blocks
If you build blocks, pass the block slug used by your build tool:
```php
$deps = $this->get_asset_info( 'my-block', 'dependencies' );
$ver = $this->get_asset_info( 'my-block', 'version' );
$handle = 'tenup_my_block';

wp_register_script( $handle, YOUR_PLUGIN_URL . 'dist/blocks/my-block.js', $deps, $ver, true );
```
The trait automatically checks `dist/blocks/my-block.asset.php` if present.

## Error handling and fallbacks
```php
try {
$deps = $this->get_asset_info( 'frontend', 'dependencies' );
$ver = $this->get_asset_info( 'frontend', 'version' );
} catch ( \RuntimeException $e ) {
// setup_asset_vars() was not called — fall back to safe defaults
$deps = [];
$ver = YOUR_PLUGIN_VERSION;
}
```

## Best practices
- Always call `setup_asset_vars()` early (constructor or on first enqueue).
- Keep your dist path stable across environments (use constants for PATH and URL).
- Use the version from `.asset.php` for reliable cache busting in production.
- For admin-only assets, enqueue on `admin_enqueue_scripts`; for frontend, use `wp_enqueue_scripts`.

## See also
- [Docs Home](README.md)
- [Autoloading and Modules](Autoloading.md)
- [Modules and Initialization](Modules-and-Initialization.md)
- [Post Types](Post-Types.md)
- [Taxonomies](Taxonomies.md)
121 changes: 121 additions & 0 deletions docs/Autoloading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Autoloading and Modules

## Overview
WP Framework follows PSR-4 autoloading and discovers your classes at runtime to initialize Modules. Instead of extending a base class, you implement ModuleInterface and use the Module trait to participate in the lifecycle.

## Composer PSR-4 setup
Add your project namespace and source directory in composer.json:

```json
{
"autoload": {
"psr-4": {
"YourVendor\\YourPlugin\\": "inc/"
}
}
}
```

Run `composer dump-autoload` after changes.

## Recommended plugin structure & bootstrap
A simple plugin layout that works well with the framework:

```
my-plugin/
├─ my-plugin.php // main plugin file
├─ composer.json
├─ inc/ // PHP source (PSR-4 autoloaded)
│ ├─ Features/
│ ├─ Posts/
│ └─ Taxonomies/
├─ dist/ // built assets from your toolchain
│ ├─ js/
│ │ ├─ admin.js
│ │ └─ admin.asset.php
│ ├─ css/
│ │ └─ admin.css
│ └─ blocks/
│ ├─ my-block.js
│ └─ my-block.asset.php
└─ readme.txt
```

Define a few useful constants in your main plugin file (or a bootstrap class), then initialize modules:

```php
// Plugin main file or bootstrap
define( 'YOUR_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'YOUR_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'YOUR_PLUGIN_INC', YOUR_PLUGIN_PATH . 'inc/' );
define( 'YOUR_PLUGIN_VERSION', '1.0.0' );

use TenupFramework\ModuleInitialization;
ModuleInitialization::instance()->init_classes( YOUR_PLUGIN_INC );
```

## Initialization
Call the framework’s initializer with the directory where your classes live (e.g., inc or src):

```php
use TenupFramework\ModuleInitialization;

ModuleInitialization::instance()->init_classes( YOUR_PLUGIN_INC );
```

- Classes must be instantiable and implement `TenupFramework\ModuleInterface`.
- The initializer sorts modules by `load_order()` (defaults to `10` via the `Module` trait) and then calls `register()` only if `can_register()` returns `true`.
- A hook fires before registration: `tenup_framework_module_init__{slug}`, where slug is a sanitized class FQN.

### Verify discovery and environment behavior
In development, you can verify discovered/initialized modules:

```php
add_action( 'plugins_loaded', function () {
$mods = TenupFramework\ModuleInitialization::instance()->get_all_classes();
// For local/dev only:
// error_log( print_r( array_keys( $mods ), true ) );
} );
```

Environment caching:
- Discovery results are cached only in production and staging environments (per `wp_get_environment_type()`).
- Cache is stored under the directory you pass to `init_classes()`, in a "class-loader-cache" folder (e.g., `YOUR_PLUGIN_INC . 'class-loader-cache'`).
- To refresh: delete that folder; it will be rebuilt automatically.
- Caching is skipped entirely when the constant `VIP_GO_APP_ENVIRONMENT` is defined or when `TENUP_FRAMEWORK_DISABLE_CLASS_CACHE` is set to `true`. Use `define( 'TENUP_FRAMEWORK_DISABLE_CLASS_CACHE', true )` in environments that don't support writable file systems.

## Defining a Module
```php
namespace YourVendor\YourPlugin\Features;

use TenupFramework\ModuleInterface;
use TenupFramework\Module;

class YourModule implements ModuleInterface {
use Module; // provides default load_order() = 10

public function can_register(): bool {
// Only run on frontend, for example
return ! is_admin();
}

public function register(): void {
add_action( 'init', function () {
// Add hooks/filters here
} );
}
}
```

## Best practices
- Keep Modules small and focused; compose behavior via multiple classes.
- Use `can_register()` to gate context-specific behavior (admin vs. frontend, REST, multisite, feature flags).
- Prefer dependency injection via constructor where practical; avoid doing heavy work before `register()`.


## See also
- [Docs Home](README.md)
- [Modules and Initialization](Modules-and-Initialization.md)
- [Post Types](Post-Types.md)
- [Taxonomies](Taxonomies.md)
- [Asset Loading](Asset-Loading.md)
101 changes: 101 additions & 0 deletions docs/Modules-and-Initialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Modules and Initialization

## Overview
The WP Framework organizes functionality into small Modules. A Module is any class that implements TenupFramework\ModuleInterface and typically uses the TenupFramework\Module trait. Modules are discovered at runtime and initialized in a defined order.

Key interfaces and utilities:
- `TenupFramework\ModuleInterface`: declares `load_order()`, `can_register()`, `register()`.
- `TenupFramework\Module` trait: provides a default `load_order()` of `10` and leaves `can_register()` and `register()` abstract for your class to implement.
- `TenupFramework\ModuleInitialization`: discovers, orders, and initializes your Modules.

## Bootstrapping
Call the initializer at plugin or theme bootstrap, pointing it at the directory containing your namespaced classes (e.g., `inc/` or `src/`):

```php
use TenupFramework\ModuleInitialization;

ModuleInitialization::instance()->init_classes( YOUR_PLUGIN_INC );
```

`YOUR_PLUGIN_INC` (or your equivalent constant/path) should resolve to an existing directory. If it does not exist, a RuntimeException will be thrown.

## How discovery and initialization work
ModuleInitialization performs the following steps:
1. Validate the directory exists; otherwise throw a RuntimeException.
2. Discover class names within the directory using spatie/structure-discoverer.
- In production and staging environments (wp_get_environment_type), results are cached for performance using a file-based cache.
- Caching is skipped entirely when the constant `VIP_GO_APP_ENVIRONMENT` is defined.
3. Reflect on each discovered class and skip any that:
- are not instantiable,
- do not implement `TenupFramework\ModuleInterface`.
4. Instantiate the class.
5. Fire an action before registration for each module: `tenup_framework_module_init__{slug}`
- `slug` is the sanitized class FQN (backslashes replaced with dashes, then passed through `sanitize_title`).
6. Sort modules by `load_order()` (lower numbers first) and iterate in order.
7. For each module, call `register()` only if `can_register()` returns true.
8. Store initialized modules for later retrieval.

Environment cache behavior
- Where cache lives: under the directory you pass to `init_classes()`, in a `class-loader-cache` folder (e.g., `YOUR_PLUGIN_INC . 'class-loader-cache'`).
- When it’s used: only in `production` and `staging` environment types (`wp_get_environment_type()`).
- How to clear: delete the `class-loader-cache` folder; it will be rebuilt on next discovery.
- How to disable in development: use `development` or `local` environment types, or define `VIP_GO_APP_ENVIRONMENT` to skip the cache.
- How to disable for hosts that don't support file-based caching: `define( 'TENUP_FRAMEWORK_DISABLE_CLASS_CACHE', true );` to skip caching altogether.

Hooks
- Action: `tenup_framework_module_init__{slug}` — fires before each module’s `register()` runs.
- Parameters: the module instance.
- Example:
```php
add_action( 'tenup_framework_module_init__yourvendor-yourplugin-features-frontendtweaks', function ( $module ) {
// Inspect or adjust before register()
} );
```

Load order dependencies example
- If Module B depends on Module A:
```php
class ModuleA implements ModuleInterface { use Module; public function load_order(): int { return 5; } }
class ModuleB implements ModuleInterface { use Module; public function load_order(): int { return 10; } }
```
Lower numbers run first. Taxonomies typically use 9 so post types (default 10) can associate afterward.

Utilities:
- `ModuleInitialization::get_module( $classFqn )` retrieves an initialized module instance by its fully qualified class name.
- `ModuleInitialization::instance()->get_all_classes()` returns all initialized module instances keyed by slug.

## Module lifecycle in your code
Your Module should be lightweight at construction time. Use the following methods effectively:
- `load_order(): int` — controls initialization order (default = 10 via Module trait). Override to run earlier/later. For example, taxonomy modules may run at 9 so they are available before post types.
- `can_register(): bool` — return true only when the module should register hooks in the current context (e.g., only in admin, only on frontend, only if a feature flag is enabled).
- `register(): void` — attach your WordPress hooks/filters and perform setup here.

### Example
```php
namespace YourVendor\YourPlugin\Features;

use TenupFramework\ModuleInterface;
use TenupFramework\Module;

class FrontendTweaks implements ModuleInterface {
use Module; // default load_order() = 10

public function can_register(): bool {
return ! is_admin(); // only on frontend
}

public function register(): void {
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue' ] );
}

public function enqueue(): void {
// ... enqueue assets here ...
}
}
```

## Troubleshooting
- Directory is required — If `init_classes()` is called with an empty or non-existent directory, a RuntimeException is thrown.
- Class not initialized — Ensure the class is instantiable and implements `TenupFramework\ModuleInterface`.
- Order of initialization — If you have inter-module dependencies, adjust `load_order()` to ensure prerequisites are registered first.
- Observability — Use the `tenup_framework_module_init__{slug}` action to inspect or modify module instances before they register.
Loading