/inc/class-workflow.php

https://github.com/humanmade/Workflows · PHP · 422 lines · 243 code · 49 blank · 130 comment · 38 complexity · e7892deb7750f90a0b6379df1d40aeb4 MD5 · raw file

  1. <?php
  2. /**
  3. * Workflow class
  4. *
  5. * This class is what we will pass configurations created in the UI to. It can also be invoked directly
  6. * programmatically.
  7. *
  8. * @link https://github.com/humanmade/Workflow/issues/3
  9. *
  10. * @package HM\Workflow
  11. * @since 0.1.0
  12. */
  13. namespace HM\Workflows;
  14. /**
  15. * Class Workflow
  16. */
  17. class Workflow {
  18. /**
  19. * Workflow ID.
  20. *
  21. * @var string
  22. */
  23. protected $id;
  24. /**
  25. * Workflow instances.
  26. *
  27. * @var array
  28. */
  29. protected static $instances = [];
  30. /**
  31. * The Workflow Event.
  32. *
  33. * @var Event
  34. */
  35. protected $event;
  36. /**
  37. * The workflow recipients.
  38. *
  39. * @var array
  40. */
  41. protected $recipients = [];
  42. /**
  43. * The message array, contains keys 'subject', 'text' and 'actions'.
  44. *
  45. * @var array
  46. */
  47. protected $message = [
  48. 'subject' => '',
  49. 'text' => '',
  50. 'actions' => [],
  51. ];
  52. /**
  53. * Workflow destinations.
  54. *
  55. * @var array
  56. */
  57. protected $destinations = [];
  58. /**
  59. * Registers a new Workflow object.
  60. *
  61. * @param string $id Identifier for the Workflow.
  62. *
  63. * @return Workflow
  64. */
  65. public static function register( string $id = '' ): Workflow {
  66. $wf = new self( $id );
  67. self::$instances[ $id ] = $wf;
  68. return $wf;
  69. }
  70. /**
  71. * Retrieve an existing Workflow object.
  72. *
  73. * @param string $id
  74. * @return Workflow|null
  75. */
  76. public static function get( string $id ) {
  77. return self::$instances[ $id ] ?? null;
  78. }
  79. /**
  80. * Remove an existing Workflow object.
  81. *
  82. * @param string $id
  83. */
  84. public static function remove( string $id ) {
  85. unset( self::$instances[ $id ] );
  86. }
  87. /**
  88. * Workflow constructor.
  89. *
  90. * @param string $id Identifier for the workflow.
  91. */
  92. protected function __construct( string $id = '' ) {
  93. $this->id = $id;
  94. add_action( "hm.workflows.run.{$this->id}", [ $this, 'run' ] );
  95. }
  96. /**
  97. * Attach the event to the workflow.
  98. *
  99. * @param Event|array|string $event Event ID or object.
  100. *
  101. * @return $this
  102. */
  103. public function when( $event ): Workflow {
  104. // Get existing or create the Event object.
  105. if ( is_string( $event ) ) {
  106. $this->event = Event::get( $event );
  107. if ( ! $this->event ) {
  108. $this->event = Event::register( $event )->set_listener( $event );
  109. }
  110. } elseif ( is_array( $event ) && isset( $event['action'] ) ) {
  111. $this->event = Event::get( $event['action'] );
  112. if ( ! $this->event ) {
  113. $this->event = Event::register( $event['action'] )->set_listener( $event );
  114. }
  115. } elseif ( is_callable( $event ) ) {
  116. $this->event = Event::register( $this->id )->set_listener( $event );
  117. } elseif ( is_a( $event, __NAMESPACE__ . '\Event' ) ) {
  118. $this->event = $event;
  119. }
  120. // Check we have a valid Event.
  121. if ( ! $this->event ) {
  122. trigger_error( 'Could not get event object for workflow ' . $this->id, E_USER_WARNING );
  123. return $this;
  124. }
  125. $listener = $this->event->get_listener();
  126. $ui_data = [];
  127. if ( $this->event->get_ui() ) {
  128. $ui_data = $this->event->get_ui()->get_data();
  129. }
  130. // Call the listener.
  131. if ( is_string( $listener ) ) {
  132. add_action( $listener, function () use ( $ui_data ) {
  133. $this->schedule( array_merge( func_get_args(), [ 'ui_data' => $ui_data ] ) );
  134. } );
  135. } elseif ( is_array( $listener ) ) {
  136. add_action( $listener['action'], function () use ( $listener, $ui_data ) {
  137. $args = func_get_args();
  138. if ( isset( $listener['callback'] ) && is_callable( $listener['callback'] ) ) {
  139. $result = call_user_func_array(
  140. $listener['callback'],
  141. array_merge( $args, [ 'ui_data' => $ui_data ] )
  142. );
  143. if ( ! is_null( $result ) ) {
  144. if ( ! is_array( $result ) ) {
  145. $result = [ $result ];
  146. }
  147. $this->schedule( $result );
  148. }
  149. } else {
  150. $this->schedule( array_merge( $args, [ 'ui_data' => $ui_data ] ) );
  151. }
  152. }, $listener['priority'], $listener['accepted_args'] );
  153. } elseif ( is_callable( $listener ) ) {
  154. $result = call_user_func( $listener, $ui_data );
  155. if ( ! is_null( $result ) ) {
  156. if ( ! is_array( $result ) ) {
  157. $result = [ $result ];
  158. }
  159. $this->schedule( $result );
  160. }
  161. }
  162. return $this;
  163. }
  164. /**
  165. * Message builder.
  166. *
  167. * @param string|callable $subject Subject line or short text for the notification.
  168. * @param string|callable $text Optional message body.
  169. * @param array $actions Actions to append to the message text.
  170. *
  171. * @return $this
  172. */
  173. public function what( $subject, $text = '', array $actions = [] ): Workflow {
  174. $this->message = [
  175. 'subject' => $subject,
  176. 'text' => $text,
  177. 'actions' => array_map( function ( $action ) {
  178. return wp_parse_args( $action, [
  179. 'text' => null,
  180. 'callback_or_url' => null,
  181. 'args' => [],
  182. 'schema' => [],
  183. 'data' => [],
  184. ] );
  185. }, $actions ),
  186. ];
  187. return $this;
  188. }
  189. /**
  190. * Sets the recipients property.
  191. *
  192. * @param array|int|string|callable $who Workflow destination.
  193. *
  194. * @return $this
  195. */
  196. public function who( $who ): Workflow {
  197. if ( is_array( $who ) ) {
  198. $this->recipients = array_merge( $this->recipients, $who );
  199. } else {
  200. $this->recipients[] = $who;
  201. }
  202. return $this;
  203. }
  204. /**
  205. * Where to send the notification(s).
  206. *
  207. * @param string|Destination $destination The Destination object.
  208. *
  209. * @return $this
  210. */
  211. public function where( $destination ): Workflow {
  212. if ( is_string( $destination ) ) {
  213. $destination = Destination::get( $destination );
  214. }
  215. if ( is_a( $destination, Destination::class ) ) {
  216. $this->destinations[] = $destination;
  217. } elseif ( is_callable( $destination ) ) {
  218. $this->destinations[] = Destination::register(
  219. 'custom-' . $this->id . '-' . count( $this->destinations ),
  220. $destination
  221. );
  222. }
  223. return $this;
  224. }
  225. /**
  226. * Schedule the workflow.
  227. *
  228. * @param array $args
  229. */
  230. protected function schedule( array $args = [] ) {
  231. if ( wp_next_scheduled( "hm.workflows.run.{$this->id}", [ $args ] ) ) {
  232. return;
  233. }
  234. wp_schedule_single_event( time(), "hm.workflows.run.{$this->id}", [ $args ] );
  235. }
  236. /**
  237. * Run the workflow.
  238. *
  239. * @param array $args The return value from the callback or arguments from the action.
  240. */
  241. public function run( array $args = [] ) {
  242. // Process recipients.
  243. $recipients = [];
  244. foreach ( $this->recipients as $recipient ) {
  245. if ( is_numeric( $recipient ) ) {
  246. $user = get_user_by( 'id', intval( $recipient ) );
  247. if ( is_a( $user, 'WP_User' ) ) {
  248. $recipients[] = $user;
  249. }
  250. } elseif ( is_string( $recipient ) && is_email( $recipient ) ) {
  251. // Get user by email or add plain email.
  252. $user = get_user_by( 'email', $recipient );
  253. if ( is_a( $user, 'WP_User' ) ) {
  254. $recipients[] = $user;
  255. }
  256. } elseif ( is_string( $recipient ) ) {
  257. // Try to get user by login, users by role or a registered callback.
  258. if ( get_role( $recipient ) ) {
  259. $users = get_users( [ 'role' => $recipient ] );
  260. if ( ! empty( $users ) ) {
  261. $recipients = array_merge( $recipients, $users );
  262. }
  263. } elseif ( $recipient === 'all' ) {
  264. $recipients = array_merge( $recipients, get_users( [
  265. 'paged' => -1,
  266. ] ) );
  267. } elseif ( $this->event->get_recipient_handler( $recipient ) ) {
  268. $results = call_user_func_array( $this->event->get_recipient_handler( $recipient ), $args );
  269. if ( ! is_array( $results ) ) {
  270. $results = [ $results ];
  271. }
  272. $results = array_filter( (array) $results, function ( $result ) {
  273. return is_a( $result, 'WP_User' );
  274. } );
  275. $recipients = array_merge( $recipients, $results );
  276. } else {
  277. $user = get_user_by( 'login', $recipient );
  278. if ( is_a( $user, 'WP_User' ) ) {
  279. $recipients[] = $user;
  280. }
  281. }
  282. } elseif ( is_callable( $recipient ) ) {
  283. // If a callback was passed directly add the results.
  284. $results = call_user_func_array( $recipient, $args );
  285. $results = array_filter( (array) $results, function ( $result ) {
  286. return is_a( $result, 'WP_User' );
  287. } );
  288. $recipients = array_merge( $recipients, $results );
  289. }
  290. }
  291. // Process message.
  292. $tags = [];
  293. foreach ( $this->event->get_message_tags() as $key => $val ) {
  294. if ( is_callable( $val ) ) {
  295. $tags[ '%' . $key . '%' ] = call_user_func_array( $val, $args );
  296. } else {
  297. $tags[ '%' . $key . '%' ] = $val;
  298. }
  299. }
  300. $message = wp_parse_args( $this->message, [
  301. 'subject' => '',
  302. 'text' => '',
  303. 'actions' => [],
  304. ] );
  305. // Guard.
  306. if ( empty( $message['subject'] ) ) {
  307. return;
  308. }
  309. if ( is_callable( $message['subject'] ) ) {
  310. $subject = call_user_func_array( $message['subject'], $args );
  311. } else {
  312. $subject = $message['subject'];
  313. }
  314. if ( is_callable( $message['text'] ) ) {
  315. $text = call_user_func_array( $message['text'], $args );
  316. } else {
  317. $text = $message['text'];
  318. }
  319. $parsed_message = [];
  320. $parsed_message['subject'] = str_replace( array_keys( $tags ), array_values( $tags ), $subject );
  321. $parsed_message['text'] = str_replace( array_keys( $tags ), array_values( $tags ), $text );
  322. $parsed_message['actions'] = [];
  323. // Add actions from the message if any.
  324. foreach ( $message['actions'] as $id => $action ) {
  325. $this->event->add_message_action(
  326. $id,
  327. $action['text'],
  328. $action['callback_or_url'],
  329. $action['args'],
  330. $action['schema'],
  331. $action['data']
  332. );
  333. }
  334. // Parse actions.
  335. foreach ( $this->event->get_message_actions() as $id => $action ) {
  336. // Get the webhook payload.
  337. $payload = [];
  338. if ( is_callable( $action['args'] ) ) {
  339. $payload = call_user_func_array( $action['args'], $args );
  340. } elseif ( is_array( $action['args'] ) ) {
  341. $payload = $action['args'];
  342. }
  343. // Add workflow ID to payload.
  344. $payload['workflow'] = $this->id;
  345. /**
  346. * Filter the webhook payload for a message action.
  347. *
  348. * @param array $payload The data sent with the action.
  349. * @param string $id The action ID.
  350. * @param array $action The action data.
  351. */
  352. $payload = apply_filters( 'hm.workflows.webhook.payload', $payload, $id, $action );
  353. // Take the string value, or set to the webhook URL if it's a callback.
  354. $url = $action['callback_or_url'];
  355. if ( is_callable( $action['callback_or_url'] ) ) {
  356. $url = get_webhook_controller()->get_webhook_url( $this->event->get_id(), $id, $payload );
  357. }
  358. // Must be a URL for the action to valid.
  359. if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
  360. continue;
  361. }
  362. $parsed_message['actions'][ $id ] = [
  363. 'text' => $action['text'],
  364. 'url' => $url,
  365. 'data' => $action['data'],
  366. ];
  367. }
  368. // Send those notifications!
  369. foreach ( $this->destinations as $destination ) {
  370. $destination->call_handler( $recipients, $parsed_message );
  371. }
  372. }
  373. }