From 6507e672e703e441cc07ab8ea2e853b27f344b7d Mon Sep 17 00:00:00 2001 From: Owais ahmed Date: Wed, 13 Aug 2025 01:18:34 +0500 Subject: [PATCH] Improved Settings Class Handling (#3) feat(core)!: overhaul settings engine, fix data loss, and improve API This major release resolves a critical data loss bug and refactors the entire framework to be more robust, configurable, and developer-friendly. BREAKING CHANGE: The `Settings` class constructor signature has changed. It now requires `$optionName` and `$pageSlug` as the first two arguments. Page titles and menu titles must now be passed via the `$options` array to allow for better configuration. Example before: new Settings('My Page', 'my-menu', 'my-option', 'my-slug'); Example now: new Settings('my-option', 'my-slug', [ 'pageTitle' => 'My Page', 'menuTitle' => 'My Menu', ]); --- ### Key Fixes and Improvements: * **Critical Data Loss Resolved:** A bug that erased settings from inactive tabs during save has been fixed. The `Sanitizer` is now state-aware, merging new input with existing options to preserve all data. * **Reliable Data Retrieval:** The public `get()` method, which was previously non-functional, has been completely repaired. It now correctly fetches saved values from the database with proper fallbacks to field defaults. * **Enhanced Configuration & API:** - New fluent methods `setCapability()` and `setParentSlug()` have been added. - All text strings are now configurable via a `labels` array in the constructor, improving support for internationalization (i18n). - External asset libraries (like Select2) can now be configured. * **Internal Architecture:** These improvements were made possible by a complete internal refactor, which introduced a new `Config` class for state management and enforced proper dependency injection. --- .github/SECURITY.md | 3 +- .github/workflows/ci.yml | 2 +- .github/workflows/commitlint.yml | 2 +- CHANGELOG.md | 60 +-- README.md | 155 ++++--- bin/README.md | 62 +-- composer.json | 90 ++-- examples/README.md | 621 +++++++++++++++------------ examples/settings-with-tabs.php | 96 ++++- examples/settings-without-tabs.php | 230 ++++++---- package-lock.json | 32 +- src/AssetManager.php | 321 ++++++-------- src/Config.php | 229 ++++++++++ src/FieldFactory.php | 12 +- src/Fields/AbstractField.php | 110 +++-- src/Fields/ButtonGroupField.php | 21 +- src/Fields/CheckboxField.php | 6 +- src/Fields/CodeField.php | 18 +- src/Fields/ColorField.php | 15 +- src/Fields/DateField.php | 12 +- src/Fields/DateTimeField.php | 12 +- src/Fields/DescriptionField.php | 4 +- src/Fields/EmailField.php | 8 +- src/Fields/MediaField.php | 28 +- src/Fields/MultiSelectField.php | 29 +- src/Fields/NumberField.php | 15 +- src/Fields/PasswordField.php | 10 +- src/Fields/RadioField.php | 24 +- src/Fields/RangeField.php | 25 +- src/Fields/SelectField.php | 29 +- src/Fields/TextField.php | 8 +- src/Fields/TextareaField.php | 10 +- src/Fields/TimeField.php | 12 +- src/Fields/ToggleField.php | 8 +- src/Fields/UrlField.php | 8 +- src/Interfaces/ConfigInterface.php | 84 ++++ src/Interfaces/FieldInterface.php | 3 +- src/Interfaces/SettingsInterface.php | 115 +++-- src/PageRenderer.php | 186 ++++---- src/Sanitizer.php | 157 ++++--- src/Settings.php | 345 ++++++++++----- 41 files changed, 1943 insertions(+), 1274 deletions(-) create mode 100644 src/Config.php create mode 100644 src/Interfaces/ConfigInterface.php diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 567856b..541a677 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,5 +1,6 @@ # Reporting a Vulnerability -If you discover a security-related issue, please report it privately by emailing **[security@wptechnix.com](mailto:security@wptechnix.com)**. +If you discover a security-related issue, please report it privately by emailing * +*[security@wptechnix.com](mailto:security@wptechnix.com)**. Avoid disclosing security vulnerabilities publicly. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72ce037..1f77c50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: [8.0, 8.4] + php-version: [ 8.0, 8.4 ] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index bd550cc..bb8426d 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -3,7 +3,7 @@ name: Lint Commit Messages on: pull_request: - types: [opened, edited, synchronize] + types: [ opened, edited, synchronize ] jobs: commitlint: diff --git a/CHANGELOG.md b/CHANGELOG.md index a78dcdd..4d31dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,33 +2,45 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.0.0] - 2025-08-11 -This is the initial public release of the WPTechnix Settings Framework. This version represents a complete architectural overhaul from its internal, monolithic predecessor, refactoring it into a modern, decoupled, and highly extensible framework ready for public use. +This is the initial public release of the WPTechnix Settings Framework. This version represents a complete architectural +overhaul from its internal, monolithic predecessor, refactoring it into a modern, decoupled, and highly extensible +framework ready for public use. ### Added -* **Complete Architectural Refactor:** The entire framework has been broken down into small, single-responsibility classes, following modern object-oriented design principles. -* **Composer Support:** The framework is now fully PSR-4 compliant and installable as a standard Composer package (`wptechnix/wp-settings-framework`). -* **Dependency Injection (DI) Friendly Design:** - * Introduced a primary `Settings` class that acts as a cohesive manager for building a page and retrieving its data. - * Added a `Interfaces\SettingsInterface` to allow for clean dependency injection and testability. - * Removed all static accessors (`SettingsRegistry`) in favor of an object-oriented, injectable architecture. -* **Extensible Field System:** - * Created a `FieldFactory` and an `AbstractField` base class, making it simple to add new, custom field types. - * All 20+ field types are now individual classes, making the system easier to maintain and extend. -* **Dedicated Asset Management:** A new `AssetManager` class now handles all CSS and JavaScript enqueueing. -* **Intelligent Asset Loading:** - * The `AssetManager` automatically detects which field types are being used and only enqueues the necessary libraries (e.g., Select2, Flatpickr, CodeMirror modes). - * Includes a fallback mechanism to prevent conflicts: it will not load a library from its CDN if a theme or another plugin has already registered it. -* **Configurable HTML Class Prefixing:** - * Introduced a new `htmlPrefix` option (defaults to `wptechnix-settings`) to prevent CSS and JS class name collisions in the WordPress admin. - * This prefix is now consistently applied to all custom field elements and their containers. -* **Flexible `Settings` Builder:** - * The `pageTitle` and `menuTitle` are now optional in the `Settings` constructor, defaulting to WordPress's standard "Settings" text for simpler setups. - * Added fluent setter methods (`setPageTitle()`, `setMenuTitle()`) for more flexible configuration. -* **Enhanced Code Editor Field:** The `code` field now accepts a `language` parameter (`css`, `javascript`, `html`) to enable the correct syntax highlighting mode. -* **Full PHPDoc Coverage:** Every class, method, and property is now fully documented according to PHPDoc and PSR-12 standards. -* **Comprehensive `README.md` and `CHANGELOG.md`:** Added official documentation for installation, usage, and project history. +* **Complete Architectural Refactor:** The entire framework has been broken down into small, single-responsibility + classes, following modern object-oriented design principles. +* **Composer Support:** The framework is now fully PSR-4 compliant and installable as a standard Composer package ( + `wptechnix/wp-settings-framework`). +* **Dependency Injection (DI) Friendly Design:** + * Introduced a primary `Settings` class that acts as a cohesive manager for building a page and retrieving its data. + * Added a `Interfaces\SettingsInterface` to allow for clean dependency injection and testability. + * Removed all static accessors (`SettingsRegistry`) in favor of an object-oriented, injectable architecture. +* **Extensible Field System:** + * Created a `FieldFactory` and an `AbstractField` base class, making it simple to add new, custom field types. + * All 20+ field types are now individual classes, making the system easier to maintain and extend. +* **Dedicated Asset Management:** A new `AssetManager` class now handles all CSS and JavaScript enqueueing. +* **Intelligent Asset Loading:** + * The `AssetManager` automatically detects which field types are being used and only enqueues the necessary + libraries (e.g., Select2, Flatpickr, CodeMirror modes). + * Includes a fallback mechanism to prevent conflicts: it will not load a library from its CDN if a theme or another + plugin has already registered it. +* **Configurable HTML Class Prefixing:** + * Introduced a new `htmlPrefix` option (defaults to `wptechnix-settings`) to prevent CSS and JS class name + collisions in the WordPress admin. + * This prefix is now consistently applied to all custom field elements and their containers. +* **Flexible `Settings` Builder:** + * The `pageTitle` and `menuTitle` are now optional in the `Settings` constructor, defaulting to WordPress's + standard "Settings" text for simpler setups. + * Added fluent setter methods (`setPageTitle()`, `setMenuTitle()`) for more flexible configuration. +* **Enhanced Code Editor Field:** The `code` field now accepts a `language` parameter (`css`, `javascript`, `html`) to + enable the correct syntax highlighting mode. +* **Full PHPDoc Coverage:** Every class, method, and property is now fully documented according to PHPDoc and PSR-12 + standards. +* **Comprehensive `README.md` and `CHANGELOG.md`:** Added official documentation for installation, usage, and project + history. diff --git a/README.md b/README.md index de8c791..bc2b293 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,55 @@ # WPTechnix Settings Framework -A modern, object-oriented PHP framework for creating powerful and professional settings pages in WordPress. Designed with a clean, fluent API, this framework saves you time by abstracting away the complexities of the WordPress Settings API. +A modern, object-oriented PHP framework for creating powerful and professional settings pages in WordPress. Designed +with a clean, fluent API, this framework saves you time by abstracting away the complexities of the WordPress Settings +API. -Build complex, tabbed settings pages with over 20 field types, conditional logic, and an enhanced user interface, all while storing your data efficiently in a single database option. +Build complex, tabbed settings pages with over 20 field types, conditional logic, and an enhanced user interface, all +while storing your data efficiently in a single database option. --- ## Features -* **Fluent, Chainable API:** Build your entire settings page in a clean, readable, and intuitive way. -* **Efficient Database Storage:** All settings are saved to a single array in the `wp_options` table, reducing database clutter and improving performance. -* **Rich & Modern Field Types:** Includes over 20 field types, with enhanced UI for color pickers, media uploaders, date/time selectors, and more. -* **Tabbed Interface:** Easily organize your settings into clean, separate tabs with support for Dashicons. -* **Conditional Field Logic:** Show or hide fields based on the value of another field (e.g., show "Log File Path" only when "Enable Debugging" is toggled on). -* **Code Editor Fields:** Includes a `code` field with syntax highlighting for CSS, JavaScript, and HTML, powered by the built-in WordPress CodeMirror library. -* **Conflict-Free Prefixing:** All custom HTML classes for CSS and JS are prefixed to prevent conflicts with other plugins and themes. This prefix is fully configurable. -* **Composer Ready:** Fully PSR-4 compliant and ready to be included as a dependency in any modern WordPress project. +* **Fluent, Chainable API:** Build your entire settings page in a clean, readable, and intuitive way. +* **Efficient Database Storage:** All settings are saved to a single array in the `wp_options` table, reducing database + clutter and improving performance. +* **Rich & Modern Field Types:** Includes over 20 field types, with enhanced UI for color pickers, media uploaders, + date/time selectors, and more. +* **Tabbed Interface:** Easily organize your settings into clean, separate tabs with support for Dashicons. +* **Conditional Field Logic:** Show or hide fields based on the value of another field (e.g., show "Log File Path" only + when "Enable Debugging" is toggled on). +* **Code Editor Fields:** Includes a `code` field with syntax highlighting for CSS, JavaScript, and HTML, powered by the + built-in WordPress CodeMirror library. +* **Conflict-Free Prefixing:** All custom HTML classes for CSS and JS are prefixed to prevent conflicts with other + plugins and themes. This prefix is fully configurable. +* **Composer Ready:** Fully PSR-4 compliant and ready to be included as a dependency in any modern WordPress project. ## Available Field Types The framework includes the following field types out of the box: -| Type | Description | -| :--- | :--- | -| `text` | A standard single-line text input. | -| `email` | A text input with `type="email"` validation. | -| `password` | A text input with `type="password"`. | -| `number` | A number input with `type="number"`. | -| `url` | A text input with `type="url"` validation. | -| `textarea` | A standard multi-line text area. | -| `checkbox` | A single checkbox. | -| `toggle` | An on/off toggle switch (saves a boolean). | -| `select` | A dropdown select menu. | -| `multiselect` | A multi-select dropdown menu. | -| `radio` | A group of radio buttons. | -| `buttongroup` | A modern button group that functions like a radio input. | -| `color` | A color picker field. | -| `date` | A date picker. | -| `datetime` | A date and time picker. | -| `time` | A time picker. | -| `range` | An enhanced range slider with a value display. | -| `media` | A media uploader that uses the WordPress Media Library. | -| `code` | A code editor with syntax highlighting. | +| Type | Description | +|:--------------|:--------------------------------------------------------------| +| `text` | A standard single-line text input. | +| `email` | A text input with `type="email"` validation. | +| `password` | A text input with `type="password"`. | +| `number` | A number input with `type="number"`. | +| `url` | A text input with `type="url"` validation. | +| `textarea` | A standard multi-line text area. | +| `checkbox` | A single checkbox. | +| `toggle` | An on/off toggle switch (saves a boolean). | +| `select` | A dropdown select menu. | +| `multiselect` | A multi-select dropdown menu. | +| `radio` | A group of radio buttons. | +| `buttongroup` | A modern button group that functions like a radio input. | +| `color` | A color picker field. | +| `date` | A date picker. | +| `datetime` | A date and time picker. | +| `time` | A time picker. | +| `range` | An enhanced range slider with a value display. | +| `media` | A media uploader that uses the WordPress Media Library. | +| `code` | A code editor with syntax highlighting. | | `description` | A read-only field used to display text, lists, or other HTML. | ## Installation @@ -49,46 +57,58 @@ The framework includes the following field types out of the box: This package is intended to be used as a Composer dependency. Install the package via the command line: + ```bash composer require wptechnix/wp-settings-framework ``` + Make sure your project's `vendor/autoload.php` file is included to autoload the framework's classes. ## Getting Started -Creating a settings page is simple. In your plugin's main bootstrap file or a dedicated service class, instantiate the `\WPTechnix\WPSettings\Settings` class and use its fluent methods to build your page. +Creating a settings page is simple. In your plugin's main bootstrap file or a dedicated service class, instantiate the +`\WPTechnix\WPSettings\Settings` class and use its fluent methods to build your page. ### Example Usage -Here is a complete example of how to build a tabbed settings page for a fictional "My Awesome Plugin". +Here is a complete example of how to build a tabbed settings page for a fictional "My Awesome Plugin". This example also +demonstrates the correct way to handle translations. ```php __('My Awesome Plugin Settings', $text_domain), + 'menuTitle' => __('Awesome Plugin', $text_domain), + ] ); // 2. Add tabs to organize your options. $settingsManager - ->addTab('general', 'General', 'dashicons-admin-generic') - ->addTab('advanced', 'Advanced', 'dashicons-admin-settings'); + ->addTab('general', __('General', $text_domain), 'dashicons-admin-generic') + ->addTab('advanced', __('Advanced', $text_domain), 'dashicons-admin-settings'); // 3. Add sections to the tabs. $settingsManager - ->addSection('api_section', 'API Credentials', 'Settings for the external API connection.', 'general') - ->addSection('display_section', 'Display Options', 'Control the look and feel.', 'general') - ->addSection('debugging_section', 'Debugging', 'Advanced developer settings.', 'advanced'); + ->addSection('api_section', __('API Credentials', $text_domain), __('Settings for the external API connection.', $text_domain), 'general') + ->addSection('display_section', __('Display Options', $text_domain), __('Control the look and feel.', $text_domain), 'general') + ->addSection('debugging_section', __('Debugging', $text_domain), __('Advanced developer settings.', $text_domain), 'advanced'); // 4. Add fields to the sections. $settingsManager @@ -96,37 +116,43 @@ add_action('plugins_loaded', function () { 'api_key', 'api_section', 'text', - 'API Key', - ['description' => 'Enter your public API key.'] + __('API Key', $text_domain), + ['description' => __('Enter your public API key.', $text_domain)] ) ->addField( 'primary_color', 'display_section', 'color', - 'Primary Color', - ['description' => 'Select a primary color for plugin elements.', 'default' => '#2271b1'] + __('Primary Color', $text_domain), + [ + 'description' => __('Select a primary color for plugin elements.', $text_domain), + 'default' => '#2271b1' + ] ) ->addField( 'brand_logo', 'display_section', 'media', - 'Brand Logo', - ['description' => 'Upload a logo to display in the header.'] + __('Brand Logo', $text_domain), + ['description' => __('Upload a logo to display in the header.', $text_domain)] ) ->addField( 'enable_debugging', // This field will control the next one 'debugging_section', 'toggle', - 'Enable Debug Mode', - ['description' => 'When enabled, advanced logging will be active.', 'default' => false] + __('Enable Debug Mode', $text_domain), + [ + 'description' => __('When enabled, advanced logging will be active.', $text_domain), + 'default' => false + ] ) ->addField( 'custom_css', 'debugging_section', 'code', // A code editor field - 'Custom CSS', + __('Custom CSS', $text_domain), [ - 'description' => 'Enter custom CSS to be loaded on the front-end.', + 'description' => __('Enter custom CSS to be loaded on the front-end.', $text_domain), 'language' => 'css', // Specify syntax highlighting mode 'conditional' => [ 'field' => 'enable_debugging', // The ID of the controlling field @@ -142,12 +168,15 @@ add_action('plugins_loaded', function () { // You can now use this $settingsManager object to retrieve values. - // In a DI container setup, you would bind the SettingsInterface to this instance. + // In a DI container setup, you would bind it to the SettingsInterface. // For this example, we'll just show how to use the object directly. function my_plugin_get_color() { - global $settingsManager; // Example of accessing the object + // This is a simplified example. In a real application, avoid using globals. + // You would typically pass the $settingsManager object to where it's needed + // or retrieve it from a service container. + global $settingsManager; return $settingsManager->get('primary_color', '#2271b1'); } }); @@ -185,7 +214,7 @@ $settingsManager->addField( 'license_key', 'general_section', 'text', - 'License Key', + __('License Key', 'my-text-domain'), [ 'conditional' => [ 'field' => 'license_type', // The ID of the field to check @@ -198,13 +227,13 @@ $settingsManager->addField( ### Customizing the HTML Prefix -By default, all custom CSS classes are prefixed with `wptechnix-settings-` (e.g., `.wptechnix-settings-toggle`). You can provide your own prefix if you want. +By default, all custom CSS classes are prefixed with `wptechnix-settings-`. You can provide your own prefix in the +constructor's options array. ```php -$settingsManager = new Settings( - 'my-plugin-slug', - 'My Plugin', - null, +$settingsManager = new \WPTechnix\WPSettings\Settings( + 'my_plugin_options', + 'my-plugin-settings', [ 'htmlPrefix' => 'myplugin' // Classes will now be prefixed with `myplugin-` ] diff --git a/bin/README.md b/bin/README.md index fbd7b71..c82d724 100755 --- a/bin/README.md +++ b/bin/README.md @@ -1,52 +1,56 @@ # Command-Line Scripts -This directory contains all the wrapper scripts used to manage and interact with the project's development environment. These scripts are designed to be run from the project root. +This directory contains all the wrapper scripts used to manage and interact with the project's development environment. +These scripts are designed to be run from the project root. --- ## `bin/docker` -This is the primary script for interacting with the Docker environment. It's a smart wrapper around `docker compose` that simplifies container management and command execution. +This is the primary script for interacting with the Docker environment. It's a smart wrapper around `docker compose` +that simplifies container management and command execution. ### Shorthand Container Access -The script's default behavior is to execute commands directly inside the main `app` container. Any command that is not a special management command (listed below) is passed through. +The script's default behavior is to execute commands directly inside the main `app` container. Any command that is not a +special management command (listed below) is passed through. -| Command | Description | -| ------------------------------------- | ------------------------------------------------------------------------- | -| `./bin/docker` | Open an interactive `bash` shell inside the default `app` container. | -| `./bin/docker ` | Run any command with its arguments inside the `app` container. | -| **Example:** `./bin/docker php -v` | Checks the PHP version inside the container. | -| **Example:** `./bin/docker ls -la` | Lists files in the container's default working directory (`/app`). | +| Command | Description | +|------------------------------------|----------------------------------------------------------------------| +| `./bin/docker` | Open an interactive `bash` shell inside the default `app` container. | +| `./bin/docker ` | Run any command with its arguments inside the `app` container. | +| **Example:** `./bin/docker php -v` | Checks the PHP version inside the container. | +| **Example:** `./bin/docker ls -la` | Lists files in the container's default working directory (`/app`). | ### Environment Management Commands These special commands are used to control the Docker Compose stack. -| Command | Description | -| ------------------------------- | ------------------------------------------------------------------------- | -| `up` | Start all services defined in `docker-compose.yml` in detached mode. | -| `down` | Stop and remove all containers, networks, and volumes. | -| `build [service...]` | Rebuild and restart services (default: all). | -| `restart [service...]` | Restart one or more services (default: all). | -| `logs [service...]` | Follow log output from one or more services (default: all). | -| `exec ` | Execute a command in a **specific** service container. | -| **Example:** `exec db mysql` | Opens a MySQL command-line client inside the `db` container. | +| Command | Description | +|------------------------------|----------------------------------------------------------------------| +| `up` | Start all services defined in `docker-compose.yml` in detached mode. | +| `down` | Stop and remove all containers, networks, and volumes. | +| `build [service...]` | Rebuild and restart services (default: all). | +| `restart [service...]` | Restart one or more services (default: all). | +| `logs [service...]` | Follow log output from one or more services (default: all). | +| `exec ` | Execute a command in a **specific** service container. | +| **Example:** `exec db mysql` | Opens a MySQL command-line client inside the `db` container. | --- ## `bin/composer` -This is a dedicated wrapper script for Composer. It simplifies running Composer commands by automatically forwarding them to be executed inside the `app` container. +This is a dedicated wrapper script for Composer. It simplifies running Composer commands by automatically forwarding +them to be executed inside the `app` container. Instead of typing `./bin/docker composer `, you can simply use: -| Command | Description | -| ----------------------------------------- | ------------------------------------------------- | -| `./bin/composer install` | Install all PHP dependencies from `composer.lock`.| -| `./bin/composer update` | Update PHP dependencies to their latest versions. | -| `./bin/composer require vendor/package` | Add a new PHP package to the project. | -| `./bin/composer remove vendor/package` | Remove a PHP package from the project. | +| Command | Description | +|-----------------------------------------|----------------------------------------------------| +| `./bin/composer install` | Install all PHP dependencies from `composer.lock`. | +| `./bin/composer update` | Update PHP dependencies to their latest versions. | +| `./bin/composer require vendor/package` | Add a new PHP package to the project. | +| `./bin/composer remove vendor/package` | Remove a PHP package from the project. | --- @@ -58,7 +62,7 @@ These scripts are typically used only during the initial setup of the project. This script prepares your local environment by copying all necessary configuration files from their templates. -| Command | Description | -| ---------------------------- | ------------------------------------------------------------ | -| `./bin/copy` | Copies template files (e.g., `.dist`, `.example`) if the destination does not already exist. | -| `./bin/copy --override` | Forces the copy, overwriting any existing configuration files. | +| Command | Description | +|-------------------------|----------------------------------------------------------------------------------------------| +| `./bin/copy` | Copies template files (e.g., `.dist`, `.example`) if the destination does not already exist. | +| `./bin/copy --override` | Forces the copy, overwriting any existing configuration files. | diff --git a/composer.json b/composer.json index b871bfd..b4fe350 100644 --- a/composer.json +++ b/composer.json @@ -1,48 +1,48 @@ { - "name": "wptechnix/wp-settings-framework", - "version": "1.0.0", - "description": "A modern, fluent, and object-oriented framework for creating powerful WordPress admin settings pages.", - "type": "library", - "license": "MIT", - "keywords": [ - "wordpress", - "settings", - "options" - ], - "authors": [ - { - "name": "WPTechnix", - "email": "developers@wptechnix.com" - } - ], - "minimum-stability": "stable", - "prefer-stable": true, - "require": { - "php": ">=8.0" - }, - "require-dev": { - "phpcompatibility/php-compatibility": "*", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-strict-rules": "^2.0", - "squizlabs/php_codesniffer": "^3", - "szepeviktor/phpstan-wordpress": "^2.0" - }, - "autoload": { - "psr-4": { - "WPTechnix\\WPSettings\\": "src/" - } - }, - "scripts": { - "fix:phpcbf": "vendor/bin/phpcbf || true", - "lint:phpcs": "vendor/bin/phpcs --report=full", - "lint:phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --error-format=table", - "lint": [ - "@fix:phpcbf", - "@lint:phpcs", - "@lint:phpstan" - ] - }, - "config": { - "sort-packages": true + "name": "wptechnix/wp-settings-framework", + "version": "1.0.0", + "description": "A modern, fluent, and object-oriented framework for creating powerful WordPress admin settings pages.", + "type": "library", + "license": "MIT", + "keywords": [ + "wordpress", + "settings", + "options" + ], + "authors": [ + { + "name": "WPTechnix", + "email": "developers@wptechnix.com" } + ], + "minimum-stability": "stable", + "prefer-stable": true, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "phpcompatibility/php-compatibility": "*", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-strict-rules": "^2.0", + "squizlabs/php_codesniffer": "^3", + "szepeviktor/phpstan-wordpress": "^2.0" + }, + "autoload": { + "psr-4": { + "WPTechnix\\WPSettings\\": "src/" + } + }, + "scripts": { + "fix:phpcbf": "vendor/bin/phpcbf || true", + "lint:phpcs": "vendor/bin/phpcs --report=full", + "lint:phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --error-format=table", + "lint": [ + "@fix:phpcbf", + "@lint:phpcs", + "@lint:phpstan" + ] + }, + "config": { + "sort-packages": true + } } diff --git a/examples/README.md b/examples/README.md index 3ff0c57..b730823 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,24 +1,29 @@ # WPTechnix Settings Framework: Usage & Field Reference -Welcome to the examples directory for the WPTechnix Settings Framework. This document provides a comprehensive guide to building a settings page and a detailed reference for every available field type and its configuration options. +Welcome to the examples directory for the WPTechnix Settings Framework. This document provides a comprehensive guide to +building a settings page and a detailed reference for every available field type and its configuration options. For installation instructions, please see the main [README.md](../README.md) in the root of the project. ## 1. Creating a Settings Page -The foundation of the framework is the `\WPTechnix\WPSettings\Settings` class. You instantiate this class to begin building your page. +The foundation of the framework is the `\WPTechnix\WPSettings\Settings` class. You instantiate this class to begin +building your page. ### The `Settings` Class Constructor The constructor creates your settings page object. ```php -new Settings(string $pageSlug, ?string $pageTitle = null, ?string $menuTitle = null, array $options = [])``` +new Settings(string $optionName, string $pageSlug, array $options = []) +``` -* `$pageSlug` (string, **required**): A unique slug for your settings page (e.g., `my-plugin-settings`). This is used in the URL. -* `$pageTitle` (string|null, optional): The main `

` title displayed at the top of your settings page. If omitted, it defaults to "Settings". -* `$menuTitle` (string|null, optional): The text displayed in the WordPress admin menu. If omitted, it defaults to the `$pageTitle`. -* `$options` (array, optional): An associative array to override default page settings. +* `$optionName` (string, **required**): A unique name for the option where all settings will be stored in the + `wp_options` table (e.g., `my_plugin_options`). +* `$pageSlug` (string, **required**): A unique slug for your settings page (e.g., `my-plugin-settings`). This is used in + the URL. +* `$options` (array, optional): An associative array to override default page settings. This is the **recommended** way + to set page titles, menu titles, and other configurations, especially for translation. #### Constructor Options (`$options` array) @@ -26,9 +31,12 @@ You can pass the following keys in the `$options` array: | Key | Type | Default | Description | | :--- | :--- | :--- | :--- | +| `pageTitle` | string | `'Settings'` | The main `

` title displayed at the top of your settings page. | +| `menuTitle` | string | `'Settings'` | The text displayed in the WordPress admin menu. | | `parentSlug` | string | `'options-general.php'` | The slug of the parent menu to attach this page to (e.g., `'edit.php?post_type=page'`, `'tools.php'`). | | `capability`| string | `'manage_options'` | The WordPress capability required for a user to view this page. | | `htmlPrefix` | string | `'wptechnix-settings'` | A prefix for all custom HTML classes to prevent CSS/JS conflicts. | +| `labels` | array | `[...]` | An array of framework-generated strings you can override for translation. | ### Basic Structure @@ -39,13 +47,20 @@ Every settings page follows this basic pattern: use WPTechnix\WPSettings\Settings; // 1. Instantiate the Settings manager -$settingsManager = new Settings('my-plugin-slug', 'My Plugin Settings'); +$settingsManager = new Settings( + 'my_plugin_options', + 'my-plugin-settings', + [ + 'pageTitle' => __('My Plugin Settings', 'my-text-domain'), + 'menuTitle' => __('My Plugin', 'my-text-domain'), + ] +); // 2. Add at least one Section -$settingsManager->addSection('main_section', 'Main Settings'); +$settingsManager->addSection('main_section', __('Main Settings', 'my-text-domain')); // 3. Add Fields to your section -$settingsManager->addField('api_key', 'main_section', 'text', 'API Key'); +$settingsManager->addField('api_key', 'main_section', 'text', __('API Key', 'my-text-domain')); // 4. Initialize the page to hook it into WordPress $settingsManager->init(); @@ -64,118 +79,132 @@ The `addField()` method has the following signature: These arguments can be used with almost every field type: -* `description` (string): Help text displayed below the field. Supports HTML. -* `default` (mixed): A default value for the field if none is saved in the database. -* `attributes` (array): An associative array of custom HTML attributes to add to the input element (see "Advanced Usage" below). -* `conditional` (array): An array to control the field's visibility based on another field's value (see "Advanced Usage" below). +* `description` (string): Help text displayed below the field. Supports HTML. +* `default` (mixed): A default value for the field if none is saved in the database. +* `attributes` (array): An associative array of custom HTML attributes to add to the input element (see "Advanced Usage" + below). +* `conditional` (array): An array to control the field's visibility based on another field's value (see "Advanced Usage" + below). --- ### Text & Input Fields #### Field: Text (`type: 'text'`) + A standard single-line text input. -* **Example:** - ```php - $settings->addField( - 'api_key', - 'main_section', - 'text', - 'API Key', - [ - 'description' => 'Enter your public API key.', - 'attributes' => [ - 'placeholder' => 'pub_xxxxxxxxxx', - 'class' => 'regular-text code', - ], - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'api_key', + 'main_section', + 'text', + 'API Key', + [ + 'description' => 'Enter your public API key.', + 'attributes' => [ + 'placeholder' => 'pub_xxxxxxxxxx', + 'class' => 'regular-text code', + ], + ] + ); + ``` #### Field: Email (`type: 'email'`) + A text input with HTML5 `type="email"` validation. -* **Example:** - ```php - $settings->addField( - 'admin_email', - 'main_section', - 'email', - 'Admin Email', - [ - 'description' => 'The email address for notifications.', - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'admin_email', + 'main_section', + 'email', + 'Admin Email', + [ + 'description' => 'The email address for notifications.', + ] + ); + ``` #### Field: Password (`type: 'password'`) + A text input where the value is obscured. -* **Example:** - ```php - $settings->addField( - 'secret_key', - 'main_section', - 'password', - 'Secret Key', - [ - 'description' => 'Your secret key will not be shown.', - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'secret_key', + 'main_section', + 'password', + 'Secret Key', + [ + 'description' => 'Your secret key will not be shown.', + ] + ); + ``` #### Field: Number (`type: 'number'`) + A number input. You can use the `attributes` argument to set `min`, `max`, and `step`. -* **Example:** - ```php - $settings->addField( - 'item_limit', - 'main_section', - 'number', - 'Item Limit', - [ - 'default' => 10, - 'attributes' => [ - 'min' => 1, - 'max' => 50, - 'step' => 1, - ], - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'item_limit', + 'main_section', + 'number', + 'Item Limit', + [ + 'default' => 10, + 'attributes' => [ + 'min' => 1, + 'max' => 50, + 'step' => 1, + ], + ] + ); + ``` #### Field: URL (`type: 'url'`) + A text input with HTML5 `type="url"` validation. -* **Example:** - ```php - $settings->addField( - 'website_url', - 'main_section', - 'url', - 'Website URL', - [ - 'attributes' => [ - 'placeholder' => 'https://example.com', - ], - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'website_url', + 'main_section', + 'url', + 'Website URL', + [ + 'attributes' => [ + 'placeholder' => 'https://example.com', + ], + ] + ); + ``` #### Field: Textarea (`type: 'textarea'`) + A standard multi-line text area. Use `attributes` to control `rows` and `cols`. -* **Example:** - ```php - $settings->addField( - 'custom_header_text', - 'main_section', - 'textarea', - 'Header Text', - [ - 'attributes' => [ - 'rows' => 4, - 'placeholder' => 'Enter a welcome message...', - ], - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'custom_header_text', + 'main_section', + 'textarea', + 'Header Text', + [ + 'attributes' => [ + 'rows' => 4, + 'placeholder' => 'Enter a welcome message...', + ], + ] + ); + ``` --- @@ -183,228 +212,259 @@ A standard multi-line text area. Use `attributes` to control `rows` and `cols`. These fields use a special `options` argument. -* `options` (array): An associative array where the `key` is the value that gets saved, and the `value` is the display label. `['saved_value' => 'Displayed Label']` +* `options` (array): An associative array where the `key` is the value that gets saved, and the `value` is the display + label. `['saved_value' => 'Displayed Label']` #### Field: Checkbox (`type: 'checkbox'`) + A single checkbox. Saves `true` if checked, `false` if not. -* **Example:** - ```php - $settings->addField( - 'enable_tracking', - 'main_section', - 'checkbox', - 'Enable Tracking', - [ - 'description' => 'Allow usage data to be collected.', - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'enable_tracking', + 'main_section', + 'checkbox', + 'Enable Tracking', + [ + 'description' => 'Allow usage data to be collected.', + ] + ); + ``` #### Field: Toggle (`type: 'toggle'`) + A modern on/off toggle switch. Functionally identical to a checkbox. -* **Example:** - ```php - $settings->addField( - 'dark_mode', - 'main_section', - 'toggle', - 'Enable Dark Mode', - [ - 'default' => true, - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'dark_mode', + 'main_section', + 'toggle', + 'Enable Dark Mode', + [ + 'default' => true, + ] + ); + ``` #### Field: Select (`type: 'select'`) + A dropdown select menu. -* **Configuration Arguments:** - * `options` (array, required): The key/value pairs for the dropdown options. -* **Example:** - ```php - $settings->addField( - 'font_size', - 'main_section', - 'select', - 'Font Size', - [ - 'default' => 'medium', - 'options' => [ - 'small' => 'Small', - 'medium' => 'Medium', - 'large' => 'Large', - ], - ] - ); - ``` + +* **Configuration Arguments:** + * `options` (array, required): The key/value pairs for the dropdown options. +* **Example:** + ```php + $settings->addField( + 'font_size', + 'main_section', + 'select', + 'Font Size', + [ + 'default' => 'medium', + 'options' => [ + 'small' => 'Small', + 'medium' => 'Medium', + 'large' => 'Large', + ], + ] + ); + ``` #### Field: Multi-Select (`type: 'multiselect'`) + A dropdown that allows for multiple selections. Saves an array of values. -* **Configuration Arguments:** - * `options` (array, required): The key/value pairs for the options. -* **Example:** - ```php - $settings->addField( - 'post_types', - 'main_section', - 'multiselect', - 'Applicable Post Types', - [ - 'default' => ['post'], - 'options' => [ - 'post' => 'Posts', - 'page' => 'Pages', - 'product' => 'Products', - ], - ] - ); - ``` + +* **Configuration Arguments:** + * `options` (array, required): The key/value pairs for the options. +* **Example:** + ```php + $settings->addField( + 'post_types', + 'main_section', + 'multiselect', + 'Applicable Post Types', + [ + 'default' => ['post'], + 'options' => [ + 'post' => 'Posts', + 'page' => 'Pages', + 'product' => 'Products', + ], + ] + ); + ``` #### Field: Radio (`type: 'radio'`) + A group of radio buttons where only one option can be selected. -* **Configuration Arguments:** - * `options` (array, required): The key/value pairs for the radio options. -* **Example:** - ```php - $settings->addField( - 'image_alignment', - 'main_section', - 'radio', - 'Image Alignment', - [ - 'default' => 'left', - 'options' => [ - 'left' => 'Align Left', - 'center' => 'Align Center', - 'right' => 'Align Right', - ], - ] - ); - ``` + +* **Configuration Arguments:** + * `options` (array, required): The key/value pairs for the radio options. +* **Example:** + ```php + $settings->addField( + 'image_alignment', + 'main_section', + 'radio', + 'Image Alignment', + [ + 'default' => 'left', + 'options' => [ + 'left' => 'Align Left', + 'center' => 'Align Center', + 'right' => 'Align Right', + ], + ] + ); + ``` #### Field: Button Group (`type: 'buttongroup'`) + A modern, styled button group that functions identically to a radio field. -* **Configuration Arguments:** - * `options` (array, required): The key/value pairs for the buttons. -* **Example:** - ```php - $settings->addField( - 'layout_style', - 'main_section', - 'buttongroup', - 'Layout Style', - [ - 'default' => 'grid', - 'options' => [ - 'grid' => 'Grid', - 'list' => 'List', - ], - ] - ); - ``` + +* **Configuration Arguments:** + * `options` (array, required): The key/value pairs for the buttons. +* **Example:** + ```php + $settings->addField( + 'layout_style', + 'main_section', + 'buttongroup', + 'Layout Style', + [ + 'default' => 'grid', + 'options' => [ + 'grid' => 'Grid', + 'list' => 'List', + ], + ] + ); + ``` --- ### Enhanced UI Fields #### Field: Color (`type: 'color'`) + A color picker that uses the native WordPress color picker. -* **Example:** - ```php - $settings->addField( - 'primary_color', - 'main_section', - 'color', - 'Primary Color', - [ - 'default' => '#2271b1', - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'primary_color', + 'main_section', + 'color', + 'Primary Color', + [ + 'default' => '#2271b1', + ] + ); + ``` #### Field: Date (`type: 'date'`) + A date picker input. -* **Example:** - ```php - $settings->addField('start_date', 'main_section', 'date', 'Campaign Start Date'); - ``` + +* **Example:** + ```php + $settings->addField('start_date', 'main_section', 'date', 'Campaign Start Date'); + ``` #### Field: DateTime (`type: 'datetime'`) + A date and time picker input. -* **Example:** - ```php - $settings->addField('event_datetime', 'main_section', 'datetime', 'Event Date & Time'); - ``` + +* **Example:** + ```php + $settings->addField('event_datetime', 'main_section', 'datetime', 'Event Date & Time'); + ``` #### Field: Time (`type: 'time'`) + A time picker input. -* **Example:** - ```php - $settings->addField('closing_time', 'main_section', 'time', 'Closing Time'); - ``` + +* **Example:** + ```php + $settings->addField('closing_time', 'main_section', 'time', 'Closing Time'); + ``` #### Field: Range (`type: 'range'`) + An enhanced slider for selecting a number within a range. -* **Example:** - ```php - $settings->addField( - 'opacity_level', - 'main_section', - 'range', - 'Opacity Level (%)', - [ - 'default' => 80, - 'attributes' => [ - 'min' => 0, - 'max' => 100, - 'step' => 5, - ], - ] - ); - ``` + +* **Example:** + ```php + $settings->addField( + 'opacity_level', + 'main_section', + 'range', + 'Opacity Level (%)', + [ + 'default' => 80, + 'attributes' => [ + 'min' => 0, + 'max' => 100, + 'step' => 5, + ], + ] + ); + ``` --- ### Advanced & Special Fields #### Field: Media (`type: 'media'`) + A media uploader that integrates with the WordPress Media Library. It saves the attachment ID. -* **Example:** - ```php - $settings->addField('site_logo', 'main_section', 'media', 'Site Logo'); - ``` + +* **Example:** + ```php + $settings->addField('site_logo', 'main_section', 'media', 'Site Logo'); + ``` #### Field: Code (`type: 'code'`) + A code editor with syntax highlighting, powered by CodeMirror. -* **Configuration Arguments:** - * `language` (string): The syntax highlighting mode. Can be `css`, `javascript` (or `js`), or `html`. Defaults to `css`. -* **Example:** - ```php - $settings->addField( - 'custom_js', - 'main_section', - 'code', - 'Footer JavaScript', - [ - 'language' => 'javascript', - 'description' => 'This code will be added to your site footer.', - ] - ); - ``` + +* **Configuration Arguments:** + * `language` (string): The syntax highlighting mode. Can be `css`, `javascript` (or `js`), or `html`. Defaults to + `css`. +* **Example:** + ```php + $settings->addField( + 'custom_js', + 'main_section', + 'code', + 'Footer JavaScript', + [ + 'language' => 'javascript', + 'description' => 'This code will be added to your site footer.', + ] + ); + ``` #### Field: Description (`type: 'description'`) -A special read-only field used to display information. It has no input and saves no value. The content is passed via the `description` argument. -* **Example:** - ```php - $settings->addField( - 'shortcode_info', - 'main_section', - 'description', - 'Shortcode', - [ - 'description' => 'To display the form, use the shortcode: [my_awesome_form]', - ] - ); - ``` + +A special read-only field used to display information. It has no input and saves no value. The content is passed via the +`description` argument. + +* **Example:** + ```php + $settings->addField( + 'shortcode_info', + 'main_section', + 'description', + 'Shortcode', + [ + 'description' => 'To display the form, use the shortcode: [my_awesome_form]', + ] + ); + ``` --- @@ -412,9 +472,12 @@ A special read-only field used to display information. It has no input and saves ### Custom HTML Attributes (`attributes`) -The `attributes` argument gives you direct access to the HTML input element. You can pass an associative array of any valid HTML attribute, and it will be added to the field. This is incredibly powerful for adding placeholders, data attributes, or accessibility enhancements. +The `attributes` argument gives you direct access to the HTML input element. You can pass an associative array of any +valid HTML attribute, and it will be added to the field. This is incredibly powerful for adding placeholders, data +attributes, or accessibility enhancements. **Example:** + ```php $settings->addField( 'api_key', @@ -434,13 +497,15 @@ $settings->addField( ### Conditional Logic (`conditional`) -The `conditional` argument makes a field appear only when another field meets a certain condition. It's an array with three keys: +The `conditional` argument makes a field appear only when another field meets a certain condition. It's an array with +three keys: -* `field` (string, required): The ID of the field to watch. -* `value` (string, required): The value the watched field must have. For toggles/checkboxes, use `'1'` for "on". -* `operator` (string, optional): The comparison operator. Can be `==` (default), `!=`, `in`, or `not in`. +* `field` (string, required): The ID of the field to watch. +* `value` (string, required): The value the watched field must have. For toggles/checkboxes, use `'1'` for "on". +* `operator` (string, optional): The comparison operator. Can be `==` (default), `!=`, `in`, or `not in`. **Example:** + ```php // The controlling field $settings->addField( diff --git a/examples/settings-with-tabs.php b/examples/settings-with-tabs.php index f2443ae..83ae74e 100644 --- a/examples/settings-with-tabs.php +++ b/examples/settings-with-tabs.php @@ -4,6 +4,9 @@ declare(strict_types=1); use WPTechnix\WPSettings\Settings; +// Please make sure you require the composer autoload. +// require_once plugin_dir_path(__FILE__) . '/vendor/autoload.php'; + add_action('plugins_loaded', 'wptechnix_settings_demo_with_tabs'); /** @@ -13,10 +16,13 @@ function wptechnix_settings_demo_with_tabs(): void { // 1. Create a new Settings instance. $settings = new Settings( - 'wptechnix-demo-tabs', // Unique Page Slug - 'Settings Demo (Tabs)', // Page Title - 'Settings Demo (Tabs)', // Menu Title - ['parentSlug' => 'tools.php'] // Place this page under the "Tools" menu. + 'wptechnix_options_tabs', // Unique option name for the database + 'wptechnix-demo-tabs', // Unique page slug + [ + 'pageTitle' => 'Settings Demo (Tabs)', // Page Title + 'menuTitle' => 'Settings Demo (Tabs)', // Menu Title + 'parentSlug' => 'tools.php' // Place this page under the "Tools" menu. + ] ); // 2. Add tabs to organize the settings page. @@ -26,10 +32,25 @@ function wptechnix_settings_demo_with_tabs(): void // 3. Add sections and assign them to the correct tabs. $settings->addSection('text_inputs', 'Text-Based Inputs', 'Fields for text, numbers, and passwords.', 'inputs') - ->addSection('choice_inputs', 'Choice-Based Inputs', 'Fields for selecting one or more options.', 'choices') + ->addSection( + 'choice_inputs', + 'Choice-Based Inputs', + 'Fields for selecting one or more options.', + 'choices' + ) ->addSection('ui_inputs', 'Enhanced UI Inputs', 'Fields with special user interfaces.', 'choices') - ->addSection('advanced_inputs', 'Advanced & Special Inputs', 'Media, code, and other powerful fields.', 'advanced') - ->addSection('conditional_section', 'Conditional Logic Demo', 'Show and hide fields based on other fields\' values.', 'advanced'); + ->addSection( + 'advanced_inputs', + 'Advanced & Special Inputs', + 'Media, code, and other powerful fields.', + 'advanced' + ) + ->addSection( + 'conditional_section', + 'Conditional Logic Demo', + 'Show and hide fields based on other fields\' values.', + 'advanced' + ); // --- FIELDS FOR "INPUTS & TEXT" TAB --- $settings @@ -44,10 +65,34 @@ function wptechnix_settings_demo_with_tabs(): void $settings ->addField('demo_checkbox', 'choice_inputs', 'checkbox', 'Checkbox Field') ->addField('demo_toggle', 'choice_inputs', 'toggle', 'Toggle Switch', ['default' => true]) - ->addField('demo_select', 'choice_inputs', 'select', 'Select Dropdown', ['options' => ['a' => 'Option A', 'b' => 'Option B']]) - ->addField('demo_multiselect', 'choice_inputs', 'multiselect', 'Multi-Select', ['options' => ['a' => 'Choice A', 'b' => 'Choice B', 'c' => 'Choice C']]) - ->addField('demo_radio', 'choice_inputs', 'radio', 'Radio Buttons', ['options' => ['yes' => 'Yes', 'no' => 'No']]) - ->addField('demo_buttongroup', 'choice_inputs', 'buttongroup', 'Button Group', ['options' => ['left' => 'Left', 'center' => 'Center', 'right' => 'Right']]) + ->addField( + 'demo_select', + 'choice_inputs', + 'select', + 'Select Dropdown', + ['options' => ['a' => 'Option A', 'b' => 'Option B'], 'placeholder' => 'Select Option'] + ) + ->addField( + 'demo_multiselect', + 'choice_inputs', + 'multiselect', + 'Multi-Select', + ['options' => ['a' => 'Choice A', 'b' => 'Choice B', 'c' => 'Choice C'], 'placeholder' => 'Select Options'] + ) + ->addField( + 'demo_radio', + 'choice_inputs', + 'radio', + 'Radio Buttons', + ['options' => ['yes' => 'Yes', 'no' => 'No']] + ) + ->addField( + 'demo_buttongroup', + 'choice_inputs', + 'buttongroup', + 'Button Group', + ['options' => ['left' => 'Left', 'center' => 'Center', 'right' => 'Right']] + ) ->addField('demo_color', 'ui_inputs', 'color', 'Color Picker') ->addField('demo_date', 'ui_inputs', 'date', 'Date Picker') ->addField('demo_datetime', 'ui_inputs', 'datetime', 'Date & Time Picker') @@ -58,31 +103,40 @@ function wptechnix_settings_demo_with_tabs(): void $settings ->addField('demo_media', 'advanced_inputs', 'media', 'Media Uploader') ->addField( - 'demo_code_html', 'advanced_inputs', 'code', 'Code Editor (HTML)', + 'demo_code_html', + 'advanced_inputs', + 'code', + 'Code Editor (HTML)', [ 'description' => 'A code editor with HTML syntax highlighting.', - 'language' => 'html', - 'attributes' => ['placeholder' => ''], + 'language' => 'html', ] ) ->addField( - 'demo_code_css', 'advanced_inputs', 'code', 'Code Editor (CSS)', + 'demo_code_css', + 'advanced_inputs', + 'code', + 'Code Editor (CSS)', [ 'description' => 'A code editor with CSS syntax highlighting.', - 'language' => 'css', - 'attributes' => ['placeholder' => ''], + 'language' => 'css', ] ) ->addField( - 'demo_code_js', 'advanced_inputs', 'code', 'Code Editor (JS)', + 'demo_code_js', + 'advanced_inputs', + 'code', + 'Code Editor (JS)', [ 'description' => 'A code editor with JavaScript syntax highlighting.', - 'language' => 'javascript', - 'attributes' => ['placeholder' => ''], + 'language' => 'javascript', ] ) ->addField( - 'demo_description', 'advanced_inputs', 'description', 'Description Field', + 'demo_description', + 'advanced_inputs', + 'description', + 'Description Field', ['description' => 'This is a read-only field used to display important information. It supports HTML.'] ) diff --git a/examples/settings-without-tabs.php b/examples/settings-without-tabs.php index e98781d..c7d5156 100644 --- a/examples/settings-without-tabs.php +++ b/examples/settings-without-tabs.php @@ -4,6 +4,9 @@ declare(strict_types=1); use WPTechnix\WPSettings\Settings; +// Please make sure you require the composer autoload. +// require_once plugin_dir_path(__FILE__) . '/vendor/autoload.php'; + add_action('plugins_loaded', 'wptechnix_settings_demo_without_tabs'); /** @@ -13,123 +16,166 @@ function wptechnix_settings_demo_without_tabs(): void { // 1. Create a new Settings instance. $settings = new Settings( - 'wptechnix-demo-simple', // Unique Page Slug - 'Settings Demo (Simple)', // Page Title - 'Settings Demo (Simple)' // Menu Title + 'wptechnix_options_no_tabs', // Unique option name for the database + 'wptechnix-demo-no-tabs', // Unique page slug + [ + 'pageTitle' => 'Settings Demo (No Tabs)', // Page Title + 'menuTitle' => 'Settings Demo (No Tabs)', // Menu Title + 'parentSlug' => 'tools.php' // Place this page under the "Tools" menu. + ] ); - // 2. Add sections to organize fields. + // 2. Add sections directly to the page (no tabs are needed). + $settings->addSection('text_inputs', 'Text-Based Inputs', 'Fields for text, numbers, and passwords.') + ->addSection( + 'choice_inputs', + 'Choice-Based Inputs', + 'Fields for selecting one or more options.' + ) + ->addSection('ui_inputs', 'Enhanced UI Inputs', 'Fields with special user interfaces.') + ->addSection( + 'advanced_inputs', + 'Advanced & Special Inputs', + 'Media, code, and other powerful fields.' + ) + ->addSection( + 'conditional_section', + 'Conditional Logic Demo', + 'Show and hide fields based on other fields\' values.' + ); + + // 3. Add fields and assign them to the correct sections. + + // --- FIELDS FOR "Text-Based Inputs" SECTION --- $settings - ->addSection( - 'basic_fields_section', - 'Basic Input Fields', - 'A showcase of standard text, choice, and UI fields.' + ->addField('demo_text', 'text_inputs', 'text', 'Text Field') + ->addField('demo_email', 'text_inputs', 'email', 'Email Field') + ->addField('demo_password', 'text_inputs', 'password', 'Password Field') + ->addField('demo_number', 'text_inputs', 'number', 'Number Field', ['default' => 42]) + ->addField('demo_url', 'text_inputs', 'url', 'URL Field') + ->addField('demo_textarea', 'text_inputs', 'textarea', 'Textarea Field'); + + // --- FIELDS FOR "Choice-Based Inputs" SECTION --- + $settings + ->addField('demo_checkbox', 'choice_inputs', 'checkbox', 'Checkbox Field') + ->addField('demo_toggle', 'choice_inputs', 'toggle', 'Toggle Switch', ['default' => true]) + ->addField( + 'demo_select', + 'choice_inputs', + 'select', + 'Select Dropdown', + ['options' => ['a' => 'Option A', 'b' => 'Option B'], 'placeholder' => 'Select Option'] ) - ->addSection( - 'advanced_fields_section', - 'Advanced & Conditional Fields', - 'A showcase of advanced fields and conditional logic.' + ->addField( + 'demo_multiselect', + 'choice_inputs', + 'multiselect', + 'Multi-Select', + ['options' => ['a' => 'Choice A', 'b' => 'Choice B', 'c' => 'Choice C'], 'placeholder' => 'Select Options'] + ) + ->addField( + 'demo_radio', + 'choice_inputs', + 'radio', + 'Radio Buttons', + ['options' => ['yes' => 'Yes', 'no' => 'No']] + ) + ->addField( + 'demo_buttongroup', + 'choice_inputs', + 'buttongroup', + 'Button Group', + ['options' => ['left' => 'Left', 'center' => 'Center', 'right' => 'Right']] ); - - // 3. Add all field types to the sections. + // --- FIELDS FOR "Enhanced UI Inputs" SECTION --- $settings - // --- Fields for the "Basic Inputs" Section --- - ->addField( - 'demo_text', 'basic_fields_section', 'text', 'Text Field', - ['description' => 'A standard single-line text input.', 'attributes' => ['placeholder' => 'Enter some text...']] - ) - ->addField( - 'demo_textarea', 'basic_fields_section', 'textarea', 'Textarea Field', - ['description' => 'A multi-line text input area.', 'attributes' => ['rows' => 4]] - ) - ->addField( - 'demo_toggle', 'basic_fields_section', 'toggle', 'Toggle Switch', - ['description' => 'A modern on/off toggle switch.', 'default' => true] - ) - ->addField( - 'demo_select', 'basic_fields_section', 'select', 'Select Dropdown', - ['options' => ['option_1' => 'Option One', 'option_2' => 'Option Two', 'option_3' => 'Option Three']] - ) - ->addField( - 'demo_radio', 'basic_fields_section', 'radio', 'Radio Buttons', - ['options' => ['yes' => 'Yes', 'no' => 'No', 'maybe' => 'Maybe'], 'default' => 'yes'] - ) - ->addField( - 'demo_color', 'basic_fields_section', 'color', 'Color Picker', - ['description' => 'A field for selecting a hex color value.', 'default' => '#52ACCC'] - ) - ->addField( - 'demo_date', 'basic_fields_section', 'date', 'Date Picker', - ['description' => 'A field for selecting a calendar date.'] - ) + ->addField('demo_color', 'ui_inputs', 'color', 'Color Picker') + ->addField('demo_date', 'ui_inputs', 'date', 'Date Picker') + ->addField('demo_datetime', 'ui_inputs', 'datetime', 'Date & Time Picker') + ->addField('demo_time', 'ui_inputs', 'time', 'Time Picker') + ->addField('demo_range', 'ui_inputs', 'range', 'Range Slider', ['default' => 75]); - // --- Fields for the "Advanced & Conditional" Section --- + // --- FIELDS FOR "Advanced & Special Inputs" SECTION --- + $settings + ->addField('demo_media', 'advanced_inputs', 'media', 'Media Uploader') ->addField( - 'demo_media', 'advanced_fields_section', 'media', 'Media Uploader', - ['description' => 'Upload an image or file using the WordPress Media Library.'] - ) - ->addField( - 'demo_code_html', 'advanced_fields_section', 'code', 'Code Editor (HTML)', + 'demo_code_html', + 'advanced_inputs', + 'code', + 'Code Editor (HTML)', [ 'description' => 'A code editor with HTML syntax highlighting.', - 'language' => 'html', - 'attributes' => ['placeholder' => ''], + 'language' => 'html', ] ) ->addField( - 'demo_code_css', 'advanced_fields_section', 'code', 'Code Editor (CSS)', + 'demo_code_css', + 'advanced_inputs', + 'code', + 'Code Editor (CSS)', [ 'description' => 'A code editor with CSS syntax highlighting.', - 'language' => 'css', - 'attributes' => ['placeholder' => ''], + 'language' => 'css', ] ) ->addField( - 'demo_code_js', 'advanced_fields_section', 'code', 'Code Editor (JS)', + 'demo_code_js', + 'advanced_inputs', + 'code', + 'Code Editor (JS)', [ 'description' => 'A code editor with JavaScript syntax highlighting.', - 'language' => 'javascript', - 'attributes' => ['placeholder' => ''], - ] - ) - ->addField( // --- Start Conditional Logic Demo --- - 'demo_enable_advanced', // This is the CONTROLLING field - 'advanced_fields_section', - 'toggle', - 'Enable Advanced Options', - ['description' => 'Turn this on to reveal hidden advanced fields below.', 'default' => false] - ) - ->addField( - 'demo_conditional_api_key', // This is the CONDITIONAL field - 'advanced_fields_section', - 'text', - 'Conditional API Key', - [ - 'description' => 'This field is only visible when the toggle above is ON.', - 'conditional' => [ - 'field' => 'demo_enable_advanced', // The ID of the controlling field - 'value' => '1', // The value to check for (1 for 'on') - 'operator' => '==', // The comparison operator - ] + 'language' => 'javascript', ] ) ->addField( - 'demo_conditional_mode', // This is another CONDITIONAL field - 'advanced_fields_section', - 'buttongroup', - 'Conditional Mode', - [ - 'description' => 'This button group is also only visible when the toggle is ON.', - 'options' => ['live' => 'Live', 'test' => 'Test'], - 'default' => 'test', - 'conditional' => [ - 'field' => 'demo_enable_advanced', - 'value' => '1', - ] - ] - ); // --- End Conditional Logic Demo --- + 'demo_description', + 'advanced_inputs', + 'description', + 'Description Field', + ['description' => 'This is a read-only field used to display important information. It supports HTML.'] + ); + // --- FIELDS FOR "Conditional Logic Demo" SECTION --- + $settings + ->addField( + 'demo_contact_method', // The CONTROLLING field + 'conditional_section', + 'radio', + 'Preferred Contact Method', + [ + 'description' => 'Select a method to see different conditional fields appear.', + 'options' => ['email' => 'Email', 'phone' => 'Phone Call', 'none' => 'No Contact'], + 'default' => 'none', + ] + ) + ->addField( + 'demo_conditional_email', // A CONDITIONAL field + 'conditional_section', + 'email', + 'Contact Email Address', + [ + 'description' => 'This only appears if "Email" is selected.', + 'conditional' => [ + 'field' => 'demo_contact_method', + 'value' => 'email', + ], + ] + ) + ->addField( + 'demo_conditional_phone', // Another CONDITIONAL field + 'conditional_section', + 'text', + 'Contact Phone Number', + [ + 'description' => 'This only appears if "Phone Call" is selected.', + 'conditional' => [ + 'field' => 'demo_contact_method', + 'value' => 'phone', + ], + ] + ); // 4. Initialize the settings page. $settings->init(); diff --git a/package-lock.json b/package-lock.json index e0cba93..bc6c4ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2923,15 +2923,11 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "dev": true, "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } @@ -3163,13 +3159,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true, - "license": "MIT" - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -4396,13 +4385,13 @@ } }, "node_modules/socks": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", - "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -4481,13 +4470,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", diff --git a/src/AssetManager.php b/src/AssetManager.php index c1967d6..a5e85f9 100644 --- a/src/AssetManager.php +++ b/src/AssetManager.php @@ -4,28 +4,24 @@ declare(strict_types=1); namespace WPTechnix\WPSettings; +use WPTechnix\WPSettings\Interfaces\ConfigInterface; + /** - * Handles the enqueueing and rendering of all static assets. + * Handles the enqueueing and rendering of all static assets for the settings page. * - * This class centralizes asset management. It intelligently loads external - * libraries and uses a configurable HTML class prefix to prevent conflicts. * @noinspection JSUnusedLocalSymbols, CssUnusedSymbol + * + * @phpstan-import-type SettingsConfig from Settings */ final class AssetManager { /** - * The complete settings configuration array. - * @var array - */ - private array $config = []; - - /** - * Defines the external libraries that can be loaded as fallbacks. + * Defines the external libraries that can be loaded. * * @var array, - * style?: array + * script?: array{src: string, deps: string[], version: string, in_footer: bool}, + * style?: array{src: string, deps: string[], version: string} * }> */ private array $libraryPackages; @@ -39,55 +35,58 @@ final class AssetManager /** * AssetManager constructor. + * + * @param ConfigInterface $config Settings Config. */ - public function __construct() - { - $this->libraryPackages = [ - 'select2' => [ - 'handle' => 'select2', - 'script' => [ - 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js', - 'deps' => ['jquery'], 'version' => '4.0.13', 'in_footer' => true, + public function __construct( + protected ConfigInterface $config + ) { + $this->config->deepMerge([ + 'assetPackages' => [ + 'select2' => [ + 'handle' => 'select2', + 'script' => [ + 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js', + 'deps' => ['jquery'], + 'version' => '4.0.13', + 'in_footer' => true, + ], + 'style' => [ + 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', + 'deps' => [], + 'version' => '4.0.13', + ], ], - 'style' => [ - 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', - 'deps' => [], 'version' => '4.0.13', + 'flatpickr' => [ + 'handle' => 'flatpickr', + 'script' => [ + 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js', + 'deps' => [], + 'version' => '4.6.13', + 'in_footer' => true, + ], + 'style' => [ + 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css', + 'deps' => [], + 'version' => '4.6.13', + ], ], - ], - 'flatpickr' => [ - 'handle' => 'flatpickr', - 'script' => [ - 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js', - 'deps' => [], 'version' => '4.6.13', 'in_footer' => true, - ], - 'style' => [ - 'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css', - 'deps' => [], 'version' => '4.6.13', - ], - ], - ]; + ] + ]); + + $this->libraryPackages = $this->config->get('assetPackages'); $this->fieldTypeToPackageMap = [ - 'select' => 'select2', + 'select' => 'select2', 'multiselect' => 'select2', - 'date' => 'flatpickr', - 'datetime' => 'flatpickr', - 'time' => 'flatpickr', + 'date' => 'flatpickr', + 'datetime' => 'flatpickr', + 'time' => 'flatpickr', ]; } /** - * Sets the settings configuration array. - * - * @param array $config The settings configuration. - */ - public function setConfig(array $config): void - { - $this->config = $config; - } - - /** - * Hooks the enqueue method into WordPress. + * Hooks the asset enqueueing method into WordPress. */ public function init(): void { @@ -95,13 +94,16 @@ final class AssetManager } /** - * Enqueues all necessary scripts and styles. + * Enqueues all necessary scripts and styles for the settings page. + * + * This method is hooked into 'admin_enqueue_scripts' and only loads assets + * on the relevant admin page. */ public function enqueueAssets(): void { - $pageSlug = $this->config['pageSlug'] ?? ''; - $screen = get_current_screen(); - if (empty($pageSlug) || null === $screen || !str_contains($screen->id, $pageSlug)) { + $pageSlug = $this->config->get('pageSlug'); + $screen = get_current_screen(); + if (empty($pageSlug) || null === $screen || ! str_contains($screen->id, $pageSlug)) { return; } @@ -113,7 +115,7 @@ final class AssetManager $this->enqueueRequiredLibraries(); $this->enqueueCodeEditorAssets(); - wp_add_inline_script('jquery', $this->getInlineScripts()); + wp_add_inline_script('jquery-core', $this->getInlineScripts()); wp_add_inline_style('wp-admin', $this->getInlineStyles()); } @@ -122,15 +124,18 @@ final class AssetManager */ private function enqueueRequiredLibraries(): void { - if (empty($this->config['fields'])) { + if (! $this->config->has('fields')) { return; } + $fields = $this->config->get('fields'); + $requiredPackages = []; - foreach ($this->config['fields'] as $field) { + + foreach ($fields as $field) { $fieldType = $field['type'] ?? ''; if (isset($this->fieldTypeToPackageMap[$fieldType])) { - $packageName = $this->fieldTypeToPackageMap[$fieldType]; + $packageName = $this->fieldTypeToPackageMap[$fieldType]; $requiredPackages[$packageName] = true; } } @@ -143,52 +148,48 @@ final class AssetManager } /** - * Enqueues a library package, checking for existing registrations first. + * Enqueues a single library package (style and/or script). + * + * It checks if the asset is already registered to avoid conflicts with other plugins. + * * @param array{ * handle: string, - * script?: array, - * style?: array + * script?: array{src: string, deps: string[], version: string, in_footer: bool}, + * style?: array{src: string, deps: string[], version: string} * } $package The package definition. */ private function enqueuePackage(array $package): void { $handle = $package['handle']; if (isset($package['style'])) { - if (!wp_style_is($handle, 'registered')) { - wp_register_style( - $handle, - $package['style']['src'], - $package['style']['deps'], - $package['style']['version'] - ); + if (! wp_style_is($handle, 'registered')) { + wp_register_style($handle, ...array_values($package['style'])); } wp_enqueue_style($handle); } if (isset($package['script'])) { - if (!wp_script_is($handle, 'registered')) { - wp_register_script( - $handle, - $package['script']['src'], - $package['script']['deps'], - $package['script']['version'], - $package['script']['in_footer'] - ); + if (! wp_script_is($handle, 'registered')) { + wp_register_script($handle, ...array_values($package['script'])); } wp_enqueue_script($handle); } } /** - * Detects required CodeMirror languages and enqueues them. + * Enqueues the WordPress Code Editor assets for the required languages. */ private function enqueueCodeEditorAssets(): void { - if (!function_exists('wp_enqueue_code_editor') || empty($this->config['fields'])) { + if (! function_exists('wp_enqueue_code_editor') || ! $this->config->has('fields')) { return; } + + $fields = $this->config->get('fields'); + $requiredLanguages = []; - foreach ($this->config['fields'] as $field) { - if (($field['type'] ?? '') === 'code') { + + foreach ($fields as $field) { + if ('code' === ($field['type'] ?? '')) { $requiredLanguages[$field['language'] ?? 'css'] = true; } } @@ -196,16 +197,14 @@ final class AssetManager return; } $mimeTypes = [ - 'css' => 'text/css', - 'js' => 'text/javascript', + 'css' => 'text/css', + 'js' => 'text/javascript', 'javascript' => 'text/javascript', - 'html' => 'text/html', - 'xml' => 'application/xml', + 'html' => 'text/html', + 'xml' => 'application/xml', ]; foreach (array_keys($requiredLanguages) as $lang) { - wp_enqueue_code_editor([ - 'type' => $mimeTypes[$lang] ?? 'text/plain' - ]); + wp_enqueue_code_editor(['type' => $mimeTypes[$lang] ?? 'text/plain']); } } @@ -216,27 +215,24 @@ final class AssetManager */ private function getInlineScripts(): string { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); + + $addMediaTitle = esc_js($this->config->get('labels.addMediaTitle', 'Add media')); + $selectMediaText = esc_js($this->config->get('labels.selectMediaText', 'Select')); + $removeMediaText = esc_js($this->config->get('labels.removeMediaText', 'Remove')); // phpcs:disable Generic.Files.LineLength return <<' - ); + previewHTML = ''; } else { - preview.html( - '

File: ' + attachment.filename + '

' - ); + previewHTML = '
' + attachment.filename + '
'; } + previewContainer.html(previewHTML); if (!button.siblings('.{$htmlPrefix}-media-remove-button').length) { - button.after(' '); + button.after(' '); } }); mediaUploader.open(); @@ -303,7 +281,7 @@ final class AssetManager button.remove(); }); - // Initialize button groups. + // Initialize button groups $('.{$htmlPrefix}-buttongroup-container').on('click', '.{$htmlPrefix}-buttongroup-option', function(e) { e.preventDefault(); var button = $(this); @@ -317,42 +295,31 @@ final class AssetManager // Range sliders function updateRangeSlider(slider) { var value = slider.val(); - slider - .closest('.{$htmlPrefix}-enhanced-range-container') - .find('.{$htmlPrefix}-range-value-input').val(value); + slider.closest('.{$htmlPrefix}-enhanced-range-container').find('.{$htmlPrefix}-range-value-input').val(value); } - $('.{$htmlPrefix}-enhanced-range-slider').on('input', function() { - updateRangeSlider($(this)); - }); + + $('.{$htmlPrefix}-enhanced-range-slider').on('input', function() { + updateRangeSlider($(this)); + }); $('.{$htmlPrefix}-enhanced-range-slider').each(function() { - updateRangeSlider($(this)); - }); + updateRangeSlider($(this)); + }); // Initialize CodeMirror if (wp.codeEditor) { $('.{$htmlPrefix}-code-editor').each(function() { var textarea = $(this); - if (textarea.data('codemirror-initialized')) { - return; - } + if (textarea.data('codemirror-initialized')) { return; } var language = textarea.data('language') || 'css'; var mimeType = 'text/' + language; if (language === 'javascript' || language === 'js') mimeType = 'text/javascript'; if (language === 'html') mimeType = 'text/html'; var editorSettings = wp.codeEditor.defaultSettings ? _.clone(wp.codeEditor.defaultSettings) : {}; - editorSettings.codemirror = _.extend({}, editorSettings.codemirror, { - mode: mimeType, - lineNumbers: true, - lineWrapping: true, - styleActiveLine: true - }); + editorSettings.codemirror = _.extend({}, editorSettings.codemirror, { mode: mimeType, lineNumbers: true, lineWrapping: true, styleActiveLine: true }); var editor = wp.codeEditor.initialize(textarea, editorSettings); - editor.codemirror.on('change', function() { - editor.codemirror.save(); - textarea.trigger('change'); - }); + editor.codemirror.on('change', function() { editor.codemirror.save(); textarea.trigger('change'); }); textarea.data('codemirror-initialized', true); }); } @@ -377,34 +344,21 @@ final class AssetManager var show = false; switch (operator) { - case '==': - show = (currentVal == condValue); - break; - case '!=': - show = (currentVal != condValue); - break; - case 'in': - show = Array.isArray(currentVal) ? currentVal.includes(condValue) : condValue.split(',').includes(currentVal); - break; - case 'not in': - show = Array.isArray(currentVal) ? !currentVal.includes(condValue) : !condValue.split(',').includes(currentVal); - break; + case '==': show = (currentVal == condValue); break; + case '!=': show = (currentVal != condValue); break; + case 'in': show = Array.isArray(currentVal) ? currentVal.includes(condValue) : condValue.split(',').includes(currentVal); break; + case 'not in': show = Array.isArray(currentVal) ? !currentVal.includes(condValue) : !condValue.split(',').includes(currentVal); break; } var container = field.closest('tr'); - if (container.length) { - show ? container.show() : container.hide(); - } else { - show ? field.show() : field.hide(); - } + show ? container.show() : container.hide(); }); } $('body').on('change', 'input, select, textarea', toggleConditionalFields); - toggleConditionalFields(); // Initial check on page load. + toggleConditionalFields(); }); JS; - - // phpcs:enable Generic.Files.LineLength + // phpcs:enable } /** @@ -414,9 +368,8 @@ JS; */ private function getInlineStyles(): string { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix'); - // Nav should remain prefix-less. // phpcs:disable Generic.Files.LineLength return << + */ + protected array $config; + + /** + * Config constructor. + * + * @param array|ConfigInterface $config Initial configuration data. + */ + public function __construct(array|ConfigInterface $config = []) + { + $this->config = $config instanceof ConfigInterface ? + $config->getAll() : + $config; + } + + /** + * {@inheritDoc} + * + * Checks if a key exists using dot notation. + * e.g., 'database.host' + */ + public function has(string $key): bool + { + // If the key exists at the top level, return true immediately. + if (array_key_exists($key, $this->config)) { + return true; + } + + // If no dot is present, it's a simple check. + if (! str_contains($key, '.')) { + return false; + } + + $current = $this->config; + foreach (explode('.', $key) as $segment) { + if (! is_array($current) || ! array_key_exists($segment, $current)) { + return false; + } + $current = $current[$segment]; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function get(string $key, mixed $default = null): mixed + { + // If the key exists at the top level, return it immediately. + if (array_key_exists($key, $this->config)) { + return $this->config[$key]; + } + + // If no dot is present, we know it doesn't exist, return default. + if (! str_contains($key, '.')) { + return $default; + } + + $current = $this->config; + foreach (explode('.', $key) as $segment) { + if (! is_array($current) || ! array_key_exists($segment, $current)) { + return $default; + } + $current = $current[$segment]; + } + + return $current; + } + + /** + * {@inheritDoc} + */ + public function set(string $key, mixed $value): void + { + if (! str_contains($key, '.')) { + $this->config[$key] = $value; + + return; + } + + $keys = explode('.', $key); + $current = &$this->config; + + while (count($keys) > 1) { + $segment = array_shift($keys); + if (! isset($current[$segment]) || ! is_array($current[$segment])) { + $current[$segment] = []; + } + $current = &$current[$segment]; + } + + $current[array_shift($keys)] = $value; + } + + /** + * {@inheritDoc} + */ + public function unset(string $key): void + { + if (! str_contains($key, '.')) { + unset($this->config[$key]); + + return; + } + + $keys = explode('.', $key); + $current = &$this->config; + + while (count($keys) > 1) { + $segment = array_shift($keys); + if (! isset($current[$segment]) || ! is_array($current[$segment])) { + // The path doesn't exist, so there's nothing to unset. + return; + } + $current = &$current[$segment]; + } + + unset($current[array_shift($keys)]); + } + + /** + * {@inheritDoc} + */ + public function setAll(array $config): void + { + $this->config = $config; + } + + /** + * {@inheritdoc} + */ + public function getAll(): array + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function deepMerge(array $partial): void + { + $this->config = array_replace_recursive($this->config, $partial); + } + + /** + * {@inheritDoc} + * + * @phpstan-return array + */ + public function jsonSerialize(): array + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function offsetExists(mixed $offset): bool + { + return $this->has((string)$offset); + } + + /** + * {@inheritDoc} + */ + public function offsetGet(mixed $offset): mixed + { + return $this->get((string)$offset); + } + + /** + * {@inheritDoc} + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->set((string)$offset, $value); + } + + /** + * {@inheritDoc} + */ + public function offsetUnset(mixed $offset): void + { + $this->unset((string)$offset); + } + + /** + * {@inheritDoc} + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->config); + } + + /** + * {@inheritDoc} + */ + public function count(): int + { + return count($this->config); + } +} diff --git a/src/FieldFactory.php b/src/FieldFactory.php index 1f603a4..0646ad2 100644 --- a/src/FieldFactory.php +++ b/src/FieldFactory.php @@ -6,6 +6,7 @@ namespace WPTechnix\WPSettings; use InvalidArgumentException; use WPTechnix\WPSettings\Interfaces\FieldInterface; +use WPTechnix\WPSettings\Interfaces\ConfigInterface; /** * Creates instances of field objects based on their type. @@ -54,20 +55,21 @@ final class FieldFactory /** * Creates a field object based on its type. * - * @param string $type The field type identifier (e.g., 'text', 'toggle'). - * @param array $config The configuration for the field. + * @param string $type The field type identifier (e.g., 'text', 'toggle'). + * @param array|ConfigInterface $config The configuration for the field. + * * @return FieldInterface The instantiated field object. * @throws InvalidArgumentException If the requested field type is not supported. */ - public function create(string $type, array $config): FieldInterface + public function create(string $type, array|ConfigInterface $config): FieldInterface { - if (!isset($this->fieldMap[$type])) { + if (! isset($this->fieldMap[$type])) { throw new InvalidArgumentException("Unsupported field type: {$type}"); } $className = $this->fieldMap[$type]; - return new $className($config); + return new $className(new Config($config)); } /** diff --git a/src/Fields/AbstractField.php b/src/Fields/AbstractField.php index 765e55e..b811db0 100644 --- a/src/Fields/AbstractField.php +++ b/src/Fields/AbstractField.php @@ -5,74 +5,34 @@ declare(strict_types=1); namespace WPTechnix\WPSettings\Fields; use WPTechnix\WPSettings\Interfaces\FieldInterface; +use WPTechnix\WPSettings\Interfaces\ConfigInterface; /** * Provides the basic structure and common functionality for all field types. + * + * @phpstan-type FieldConfig array{ + * id: string, + * name: string, + * label: string, + * description: string, + * default?: mixed, + * options?: array, + * attributes?: array, + * sanitize_callback?: callable, + * validate_callback?: callable, + * conditional?: array{field: string, value: mixed, operator?: string} + * } */ abstract class AbstractField implements FieldInterface { - /** - * The field's configuration properties. - * - * @var array{ - * id: string, - * name: string, - * label: string, - * description: string, - * default?: mixed, - * options?: array, - * attributes?: array, - * sanitize_callback?: callable, - * validate_callback?: callable, - * conditional?: array{field: string, value: mixed, operator?: string} - * } - */ - protected array $config; - /** * AbstractField constructor. * - * @param array{ - * id: string, - * name: string, - * label: string, - * description: string, - * default?: mixed, - * options?: array, - * attributes?: array, - * sanitize_callback?: callable, - * validate_callback?: callable, - * conditional?: array{field: string, value: mixed, operator?: string} - * } $config The field configuration array. + * @param ConfigInterface $config The field's configuration properties. */ - public function __construct(array $config) - { - $this->config = $config; - } - - /** - * Build an HTML attributes string from an array. - * - * This helper method constructs a valid HTML attribute string from an - * associative array, with proper escaping. - * - * @param array $attributes The array of attributes (key => value). - * @return string The generated HTML attributes string. - */ - protected function buildAttributesString(array $attributes): string - { - $attrParts = []; - foreach ($attributes as $key => $value) { - if (is_bool($value)) { - if ($value) { - $attrParts[] = esc_attr($key); - } - } else { - $attrParts[] = sprintf('%s="%s"', esc_attr($key), esc_attr((string) $value)); - } - } - - return implode(' ', $attrParts); + public function __construct( + protected ConfigInterface $config, + ) { } /** @@ -85,15 +45,43 @@ abstract class AbstractField implements FieldInterface */ public function getDefaultValue(): mixed { - if (array_key_exists('default', $this->config)) { - return $this->config['default']; + if ($this->config->has('default')) { + return $this->config->get('default'); } + $arrayValueFields = [MultiSelectField::class]; + // Fallback for fields that might be arrays. - if (in_array(static::class, [MultiSelectField::class], true)) { + if (in_array(static::class, $arrayValueFields, true)) { return []; } return ''; } + + /** + * Build an HTML attributes string from an array. + * + * This helper method constructs a valid HTML attribute string from an + * associative array, with proper escaping. + * + * @param array $attributes The array of attributes (key => value). + * + * @return string The generated HTML attributes string. + */ + protected function buildAttributesString(array $attributes): string + { + $attrParts = []; + foreach ($attributes as $key => $value) { + if (is_bool($value)) { + if ($value) { + $attrParts[] = esc_attr($key); + } + } else { + $attrParts[] = sprintf('%s="%s"', esc_attr($key), esc_attr((string)$value)); + } + } + + return implode(' ', $attrParts); + } } diff --git a/src/Fields/ButtonGroupField.php b/src/Fields/ButtonGroupField.php index 9ecb42f..c3fb409 100644 --- a/src/Fields/ButtonGroupField.php +++ b/src/Fields/ButtonGroupField.php @@ -14,26 +14,26 @@ final class ButtonGroupField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); printf( '', - esc_attr($this->config['name']), - esc_attr($this->config['id']), - esc_attr((string) $value), + esc_attr($this->config->get('name')), + esc_attr($this->config->get('id')), + esc_attr((string)$value), $this->buildAttributesString($attributes) ); printf('
', esc_attr($htmlPrefix)); - $options = $this->config['options'] ?? []; + $options = $this->config->get('options', []); foreach ($options as $optionValue => $optionLabel) { - $activeClass = ((string) $value === (string) $optionValue) ? ' active' : ''; + $activeClass = ((string)$value === (string)$optionValue) ? ' active' : ''; printf( '', esc_attr($htmlPrefix), esc_attr($activeClass), - esc_attr((string) $optionValue), + esc_attr((string)$optionValue), esc_html($optionLabel) ); } @@ -45,10 +45,11 @@ final class ButtonGroupField extends AbstractField */ public function sanitize(mixed $value): string { - $allowedValues = array_keys($this->config['options'] ?? []); - if (in_array((string) $value, $allowedValues, true)) { - return (string) $value; + $allowedValues = array_keys($this->config->get('options', [])); + if (in_array((string)$value, $allowedValues, true)) { + return (string)$value; } + return ''; } } diff --git a/src/Fields/CheckboxField.php b/src/Fields/CheckboxField.php index bbc7445..5193692 100644 --- a/src/Fields/CheckboxField.php +++ b/src/Fields/CheckboxField.php @@ -16,8 +16,8 @@ final class CheckboxField extends AbstractField { printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), checked($value, true, false), $this->buildAttributesString($attributes) ); @@ -36,6 +36,6 @@ final class CheckboxField extends AbstractField */ public function getDefaultValue(): bool { - return (bool) ($this->config['default'] ?? false); + return (bool)($this->config->get('default', false)); } } diff --git a/src/Fields/CodeField.php b/src/Fields/CodeField.php index 2c3089d..f0ade9b 100644 --- a/src/Fields/CodeField.php +++ b/src/Fields/CodeField.php @@ -14,14 +14,14 @@ final class CodeField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $language = $this->config['language'] ?? 'css'; + $language = $this->config->get('language', 'css'); - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); $defaultAttributes = [ - 'rows' => 10, - 'cols' => 50, - 'class' => "large-text {$htmlPrefix}-code-editor", + 'rows' => 10, + 'cols' => 50, + 'class' => "large-text {$htmlPrefix}-code-editor", 'data-language' => $language ]; @@ -29,10 +29,10 @@ final class CodeField extends AbstractField printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), $this->buildAttributesString($mergedAttributes), - esc_textarea((string) $value) + esc_textarea((string)$value) ); } @@ -42,6 +42,6 @@ final class CodeField extends AbstractField public function sanitize(mixed $value): string { // Basic sanitization for code to preserve its structure. - return str_replace(["\x00", "\r\n", "\r"], ['', "\n", "\n"], (string) $value); + return str_replace(["\x00", "\r\n", "\r"], ['', "\n", "\n"], (string)$value); } } diff --git a/src/Fields/ColorField.php b/src/Fields/ColorField.php index 698363e..771ec57 100644 --- a/src/Fields/ColorField.php +++ b/src/Fields/ColorField.php @@ -14,15 +14,15 @@ final class ColorField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); $defaultAttributes = ['class' => "{$htmlPrefix}-color-picker"]; - $mergedAttributes = array_merge($defaultAttributes, $attributes); + $mergedAttributes = array_merge($defaultAttributes, $attributes); printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -32,7 +32,8 @@ final class ColorField extends AbstractField */ public function sanitize(mixed $value): string { - $color = sanitize_hex_color((string) $value); + $color = sanitize_hex_color((string)$value); + return $color ?? $this->getDefaultValue(); } @@ -41,6 +42,6 @@ final class ColorField extends AbstractField */ public function getDefaultValue(): string { - return $this->config['default'] ?? '#000000'; + return $this->config->get('default', '#000000'); } } diff --git a/src/Fields/DateField.php b/src/Fields/DateField.php index f77baa4..7ca3520 100644 --- a/src/Fields/DateField.php +++ b/src/Fields/DateField.php @@ -14,15 +14,15 @@ final class DateField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); $defaultAttributes = ['class' => "regular-text {$htmlPrefix}-flatpickr-date", 'readonly' => 'readonly']; - $mergedAttributes = array_merge($defaultAttributes, $attributes); + $mergedAttributes = array_merge($defaultAttributes, $attributes); printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -32,6 +32,6 @@ final class DateField extends AbstractField */ public function sanitize(mixed $value): string { - return sanitize_text_field((string) $value); + return sanitize_text_field((string)$value); } } diff --git a/src/Fields/DateTimeField.php b/src/Fields/DateTimeField.php index eae8253..3305226 100644 --- a/src/Fields/DateTimeField.php +++ b/src/Fields/DateTimeField.php @@ -14,15 +14,15 @@ final class DateTimeField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); $defaultAttributes = ['class' => "regular-text {$htmlPrefix}-flatpickr-datetime", 'readonly' => 'readonly']; - $mergedAttributes = array_merge($defaultAttributes, $attributes); + $mergedAttributes = array_merge($defaultAttributes, $attributes); printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -32,6 +32,6 @@ final class DateTimeField extends AbstractField */ public function sanitize(mixed $value): string { - return sanitize_text_field((string) $value); + return sanitize_text_field((string)$value); } } diff --git a/src/Fields/DescriptionField.php b/src/Fields/DescriptionField.php index dbd2a7c..5027e39 100644 --- a/src/Fields/DescriptionField.php +++ b/src/Fields/DescriptionField.php @@ -16,8 +16,8 @@ final class DescriptionField extends AbstractField { // This field only displays its description, which is handled by the renderer. // It has no input element. - if (!empty($this->config['description'])) { - echo '
' . wp_kses_post($this->config['description']) . '
'; + if (! empty($this->config->get('description'))) { + echo '
' . wp_kses_post($this->config->get('description')) . '
'; } } diff --git a/src/Fields/EmailField.php b/src/Fields/EmailField.php index 6acab3b..28a0c67 100644 --- a/src/Fields/EmailField.php +++ b/src/Fields/EmailField.php @@ -20,9 +20,9 @@ final class EmailField extends AbstractField printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -32,6 +32,6 @@ final class EmailField extends AbstractField */ public function sanitize(mixed $value): string { - return sanitize_email((string) $value); + return sanitize_email((string)$value); } } diff --git a/src/Fields/MediaField.php b/src/Fields/MediaField.php index bb57c77..889ef93 100644 --- a/src/Fields/MediaField.php +++ b/src/Fields/MediaField.php @@ -14,9 +14,9 @@ final class MediaField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; - $mediaUrl = ''; - $mediaId = absint($value); + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); + $mediaUrl = ''; + $mediaId = absint($value); if ($mediaId > 0) { $mediaUrl = wp_get_attachment_url($mediaId); } @@ -28,35 +28,35 @@ final class MediaField extends AbstractField printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($attributes) ); printf( '', esc_attr($htmlPrefix), - esc_attr($this->config['id']), + esc_attr($this->config->get('id')), esc_html__('Select Media', 'default') ); - if (!empty($mediaUrl)) { + if (! empty($mediaUrl)) { printf( ' ', esc_attr($htmlPrefix), - esc_attr($this->config['id']), + esc_attr($this->config->get('id')), esc_html__('Remove', 'default') ); } printf('
', esc_attr($htmlPrefix)); - if (!empty($mediaUrl) && wp_attachment_is_image($mediaId)) { + if (! empty($mediaUrl) && wp_attachment_is_image($mediaId)) { printf( - '', + '', esc_url($mediaUrl) ); - } elseif (!empty($mediaUrl)) { - $file = (string)get_attached_file($mediaId); + } elseif (! empty($mediaUrl)) { + $file = (string)get_attached_file($mediaId); $fileName = ! empty($file) ? basename($file) : ''; printf('

File: %s

', esc_html($fileName)); } @@ -76,6 +76,6 @@ final class MediaField extends AbstractField */ public function getDefaultValue(): int { - return (int) ($this->config['default'] ?? 0); + return (int)($this->config->get('default', 0)); } } diff --git a/src/Fields/MultiSelectField.php b/src/Fields/MultiSelectField.php index b178ee6..6697cef 100644 --- a/src/Fields/MultiSelectField.php +++ b/src/Fields/MultiSelectField.php @@ -14,24 +14,24 @@ final class MultiSelectField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); $defaultAttributes = ['multiple' => 'multiple', 'class' => "{$htmlPrefix}-select2-field"]; - $mergedAttributes = array_merge($defaultAttributes, $attributes); - $selectedValues = is_array($value) ? array_map('strval', $value) : []; + $mergedAttributes = array_merge($defaultAttributes, $attributes); + $selectedValues = is_array($value) ? array_map('strval', $value) : []; printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -30,11 +30,12 @@ final class NumberField extends AbstractField */ public function sanitize(mixed $value): int|float { - if (!is_numeric($value)) { + if (! is_numeric($value)) { return 0; } $numericValue = $value + 0; // Cast to number - return is_float($numericValue) ? (float) $value : (int) $value; + + return is_float($numericValue) ? (float)$value : (int)$value; } /** @@ -42,6 +43,6 @@ final class NumberField extends AbstractField */ public function getDefaultValue(): int { - return (int) ($this->config['default'] ?? 0); + return (int)($this->config->get('default', 0)); } } diff --git a/src/Fields/PasswordField.php b/src/Fields/PasswordField.php index 1fd9b15..56c7ba1 100644 --- a/src/Fields/PasswordField.php +++ b/src/Fields/PasswordField.php @@ -15,12 +15,12 @@ final class PasswordField extends AbstractField public function render(mixed $value, array $attributes): void { $defaultAttributes = ['class' => 'regular-text']; - $mergedAttributes = array_merge($defaultAttributes, $attributes); + $mergedAttributes = array_merge($defaultAttributes, $attributes); printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -32,6 +32,6 @@ final class PasswordField extends AbstractField { // Passwords should not be altered during sanitization beyond // basic string conversion. - return (string) $value; + return (string)$value; } } diff --git a/src/Fields/RadioField.php b/src/Fields/RadioField.php index 1459e9d..ff4cb23 100644 --- a/src/Fields/RadioField.php +++ b/src/Fields/RadioField.php @@ -14,20 +14,23 @@ final class RadioField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $options = $this->config['options'] ?? []; + $options = $this->config->get('options', []); foreach ($options as $optionValue => $optionLabel) { - $radioId = $this->config['id'] . '_' . $optionValue; + $radioId = $this->config->get('id') . '_' . sanitize_key($optionValue); + + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings') ?? ''; printf( - '', esc_attr($radioId), + $htmlPrefix, esc_attr($radioId), - esc_attr($this->config['name']), - esc_attr((string) $optionValue), - checked((string) $value, (string) $optionValue, false), + esc_attr($this->config->get('name')), + esc_attr((string)$optionValue), + checked((string)$value, (string)$optionValue, false), $this->buildAttributesString($attributes), esc_html($optionLabel) ); @@ -39,10 +42,11 @@ final class RadioField extends AbstractField */ public function sanitize(mixed $value): string { - $allowedValues = array_keys($this->config['options'] ?? []); - if (in_array((string) $value, $allowedValues, true)) { - return (string) $value; + $allowedValues = array_keys($this->config->get('options', [])); + if (in_array((string)$value, $allowedValues, true)) { + return (string)$value; } + return ''; } } diff --git a/src/Fields/RangeField.php b/src/Fields/RangeField.php index 5e531dc..5f87692 100644 --- a/src/Fields/RangeField.php +++ b/src/Fields/RangeField.php @@ -14,25 +14,25 @@ final class RangeField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); $defaultAttributes = ['min' => 0, 'max' => 100, 'step' => 1, 'class' => "{$htmlPrefix}-enhanced-range-slider"]; - $mergedAttributes = array_merge($defaultAttributes, $attributes); - $min = (int) ($mergedAttributes['min'] ?? 0); - $max = (int) ($mergedAttributes['max'] ?? 100); - $currentValue = $value ?? $min; + $mergedAttributes = array_merge($defaultAttributes, $attributes); + $min = (int)($mergedAttributes['min'] ?? 0); + $max = (int)($mergedAttributes['max'] ?? 100); + $currentValue = $value ?? $min; printf('
', esc_attr($htmlPrefix)); printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $currentValue), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$currentValue), $this->buildAttributesString($mergedAttributes) ); printf( '', esc_attr($htmlPrefix), - esc_attr((string) $currentValue), + esc_attr((string)$currentValue), $min, $max ); @@ -44,11 +44,12 @@ final class RangeField extends AbstractField */ public function sanitize(mixed $value): int|float { - if (!is_numeric($value)) { + if (! is_numeric($value)) { return 0; } $numericValue = $value + 0; - return is_float($numericValue) ? (float) $value : (int) $value; + + return is_float($numericValue) ? (float)$value : (int)$value; } /** @@ -56,6 +57,6 @@ final class RangeField extends AbstractField */ public function getDefaultValue(): int { - return (int) ($this->config['default'] ?? 0); + return (int)($this->config->get('default', 0)); } } diff --git a/src/Fields/SelectField.php b/src/Fields/SelectField.php index 523bb3e..a30368e 100644 --- a/src/Fields/SelectField.php +++ b/src/Fields/SelectField.php @@ -14,23 +14,31 @@ final class SelectField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); $defaultAttributes = ['class' => "{$htmlPrefix}-select2-field"]; - $mergedAttributes = array_merge($defaultAttributes, $attributes); + $mergedAttributes = array_merge($defaultAttributes, $attributes); + + $options = $this->config->get('options', []); + + if (isset($mergedAttributes['data-placeholder'])) { + $options = [ + '' => esc_attr((string)$mergedAttributes['data-placeholder']), + ...$options + ]; + } printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -32,6 +32,6 @@ final class TextField extends AbstractField */ public function sanitize(mixed $value): string { - return sanitize_text_field((string) $value); + return sanitize_text_field((string)$value); } } diff --git a/src/Fields/TextareaField.php b/src/Fields/TextareaField.php index a6cded6..b249b11 100644 --- a/src/Fields/TextareaField.php +++ b/src/Fields/TextareaField.php @@ -15,13 +15,13 @@ final class TextareaField extends AbstractField public function render(mixed $value, array $attributes): void { $defaultAttributes = ['rows' => 5, 'cols' => 50, 'class' => 'large-text']; - $mergedAttributes = array_merge($defaultAttributes, $attributes); + $mergedAttributes = array_merge($defaultAttributes, $attributes); printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), $this->buildAttributesString($mergedAttributes), - esc_textarea((string) $value) + esc_textarea((string)$value) ); } @@ -30,6 +30,6 @@ final class TextareaField extends AbstractField */ public function sanitize(mixed $value): string { - return sanitize_textarea_field((string) $value); + return sanitize_textarea_field((string)$value); } } diff --git a/src/Fields/TimeField.php b/src/Fields/TimeField.php index 1e2fb03..4362972 100644 --- a/src/Fields/TimeField.php +++ b/src/Fields/TimeField.php @@ -14,8 +14,10 @@ final class TimeField extends AbstractField */ public function render(mixed $value, array $attributes): void { + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); + $defaultAttributes = [ - 'class' => 'regular-text flatpickr-time', + 'class' => "regular-text {$htmlPrefix}-flatpickr-time", 'readonly' => 'readonly' ]; @@ -23,9 +25,9 @@ final class TimeField extends AbstractField printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -35,6 +37,6 @@ final class TimeField extends AbstractField */ public function sanitize(mixed $value): string { - return sanitize_text_field((string) $value); + return sanitize_text_field((string)$value); } } diff --git a/src/Fields/ToggleField.php b/src/Fields/ToggleField.php index 2186c83..c9f39e8 100644 --- a/src/Fields/ToggleField.php +++ b/src/Fields/ToggleField.php @@ -14,7 +14,7 @@ final class ToggleField extends AbstractField */ public function render(mixed $value, array $attributes): void { - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; + $htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings'); printf( '', esc_attr($htmlPrefix), - esc_attr($this->config['id']), - esc_attr($this->config['name']), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), checked($value, true, false), $this->buildAttributesString($attributes), esc_attr($htmlPrefix) @@ -43,6 +43,6 @@ final class ToggleField extends AbstractField */ public function getDefaultValue(): bool { - return (bool) ($this->config['default'] ?? false); + return (bool)($this->config->get('default', false)); } } diff --git a/src/Fields/UrlField.php b/src/Fields/UrlField.php index c4cff7f..9b60458 100644 --- a/src/Fields/UrlField.php +++ b/src/Fields/UrlField.php @@ -23,9 +23,9 @@ final class UrlField extends AbstractField printf( '', - esc_attr($this->config['id']), - esc_attr($this->config['name']), - esc_attr((string) $value), + esc_attr($this->config->get('id')), + esc_attr($this->config->get('name')), + esc_attr((string)$value), $this->buildAttributesString($mergedAttributes) ); } @@ -35,6 +35,6 @@ final class UrlField extends AbstractField */ public function sanitize(mixed $value): string { - return esc_url_raw((string) $value); + return esc_url_raw((string)$value); } } diff --git a/src/Interfaces/ConfigInterface.php b/src/Interfaces/ConfigInterface.php new file mode 100644 index 0000000..578e6be --- /dev/null +++ b/src/Interfaces/ConfigInterface.php @@ -0,0 +1,84 @@ + + * @extends ArrayAccess + */ +interface ConfigInterface extends + ArrayAccess, + JsonSerializable, + IteratorAggregate, + Countable +{ + /** + * Check if the given key exists in config. + * Supports dot notation for nested keys. + * + * @param string $key The key to check (dot notation supported). + * + * @return bool True if exists, false otherwise. + */ + public function has(string $key): bool; + + /** + * Get the value of given config key. + * Supports dot notation for nested keys. + * + * @param string $key Config key (dot notation supported). + * @param mixed $default Default value as fallback. + * + * @return mixed The value of key. + */ + public function get(string $key, mixed $default = null): mixed; + + /** + * Get all config values. + * + * @return array + */ + public function getAll(): array; + + /** + * Set the value of given config key. + * Supports dot notation for nested keys. + * + * @param string $key The config key to update (dot notation supported). + * @param mixed $value The updated config value. + */ + public function set(string $key, mixed $value): void; + + /** + * Unset the given config key. + * Supports dot notation for nested keys. + * + * @param string $key The config key to unset (dot notation supported). + */ + public function unset(string $key): void; + + /** + * Updates the whole config array. + * + * @param array $config Settings Configuration. + */ + public function setAll(array $config): void; + + /** + * Merge the config recursively. + * + * @param array $partial Settings Configuration. + */ + public function deepMerge(array $partial): void; +} diff --git a/src/Interfaces/FieldInterface.php b/src/Interfaces/FieldInterface.php index 90e47ac..037d7d2 100644 --- a/src/Interfaces/FieldInterface.php +++ b/src/Interfaces/FieldInterface.php @@ -14,7 +14,7 @@ interface FieldInterface * * This method is responsible for echoing the complete HTML for the form element. * - * @param mixed $value The current value of the field. + * @param mixed $value The current value of the field. * @param array $attributes Additional HTML attributes for the field. */ public function render(mixed $value, array $attributes): void; @@ -26,6 +26,7 @@ interface FieldInterface * before being persisted to the database. * * @param mixed $value The raw input value to be sanitized. + * * @return mixed The sanitized value. */ public function sanitize(mixed $value): mixed; diff --git a/src/Interfaces/SettingsInterface.php b/src/Interfaces/SettingsInterface.php index 586b223..25b3ebe 100644 --- a/src/Interfaces/SettingsInterface.php +++ b/src/Interfaces/SettingsInterface.php @@ -5,79 +5,118 @@ declare(strict_types=1); namespace WPTechnix\WPSettings\Interfaces; /** - * Defines the contract for a settings page builder. + * Defines the public contract for a settings page builder. */ interface SettingsInterface { /** - * Sets or overrides the main page title. + * Sets the main title of the settings page (the `

` tag). * - * @param string $pageTitle The new title for the settings page. - * @return static + * @param string $pageTitle The main title for the settings page. + * + * @return static Provides a fluent interface. */ public function setPageTitle(string $pageTitle): static; /** - * Sets or overrides the menu title. + * Sets the title displayed in the WordPress admin menu. * - * @param string $menuTitle The new title for the admin menu item. - * @return static + * @param string $menuTitle The title for the admin menu item. + * + * @return static Provides a fluent interface. */ public function setMenuTitle(string $menuTitle): static; /** - * Adds a tab for organizing settings sections. + * Sets the required capability to view and save the settings page. * - * @param string $id The unique identifier for the tab. - * @param string $title The text to display on the tab. - * @param string $icon Optional. A Dashicon class to display next to the title. - * @return static + * @param string $capability The WordPress capability string (e.g., 'manage_options'). + * + * @return static Provides a fluent interface. + */ + public function setCapability(string $capability): static; + + /** + * Sets the parent menu page slug under which this settings page will appear. + * + * @param string $parentSlug The slug of the parent menu (e.g., 'options-general.php', 'themes.php'). + * + * @return static Provides a fluent interface. + */ + public function setParentSlug(string $parentSlug): static; + + /** + * Adds a navigation tab to the settings page. + * This automatically enables the tabbed interface. + * + * @param string $id A unique identifier for the tab. + * @param string $title The visible title of the tab. + * @param string $icon (Optional) A Dashicons class for an icon (e.g., 'dashicons-admin-generic'). + * + * @return static Provides a fluent interface. */ public function addTab(string $id, string $title, string $icon = ''): static; /** - * Adds a settings section to the page. + * Adds a settings section to group related fields. * - * @param string $id The unique identifier for the section. - * @param string $title The title displayed for the section. - * @param string $description Optional. A description displayed below the section title. - * @param string $tabId Optional. The ID of the tab this section should appear under. - * @return static + * @param string $id A unique identifier for the section. + * @param string $title The visible title of the section (an `

` tag). + * @param string $description (Optional) A short description displayed below the section title. + * @param string $tabId (Optional) The ID of the tab this section belongs to. Required for tabbed interfaces. + * + * @return static Provides a fluent interface. */ public function addSection(string $id, string $title, string $description = '', string $tabId = ''): static; /** - * Adds a field to a section. + * Adds a setting field to a section. * - * @param string $id The unique identifier for the field. - * @param string $sectionId The ID of the section this field belongs to. - * @param string $type The field type (e.g., 'text', 'toggle', 'code'). - * @param string $label The label displayed for the field. - * @param array $args Optional. An array of additional arguments. - * @return static + * @param string $id A unique identifier for the field, used as the key in the options array. + * @param string $sectionId The ID of the section this field belongs to. + * @param string $type The type of field (e.g., 'text', 'toggle', 'select'). + * @param string $label The label displayed for the field. + * @param array $args (Optional) Additional arguments for the field, such as 'default', + * 'description', 'options', 'attributes', etc. + * + * @return static Provides a fluent interface. */ - public function addField(string $id, string $sectionId, string $type, string $label, array $args = []): static; + public function addField( + string $id, + string $sectionId, + string $type, + string $label, + array $args = [] + ): static; /** - * Initializes the settings page and hooks all components into WordPress. + * Hooks the settings framework into the appropriate WordPress actions. + * This method must be called to activate the settings page and make it appear. * - * This method must be called after all configuration is complete. + * @return void */ public function init(): void; /** - * Gets the WordPress option name where settings are stored. + * Gets a saved option value from the database. * - * @return string The option name. - */ - public function getOptionName(): string; - - /** - * Retrieves a setting's value for this settings page. + * This is the primary method for retrieving a field's current value for rendering. + * It intelligently falls back to the field's configured 'default' value if no + * saved value exists in the database. * - * @param string $key The unique key of the setting to retrieve. - * @param mixed $default A fallback value to return if the setting is not found. - * @return mixed The stored setting value, or the default if not found. + * @param string $key The specific option key (field ID) to retrieve. + * @param mixed|null $default A final fallback value if no saved option or field default is found. + * + * @return mixed The saved value, the field's default value, or the provided default. */ public function get(string $key, mixed $default = null): mixed; + + /** + * Gets the main option name used in the database. + * + * This is the top-level key for the array stored in the `wp_options` table. + * + * @return string The name of the option array. + */ + public function getOptionName(): string; } diff --git a/src/PageRenderer.php b/src/PageRenderer.php index e6d4479..8a9cf23 100644 --- a/src/PageRenderer.php +++ b/src/PageRenderer.php @@ -4,41 +4,26 @@ declare(strict_types=1); namespace WPTechnix\WPSettings; +use InvalidArgumentException; +use WPTechnix\WPSettings\Interfaces\ConfigInterface; + /** * Handles all HTML output for the settings page. - * - * This class isolates the presentation logic from the business logic. It is - * responsible for rendering the main page wrapper, navigation tabs, and the - * settings form itself. - * - * @noinspection HtmlUnknownAttribute */ final class PageRenderer { - /** - * The main settings configuration array. - * - * @var array - */ - private array $config; - - /** - * The FieldFactory instance for creating field objects. - * - * @var FieldFactory - */ - private FieldFactory $fieldFactory; - /** * PageRenderer constructor. * - * @param array $config The main settings configuration. - * @param FieldFactory $fieldFactory The factory for creating field objects. + * @param ConfigInterface $config The shared configuration object. + * @param FieldFactory $fieldFactory The factory for creating field objects. + * @param Settings $settings The main Settings instance to access get(). */ - public function __construct(array $config, FieldFactory $fieldFactory) - { - $this->config = $config; - $this->fieldFactory = $fieldFactory; + public function __construct( + protected ConfigInterface $config, + protected FieldFactory $fieldFactory, + protected Settings $settings + ) { } /** @@ -49,28 +34,39 @@ final class PageRenderer */ public function renderPage(): void { - if (!empty($this->config['capability']) && !current_user_can($this->config['capability'])) { - wp_die(esc_html($this->config['labels']['noPermission'] ?? 'Permission denied.')); + if (! current_user_can($this->config->get('capability'))) { + wp_die( + esc_html($this->config->get('labels.noPermission', 'Permission Denied.')) + ); } - $activeTab = $this->getActiveTab(); ?>
-

config['pageTitle']); ?>

+

config->get('pageTitle')); ?>

- config['useTabs']) && !empty($this->config['tabs'])) : ?> - renderTabs($activeTab); ?> - + + + config->get('useTabs') && $this->config->has('tabs')) : ?> + renderTabs(); ?> +
config['optionGroup']); + settings_fields($this->config->get('optionGroup')); - if (!empty($this->config['useTabs']) && !empty($activeTab)) { - $this->renderSectionsForTab($activeTab); + if (! empty($this->config->get('useTabs'))) { + $activeTab = $this->config->get('activeTab'); + printf('', $activeTab); + $this->renderSections(); } else { - do_settings_sections($this->config['pageSlug']); + do_settings_sections($this->config->get('pageSlug')); } submit_button(); @@ -87,90 +83,104 @@ final class PageRenderer * that the rendered field's HTML has the correct CSS classes. * * @param array $args Arguments passed from `add_settings_field`. - * @return void */ public function renderField(array $args): void { - $fieldId = $args['id'] ?? ''; - $fieldConfig = $this->config['fields'][$fieldId] ?? null; - - if (empty($fieldConfig)) { - return; // Safety check. + $fieldId = $args['id'] ?? null; + if (empty($fieldId)) { + return; } - $htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings'; - $fieldConfig['htmlPrefix'] = $htmlPrefix; // Pass the prefix so that later we can use in fields. + $fieldConfig = $this->config->get("fields.{$fieldId}"); - $options = get_option($this->config['optionName'], []); - $value = $options[$fieldId] ?? $fieldConfig['default'] ?? null; + if (empty($fieldConfig) || ! is_array($fieldConfig)) { + return; + } + + $htmlPrefix = $this->config->get('htmlPrefix'); + + $fieldConfig['htmlPrefix'] = $htmlPrefix; try { - $field = $this->fieldFactory->create($fieldConfig['type'], $fieldConfig); + $fieldObject = $this->fieldFactory->create($fieldConfig['type'], $fieldConfig); + + $value = $this->settings->get($fieldId, $fieldObject->getDefaultValue()); + + $fieldAttributes = $fieldConfig['attributes'] ?? []; + if (! is_array($fieldAttributes)) { + $fieldAttributes = []; + } $conditionalAttr = ''; - if (!empty($fieldConfig['conditional'])) { - $cond = $fieldConfig['conditional']; + if (! empty($fieldConfig['conditional'])) { + $cond = $fieldConfig['conditional']; $conditionalAttr = sprintf( 'data-conditional="%s" data-conditional-value="%s" data-conditional-operator="%s"', esc_attr($cond['field'] ?? ''), - esc_attr((string) ($cond['value'] ?? '')), + esc_attr((string)($cond['value'] ?? '')), esc_attr($cond['operator'] ?? '==') ); } printf('
', esc_attr($htmlPrefix), $conditionalAttr); - $field->render($value, $fieldConfig['attributes'] ?? []); - if (!empty($fieldConfig['description']) && 'description' !== $fieldConfig['type']) { + $fieldObject->render($value, $fieldAttributes); + + if (! empty($fieldConfig['description']) && 'description' !== $fieldConfig['type']) { echo '

' . wp_kses_post($fieldConfig['description']) . '

'; } echo '
'; - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { echo '

Error: ' . esc_html($e->getMessage()) . '

'; } } /** * Renders the navigation tabs for the settings page. - * - * @param string $activeTab The slug of the currently active tab. */ - private function renderTabs(string $activeTab): void + private function renderTabs(): void { - echo '