From ad2b1d9a254f3058150d395653dfd4a451e7c73d Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Wed, 17 Jun 2026 05:55:35 +0000 Subject: [PATCH] fix: resolve PR_Core_Settings_Renderer undefined constant fatal (v0.7.1) All self::CONST refs in renderer pointed at constants that only exist on PR_Core_Settings. PR #22 split the renderer into its own class but omitted the constants. Promote 6 option-key private consts to public on PR_Core_Settings; update renderer to reference them as PR_Core_Settings::*. Closes #(no issue). Regression introduced in v0.7.0 (#22). Agent-Session: 2026-06-17-prcore-500fix --- CHANGELOG.md | 5 + .../admin/class-pr-core-settings-renderer.php | 24 +-- includes/admin/class-pr-core-settings.php | 12 +- peptide-repo-core.php | 4 +- tests/unit/SettingsRendererTest.php | 204 ++++++++++++++++++ 5 files changed, 229 insertions(+), 20 deletions(-) create mode 100644 tests/unit/SettingsRendererTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e82bb74..3d2eb59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to Peptide Repo Core are documented here. Format: [Semantic Versioning](https://semver.org/). +## [0.7.1] - 2026-06-17 + +### Fixed +- **Critical**: Settings page PHP Fatal `Undefined constant PR_Core_Settings_Renderer::OPTION_GROUP` (and 6 other `self::CONST` references). Root cause: PR #22 split the renderer into its own class but left all constants defined only on `PR_Core_Settings`. Fix: promote the 6 option-key private constants to `public const` on `PR_Core_Settings`; update renderer to reference them via `PR_Core_Settings::*`. Regression introduced in v0.7.0. + ## [0.7.0] - 2026-06-16 ### Changed diff --git a/includes/admin/class-pr-core-settings-renderer.php b/includes/admin/class-pr-core-settings-renderer.php index 2056e20..8c4b43c 100644 --- a/includes/admin/class-pr-core-settings-renderer.php +++ b/includes/admin/class-pr-core-settings-renderer.php @@ -35,8 +35,8 @@ class PR_Core_Settings_Renderer {

