diff --git a/src/Utils/APIHelper.php b/src/Utils/APIHelper.php index 5f6adbe..74ad4e2 100644 --- a/src/Utils/APIHelper.php +++ b/src/Utils/APIHelper.php @@ -39,10 +39,11 @@ abstract class APIHelper /** * API endpoints configuration - should be overridden in child classes. * - * @var array, + * body?: array, * headers?: array, * cache?: bool|int|callable * }> @@ -190,11 +191,12 @@ public static function remember(string $key, callable $callback, ?int $expiratio * @param string $method HTTP method * @param string $url Request URL * @param array $params Request parameters + * @param array $body Request body data * @param array $args Additional request arguments * @return mixed Response data * @throws \Exception When request fails or response is invalid */ - public static function request(string $method, string $url, array $params = [], array $args = []): mixed + public static function request(string $method, string $url, array $params = [], array $body = [], array $args = []): mixed { $method = strtoupper(sanitize_text_field($method)); $url = esc_url_raw($url); @@ -209,9 +211,15 @@ public static function request(string $method, string $url, array $params = [], ), ]); + // Add query parameters to URL for all methods + if (!empty($params)) { + $url = static::prepare_request_url($url, $params); + } + // Add body for non-GET requests - if ($method !== 'GET' && !empty($params)) { - $request_args['body'] = $params; + if ($method !== 'GET' && !empty($body)) { + $content_type = static::resolve_content_type($request_args['headers']); + $request_args['body'] = static::prepare_request_body($body, $content_type); } $response = wp_remote_request($url, $request_args); @@ -233,8 +241,8 @@ public static function request(string $method, string $url, array $params = [], ); } - $body = wp_remote_retrieve_body($response); - $data = json_decode($body, true); + $body_response = wp_remote_retrieve_body($response); + $data = json_decode($body_response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception( @@ -258,6 +266,55 @@ public static function request(string $method, string $url, array $params = [], return $data; } + /** + * Prepare request body based on content type and data. + * + * @param array $body Request body data + * @param string $content_type Content type header value + * @param bool $force_json_encode Force JSON encoding regardless of content type + * @return string|array Prepared request body + */ + protected static function prepare_request_body(array $body, string $content_type = 'application/json', bool $force_json_encode = true): string|array + { + if (empty($body)) { + return []; + } + + // Force JSON encoding by default or when content type is JSON + if ($force_json_encode || str_contains($content_type, 'application/json')) { + return json_encode($body); + } + + // Handle form data + if (str_contains($content_type, 'application/x-www-form-urlencoded')) { + return http_build_query($body); + } + + // Handle multipart form data or other types - return as array + return $body; + } + + /** + * Get default content type for requests. + * + * @return string Default content type + */ + protected static function get_default_content_type(): string + { + return 'application/json'; + } + + /** + * Determine content type from request headers or use default. + * + * @param array $headers Request headers + * @return string Content type + */ + protected static function resolve_content_type(array $headers): string + { + return $headers['Content-Type'] ?? static::get_default_content_type(); + } + /** * Determine if response should be cached. * @@ -362,11 +419,12 @@ public static function set_cache_duration(int $duration): void * * @param string $endpoint_name Endpoint identifier * @param array $params Request parameters + * @param array $body Request body data * @param array $substitutions URL placeholder substitutions * @return mixed Response data * @throws \Exception When endpoint is invalid or request fails */ - public static function make_request(string $endpoint_name, array $params = [], array $substitutions = []): mixed + public static function make_request(string $endpoint_name, array $params = [], array $body = [], array $substitutions = []): mixed { $endpoint = static::get_endpoints()[$endpoint_name] ?? null; if (!$endpoint) { @@ -401,6 +459,22 @@ public static function make_request(string $endpoint_name, array $params = [], a } } + + $endpoint_body = $endpoint['body'] ?? []; + $body = array_merge($endpoint_body, $body); + + // Filter body to only include those defined in endpoint + if (!empty($endpoint_body)) { + $body = array_intersect_key($body, $endpoint_body); + } + + // Execute callable body values + foreach ($body as $key => $value) { + if (is_callable($value)) { + $body[$key] = $value(); + } + } + $method = $endpoint['method'] ?? 'GET'; $url = rtrim(static::get_base_url(), '/') . '/' . ltrim($route, '/'); @@ -417,7 +491,7 @@ public static function make_request(string $endpoint_name, array $params = [], a $params = []; // Parameters already in URL } - return static::request($method, $url, $params, $endpoint); + return static::request($method, $url, $params, $body, $endpoint); } /** diff --git a/src/Utils/EnqueueManager.php b/src/Utils/EnqueueManager.php index f79076f..ff990ee 100644 --- a/src/Utils/EnqueueManager.php +++ b/src/Utils/EnqueueManager.php @@ -827,6 +827,301 @@ protected function enqueueIndividualStyle(string $handle, array $config): void } } + /** + * Register a script group without enqueuing it. + */ + public function registerScriptGroup(string $group_name): static + { + if (!isset($this->script_groups[$group_name])) { + throw new InvalidArgumentException("Script group '{$group_name}' does not exist"); + } + + $group_data = $this->script_groups[$group_name]; + + if (!$this->shouldLoadGroup($group_data['config'])) { + return $this; + } + + foreach ($group_data['scripts'] as $handle => $config) { + $this->registerIndividualScript($handle, $config); + } + + return $this; + } + + /** + * Register a style group without enqueuing it. + */ + public function registerStyleGroup(string $group_name): static + { + if (!isset($this->style_groups[$group_name])) { + throw new InvalidArgumentException("Style group '{$group_name}' does not exist"); + } + + $group_data = $this->style_groups[$group_name]; + + if (!$this->shouldLoadGroup($group_data['config'])) { + return $this; + } + + foreach ($group_data['styles'] as $handle => $config) { + $this->registerIndividualStyle($handle, $config); + } + + return $this; + } + + /** + * Register multiple groups. + */ + public function registerGroups(array $group_names, string $type = 'both'): static + { + foreach ($group_names as $group_name) { + if ($type === 'both' || $type === 'scripts') { + $this->registerScriptGroup($group_name); + } + + if ($type === 'both' || $type === 'styles') { + $this->registerStyleGroup($group_name); + } + } + + return $this; + } + + /** + * Register an individual script. + */ + public function registerScript(string $handle): static + { + if (!isset($this->individual_scripts[$handle])) { + throw new InvalidArgumentException("Script '{$handle}' does not exist"); + } + + $config = $this->individual_scripts[$handle]; + $this->registerIndividualScript($handle, $config); + + return $this; + } + + /** + * Register an individual style. + */ + public function registerStyle(string $handle): static + { + if (!isset($this->individual_styles[$handle])) { + throw new InvalidArgumentException("Style '{$handle}' does not exist"); + } + + $config = $this->individual_styles[$handle]; + $this->registerIndividualStyle($handle, $config); + + return $this; + } + + /** + * Register scripts and styles by handles. + */ + public function registerByHandles(array $handles): static + { + foreach ($handles as $handle) { + // Check individual scripts first + if (isset($this->individual_scripts[$handle])) { + $this->registerIndividualScript($handle, $this->individual_scripts[$handle]); + continue; + } + + // Check individual styles + if (isset($this->individual_styles[$handle])) { + $this->registerIndividualStyle($handle, $this->individual_styles[$handle]); + continue; + } + + // Check in groups + foreach ($this->script_groups as $group_name => $group_data) { + if (isset($group_data['scripts'][$handle])) { + $this->registerIndividualScript($handle, $group_data['scripts'][$handle]); + break; + } + } + + foreach ($this->style_groups as $group_name => $group_data) { + if (isset($group_data['styles'][$handle])) { + $this->registerIndividualStyle($handle, $group_data['styles'][$handle]); + break; + } + } + } + + return $this; + } + + /** + * Register an individual script without enqueuing. + */ + protected function registerIndividualScript(string $handle, array $config): void + { + // Check condition + if ($config['condition'] && is_callable($config['condition'])) { + if (!call_user_func($config['condition'])) { + return; + } + } + + wp_register_script( + $handle, + $config['src'], + $config['deps'], + $config['version'], + $config['in_footer'] + ); + + // Add strategy (WP 6.3+) + if (!empty($config['strategy']) && function_exists('wp_script_add_data')) { + wp_script_add_data($handle, 'strategy', $config['strategy']); + } + + // Note: Localization and inline scripts are NOT added during registration + // They should only be added when the script is actually enqueued + } + + /** + * Register an individual style without enqueuing. + */ + protected function registerIndividualStyle(string $handle, array $config): void + { + // Check condition + if ($config['condition'] && is_callable($config['condition'])) { + if (!call_user_func($config['condition'])) { + return; + } + } + + wp_register_style( + $handle, + $config['src'], + $config['deps'], + $config['version'], + $config['media'] + ); + + // Note: Inline styles are NOT added during registration + // They should only be added when the style is actually enqueued + } + + /** + * Check if a script is registered. + */ + public function isScriptRegistered(string $handle): bool + { + return wp_script_is($handle, 'registered'); + } + + /** + * Check if a style is registered. + */ + public function isStyleRegistered(string $handle): bool + { + return wp_style_is($handle, 'registered'); + } + + /** + * Enqueue a previously registered script by handle. + */ + public function enqueueRegisteredScript(string $handle): EnqueueManager + { + if (!$this->isScriptRegistered($handle)) { + throw new InvalidArgumentException("Script '{$handle}' is not registered"); + } + + wp_enqueue_script($handle); + + // Find and apply localization and inline scripts + $config = $this->findScriptConfig($handle); + if ($config) { + $this->handleScriptLocalization($handle, $config); + $this->addInlineScripts($handle, $config); + } + + return $this; + } + + /** + * Enqueue a previously registered style by handle. + */ + public function enqueueRegisteredStyle(string $handle): static + { + if (!$this->isStyleRegistered($handle)) { + throw new InvalidArgumentException("Style '{$handle}' is not registered"); + } + + wp_enqueue_style($handle); + + // Find and apply inline styles + $config = $this->findStyleConfig($handle); + if ($config) { + if (!empty($config['inline_before'])) { + wp_add_inline_style($handle, $config['inline_before']); + } + + if (!empty($config['inline_after'])) { + wp_add_inline_style($handle, $config['inline_after']); + } + + if (isset($this->inline_styles[$handle])) { + foreach ($this->inline_styles[$handle]['before'] as $style) { + wp_add_inline_style($handle, $style); + } + + foreach ($this->inline_styles[$handle]['after'] as $style) { + wp_add_inline_style($handle, $style); + } + } + } + + return $this; + } + + /** + * Find script configuration by handle. + */ + protected function findScriptConfig(string $handle): ?array + { + // Check individual scripts + if (isset($this->individual_scripts[$handle])) { + return $this->individual_scripts[$handle]; + } + + // Check in groups + foreach ($this->script_groups as $group_data) { + if (isset($group_data['scripts'][$handle])) { + return $group_data['scripts'][$handle]; + } + } + + return null; + } + + /** + * Find style configuration by handle. + */ + protected function findStyleConfig(string $handle): ?array + { + // Check individual styles + if (isset($this->individual_styles[$handle])) { + return $this->individual_styles[$handle]; + } + + // Check in groups + foreach ($this->style_groups as $group_data) { + if (isset($group_data['styles'][$handle])) { + return $group_data['styles'][$handle]; + } + } + + return null; + } + /** * Handle script localization with global data support. */ diff --git a/src/Utils/Page.php b/src/Utils/Page.php index c8689c2..d15e0fd 100644 --- a/src/Utils/Page.php +++ b/src/Utils/Page.php @@ -1084,6 +1084,17 @@ public function handleFrontendTemplateInclude(string $template): string } } + + // if callback just echo the output of the callback and return + if (isset($config['callback']) && is_callable($config['callback'])) { + // Call the callback function and return its output + ob_start(); + call_user_func($config['callback'], $config); + $output = ob_get_clean(); + echo $output; + return ''; // Return empty to prevent further template processing + } + // Handle the request and return custom template return $this->getFrontendTemplate($plugin_page, $config); } @@ -1115,12 +1126,6 @@ protected function getFrontendTemplate(string $slug, array $config): string } } - // Use callback if provided -// if (isset($config['callback']) && is_callable($config['callback'])) { -// // Create a temporary template file for callback -// return $this->createCallbackTemplate($config['callback'], $data); -// } - // Use template if provided $template = $config['template'] ?? null; if ($template) { @@ -1133,7 +1138,7 @@ protected function getFrontendTemplate(string $slug, array $config): string } // Create default template - return $this->createDefaultFrontendTemplate($slug, $config, $data); + return "This page has no template configured. Please create a template file named '{$slug}.php' in your plugin or theme directory."; } /** diff --git a/src/Utils/ViewHelper.php b/src/Utils/ViewHelper.php new file mode 100644 index 0000000..e0851f8 --- /dev/null +++ b/src/Utils/ViewHelper.php @@ -0,0 +1,96 @@ +default_base_path = $base_path; + $this->default_plugin_prefix = $plugin_prefix; + $this->overridable = $overridable; + } + + public function load(string $view, array $data = [], ?string $base_path = null, bool $overridable = null, ?string $plugin_prefix = null): string + { + $overridable = $overridable === null ? $this->overridable : $overridable; + return ViewLoader::load( + $view, + $data, + false, + $base_path ?? $this->default_base_path, + $overridable, + $plugin_prefix ?? $this->default_plugin_prefix + ) ?: ''; + } + + public function include(string $view, array $data = [], ?string $base_path = null, bool $overridable = null, ?string $plugin_prefix = null): void + { + $overridable = $overridable === null ? $this->overridable : $overridable; + ViewLoader::load( + $view, + $data, + true, + $base_path ?? $this->default_base_path, + $overridable, + $plugin_prefix ?? $this->default_plugin_prefix + ); + } + + public function load_overridable(string $view, array $data = []): string + { + return ViewLoader::load( + $view, + $data, + false, + $this->default_base_path, + true, + $this->default_plugin_prefix + ) ?: ''; + } + + public function include_overridable(string $view, array $data = []): void + { + ViewLoader::load( + $view, + $data, + true, + $this->default_base_path, + true, + $this->default_plugin_prefix + ); + } + + /** + * @throws \Exception + */ + public function section(string $name): void + { + ViewLoader::start_section($name); + } + + /** + * @throws \Exception + */ + public function end_section(): void + { + ViewLoader::end_section(); + } + + public function yield(string $name, string $default = ''): void + { + echo ViewLoader::get_section($name, $default); + } +} \ No newline at end of file diff --git a/src/Utils/ViewLoader.php b/src/Utils/ViewLoader.php index 25337c4..0fc4710 100644 --- a/src/Utils/ViewLoader.php +++ b/src/Utils/ViewLoader.php @@ -12,8 +12,6 @@ namespace Codad5\WPToolkit\Utils; -use Codad5\WPToolkit\Utils\Cache; - /** * ViewLoader utility class for loading template files. * @@ -85,11 +83,13 @@ class ViewLoader * @param array $data Data to be extracted as variables * @param bool $echo Whether to echo the output * @param string|null $base_path Override base path for this load + * @param bool $overridable Whether to check for theme overrides + * @param string $plugin_prefix Plugin prefix for theme override path * @return string|false Rendered output or false on failure */ - public static function load(string $view, array $data = [], bool $echo = true, ?string $base_path = null): string|false - { - $template_path = self::resolve_template_path($view, $base_path); + public static function load(string $view, array $data = [], bool $echo = true, ?string $base_path = null, bool $overridable = false, string $plugin_prefix = 'wptoolkit'): string|false + { + $template_path = self::resolve_template_path($view, $base_path, $overridable, $plugin_prefix); if (!$template_path) { return self::handle_template_not_found($view, $echo); @@ -112,34 +112,10 @@ public static function load(string $view, array $data = [], bool $echo = true, ? $data = array_merge(self::$global_data, $data); // Add helper functions to data - $data['view'] = new class { - public static function load(string $view, array $data = []): string - { - return ViewLoader::load($view, $data, false) ?: ''; - } - - public static function include(string $view, array $data = []): void - { - ViewLoader::load($view, $data, true); - } + $data['view'] = self::create_view_helper($base_path, $plugin_prefix, $overridable); - public static function section(string $name): void - { - ViewLoader::start_section($name); - } - - public static function end_section(): void - { - ViewLoader::end_section(); - } - - public static function yield(string $name, string $default = ''): void - { - echo ViewLoader::get_section($name, $default); - } - }; - // Render the template + // Render the template $output = self::render_template($template_path, $data); if ($output === false) { @@ -158,6 +134,20 @@ public static function yield(string $name, string $default = ''): void return $output; } + + /** + * Create a view helper instance. + * + * @param string|null $base_path Default base path + * @param string $plugin_prefix Default plugin prefix + * @param bool $overridable Whether the view is overridable by theme + * @return ViewHelper View helper instance + */ + private static function create_view_helper(?string $base_path, string $plugin_prefix, bool $overridable = false): ViewHelper + { + return new ViewHelper($base_path, $plugin_prefix, $overridable); + } + /** * Load a view and return output without echoing. * @@ -171,6 +161,20 @@ public static function get(string $view, array $data = [], ?string $base_path = return self::load($view, $data, false, $base_path) ?: ''; } + /** + * Load a view with WordPress theme override support. + * + * @param string $view View path relative to plugin templates + * @param array $data Data to be extracted as variables + * @param bool $echo Whether to echo the output + * @param string $plugin_prefix Plugin prefix for theme override path + * @return string|false Rendered output or false on failure + */ + public static function get_overridable(string $view, array $data = [], bool $echo = true, string $plugin_prefix = 'wptoolkit'): string|false + { + return self::load($view, $data, $echo, null, true, $plugin_prefix); + } + /** * Check if a view exists. * @@ -443,16 +447,34 @@ public static function get_extensions(): array return self::$extensions; } - /** - * Resolve template path from view name. - * - * @param string $view View name - * @param string|null $base_path Override base path - * @return string|false Resolved path or false if not found - */ - private static function resolve_template_path(string $view, ?string $base_path = null): string|false - { - $view = ltrim($view, '/'); + /** + * Resolve template path from view name. + * + * @param string $view View name + * @param string|null $base_path Override base path + * @param bool $overridable Whether to check for theme overrides + * @param string $plugin_prefix Plugin prefix for theme override path + * @return string|false Resolved path or false if not found + */ + private static function resolve_template_path(string $view, ?string $base_path = null, bool $overridable = false, string $plugin_prefix = 'wptoolkit'): string|false + { + $view = ltrim($view, '/'); + + // Check for WordPress theme override first if overridable + if ($overridable) { + $theme_template = locate_template("{$plugin_prefix}/{$view}"); + if ($theme_template) { + return $theme_template; + } + + // Also check with .php extension if not present + if (!pathinfo($view, PATHINFO_EXTENSION)) { + $theme_template = locate_template("{$plugin_prefix}/{$view}.php"); + if ($theme_template) { + return $theme_template; + } + } + } $search_paths = []; // Add override base path first @@ -596,7 +618,7 @@ private static function generate_cache_key(string $template_path, array $data): * @param bool $echo Whether to echo error * @return false */ - private static function handle_template_not_found(string $view, bool $echo): false + private static function handle_template_not_found(string $view, bool $echo): bool { $error_message = "Template not found: {$view}"; @@ -617,7 +639,7 @@ private static function handle_template_not_found(string $view, bool $echo): fal * @param bool $echo Whether to echo error * @return false */ - private static function handle_render_error(string $view, bool $echo): false + private static function handle_render_error(string $view, bool $echo): bool { $error_message = "Error rendering template: {$view}";