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
41 changes: 41 additions & 0 deletions assets/dist/fsm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use strict";
/**
* Shared FSM implementation in TypeScript.
* Mirrors PHP backend to maintain SSoT across contexts.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateMachine = void 0;
class StateMachine {
constructor(transitions, initial) {
this.transitions = transitions;
this.history = [];
this.state = initial;
this.history.push(initial);
}
transition(to) {
const allowed = this.transitions[this.state] || [];
if (allowed.includes(to)) {
this.state = to;
this.history.push(to);
return true;
}
return false;
}
getState() {
return this.state;
}
getHistory() {
return this.history;
}
}
exports.StateMachine = StateMachine;
// Example FSM instance for dashboard visualiser.
const definition = {
init: ['ready'],
ready: ['running', 'error'],
running: ['ready', 'error'],
error: ['ready'],
};
const machine = new StateMachine(definition, 'init');
// Expose machine for debugging in admin screens.
window.nhkPocMachine = machine;
49 changes: 49 additions & 0 deletions assets/ts/fsm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Shared FSM implementation in TypeScript.
* Mirrors PHP backend to maintain SSoT across contexts.
*/

export interface MachineDefinition {
[state: string]: string[];
}

export class StateMachine {
private state: string;
private history: string[] = [];

constructor(private transitions: MachineDefinition, initial: string) {
this.state = initial;
this.history.push(initial);
}

public transition(to: string): boolean {
const allowed = this.transitions[this.state] || [];
if (allowed.includes(to)) {
this.state = to;
this.history.push(to);
return true;
}
return false;
}

public getState(): string {
return this.state;
}

public getHistory(): string[] {
return this.history;
}
}

// Example FSM instance for dashboard visualiser.
const definition: MachineDefinition = {
init: ['ready'],
ready: ['running', 'error'],
running: ['ready', 'error'],
error: ['ready'],
};

const machine = new StateMachine(definition, 'init');

// Expose machine for debugging in admin screens.
(window as any).nhkPocMachine = machine;
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## 1.0.0
- Initial proof-of-concept release with FSM-based architecture.
12 changes: 12 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "nhk/poc",
"description": "NHK Proof of Concept WordPress plugin framework",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"autoload": {
"psr-4": {
"NHK\\Poc\\": "src/"
}
},
"require": {}
}
37 changes: 37 additions & 0 deletions nhk-poc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* Plugin Name: NHK PoC
* Description: NHK proof-of-concept WordPress plugin starter using an FSM-first architecture.
* Version: 1.0.0
* Author: NHK
* Text Domain: nhk-poc
*/

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// Define key plugin constants.
define( 'NHK_POC_VERSION', '1.0.0' );
define( 'NHK_POC_PATH', plugin_dir_path( __FILE__ ) );
define( 'NHK_POC_URL', plugin_dir_url( __FILE__ ) );

// Load Composer autoloader for PSR-4 classes.
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
require_once __DIR__ . '/vendor/autoload.php';
}

use NHK\Poc\Core\Plugin;

// Boot the plugin using an FSM-centric Plugin class.
add_action( 'plugins_loaded', function () {
Plugin::get_instance()->boot();
} );

// Add settings link on Plugins listing page.
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function ( $links ) {
$url = admin_url( 'admin.php?page=nhk-poc-settings' );
$links[] = '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Settings', 'nhk-poc' ) . '</a>';
return $links;
} );
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "nhk-poc",
"version": "1.0.0",
"devDependencies": {
"typescript": "^5.0.0"
},
"scripts": {
"build": "tsc assets/ts/fsm.ts --target ES2017 --module commonjs --outDir assets/dist"
}
}
73 changes: 73 additions & 0 deletions src/Admin/Menu.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
namespace NHK\Poc\Admin;

use NHK\Poc\Core\FSM\StateMachine;