@@ -50,10 +50,10 @@ class PR_Core_Settings_Renderer { * @return void */ public static function render_enabled_field(): void { - $enabled = (bool) get_option( self::RELATED_ENABLED, true ); + $enabled = (bool) get_option( PR_Core_Settings::RELATED_ENABLED, true ); printf( ' %s', - esc_attr( self::RELATED_ENABLED ), + esc_attr( PR_Core_Settings::RELATED_ENABLED ), checked( $enabled, true, false ), esc_html__( 'Show related monographs on single peptide pages', 'peptide-repo-core' ) ); @@ -67,8 +67,8 @@ class PR_Core_Settings_Renderer { public static function render_limit_field(): void { printf( '', - esc_attr( self::RELATED_LIMIT ), - absint( get_option( self::RELATED_LIMIT, 3 ) ) + esc_attr( PR_Core_Settings::RELATED_LIMIT ), + absint( get_option( PR_Core_Settings::RELATED_LIMIT, 3 ) ) ); echo '

' . esc_html__( 'Number of articles to display (1-6). Default: 3.', 'peptide-repo-core' ) . '

'; } @@ -88,8 +88,8 @@ class PR_Core_Settings_Renderer { * @return void */ public static function render_cadence_field(): void { - $current = get_option( self::SCAN_CADENCE, 'weekly' ); - echo ''; foreach ( array( 'daily' => 'Daily', 'weekly' => 'Weekly', @@ -106,7 +106,7 @@ class PR_Core_Settings_Renderer { * @return void */ public static function render_threshold_field(): void { - printf( '', esc_attr( self::DEFAULT_THRESHOLD ), absint( get_option( self::DEFAULT_THRESHOLD, 180 ) ) ); + printf( '', esc_attr( PR_Core_Settings::DEFAULT_THRESHOLD ), absint( get_option( PR_Core_Settings::DEFAULT_THRESHOLD, 180 ) ) ); } /** @@ -115,7 +115,7 @@ class PR_Core_Settings_Renderer { * @return void */ public static function render_high_velocity_field(): void { - printf( '', esc_attr( self::HIGH_VELOCITY_THRESHOLD ), absint( get_option( self::HIGH_VELOCITY_THRESHOLD, 60 ) ) ); + printf( '', esc_attr( PR_Core_Settings::HIGH_VELOCITY_THRESHOLD ), absint( get_option( PR_Core_Settings::HIGH_VELOCITY_THRESHOLD, 60 ) ) ); } /** @@ -126,8 +126,8 @@ class PR_Core_Settings_Renderer { public static function render_email_field(): void { printf( '', - esc_attr( self::VERIFICATION_EMAIL ), - esc_attr( (string) get_option( self::VERIFICATION_EMAIL, '' ) ) + esc_attr( PR_Core_Settings::VERIFICATION_EMAIL ), + esc_attr( (string) get_option( PR_Core_Settings::VERIFICATION_EMAIL, '' ) ) ); echo '

' . esc_html__( 'Comma-separated. Leave blank to disable email digests.', 'peptide-repo-core' ) . '

'; } diff --git a/includes/admin/class-pr-core-settings.php b/includes/admin/class-pr-core-settings.php index 6a6e2a5..5520cfb 100644 --- a/includes/admin/class-pr-core-settings.php +++ b/includes/admin/class-pr-core-settings.php @@ -23,22 +23,22 @@ class PR_Core_Settings { public const OPTION_GROUP = 'pr_core_settings'; /** @var string Option key: enable/disable related articles. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort - private const RELATED_ENABLED = 'pr_core_related_posts_enabled'; + public const RELATED_ENABLED = 'pr_core_related_posts_enabled'; /** @var string Option key: related articles limit (1-6). */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort - private const RELATED_LIMIT = 'pr_core_related_posts_limit'; + public const RELATED_LIMIT = 'pr_core_related_posts_limit'; /** @var string Option key: WP-cron scan cadence. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort - private const SCAN_CADENCE = 'pr_core_scan_cadence'; + public const SCAN_CADENCE = 'pr_core_scan_cadence'; /** @var string Option key: default staleness threshold in days. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort - private const DEFAULT_THRESHOLD = 'pr_core_default_threshold'; + public const DEFAULT_THRESHOLD = 'pr_core_default_threshold'; /** @var string Option key: high-velocity staleness threshold in days. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort - private const HIGH_VELOCITY_THRESHOLD = 'pr_core_high_velocity_threshold'; + public const HIGH_VELOCITY_THRESHOLD = 'pr_core_high_velocity_threshold'; /** @var string Option key: comma-separated notification email list. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort - private const VERIFICATION_EMAIL = 'pr_core_verification_email'; + public const VERIFICATION_EMAIL = 'pr_core_verification_email'; /** * Register the settings submenu page under Peptides. diff --git a/peptide-repo-core.php b/peptide-repo-core.php index 434e259..11d7b9a 100644 --- a/peptide-repo-core.php +++ b/peptide-repo-core.php @@ -9,7 +9,7 @@ * Plugin Name: Peptide Repo Core * Plugin URI: https://peptiderepo.com * Description: Canonical peptide schema — shared data layer for the peptiderepo.com ecosystem. Provides the peptide CPT, dosing rows, legal status cells, AI candidate queue, disclaimer component, and JSON-LD output. - * Version: 0.7.0 + * Version: 0.7.1 * Author: peptiderepo * Author URI: https://peptiderepo.com * License: GPL-2.0-or-later @@ -29,7 +29,7 @@ if ( ! defined( 'ABSPATH' ) ) { /* ── Constants ────────────────────────────────────────────────────────── */ -define( 'PR_CORE_VERSION', '0.7.0' ); +define( 'PR_CORE_VERSION', '0.7.1' ); define( 'PR_CORE_PLUGIN_FILE', __FILE__ ); define( 'PR_CORE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PR_CORE_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); diff --git a/tests/unit/SettingsRendererTest.php b/tests/unit/SettingsRendererTest.php new file mode 100644 index 0000000..075f889 --- /dev/null +++ b/tests/unit/SettingsRendererTest.php @@ -0,0 +1,204 @@ + [], + 'existing_taxonomies' => [], + 'registered_post_types' => [], + 'registered_taxonomies' => [], + 'registered_meta' => [], + 'added_actions' => [], + 'added_filters' => [], + ]; + require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php'; + require_once PR_CORE_PLUGIN_DIR . 'includes/admin/class-pr-core-settings.php'; + require_once PR_CORE_PLUGIN_DIR . 'includes/admin/class-pr-core-settings-renderer.php'; + } + + // ── Cross-class constant accessibility ────────────────────────────────. + + /** + * OPTION_GROUP must be a non-empty string on PR_Core_Settings. + * Renderer calls PR_Core_Settings::OPTION_GROUP — verifies it resolves. + */ + public function test_option_group_constant_accessible_on_settings(): void { + $this->assertNotEmpty( PR_Core_Settings::OPTION_GROUP ); + $this->assertIsString( PR_Core_Settings::OPTION_GROUP ); + } + + public function test_related_enabled_constant_accessible(): void { + $this->assertNotEmpty( PR_Core_Settings::RELATED_ENABLED ); + } + + public function test_related_limit_constant_accessible(): void { + $this->assertNotEmpty( PR_Core_Settings::RELATED_LIMIT ); + } + + public function test_scan_cadence_constant_accessible(): void { + $this->assertNotEmpty( PR_Core_Settings::SCAN_CADENCE ); + } + + public function test_default_threshold_constant_accessible(): void { + $this->assertNotEmpty( PR_Core_Settings::DEFAULT_THRESHOLD ); + } + + public function test_high_velocity_threshold_constant_accessible(): void { + $this->assertNotEmpty( PR_Core_Settings::HIGH_VELOCITY_THRESHOLD ); + } + + public function test_verification_email_constant_accessible(): void { + // Value is intentionally a non-empty option key string. + $this->assertIsString( PR_Core_Settings::VERIFICATION_EMAIL ); + $this->assertNotEmpty( PR_Core_Settings::VERIFICATION_EMAIL ); + } + + // ── Renderer render_*_field: no fatal on constant resolution ─────────. + + /** + * render_enabled_field() calls PR_Core_Settings::RELATED_ENABLED twice. + * Before the fix this fatalled. Capture output to confirm no crash. + */ + public function test_render_enabled_field_no_fatal(): void { + ob_start(); + PR_Core_Settings_Renderer::render_enabled_field(); + $output = ob_get_clean(); + $this->assertStringContainsString( 'type="checkbox"', $output ); + } + + /** + * render_limit_field() calls PR_Core_Settings::RELATED_LIMIT twice. + */ + public function test_render_limit_field_no_fatal(): void { + ob_start(); + PR_Core_Settings_Renderer::render_limit_field(); + $output = ob_get_clean(); + $this->assertStringContainsString( 'type="number"', $output ); + } + + /** + * render_cadence_field() calls PR_Core_Settings::SCAN_CADENCE twice. + */ + public function test_render_cadence_field_no_fatal(): void { + ob_start(); + PR_Core_Settings_Renderer::render_cadence_field(); + $output = ob_get_clean(); + $this->assertStringContainsString( 'assertStringContainsString( 'type="number"', $output ); + } + + /** + * render_high_velocity_field() calls PR_Core_Settings::HIGH_VELOCITY_THRESHOLD twice. + */ + public function test_render_high_velocity_field_no_fatal(): void { + ob_start(); + PR_Core_Settings_Renderer::render_high_velocity_field(); + $output = ob_get_clean(); + $this->assertStringContainsString( 'type="number"', $output ); + } + + /** + * render_email_field() calls PR_Core_Settings::VERIFICATION_EMAIL twice. + */ + public function test_render_email_field_no_fatal(): void { + ob_start(); + PR_Core_Settings_Renderer::render_email_field(); + $output = ob_get_clean(); + $this->assertStringContainsString( 'type="text"', $output ); + } + + // ── Constant value contracts ──────────────────────────────────────────. + + public function test_option_group_value(): void { + $this->assertSame( 'pr_core_settings', PR_Core_Settings::OPTION_GROUP ); + } + + public function test_related_enabled_value(): void { + $this->assertSame( 'pr_core_related_posts_enabled', PR_Core_Settings::RELATED_ENABLED ); + } + + public function test_related_limit_value(): void { + $this->assertSame( 'pr_core_related_posts_limit', PR_Core_Settings::RELATED_LIMIT ); + } + + public function test_scan_cadence_value(): void { + $this->assertSame( 'pr_core_scan_cadence', PR_Core_Settings::SCAN_CADENCE ); + } + + public function test_default_threshold_value(): void { + $this->assertSame( 'pr_core_default_threshold', PR_Core_Settings::DEFAULT_THRESHOLD ); + } + + public function test_high_velocity_threshold_value(): void { + $this->assertSame( 'pr_core_high_velocity_threshold', PR_Core_Settings::HIGH_VELOCITY_THRESHOLD ); + } + + public function test_verification_email_value(): void { + $this->assertSame( 'pr_core_verification_email', PR_Core_Settings::VERIFICATION_EMAIL ); + } +}