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:
+23
-6
@@ -8,23 +8,25 @@ use Unsupervised\Schedular\Val;
|
||||
class LoginPage {
|
||||
|
||||
/**
|
||||
* Renders the student login shortcode output.
|
||||
* Renders the student login shortcode/block output.
|
||||
*
|
||||
* @param array<string> $atts Shortcode attributes (unused — reserved for future options).
|
||||
* @param array<int|string, mixed> $atts Block attributes (`bookingPageId`) or
|
||||
* shortcode attributes (`booking_page_id`).
|
||||
*/
|
||||
public function render( array $atts ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
public function render( array $atts ): string {
|
||||
$bookingPageId = Val::int( $atts['bookingPageId'] ?? $atts['booking_page_id'] ?? 0 );
|
||||
|
||||
if ( is_user_logged_in() ) {
|
||||
$redirect = esc_url( (string) get_permalink() );
|
||||
return sprintf(
|
||||
'<p>%s <a href="%s">%s</a>.</p>',
|
||||
esc_html__( 'You are already logged in.', 'unsupervised-schedular' ),
|
||||
$redirect,
|
||||
esc_url( $this->bookingUrl( $bookingPageId ) ?? (string) get_permalink() ),
|
||||
esc_html__( 'View available lessons', 'unsupervised-schedular' )
|
||||
);
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$redirect = sanitize_url( (string) get_permalink() );
|
||||
$redirect = sanitize_url( $this->bookingUrl( $bookingPageId ) ?? (string) get_permalink() );
|
||||
|
||||
if ( isset( $_POST['us_login'] ) && check_admin_referer( 'us_student_login' ) ) {
|
||||
$credentials = [
|
||||
@@ -48,4 +50,19 @@ class LoginPage {
|
||||
include USC_PLUGIN_DIR . 'templates/frontend/login-page.php';
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Permalink of the configured booking page, or null when no page is
|
||||
* chosen (or the chosen page no longer exists). Logged-in visitors are
|
||||
* linked (and redirected after login) there instead of the current page.
|
||||
*/
|
||||
public function bookingUrl( int $bookingPageId ): ?string {
|
||||
if ( $bookingPageId <= 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = get_permalink( $bookingPageId );
|
||||
|
||||
return is_string( $url ) ? $url : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class RegistrationPage {
|
||||
/**
|
||||
* Renders the student registration shortcode output.
|
||||
*
|
||||
* @param array<string> $atts Shortcode attributes (unused — reserved for future options).
|
||||
* @param array<int|string, mixed> $atts Shortcode attributes (unused — reserved for future options).
|
||||
*/
|
||||
public function render( array $atts ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
if ( is_user_logged_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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,22 +4,24 @@ declare(strict_types=1);
|
||||
namespace Unsupervised\Schedular\Booking;
|
||||
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Val;
|
||||
|
||||
class BookingPage {
|
||||
|
||||
/**
|
||||
* Renders the booking shortcode output.
|
||||
* Renders the booking shortcode/block output.
|
||||
*
|
||||
* @param array<string> $atts Shortcode attributes (unused — reserved for future options).
|
||||
* @param array<int|string, mixed> $atts Block attributes (`loginPageId`) or
|
||||
* shortcode attributes (`login_page_id`).
|
||||
*/
|
||||
public function render( array $atts ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
public function render( array $atts ): string {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
$permalink = get_permalink();
|
||||
$loginPageId = Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 );
|
||||
|
||||
return sprintf(
|
||||
'<p>%s <a href="%s">%s</a>.</p>',
|
||||
esc_html__( 'Please', 'unsupervised-schedular' ),
|
||||
esc_url( wp_login_url( false === $permalink ? '' : $permalink ) ),
|
||||
esc_url( $this->loginUrl( $loginPageId ) ),
|
||||
esc_html__( 'log in to book a lesson', 'unsupervised-schedular' )
|
||||
);
|
||||
}
|
||||
@@ -35,4 +37,23 @@ class BookingPage {
|
||||
include USC_PLUGIN_DIR . 'templates/frontend/booking-page.php';
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* URL the logged-out prompt sends visitors to: the chosen login page when
|
||||
* one is configured (and still exists), otherwise the WordPress login
|
||||
* screen with a redirect back to the current page.
|
||||
*/
|
||||
public function loginUrl( int $loginPageId ): string {
|
||||
if ( $loginPageId > 0 ) {
|
||||
$url = get_permalink( $loginPageId );
|
||||
|
||||
if ( is_string( $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
$permalink = get_permalink();
|
||||
|
||||
return wp_login_url( false === $permalink ? '' : $permalink );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ class GroupClassPage {
|
||||
/**
|
||||
* Renders the group-class enrolment shortcode output.
|
||||
*
|
||||
* @param array<string> $atts Shortcode attributes (unused — reserved for future options).
|
||||
* @param array<int|string, mixed> $atts Shortcode attributes (unused — reserved for future options).
|
||||
*/
|
||||
public function render( array $atts ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
if ( ! is_user_logged_in() ) {
|
||||
|
||||
@@ -19,14 +19,26 @@ class ShortcodeRegistrar {
|
||||
) {}
|
||||
|
||||
public function register(): void {
|
||||
add_shortcode( 'us_booking', [ $this->bookingPage, 'render' ] );
|
||||
add_shortcode( 'us_student_login', [ $this->loginPage, 'render' ] );
|
||||
add_shortcode( 'us_student_register', [ $this->registrationPage, 'render' ] );
|
||||
add_shortcode( 'us_group_classes', [ $this->groupClassPage, 'render' ] );
|
||||
add_shortcode( 'us_booking', self::shortcode( [ $this->bookingPage, 'render' ] ) );
|
||||
add_shortcode( 'us_student_login', self::shortcode( [ $this->loginPage, 'render' ] ) );
|
||||
add_shortcode( 'us_student_register', self::shortcode( [ $this->registrationPage, 'render' ] ) );
|
||||
add_shortcode( 'us_group_classes', self::shortcode( [ $this->groupClassPage, 'render' ] ) );
|
||||
add_action( 'template_redirect', [ $this->registrationPage, 'maybeRedirectToRegistrationPage' ] );
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'enqueueAssets' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a page renderer so bare shortcode usage is safe: WordPress passes
|
||||
* an empty string, not an array, to the callback when a shortcode is used
|
||||
* without attributes (`shortcode_parse_atts( '' )` returns `''`).
|
||||
*
|
||||
* @param callable(array<int|string, mixed>): string $render
|
||||
* @return \Closure(mixed): string
|
||||
*/
|
||||
private static function shortcode( callable $render ): \Closure {
|
||||
return static fn( mixed $atts ): string => $render( is_array( $atts ) ? $atts : [] );
|
||||
}
|
||||
|
||||
public function enqueueAssets(): void {
|
||||
wp_register_style( 'us-scheduler', USC_PLUGIN_URL . 'assets/css/frontend.css', [], USC_VERSION );
|
||||
|
||||
|
||||
Reference in New Issue
Block a user