blocks() as $name => $config ) { register_block_type( $name, [ 'api_version' => '3', 'editor_script' => self::SCRIPT_HANDLE, 'style' => self::STYLE_HANDLE, 'attributes' => $config['attributes'], 'render_callback' => $config['render'], ] ); } } /** * Block definitions: render callback plus the attribute schema. The * schema must be declared server-side too, or the block-renderer preview * endpoint rejects the attributes wp.serverSideRender sends. * * @return array=): string, attributes: array}> */ private function blocks(): array { $redirectToggle = [ 'type' => 'boolean', 'default' => false, ]; return [ 'us-scheduler/booking' => [ 'render' => [ $this, 'renderBooking' ], 'attributes' => [ 'loginPageId' => [ 'type' => 'number', 'default' => 0, ], 'autoRedirect' => $redirectToggle, 'lessonTypeId' => [ 'type' => 'number', 'default' => 0, ], 'showTypeFilter' => [ 'type' => 'boolean', 'default' => true, ], 'displayMode' => [ 'type' => 'string', 'default' => BookingPage::MODE_BOTH, ], ], ], 'us-scheduler/student-login' => [ 'render' => [ $this, 'renderLogin' ], 'attributes' => [ 'bookingPageId' => [ 'type' => 'number', 'default' => 0, ], 'autoRedirect' => $redirectToggle, ], ], 'us-scheduler/student-register' => [ 'render' => [ $this, 'renderRegistration' ], 'attributes' => [ 'loginPageId' => [ 'type' => 'number', 'default' => 0, ], 'autoRedirect' => $redirectToggle, 'inviteOnlyMessage' => [ 'type' => 'string', 'default' => '', ], ], ], 'us-scheduler/group-classes' => [ 'render' => [ $this, 'renderGroupClasses' ], 'attributes' => [ 'offeringId' => [ 'type' => 'number', 'default' => 0, ], ], ], ]; } /** * Renders the booking block. * * @param array $attributes Block attributes. */ public function renderBooking( array $attributes = [] ): string { if ( ! $this->isEditorPreview() ) { return $this->bookingPage->render( $attributes ); } return BlockPreview::booking( Val::string( $attributes['displayMode'] ?? BookingPage::MODE_BOTH ) ); } /** * Renders the student-login block. * * @param array $attributes Block attributes. */ public function renderLogin( array $attributes = [] ): string { return $this->isEditorPreview() ? BlockPreview::login() : $this->loginPage->render( $attributes ); } /** * Renders the student-registration block. * * @param array $attributes Block attributes. */ public function renderRegistration( array $attributes = [] ): string { return $this->isEditorPreview() ? BlockPreview::registration() : $this->registrationPage->render( $attributes ); } /** * Renders the group-classes block. * * @param array $attributes Block attributes. */ public function renderGroupClasses( array $attributes = [] ): string { if ( ! $this->isEditorPreview() ) { return $this->groupClassPage->render( $attributes ); } return BlockPreview::groupClasses( Val::int( $attributes['offeringId'] ?? 0 ) > 0 ); } /** * Server-side auto-redirect for blocks that opt in via their autoRedirect * attribute: logged-out visitors on a page containing the booking block * are sent to its login page, logged-in visitors on a page containing the * student-login block are sent to its booking page, and a student who has * just finished registering is sent to the register block's chosen page. * Hooked on `template_redirect` because block rendering happens after * output has started, too late to send a Location header. */ public function maybeAutoRedirect(): void { if ( is_admin() || ! is_singular() ) { return; } $post = get_post(); if ( ! $post instanceof \WP_Post ) { return; } if ( $this->maybeRedirectAfterRegistration( $post ) ) { return; } if ( is_user_logged_in() ) { $attrs = $this->firstBlockAttrs( $post->post_content, 'us-scheduler/student-login' ); if ( null === $attrs || ! Val::bool( $attrs['autoRedirect'] ?? false ) ) { return; } $bookingPageId = Val::int( $attrs['bookingPageId'] ?? 0 ); if ( $bookingPageId === $post->ID ) { return; // Redirecting the page to itself would loop. } $url = $this->loginPage->bookingUrl( $bookingPageId ); if ( null !== $url ) { $this->redirect( $url ); } return; } $attrs = $this->firstBlockAttrs( $post->post_content, 'us-scheduler/booking' ); if ( null === $attrs || ! Val::bool( $attrs['autoRedirect'] ?? false ) ) { return; } $loginPageId = Val::int( $attrs['loginPageId'] ?? 0 ); if ( $loginPageId === $post->ID ) { return; // Redirecting the page to itself would loop. } $this->redirect( $this->bookingPage->loginUrl( $loginPageId ) ); } /** * Sends a student whose registration has just completed to the register * block's chosen page, when the block opts in. Only the finished states * qualify (see {@see RegistrationPage::isRegistrationComplete()}): a * failure or the "check your email" step stays put so its message is read. * Unlike the other blocks there is no login-screen fallback — with no page * chosen there is nowhere to send them, so the link is shown instead. * * Returns whether the redirect was issued (it only ever returns in tests; * {@see redirect()} exits in production). */ private function maybeRedirectAfterRegistration( \WP_Post $post ): bool { // Checked before parsing the content because it is a couple of query // args, whereas every front-end request would otherwise pay for a // third block scan. if ( ! $this->registrationPage->isRegistrationComplete() ) { return false; } $attrs = $this->firstBlockAttrs( $post->post_content, 'us-scheduler/student-register' ); if ( null === $attrs || ! Val::bool( $attrs['autoRedirect'] ?? false ) ) { return false; } $pageId = Val::int( $attrs['loginPageId'] ?? 0 ); if ( $pageId === $post->ID ) { return false; // Redirecting the page to itself would loop. } $url = $this->registrationPage->continueUrl( $pageId ); if ( null === $url ) { return false; } $this->redirect( $url ); return true; } /** * Attributes of the first occurrence of the named block in the content, * searching inner blocks so blocks nested inside groups or columns are * still found. Null when the block is absent. Attributes equal to their * schema default are omitted from the serialized block, so callers must * apply defaults themselves. * * @return array|null */ private function firstBlockAttrs( string $content, string $blockName ): ?array { if ( ! has_block( $blockName, $content ) ) { return null; } $queue = parse_blocks( $content ); while ( [] !== $queue ) { $block = array_shift( $queue ); if ( ! is_array( $block ) ) { continue; } if ( ( $block['blockName'] ?? null ) === $blockName ) { $attrs = $block['attrs'] ?? null; return is_array( $attrs ) ? $attrs : []; } $inner = $block['innerBlocks'] ?? null; if ( is_array( $inner ) && [] !== $inner ) { $queue = array_merge( $queue, array_values( $inner ) ); } } return null; } /** * Issues the redirect and stops the request. Split out so tests can * observe redirects without the process exiting. */ protected function redirect( string $url ): void { wp_safe_redirect( $url ); exit; } /** * Whether this render is the editor's block-renderer REST preview rather * than a real front-end page render. Front-end template rendering never * happens inside a REST request, so REST_REQUEST is a reliable signal. */ protected function isEditorPreview(): bool { return defined( 'REST_REQUEST' ) && (bool) constant( 'REST_REQUEST' ); } }