Add link-target and auto-redirect options to booking/login blocks
CI / Coding Standards (pull_request) Successful in 51s
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m2s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 1m24s
CI / Coding Standards (pull_request) Successful in 51s
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m2s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 1m24s
The booking block gains a loginPageId attribute choosing which page its logged-out "log in to book a lesson" link points to (default remains the WordPress login screen), and the student-login block gains a bookingPageId attribute controlling the logged-in "View available lessons" link and the post-login redirect target (default remains the current page). Both blocks also gain an autoRedirect toggle, off by default, that sends the visitor straight to the target page; block rendering starts after output, so the redirect runs on template_redirect by parsing the queried page's content for the block, with a self-target guard against redirect loops. The link targets are also available to the shortcodes as login_page_id/booking_page_id. Also fixes a pre-existing fatal: WordPress passes an empty string (not an array) to shortcode callbacks when a shortcode is used without attributes, so bare [us_booking] etc. threw a TypeError against the strictly-typed render(array $atts) methods. ShortcodeRegistrar now wraps each callback to normalize non-array attribute values. Closes #51 Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
+153
-22
@@ -32,15 +32,17 @@ class BlockRegistrar {
|
||||
|
||||
public function register(): void {
|
||||
add_action( 'init', [ $this, 'registerBlocks' ] );
|
||||
add_action( 'template_redirect', [ $this, 'maybeAutoRedirect' ] );
|
||||
}
|
||||
|
||||
public function registerBlocks(): void {
|
||||
// The editor script registers the client side of each block (title,
|
||||
// icon, shortcode transform) and previews it via wp.serverSideRender.
|
||||
// icon, shortcode transform, inspector controls) and previews it via
|
||||
// wp.serverSideRender.
|
||||
wp_register_script(
|
||||
self::SCRIPT_HANDLE,
|
||||
USC_PLUGIN_URL . 'assets/js/blocks.js',
|
||||
[ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-server-side-render', 'wp-i18n' ],
|
||||
[ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-core-data', 'wp-server-side-render', 'wp-i18n' ],
|
||||
USC_VERSION,
|
||||
true
|
||||
);
|
||||
@@ -52,67 +54,196 @@ class BlockRegistrar {
|
||||
wp_register_style( self::STYLE_HANDLE, USC_PLUGIN_URL . 'assets/css/frontend.css', [], USC_VERSION );
|
||||
}
|
||||
|
||||
foreach ( $this->blocks() as $name => $renderCallback ) {
|
||||
foreach ( $this->blocks() as $name => $config ) {
|
||||
register_block_type(
|
||||
$name,
|
||||
[
|
||||
'api_version' => '3',
|
||||
'editor_script' => self::SCRIPT_HANDLE,
|
||||
'style' => self::STYLE_HANDLE,
|
||||
'render_callback' => $renderCallback,
|
||||
'attributes' => $config['attributes'],
|
||||
'render_callback' => $config['render'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block names mapped to their render callbacks.
|
||||
* 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, callable(array<string, mixed>): string>
|
||||
* @return array<string, array{render: callable(array<string, mixed>=): string, attributes: array<string, array{type: string, default: mixed}>}>
|
||||
*/
|
||||
private function blocks(): array {
|
||||
$redirectToggle = [
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
];
|
||||
|
||||
return [
|
||||
'us-scheduler/booking' => [ $this, 'renderBooking' ],
|
||||
'us-scheduler/student-login' => [ $this, 'renderLogin' ],
|
||||
'us-scheduler/student-register' => [ $this, 'renderRegistration' ],
|
||||
'us-scheduler/group-classes' => [ $this, 'renderGroupClasses' ],
|
||||
'us-scheduler/booking' => [
|
||||
'render' => [ $this, 'renderBooking' ],
|
||||
'attributes' => [
|
||||
'loginPageId' => [
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
],
|
||||
'autoRedirect' => $redirectToggle,
|
||||
],
|
||||
],
|
||||
'us-scheduler/student-login' => [
|
||||
'render' => [ $this, 'renderLogin' ],
|
||||
'attributes' => [
|
||||
'bookingPageId' => [
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
],
|
||||
'autoRedirect' => $redirectToggle,
|
||||
],
|
||||
],
|
||||
'us-scheduler/student-register' => [
|
||||
'render' => [ $this, 'renderRegistration' ],
|
||||
'attributes' => [],
|
||||
],
|
||||
'us-scheduler/group-classes' => [
|
||||
'render' => [ $this, 'renderGroupClasses' ],
|
||||
'attributes' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the booking block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
* @param array<string, mixed> $attributes Block attributes.
|
||||
*/
|
||||
public function renderBooking( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::booking() : $this->bookingPage->render( [] );
|
||||
public function renderBooking( array $attributes = [] ): string {
|
||||
return $this->isEditorPreview() ? BlockPreview::booking() : $this->bookingPage->render( $attributes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the student-login block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
* @param array<string, mixed> $attributes Block attributes.
|
||||
*/
|
||||
public function renderLogin( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::login() : $this->loginPage->render( [] );
|
||||
public function renderLogin( array $attributes = [] ): string {
|
||||
return $this->isEditorPreview() ? BlockPreview::login() : $this->loginPage->render( $attributes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the student-registration block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
* @param array<string, mixed> $attributes Block attributes.
|
||||
*/
|
||||
public function renderRegistration( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::registration() : $this->registrationPage->render( [] );
|
||||
public function renderRegistration( array $attributes = [] ): string {
|
||||
return $this->isEditorPreview() ? BlockPreview::registration() : $this->registrationPage->render( $attributes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the group-classes block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
* @param array<string, mixed> $attributes Block attributes.
|
||||
*/
|
||||
public function renderGroupClasses( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::groupClasses() : $this->groupClassPage->render( [] );
|
||||
public function renderGroupClasses( array $attributes = [] ): string {
|
||||
return $this->isEditorPreview() ? BlockPreview::groupClasses() : $this->groupClassPage->render( $attributes );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, and logged-in visitors on a page containing
|
||||
* the student-login block are sent to its booking 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 ( 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 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<mixed>|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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user