PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / framework / app / class-runtime.php

class-runtime.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/framework/app/class-runtime.php

209 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation App Framework — dispatch runtime.
4 *
5 * The whole request cycle of a window, host-agnostic:
6 *
7 * request { action, state, args, client }
8 * → rebuild State from the app's declared defaults
9 * → run the action (or `mount` / the built-in `set`)
10 * → render the view
11 * response { state, html, effects }
12 *
13 * The host (a REST route on WordPress, anything on a bare PHP host)
14 * only has to move those two arrays over the wire. A failure comes
15 * back as `array( 'ok' => false, 'error' => <code>, 'status' => <http> )`
16 * with an English `message` the host may translate.
17 *
18 * @package OpenStation
19 */
20
21 namespace OpenStation\App;
22
23 use OpenStation\App;
24
25 // Direct access, unless a standalone host is booting on bare PHP.
26 if ( ! defined( 'ABSPATH' ) ) {
27 defined( 'OPENSTATION_STANDALONE' ) || exit;
28 }
29
30 /**
31 * Runs dispatches against the registry.
32 */
33 final class Runtime {
34
35 /** First render of a window. Runs the app's `mount` hook, if any. */
36 const ACTION_MOUNT = 'mount';
37
38 /** Built-in: the client changed a bound key; nothing to run, just re-render. */
39 const ACTION_SET = 'set';
40
41 /**
42 * Built-in: recompute `data()` and re-render, nothing else. Both
43 * first apps declared an empty action just to get this; declaring
44 * a `refresh` handler still works and wins, for the app that also
45 * wants to reset something on the way.
46 */
47 const ACTION_REFRESH = 'refresh';
48
49 /**
50 * @var Registry
51 */
52 private $registry;
53
54 public function __construct( Registry $registry ) {
55 $this->registry = $registry;
56 }
57
58 /**
59 * The registry this runtime dispatches into.
60 *
61 * @return Registry
62 */
63 public function registry() {
64 return $this->registry;
65 }
66
67 /**
68 * Run one dispatch.
69 *
70 * @param string $app_id App id.
71 * @param array<string,mixed> $request `action` (string), `state` (array), `args` (array), `client` (array).
72 * @param Os $os Host handle for the acting user.
73 * @return array<string,mixed> `ok`, then `state` / `html` / `effects` on success or
74 * `error` / `message` / `status` on failure.
75 */
76 public function dispatch( $app_id, array $request, Os $os ) {
77 $app = $this->registry->get( $app_id );
78 if ( ! $app ) {
79 return self::failure( 'not_found', 'Unknown app.', 404 );
80 }
81 if ( ! $app->allows( $os ) ) {
82 return self::failure( 'forbidden', 'You are not allowed to use this window.', 403 );
83 }
84
85 $action = isset( $request['action'] ) ? strtolower( (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $request['action'] ) ) : '';
86 $args = isset( $request['args'] ) && is_array( $request['args'] ) ? $request['args'] : array();
87 $view = isset( $request['view'] ) ? strtolower( (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $request['view'] ) ) : 'main';
88 if ( '' === $view ) {
89 $view = 'main';
90 }
91 if ( ! $app->has_view( $view ) ) {
92 return self::failure( 'unknown_view', sprintf( 'Unknown view "%s".', $view ), 400 );
93 }
94 $state = new State(
95 $app->defaults(),
96 isset( $request['state'] ) && is_array( $request['state'] ) ? $request['state'] : array()
97 );
98 $os->begin(
99 isset( $request['client'] ) && is_array( $request['client'] ) ? $request['client'] : array(),
100 isset( $request['params'] ) && is_array( $request['params'] ) ? $request['params'] : array(),
101 $app->id(),
102 $view
103 );
104
105 try {
106 if ( self::ACTION_MOUNT === $action ) {
107 $app->run_mount( $state, $os );
108 } elseif ( self::ACTION_SET === $action ) {
109 // State already carries the bound value.
110 $app->run_action( self::ACTION_SET, $state, $os, $args, false );
111 } elseif ( $app->has_action( $action ) ) {
112 $app->run_action( $action, $state, $os, $args );
113 } elseif ( self::ACTION_REFRESH !== $action ) {
114 // A bare `refresh` (no declared handler) falls through on
115 // purpose: recomputing `data()` below IS the action, and
116 // declaring an empty handler to get it is boilerplate.
117 return self::failure( 'unknown_action', sprintf( 'Unknown action "%s".', $action ), 400 );
118 }
119
120 $data = $app->has_data() ? $app->compute_data( $state, $os ) : null;
121 $html = $app->render( $state, $os, $view );
122 } catch ( \Throwable $e ) {
123 return self::failure( 'action_failed', $e->getMessage(), 500 );
124 }
125
126 $response = array(
127 'ok' => true,
128 'state' => $state->all(),
129 'html' => $html,
130 'effects' => $os->effects->all(),
131 );
132 if ( null !== $data ) {
133 $response['data'] = $data;
134 }
135
136 /**
137 * Filter a dispatch response before it leaves the runtime.
138 *
139 * @param array<string,mixed> $response `ok`, `state`, `html`, `effects`.
140 * @param string $app_id App id.
141 * @param string $action Action that ran.
142 * @param State $state Final state.
143 */
144 $filtered = $os->filter( 'openstation_app_response', $response, $app->id(), $action, $state );
145
146 return is_array( $filtered ) ? $filtered : $response;
147 }
148
149 /**
150 * Render an app straight from a state array — no action, no
151 * request cycle. What a host calls to get "the whole window" as
152 * a value: the manifest plus the body it would paint.
153 *
154 * @param string $app_id App id.
155 * @param array<string,mixed> $state State values (partial; defaults fill the rest).
156 * @param Os $os Host handle.
157 * @return array<string,mixed> `manifest`, `state`, `html`, `effects` — or a failure array.
158 */
159 public function describe( $app_id, array $state, Os $os ) {
160 $app = $this->registry->get( $app_id );
161 if ( ! $app ) {
162 return self::failure( 'not_found', 'Unknown app.', 404 );
163 }
164 if ( ! $app->allows( $os ) ) {
165 return self::failure( 'forbidden', 'You are not allowed to use this window.', 403 );
166 }
167 $os->begin( array(), array(), $app->id(), 'main' );
168 $window_state = new State( $app->defaults(), $state );
169 try {
170 $app->run_mount( $window_state, $os );
171 $data = $app->has_data() ? $app->compute_data( $window_state, $os ) : null;
172 $html = $app->render( $window_state, $os );
173 $tabs = array();
174 foreach ( $app->tabs() as $tab ) {
175 $os->view = $tab['value'];
176 $tabs[ $tab['value'] ] = $app->render( new State( $app->defaults(), $state ), $os, $tab['value'] );
177 }
178 } catch ( \Throwable $e ) {
179 return self::failure( 'action_failed', $e->getMessage(), 500 );
180 }
181 return array(
182 'ok' => true,
183 'manifest' => $app->manifest(),
184 'state' => $window_state->all(),
185 'html' => $html,
186 'data' => $data,
187 'tabs' => $tabs,
188 'effects' => $os->effects->all(),
189 );
190 }
191
192 /**
193 * Shape a failure.
194 *
195 * @param string $code Machine code.
196 * @param string $message English message.
197 * @param int $status HTTP status the host should use.
198 * @return array<string,mixed>
199 */
200 private static function failure( $code, $message, $status ) {
201 return array(
202 'ok' => false,
203 'error' => $code,
204 'message' => $message,
205 'status' => (int) $status,
206 );
207 }
208 }
209