$variables Template variables, used for pattern sends. * @param string $subject Email subject. Ignored by SMS. * @param bool $urgent Whether a person is waiting on this right now. * @param string $pattern Approved provider pattern to send with. Empty lets the * driver resolve one for the event, or send the body. * @param string $id Stable id of this message across retries; generated when empty. * @param array $context Non-personal ids the message is about, e.g. `cart_id`; reported * with `message.*` so the dashboard can tie a send to its cart. * @param string $module Slug of the module sending it, for the dashboard's cost per module. */ public function __construct( public string $event, public string $to, public string $body = '', public array $variables = [], public string $subject = '', public bool $urgent = false, public string $pattern = '', public string $id = '', public array $context = [], public string $module = '' ) { if ( '' === $this->id ) { $this->id = wp_generate_uuid4(); } } /** * Serialise for the job queue. * * @return array */ public function toArray(): array { return [ 'event' => $this->event, 'to' => $this->to, 'body' => $this->body, 'variables' => $this->variables, 'subject' => $this->subject, 'urgent' => $this->urgent, 'pattern' => $this->pattern, 'id' => $this->id, 'context' => $this->context, 'module' => $this->module, ]; } /** * Rebuild from a queued payload. * * @param array $data Stored payload. */ public static function fromArray( array $data ): self { $variables = $data['variables'] ?? []; return new self( (string) ( $data['event'] ?? '' ), (string) ( $data['to'] ?? '' ), (string) ( $data['body'] ?? '' ), is_array( $variables ) ? $variables : [], (string) ( $data['subject'] ?? '' ), (bool) ( $data['urgent'] ?? false ), (string) ( $data['pattern'] ?? '' ), // Jobs queued before 0.26.0 have no id; they get one now. (string) ( $data['id'] ?? '' ), // Jobs queued before 0.27.0 have neither; both are optional. self::context( $data['context'] ?? [] ), sanitize_key( (string) ( $data['module'] ?? '' ) ) ); } /** * Loggable form. * * The recipient is masked and the variables are dropped entirely: for an OTP * event they hold the code itself, and a log that records live codes is a way * to hand someone an account. * * @return array */ public function toLog(): array { $phone = PhoneNumber::parse( $this->to ); return [ 'event' => $this->event, 'to' => null !== $phone ? $phone->masked() : self::maskEmail( $this->to ), ]; } /** * Context ids as the contract allows them: lower-case keys, non-empty string * values of at most 64 characters (EVENTS-v1.md ยง9). * * @param mixed $raw Stored context. * * @return array */ public static function context( $raw ): array { $context = []; foreach ( is_array( $raw ) ? $raw : [] as $key => $value ) { if ( is_string( $key ) && 1 === preg_match( '/^[a-z_]+$/', $key ) && ( is_string( $value ) || is_int( $value ) ) ) { $value = (string) $value; if ( '' !== $value && strlen( $value ) <= 64 ) { $context[ $key ] = $value; } } } return $context; } /** * Partly hide an email address. * * @param string $email Address to mask. */ private static function maskEmail( string $email ): string { $at = strpos( $email, '@' ); if ( false === $at || $at < 2 ) { return '***'; } return substr( $email, 0, 2 ) . '***' . substr( $email, $at ); } }