HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: /wordpress/plugins/wp-cloud-client/1.1.3/src/BusinessProfile/ProfileBlock.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\BusinessProfile;

/**
 * Registers the Business Profile Gutenberg block.
 */
final class ProfileBlock {

	private const BLOCK_NAME    = 'business-profile-render/bpr-block';
	private const SCRIPT_HANDLE = 'wpcc-business-profile-block';

	/**
	 * Constructor.
	 *
	 * @param ProfileRepository $repository The profile data repository.
	 */
	public function __construct(
		private readonly ProfileRepository $repository,
	) {}

	/**
	 * Register the block on WordPress init.
	 *
	 * @return void
	 */
	public function register(): void {
		add_action( 'init', [ $this, 'registerBlock' ] );
	}

	/**
	 * Register the Gutenberg block type and localize profile data.
	 *
	 * @return void
	 */
	public function registerBlock(): void {
		// Only register editor assets in admin context to avoid unnecessary processing on frontend.
		if ( ! is_admin() ) {
			return;
		}

		$assetFile = VPLUGINS_WPCC_PATH . 'assets/business-profile/build/gutenberg-block.asset.php';
		$assets    = file_exists( $assetFile ) ? require $assetFile : [
			'dependencies' => [],
			'version'      => '',
		];

		$jsPath  = VPLUGINS_WPCC_PATH . 'assets/business-profile/build/gutenberg-block.js';
		$version = $assets['version'] ?? ( file_exists( $jsPath ) ? (string) filemtime( $jsPath ) : VPLUGINS_WPCC_VERSION );

		wp_register_script(
			self::SCRIPT_HANDLE,
			plugins_url( 'assets/business-profile/build/gutenberg-block.js', VPLUGINS_WPCC_FILE ),
			array_merge( $assets['dependencies'] ?? [], [ 'wp-blocks', 'wp-element', 'wp-components' ] ),
			$version,
			true,
		);

		$profile = $this->repository->get();
		$data    = null !== $profile
			? $this->preprocessData( $profile )
			: [ 'Company Name' => __( 'No business profile data available', 'wp-cloud-client' ) ];

		wp_localize_script(
			self::SCRIPT_HANDLE,
			'businessProfileData',
			$data,
		);

		register_block_type(
			self::BLOCK_NAME,
			[ 'editor_script' => self::SCRIPT_HANDLE ],
		);
	}

	/**
	 * Preprocess profile data for JavaScript localization.
	 *
	 * Humanizes keys and sanitizes values.
	 *
	 * @param array<string, mixed> $data Raw profile data.
	 * @return array<string, string> Processed key-value pairs.
	 */
	private function preprocessData( array $data ): array {
		$processed = [];

		foreach ( $data as $key => $value ) {
			$processedKey = ucwords( str_replace( '_', ' ', (string) $key ) );

			if ( '' === $value || null === $value ) {
				$processedValue = __( 'No data available', 'wp-cloud-client' );
			} elseif ( is_array( $value ) ) {
				$processedValue = $this->formatArrayValue( (string) $key, $value );
			} else {
				$processedValue = sanitize_text_field( (string) $value );
			}

			$processed[ $processedKey ] = $processedValue;
		}

		return $processed;
	}

	/**
	 * Format an array value for Gutenberg localization.
	 *
	 * @param string       $key   The profile key.
	 * @param array<mixed> $value The array value.
	 * @return string Formatted string representation.
	 */
	private function formatArrayValue( string $key, array $value ): string {
		if ( 'hours_of_operation' === $key ) {
			return $this->formatHoursForBlock( $value );
		}

		// Single-element arrays like work_number: ["+1-306-555-0100"]
		if ( 1 === count( $value ) && isset( $value[0] ) && is_string( $value[0] ) ) {
			return sanitize_text_field( $value[0] );
		}

		// Filter out nested arrays, keep only scalar values.
		$flat = array_filter( $value, static fn ( mixed $v ): bool => ! is_array( $v ) );

		if ( empty( $flat ) ) {
			return __( 'Complex data — use shortcode for display', 'wp-cloud-client' );
		}

		return implode( ', ', array_map( 'sanitize_text_field', array_map( 'strval', $flat ) ) );
	}

	/**
	 * Format hours_of_operation for the Gutenberg block preview.
	 *
	 * @param array<mixed> $hours Raw hours data.
	 * @return string Pipe-separated summary string.
	 */
	private function formatHoursForBlock( array $hours ): string {
		$dayOrder = [
			'Sunday'    => 0,
			'Monday'    => 1,
			'Tuesday'   => 2,
			'Wednesday' => 3,
			'Thursday'  => 4,
			'Friday'    => 5,
			'Saturday'  => 6,
		];

		$grouped = [];
		$plain   = [];

		foreach ( $hours as $timeBlock ) {
			if ( ! is_array( $timeBlock ) ) {
				$plain[] = sanitize_text_field( (string) $timeBlock );
				continue;
			}

			$days = $timeBlock['day_of_week'] ?? [];
			if ( ! is_array( $days ) || empty( $days ) ) {
				continue;
			}

			$opens       = isset( $timeBlock['opens'] ) ? sanitize_text_field( (string) $timeBlock['opens'] ) : '';
			$closes      = isset( $timeBlock['closes'] ) ? sanitize_text_field( (string) $timeBlock['closes'] ) : '';
			$description = (string) ( $timeBlock['description'] ?? '' );

			$dayKey    = implode( ',', $days );
			$sortOrder = $dayOrder[ $days[0] ] ?? 99;

			if ( ! isset( $grouped[ $dayKey ] ) ) {
				$grouped[ $dayKey ] = [
					'days'   => $days,
					'ranges' => [],
					'sort'   => $sortOrder,
				];
			}

			if ( '' !== $opens && '' !== $closes ) {
				$range = "{$opens} - {$closes}";
				if ( '' !== $description ) {
					$range .= ' (' . sanitize_text_field( $description ) . ')';
				}
				$grouped[ $dayKey ]['ranges'][] = $range;
			} elseif ( '' !== $description ) {
				$grouped[ $dayKey ]['ranges'][] = sanitize_text_field( $description );
			}
		}

		uasort( $grouped, static fn ( array $a, array $b ): int => ( $a['sort'] ?? 99 ) <=> ( $b['sort'] ?? 99 ) );

		$result = $plain;

		foreach ( $grouped as $entry ) {
			$daysText = implode( ', ', $entry['days'] );
			if ( ! empty( $entry['ranges'] ) ) {
				$result[] = $daysText . ': ' . implode( ', ', $entry['ranges'] );
			}
		}

		return ! empty( $result )
			? implode( ' | ', $result )
			: __( 'No hours available', 'wp-cloud-client' );
	}
}