/**
* Registers admin menus and pages.
* Each page interacts with the shared FSM for predictable flows.
*/
class Menu {
public static function init( StateMachine $sm ): void {
add_action( 'admin_menu', function () use ( $sm ) {
add_menu_page(
'NHK PoC',
'NHK PoC',
'manage_options',
'nhk-poc',
[ self::class, 'render_dashboard' ],
'dashicons-admin-generic'
);

add_submenu_page(
'nhk-poc',
'Dashboard',
'Dashboard',
'manage_options',
'nhk-poc',
[ self::class, 'render_dashboard' ]
);

add_submenu_page(
'nhk-poc',
'Settings',
'Settings',
'manage_options',
'nhk-poc-settings',
[ self::class, 'render_settings' ]
);

add_submenu_page(
'nhk-poc',
'Changelog - v' . NHK_POC_VERSION,
'Changelog - v' . NHK_POC_VERSION,
'manage_options',
'nhk-poc-changelog',
[ self::class, 'render_changelog' ]
);
} );

// Enqueue shared FSM script on plugin pages and pass serialized machine state.
add_action( 'admin_enqueue_scripts', function ( $hook ) use ( $sm ) {
if ( false === strpos( $hook, 'nhk-poc' ) ) {
return;
}
wp_enqueue_script( 'nhk-poc-fsm', NHK_POC_URL . 'assets/dist/fsm.js', [], NHK_POC_VERSION, true );
wp_localize_script( 'nhk-poc-fsm', 'nhkPocState', $sm->serialize() );
} );
}

public static function render_dashboard(): void {
echo '<div class="wrap"><h1>NHK PoC Dashboard</h1><div id="nhk-poc-dashboard"></div></div>';
}

public static function render_settings(): void {
echo '<div class="wrap"><h1>Settings</h1><p>FSM-driven settings page.</p></div>';
}

public static function render_changelog(): void {
$file = NHK_POC_PATH . 'changelog.md';
$content = file_exists( $file ) ? wp_kses_post( nl2br( file_get_contents( $file ) ) ) : __( 'No changelog available', 'nhk-poc' );
echo '<div class="wrap"><h1>Changelog</h1><div class="nhk-poc-changelog">' . $content . '</div></div>';
}
}
33 changes: 33 additions & 0 deletions src/Api/AjaxHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
namespace NHK\Poc\Api;

use NHK\Poc\Core\FSM\StateMachine;

/**
* Central AJAX handler.
* Provides discrete error responses highlighting failing steps.
*/
class AjaxHandler {
public static function init( StateMachine $sm ): void {
add_action( 'wp_ajax_nhk_poc_transition', function () use ( $sm ) {
try {
$next = sanitize_text_field( $_POST['next'] ?? '' );

if ( ! $sm->transition( $next ) ) {
throw new \Exception( 'Invalid transition to ' . $next );
}

wp_send_json_success( [
'state' => $sm->state(),
'history' => $sm->history(),
] );
} catch ( \Throwable $e ) {
error_log( 'NHK PoC AJAX error: ' . $e->getMessage() );
wp_send_json_error( [
'message' => $e->getMessage(),
'history' => $sm->history(),
] );
}
} );
}
}
61 changes: 61 additions & 0 deletions src/Core/FSM/StateMachine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
namespace NHK\Poc\Core\FSM;

/**
* Minimal finite state machine implementation.
* Acts as the SSoT for any feature that flows through it.
*/
class StateMachine {
/** @var array<string, array<int, string>> */
private $transitions;

/** @var string */
private $state;

/** @var array<int, string> */
private $history = [];

public function __construct( array $transitions, string $initial ) {
$this->transitions = $transitions;
$this->state = $initial;
$this->history[] = $initial;
}

/**
* Move to a new state if the transition is allowed.
*/
public function transition( string $to ): bool {
$allowed = $this->transitions[ $this->state ] ?? [];
if ( in_array( $to, $allowed, true ) ) {
$this->state = $to;
$this->history[] = $to;
return true;
}
return false;
}

/**
* Current state getter.
*/
public function state(): string {
return $this->state;
}

/**
* Returns the transition history for debugging and traceability.
*/
public function history(): array {
return $this->history;
}

/**
* Serialized representation for passing to the frontend.
*/
public function serialize(): array {
return [
'state' => $this->state,
'history' => $this->history,
'transitions' => $this->transitions,
];
}
}
70 changes: 70 additions & 0 deletions src/Core/Plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
namespace NHK\Poc\Core;

use NHK\Poc\Core\FSM\StateMachine;
use NHK\Poc\Admin\Menu;
use NHK\Poc\Api\AjaxHandler;

/**
* Main plugin bootstrapper.
* Orchestrates FSM and acts as the SSoT container for global state.
*/
class Plugin {
/** @var self */
private static $instance;

/** @var StateMachine */
private $sm;

private function __construct() {
// Define a simple FSM shared across plugin features.
$definition = [
'init' => ['ready'],
'ready' => ['running', 'error'],
'running' => ['ready', 'error'],
'error' => ['ready'],
];
$this->sm = new StateMachine( $definition, 'init' );
}

/**
* Singleton accessor.
*/
public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* Boot plugin features.
*/
public function boot(): void {
// Transition from init -> ready once plugin loads.
$this->sm->transition( 'ready' );

// Register admin UI & AJAX handlers through FSM-aware classes.
Menu::init( $this->sm );
AjaxHandler::init( $this->sm );

// Optional on-screen debug panel in admin when query arg present.
if ( isset( $_GET['nhk_poc_debug'] ) ) {
add_action( 'admin_notices', [ $this, 'debug_panel' ] );
}
}

/**
* Expose current state machine.
*/
public function sm(): StateMachine {
return $this->sm;
}

/**
* Render minimal debug info showing state history.
*/
public function debug_panel(): void {
echo '<div class="notice notice-info"><p>FSM History: ' . esc_html( implode( ' → ', $this->sm->history() ) ) . '</p></div>';
}
}