';
foreach ( $rows as $row ) {
- $dose = $row->dose_min !== null ? number_format( $row->dose_min, 2 ) : '?';
- if ( $row->dose_max !== null && $row->dose_max !== $row->dose_min ) {
+ $dose = null !== $row->dose_min ? number_format( $row->dose_min, 2 ) : '?';
+ if ( null !== $row->dose_max && $row->dose_max !== $row->dose_min ) {
$dose .= ' – ' . number_format( $row->dose_max, 2 );
}
$dose .= ' ' . esc_html( $row->dose_unit );
@@ -233,11 +245,11 @@ class PR_Core_Peptide_Metaboxes {
foreach ( $meta_fields as $key => $config ) {
$input_key = 'pr_core_' . $key;
- if ( ! isset( $_POST[ $input_key ] ) ) {
+ if ( ! isset( $_POST[ $input_key ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- existence check only; unslash+sanitize below
continue;
}
- $raw = wp_unslash( $_POST[ $input_key ] );
+ $raw = wp_unslash( $_POST[ $input_key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized via config['sanitize'] callback on next line
$clean = is_callable( $config['sanitize'] )
? call_user_func( $config['sanitize'], $raw )
: sanitize_text_field( (string) $raw );
@@ -246,10 +258,10 @@ class PR_Core_Peptide_Metaboxes {
}
// Set last_editorial_review_at when status changes to published.
- $new_review_status = sanitize_text_field( $_POST['pr_core_editorial_review_status'] ?? '' );
+ $new_review_status = sanitize_text_field( wp_unslash( $_POST['pr_core_editorial_review_status'] ?? '' ) );
if ( 'published' === $new_review_status ) {
$old_status = get_post_meta( $post_id, 'editorial_review_status', true );
- if ( $old_status !== 'published' ) {
+ if ( 'published' !== $old_status ) {
update_post_meta( $post_id, 'last_editorial_review_at', current_time( 'mysql' ) );
update_post_meta( $post_id, 'medical_editor_id', get_current_user_id() );
}
diff --git a/includes/admin/class-pr-core-repo-daily-metaboxes.php b/includes/admin/class-pr-core-repo-daily-metaboxes.php
index 0393d46..afe2551 100755
--- a/includes/admin/class-pr-core-repo-daily-metaboxes.php
+++ b/includes/admin/class-pr-core-repo-daily-metaboxes.php
@@ -1,6 +1,18 @@
ID, '_repo_daily_author', true ) ?: self::DEFAULT_AUTHOR
+ $author = sanitize_text_field(
+ get_post_meta( $post->ID, '_repo_daily_author', true ) ?: self::DEFAULT_AUTHOR // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
);
$requires_review = get_post_meta( $post->ID, '_repo_daily_clinical_review_required', true );
diff --git a/includes/admin/class-pr-core-settings-renderer.php b/includes/admin/class-pr-core-settings-renderer.php
new file mode 100644
index 0000000..2056e20
--- /dev/null
+++ b/includes/admin/class-pr-core-settings-renderer.php
@@ -0,0 +1,134 @@
+
+
+
+
+
+ %s',
+ esc_attr( self::RELATED_ENABLED ),
+ checked( $enabled, true, false ),
+ esc_html__( 'Show related monographs on single peptide pages', 'peptide-repo-core' )
+ );
+ }
+
+ /**
+ * Render output.
+ *
+ * @return void
+ */
+ public static function render_limit_field(): void {
+ printf(
+ '',
+ esc_attr( self::RELATED_LIMIT ),
+ absint( get_option( self::RELATED_LIMIT, 3 ) )
+ );
+ echo '' . esc_html__( 'Number of articles to display (1-6). Default: 3.', 'peptide-repo-core' ) . '
';
+ }
+
+ /**
+ * Render output.
+ *
+ * @return void
+ */
+ public static function render_verification_section(): void {
+ echo '' . esc_html__( 'Configure automatic staleness scanning and digest notifications.', 'peptide-repo-core' ) . '
';
+ }
+
+ /**
+ * Render output.
+ *
+ * @return void
+ */
+ public static function render_cadence_field(): void {
+ $current = get_option( self::SCAN_CADENCE, 'weekly' );
+ echo '';
+ }
+
+ /**
+ * Render output.
+ *
+ * @return void
+ */
+ public static function render_threshold_field(): void {
+ printf( '', esc_attr( self::DEFAULT_THRESHOLD ), absint( get_option( self::DEFAULT_THRESHOLD, 180 ) ) );
+ }
+
+ /**
+ * Render output.
+ *
+ * @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 ) ) );
+ }
+
+ /**
+ * Render output.
+ *
+ * @return void
+ */
+ public static function render_email_field(): void {
+ printf(
+ '',
+ esc_attr( self::VERIFICATION_EMAIL ),
+ esc_attr( (string) get_option( self::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 7bc2246..6a6e2a5 100644
--- a/includes/admin/class-pr-core-settings.php
+++ b/includes/admin/class-pr-core-settings.php
@@ -1,4 +1,10 @@
-
-
-
-
- 'boolean', 'default' => true, 'sanitize_callback' => 'rest_sanitize_boolean' ] );
- register_setting( self::OPTION_GROUP, self::RELATED_LIMIT, [ 'type' => 'integer', 'default' => 3, 'sanitize_callback' => [ __CLASS__, 'sanitize_limit' ] ] );
+ register_setting(
+ self::OPTION_GROUP,
+ self::RELATED_ENABLED,
+ array(
+ 'type' => 'boolean',
+ 'default' => true,
+ 'sanitize_callback' => 'rest_sanitize_boolean',
+ )
+ );
+ register_setting(
+ self::OPTION_GROUP,
+ self::RELATED_LIMIT,
+ array(
+ 'type' => 'integer',
+ 'default' => 3,
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_limit' ),
+ )
+ );
add_settings_section( 'pr_core_related_articles', __( 'Related Articles', 'peptide-repo-core' ), '__return_false', self::OPTION_GROUP );
- add_settings_field( self::RELATED_ENABLED, __( 'Enable Related Articles', 'peptide-repo-core' ), [ __CLASS__, 'render_enabled_field' ], self::OPTION_GROUP, 'pr_core_related_articles' );
- add_settings_field( self::RELATED_LIMIT, __( 'Number of Articles', 'peptide-repo-core' ), [ __CLASS__, 'render_limit_field' ], self::OPTION_GROUP, 'pr_core_related_articles' );
+ add_settings_field( self::RELATED_ENABLED, __( 'Enable Related Articles', 'peptide-repo-core' ), array( __CLASS__, 'render_enabled_field' ), self::OPTION_GROUP, 'pr_core_related_articles' );
+ add_settings_field( self::RELATED_LIMIT, __( 'Number of Articles', 'peptide-repo-core' ), array( __CLASS__, 'render_limit_field' ), self::OPTION_GROUP, 'pr_core_related_articles' );
}
- /** @return void */
+ /**
+ * Render the enable/disable checkbox field. Delegates to PR_Core_Settings_Renderer.
+ *
+ * @return void
+ */
public static function render_enabled_field(): void {
- $enabled = (bool) get_option( self::RELATED_ENABLED, true );
- printf(
- ' %s',
- esc_attr( self::RELATED_ENABLED ),
- checked( $enabled, true, false ),
- esc_html__( 'Show related monographs on single peptide pages', 'peptide-repo-core' )
- );
+ PR_Core_Settings_Renderer::render_enabled_field();
}
- /** @return void */
+ /**
+ * Render the article count limit field. Delegates to PR_Core_Settings_Renderer.
+ *
+ * @return void
+ */
public static function render_limit_field(): void {
- printf(
- '',
- esc_attr( self::RELATED_LIMIT ),
- absint( get_option( self::RELATED_LIMIT, 3 ) )
- );
- echo '' . esc_html__( 'Number of articles to display (1-6). Default: 3.', 'peptide-repo-core' ) . '
';
+ PR_Core_Settings_Renderer::render_limit_field();
}
/**
@@ -133,7 +141,7 @@ class PR_Core_Settings {
return min( 6, max( 1, (int) $value ) );
}
- // ── Verification ─────────────────────────────────────────────────────
+ // ── Verification ─────────────────────────────────────────────────────.
/**
* Register Verification settings.
@@ -141,51 +149,93 @@ class PR_Core_Settings {
* @return void
*/
private static function register_verification_section(): void {
- register_setting( self::OPTION_GROUP, self::SCAN_CADENCE, [ 'type' => 'string', 'default' => 'weekly', 'sanitize_callback' => [ __CLASS__, 'sanitize_cadence' ] ] );
- register_setting( self::OPTION_GROUP, self::DEFAULT_THRESHOLD, [ 'type' => 'integer', 'default' => 180, 'sanitize_callback' => 'absint' ] );
- register_setting( self::OPTION_GROUP, self::HIGH_VELOCITY_THRESHOLD, [ 'type' => 'integer', 'default' => 60, 'sanitize_callback' => 'absint' ] );
- register_setting( self::OPTION_GROUP, self::VERIFICATION_EMAIL, [ 'type' => 'string', 'default' => '', 'sanitize_callback' => [ __CLASS__, 'sanitize_emails' ] ] );
-
- add_settings_section( 'pr_core_verification', __( 'Verification', 'peptide-repo-core' ), [ __CLASS__, 'render_verification_section' ], self::OPTION_GROUP );
- add_settings_field( self::SCAN_CADENCE, __( 'Scan cadence', 'peptide-repo-core' ), [ __CLASS__, 'render_cadence_field' ], self::OPTION_GROUP, 'pr_core_verification' );
- add_settings_field( self::DEFAULT_THRESHOLD, __( 'Default staleness threshold (days)', 'peptide-repo-core' ), [ __CLASS__, 'render_threshold_field' ], self::OPTION_GROUP, 'pr_core_verification' );
- add_settings_field( self::HIGH_VELOCITY_THRESHOLD, __( 'High-velocity threshold (days)', 'peptide-repo-core' ), [ __CLASS__, 'render_high_velocity_field' ], self::OPTION_GROUP, 'pr_core_verification' );
- add_settings_field( self::VERIFICATION_EMAIL, __( 'Notification email(s)', 'peptide-repo-core' ), [ __CLASS__, 'render_email_field' ], self::OPTION_GROUP, 'pr_core_verification' );
- }
-
- /** @return void */
- public static function render_verification_section(): void {
- echo '' . esc_html__( 'Configure automatic staleness scanning and digest notifications.', 'peptide-repo-core' ) . '
';
- }
-
- /** @return void */
- public static function render_cadence_field(): void {
- $current = get_option( self::SCAN_CADENCE, 'weekly' );
- echo '';
- }
-
- /** @return void */
- public static function render_threshold_field(): void {
- printf( '', esc_attr( self::DEFAULT_THRESHOLD ), absint( get_option( self::DEFAULT_THRESHOLD, 180 ) ) );
- }
-
- /** @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 ) ) );
- }
-
- /** @return void */
- public static function render_email_field(): void {
- printf(
- '',
- esc_attr( self::VERIFICATION_EMAIL ),
- esc_attr( (string) get_option( self::VERIFICATION_EMAIL, '' ) )
+ register_setting(
+ self::OPTION_GROUP,
+ self::SCAN_CADENCE,
+ array(
+ 'type' => 'string',
+ 'default' => 'weekly',
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_cadence' ),
+ )
);
- echo '' . esc_html__( 'Comma-separated. Leave blank to disable email digests.', 'peptide-repo-core' ) . '
';
+ register_setting(
+ self::OPTION_GROUP,
+ self::DEFAULT_THRESHOLD,
+ array(
+ 'type' => 'integer',
+ 'default' => 180,
+ 'sanitize_callback' => 'absint',
+ )
+ );
+ register_setting(
+ self::OPTION_GROUP,
+ self::HIGH_VELOCITY_THRESHOLD,
+ array(
+ 'type' => 'integer',
+ 'default' => 60,
+ 'sanitize_callback' => 'absint',
+ )
+ );
+ register_setting(
+ self::OPTION_GROUP,
+ self::VERIFICATION_EMAIL,
+ array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_emails' ),
+ )
+ );
+
+ add_settings_section( 'pr_core_verification', __( 'Verification', 'peptide-repo-core' ), array( __CLASS__, 'render_verification_section' ), self::OPTION_GROUP );
+ add_settings_field( self::SCAN_CADENCE, __( 'Scan cadence', 'peptide-repo-core' ), array( __CLASS__, 'render_cadence_field' ), self::OPTION_GROUP, 'pr_core_verification' );
+ add_settings_field( self::DEFAULT_THRESHOLD, __( 'Default staleness threshold (days)', 'peptide-repo-core' ), array( __CLASS__, 'render_threshold_field' ), self::OPTION_GROUP, 'pr_core_verification' );
+ add_settings_field( self::HIGH_VELOCITY_THRESHOLD, __( 'High-velocity threshold (days)', 'peptide-repo-core' ), array( __CLASS__, 'render_high_velocity_field' ), self::OPTION_GROUP, 'pr_core_verification' );
+ add_settings_field( self::VERIFICATION_EMAIL, __( 'Notification email(s)', 'peptide-repo-core' ), array( __CLASS__, 'render_email_field' ), self::OPTION_GROUP, 'pr_core_verification' );
+ }
+
+ /**
+ * Render the verification section header. Delegates to PR_Core_Settings_Renderer.
+ *
+ * @return void
+ */
+ public static function render_verification_section(): void {
+ PR_Core_Settings_Renderer::render_verification_section();
+ }
+
+ /**
+ * Render the scan cadence select field. Delegates to PR_Core_Settings_Renderer.
+ *
+ * @return void
+ */
+ public static function render_cadence_field(): void {
+ PR_Core_Settings_Renderer::render_cadence_field();
+ }
+
+ /**
+ * Render the score threshold input field. Delegates to PR_Core_Settings_Renderer.
+ *
+ * @return void
+ */
+ public static function render_threshold_field(): void {
+ PR_Core_Settings_Renderer::render_threshold_field();
+ }
+
+ /**
+ * Render the high-velocity scan toggle field. Delegates to PR_Core_Settings_Renderer.
+ *
+ * @return void
+ */
+ public static function render_high_velocity_field(): void {
+ PR_Core_Settings_Renderer::render_high_velocity_field();
+ }
+
+ /**
+ * Render the alert email input field. Delegates to PR_Core_Settings_Renderer.
+ *
+ * @return void
+ */
+ public static function render_email_field(): void {
+ PR_Core_Settings_Renderer::render_email_field();
}
/**
@@ -195,7 +245,7 @@ class PR_Core_Settings {
* @return string
*/
public static function sanitize_cadence( string $value ): string {
- return in_array( $value, [ 'daily', 'weekly', 'monthly' ], true ) ? $value : 'weekly';
+ return in_array( $value, array( 'daily', 'weekly', 'monthly' ), true ) ? $value : 'weekly';
}
/**
diff --git a/includes/admin/class-pr-core-verification-metabox.php b/includes/admin/class-pr-core-verification-metabox.php
index 386d319..54fedaa 100644
--- a/includes/admin/class-pr-core-verification-metabox.php
+++ b/includes/admin/class-pr-core-verification-metabox.php
@@ -1,6 +1,18 @@
ID, '_pr_last_source_verified', true );
- $velocity = get_post_meta( $post->ID, '_pr_verification_velocity', true ) ?: 'medium';
- $status = get_post_meta( $post->ID, '_pr_verification_status', true ) ?: 'current';
- $notes = get_post_meta( $post->ID, '_pr_verification_notes', true ) ?: '';
+ $velocity = get_post_meta( $post->ID, '_pr_verification_velocity', true ) ?: 'medium'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ $status = get_post_meta( $post->ID, '_pr_verification_status', true ) ?: 'current'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ $notes = get_post_meta( $post->ID, '_pr_verification_notes', true ) ?: ''; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
echo '' . esc_html__( 'Last Verified', 'peptide-repo-core' ) . ':
';
- echo '' . esc_html( $last_verified ?: '—' ) . '
';
+ echo '' . esc_html( $last_verified ?: '—' ) . ''; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
echo '' . esc_html__( 'Verification Velocity', 'peptide-repo-core' ) . ':
';
echo '
';
echo '' . esc_html__( 'Status', 'peptide-repo-core' ) . ':
';
- $badge_class = match( $status ) {
+ $badge_class = match ( $status ) {
'due' => 'style="background:#ff9800;color:white;padding:4px 8px;border-radius:3px;"',
'overdue' => 'style="background:#f44336;color:white;padding:4px 8px;border-radius:3px;"',
default => 'style="background:#4caf50;color:white;padding:4px 8px;border-radius:3px;"',
};
- echo '' . esc_html( ucfirst( $status ) ) . '
';
+ echo '' . esc_html( ucfirst( $status ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- badge_class is a hardcoded match expression
echo '' . esc_html__( 'Verification Notes', 'peptide-repo-core' ) . ':
';
printf(
diff --git a/includes/admin/class-pr-core-verification-widget.php b/includes/admin/class-pr-core-verification-widget.php
index 885cfe8..d2b86f6 100644
--- a/includes/admin/class-pr-core-verification-widget.php
+++ b/includes/admin/class-pr-core-verification-widget.php
@@ -1,6 +1,18 @@
find_all( [ 'status' => 'publish' ] );
+ $repo = new PR_Core_Peptide_Repository();
+ $peptides = $repo->find_all( array( 'status' => 'publish' ) );
- $due_overdue = [];
+ $due_overdue = array();
foreach ( $peptides as $dto ) {
- $status = get_post_meta( $dto->id, '_pr_verification_status', true ) ?: 'current';
+ $status = get_post_meta( $dto->id, '_pr_verification_status', true ) ?: 'current'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
- if ( in_array( $status, [ 'due', 'overdue' ], true ) ) {
+ if ( in_array( $status, array( 'due', 'overdue' ), true ) ) {
$last_verified = get_post_meta( $dto->id, '_pr_last_source_verified', true );
$days_since = $last_verified
? (int) ( ( time() - strtotime( $last_verified ) ) / DAY_IN_SECONDS )
: 999;
- $due_overdue[] = [
- 'id' => $dto->id,
- 'title' => $dto->title,
- 'status' => $status,
- 'velocity' => get_post_meta( $dto->id, '_pr_verification_velocity', true ) ?: 'medium',
+ $due_overdue[] = array(
+ 'id' => $dto->id,
+ 'title' => $dto->title,
+ 'status' => $status,
+ 'velocity' => get_post_meta( $dto->id, '_pr_verification_velocity', true ) ?: 'medium', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
'last_verified' => $last_verified,
- 'days_since' => $days_since,
- ];
+ 'days_since' => $days_since,
+ );
}
}
// Sort by days_since descending (most overdue first).
- usort( $due_overdue, static function ( $a, $b ) {
- return $b['days_since'] <=> $a['days_since'];
- } );
+ usort(
+ $due_overdue,
+ static function ( $a, $b ) {
+ return $b['days_since'] <=> $a['days_since'];
+ }
+ );
if ( empty( $due_overdue ) ) {
echo '
' . esc_html__( 'All monographs are current. ', 'peptide-repo-core' );
@@ -93,7 +108,7 @@ class PR_Core_Verification_Widget {
esc_html( ucfirst( $item['status'] ) ),
esc_html( ucfirst( $item['velocity'] ) ),
esc_html( $item['last_verified'] ?? '—' ),
- $item['days_since'],
+ (int) $item['days_since'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- cast to int, safe
esc_url( $edit_url )
);
}
diff --git a/includes/api/class-pr-core-rest-controller.php b/includes/api/class-pr-core-rest-controller.php
index f2e8d5e..c7ad5ef 100644
--- a/includes/api/class-pr-core-rest-controller.php
+++ b/includes/api/class-pr-core-rest-controller.php
@@ -1,6 +1,18 @@
'GET',
- 'callback' => [ $this, 'list_peptides' ],
- 'permission_callback' => '__return_true',
- 'args' => [
- 'category' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
- 'evidence_strength' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
- 'search' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
- 'per_page' => [ 'type' => 'integer', 'default' => 25, 'minimum' => 1, 'maximum' => 100 ],
- 'page' => [ 'type' => 'integer', 'default' => 1, 'minimum' => 1 ],
- ],
- ] );
+ // GET /peptides.
+ register_rest_route(
+ self::REST_NAMESPACE,
+ '/peptides',
+ array(
+ 'methods' => 'GET',
+ 'callback' => array( $this, 'list_peptides' ),
+ 'permission_callback' => '__return_true',
+ 'args' => array(
+ 'category' => array(
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ 'evidence_strength' => array(
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ 'search' => array(
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ 'per_page' => array(
+ 'type' => 'integer',
+ 'default' => 25,
+ 'minimum' => 1,
+ 'maximum' => 100,
+ ),
+ 'page' => array(
+ 'type' => 'integer',
+ 'default' => 1,
+ 'minimum' => 1,
+ ),
+ ),
+ )
+ );
- // GET /peptides/{id}
- register_rest_route( self::REST_NAMESPACE, '/peptides/(?P\d+)', [
- 'methods' => 'GET',
- 'callback' => [ $this, 'get_peptide' ],
- 'permission_callback' => '__return_true',
- ] );
+ // GET /peptides/{id}.
+ register_rest_route(
+ self::REST_NAMESPACE,
+ '/peptides/(?P\d+)',
+ array(
+ 'methods' => 'GET',
+ 'callback' => array( $this, 'get_peptide' ),
+ 'permission_callback' => '__return_true',
+ )
+ );
- // GET /peptides/{id}/dosing
- register_rest_route( self::REST_NAMESPACE, '/peptides/(?P\d+)/dosing', [
- 'methods' => 'GET',
- 'callback' => [ $this, 'list_dosing' ],
- 'permission_callback' => '__return_true',
- 'args' => [
- 'route' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
- 'population' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
- ],
- ] );
+ // GET /peptides/{id}/dosing.
+ register_rest_route(
+ self::REST_NAMESPACE,
+ '/peptides/(?P\d+)/dosing',
+ array(
+ 'methods' => 'GET',
+ 'callback' => array( $this, 'list_dosing' ),
+ 'permission_callback' => '__return_true',
+ 'args' => array(
+ 'route' => array(
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ 'population' => array(
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ )
+ );
- // POST /peptides/{id}/dosing (auth required)
- register_rest_route( self::REST_NAMESPACE, '/peptides/(?P\d+)/dosing', [
- 'methods' => 'POST',
- 'callback' => [ $this, 'create_dosing' ],
- 'permission_callback' => [ $this, 'check_write_permission' ],
- ] );
+ // POST /peptides/{id}/dosing (auth required).
+ register_rest_route(
+ self::REST_NAMESPACE,
+ '/peptides/(?P\d+)/dosing',
+ array(
+ 'methods' => 'POST',
+ 'callback' => array( $this, 'create_dosing' ),
+ 'permission_callback' => array( $this, 'check_write_permission' ),
+ )
+ );
- // GET /peptides/{id}/legal
- register_rest_route( self::REST_NAMESPACE, '/peptides/(?P\d+)/legal', [
- 'methods' => 'GET',
- 'callback' => [ $this, 'list_legal' ],
- 'permission_callback' => '__return_true',
- ] );
+ // GET /peptides/{id}/legal.
+ register_rest_route(
+ self::REST_NAMESPACE,
+ '/peptides/(?P\d+)/legal',
+ array(
+ 'methods' => 'GET',
+ 'callback' => array( $this, 'list_legal' ),
+ 'permission_callback' => '__return_true',
+ )
+ );
- // POST /peptides/{id}/legal (auth required)
- register_rest_route( self::REST_NAMESPACE, '/peptides/(?P\d+)/legal', [
- 'methods' => 'POST',
- 'callback' => [ $this, 'create_legal' ],
- 'permission_callback' => [ $this, 'check_write_permission' ],
- ] );
+ // POST /peptides/{id}/legal (auth required).
+ register_rest_route(
+ self::REST_NAMESPACE,
+ '/peptides/(?P\d+)/legal',
+ array(
+ 'methods' => 'POST',
+ 'callback' => array( $this, 'create_legal' ),
+ 'permission_callback' => array( $this, 'check_write_permission' ),
+ )
+ );
}
/**
@@ -109,12 +169,14 @@ class PR_Core_Rest_Controller {
if ( $search ) {
$peptides = $repo->search( $search, (int) $request->get_param( 'per_page' ) );
} else {
- $peptides = $repo->find_all( [
- 'category' => $request->get_param( 'category' ),
- 'evidence_strength' => $request->get_param( 'evidence_strength' ),
- 'per_page' => $request->get_param( 'per_page' ),
- 'page' => $request->get_param( 'page' ),
- ] );
+ $peptides = $repo->find_all(
+ array(
+ 'category' => $request->get_param( 'category' ),
+ 'evidence_strength' => $request->get_param( 'evidence_strength' ),
+ 'per_page' => $request->get_param( 'per_page' ),
+ 'page' => $request->get_param( 'page' ),
+ )
+ );
}
$data = array_map( static fn( PR_Core_Peptide_DTO $p ) => $p->to_array(), $peptides );
@@ -133,7 +195,7 @@ class PR_Core_Rest_Controller {
$peptide = $repo->find_by_id( (int) $request->get_param( 'id' ) );
if ( ! $peptide ) {
- return new \WP_Error( 'not_found', __( 'Peptide not found.', 'peptide-repo-core' ), [ 'status' => 404 ] );
+ return new \WP_Error( 'not_found', __( 'Peptide not found.', 'peptide-repo-core' ), array( 'status' => 404 ) );
}
return new \WP_REST_Response( $peptide->to_array(), 200 );
@@ -149,10 +211,12 @@ class PR_Core_Rest_Controller {
$repo = new PR_Core_Dosing_Repository();
$rows = $repo->find_by_peptide(
(int) $request->get_param( 'id' ),
- array_filter( [
- 'route' => $request->get_param( 'route' ),
- 'population' => $request->get_param( 'population' ),
- ] )
+ array_filter(
+ array(
+ 'route' => $request->get_param( 'route' ),
+ 'population' => $request->get_param( 'population' ),
+ )
+ )
);
$data = array_map( static fn( PR_Core_Dosing_Row_DTO $r ) => $r->to_array(), $rows );
@@ -176,12 +240,12 @@ class PR_Core_Rest_Controller {
$id = $repo->insert( $data );
if ( 0 === $id ) {
- return new \WP_Error( 'insert_failed', __( 'Failed to insert dosing row.', 'peptide-repo-core' ), [ 'status' => 500 ] );
+ return new \WP_Error( 'insert_failed', __( 'Failed to insert dosing row.', 'peptide-repo-core' ), array( 'status' => 500 ) );
}
$row = $repo->find_by_id( $id );
- return new \WP_REST_Response( $row ? $row->to_array() : [ 'id' => $id ], 201 );
+ return new \WP_REST_Response( $row ? $row->to_array() : array( 'id' => $id ), 201 );
}
/**
@@ -208,19 +272,19 @@ class PR_Core_Rest_Controller {
public function create_legal( \WP_REST_Request $request ) {
$repo = new PR_Core_Legal_Repository();
- $data = $request->get_json_params();
- $data['peptide_id'] = (int) $request->get_param( 'id' );
+ $data = $request->get_json_params();
+ $data['peptide_id'] = (int) $request->get_param( 'id' );
$data['reviewer_id'] = get_current_user_id();
$id = $repo->insert( $data );
if ( 0 === $id ) {
- return new \WP_Error( 'insert_failed', __( 'Failed to insert legal cell.', 'peptide-repo-core' ), [ 'status' => 500 ] );
+ return new \WP_Error( 'insert_failed', __( 'Failed to insert legal cell.', 'peptide-repo-core' ), array( 'status' => 500 ) );
}
$cell = $repo->find_by_id( $id );
- return new \WP_REST_Response( $cell ? $cell->to_array() : [ 'id' => $id ], 201 );
+ return new \WP_REST_Response( $cell ? $cell->to_array() : array( 'id' => $id ), 201 );
}
/**
diff --git a/includes/class-pr-core-activator.php b/includes/class-pr-core-activator.php
index 43efb35..7be9cca 100644
--- a/includes/class-pr-core-activator.php
+++ b/includes/class-pr-core-activator.php
@@ -1,6 +1,18 @@
foo-bar => class-pr-core-foo-bar.php.
- $suffix = substr( $class, 8 ); // Strip 'PR_Core_'.
+ $suffix = substr( $class_name, 8 ); // Strip 'PR_Core_'.
$filename = 'class-pr-core-' . str_replace( '_', '-', strtolower( $suffix ) ) . '.php';
foreach ( self::$dirs as $dir ) {
diff --git a/includes/class-pr-core-deactivator.php b/includes/class-pr-core-deactivator.php
index 49fa45d..d7f3513 100644
--- a/includes/class-pr-core-deactivator.php
+++ b/includes/class-pr-core-deactivator.php
@@ -1,6 +1,18 @@
count( $peptide_ids ),
'dosing_rows' => $dosing,
'legal_cells' => $legal,
- ];
+ );
}
/**
- * Create 3 canonical peptide posts.
+ * Create 3 canonical peptide posts using fixture data from PR_Core_Seed_Fixtures.
*
* @return array Map of slug => post ID.
*/
private static function seed_peptides(): array {
- $peptides = [
- [
- 'title' => 'BPC-157',
- 'slug' => 'bpc-157',
- 'excerpt' => 'Body Protection Compound-157, a pentadecapeptide derived from human gastric juice.',
- 'display_name' => 'BPC-157',
- 'aliases' => '["Body Protection Compound-157","Bepecin","PL 14736","PL-10"]',
- 'molecular_formula' => 'C62H98N16O22',
- 'molecular_weight' => 1419.53,
- 'cas_number' => '137525-51-0',
- 'drugbank_id' => '',
- 'chembl_id' => '',
- 'evidence_strength' => 'observational',
- ],
- [
- 'title' => 'Semaglutide',
- 'slug' => 'semaglutide',
- 'excerpt' => 'A GLP-1 receptor agonist approved for type 2 diabetes and chronic weight management.',
- 'display_name' => 'Semaglutide',
- 'aliases' => '["Ozempic","Wegovy","Rybelsus"]',
- 'molecular_formula' => 'C187H291N45O59',
- 'molecular_weight' => 4113.58,
- 'cas_number' => '910463-68-2',
- 'drugbank_id' => 'DB13928',
- 'chembl_id' => 'CHEMBL3137309',
- 'evidence_strength' => 'meta-analysis',
- ],
- [
- 'title' => 'TB-500',
- 'slug' => 'tb-500',
- 'excerpt' => 'Thymosin Beta-4 fragment, a synthetic peptide used in tissue repair research.',
- 'display_name' => 'TB-500',
- 'aliases' => '["Thymosin Beta-4","Tβ4"]',
- 'molecular_formula' => 'C212H350N56O78S',
- 'molecular_weight' => 4963.44,
- 'cas_number' => '77591-33-4',
- 'drugbank_id' => '',
- 'chembl_id' => '',
- 'evidence_strength' => 'preclinical',
- ],
- ];
+ $peptides = PR_Core_Seed_Fixtures::get_peptides();
+ $ids = array();
- $ids = [];
foreach ( $peptides as $p ) {
- $existing = get_posts( [
- 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
- 'name' => $p['slug'],
- 'posts_per_page' => 1,
- 'post_status' => 'any',
- ] );
+ $existing = get_posts(
+ array(
+ 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
+ 'name' => $p['slug'],
+ 'posts_per_page' => 1,
+ 'post_status' => 'any',
+ )
+ );
if ( ! empty( $existing ) ) {
$ids[ $p['slug'] ] = $existing[0]->ID;
continue;
}
- $post_id = wp_insert_post( [
- 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
- 'post_title' => $p['title'],
- 'post_name' => $p['slug'],
- 'post_excerpt' => $p['excerpt'],
- 'post_status' => 'publish',
- 'post_content' => '',
- ] );
+ $post_id = wp_insert_post(
+ array(
+ 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
+ 'post_title' => $p['title'],
+ 'post_name' => $p['slug'],
+ 'post_excerpt' => $p['excerpt'],
+ 'post_status' => 'publish',
+ 'post_content' => '',
+ )
+ );
if ( is_wp_error( $post_id ) ) {
continue;
}
- foreach ( [ 'display_name', 'aliases', 'molecular_formula', 'molecular_weight', 'cas_number', 'drugbank_id', 'chembl_id', 'evidence_strength' ] as $key ) {
+ $meta_keys = array( 'display_name', 'aliases', 'molecular_formula', 'molecular_weight', 'cas_number', 'drugbank_id', 'chembl_id', 'evidence_strength' );
+ foreach ( $meta_keys as $key ) {
update_post_meta( $post_id, $key, $p[ $key ] );
}
update_post_meta( $post_id, 'editorial_review_status', 'published' );
@@ -119,30 +98,16 @@ class PR_Core_Seed_Data {
}
/**
- * Seed 10 dosing rows across the 3 peptides.
+ * Seed 10 dosing rows across the 3 peptides using fixture data.
*
* @param array $peptide_ids Map of slug => post ID.
* @return int Number of rows inserted.
*/
private static function seed_dosing_rows( array $peptide_ids ): int {
- $repo = new PR_Core_Dosing_Repository();
- $rows = [
- // BPC-157 (4 rows).
- [ 'peptide' => 'bpc-157', 'dose_min' => 200, 'dose_max' => 300, 'dose_unit' => 'mcg', 'route' => 'subq', 'frequency' => 'twice daily', 'population' => 'healthy', 'evidence_strength' => 'observational', 'study_title' => 'BPC 157 in wound healing review', 'study_year' => 2021, 'citation_pubmed_id' => '34537800' ],
- [ 'peptide' => 'bpc-157', 'dose_min' => 500, 'dose_max' => 500, 'dose_unit' => 'mcg', 'route' => 'subq', 'frequency' => 'daily', 'population' => 'healthy', 'evidence_strength' => 'case-series', 'study_title' => 'Subcutaneous BPC-157 in tendon repair', 'study_year' => 2019, 'citation_pubmed_id' => '30915550' ],
- [ 'peptide' => 'bpc-157', 'dose_min' => 10, 'dose_max' => 10, 'dose_unit' => 'mcg', 'route' => 'oral', 'frequency' => 'daily', 'population' => 'animal', 'evidence_strength' => 'preclinical', 'study_title' => 'Oral BPC-157 in rat gastric ulcer model', 'study_year' => 2018, 'citation_pubmed_id' => '29869189' ],
- [ 'peptide' => 'bpc-157', 'dose_min' => 250, 'dose_max' => 750, 'dose_unit' => 'mcg', 'route' => 'im', 'frequency' => 'daily', 'population' => 'clinical', 'evidence_strength' => 'case-series', 'study_title' => 'Intramuscular BPC-157 for muscle injuries', 'study_year' => 2020, 'citation_pubmed_id' => '33312000' ],
- // Semaglutide (4 rows).
- [ 'peptide' => 'semaglutide', 'dose_min' => 0.25, 'dose_max' => 0.25, 'dose_unit' => 'mg', 'route' => 'subq', 'frequency' => 'weekly', 'population' => 'clinical', 'evidence_strength' => 'rct-large', 'study_title' => 'SUSTAIN 1: Semaglutide in T2DM', 'study_year' => 2017, 'citation_pubmed_id' => '28930514', 'indication' => 'Type 2 diabetes — initiation dose' ],
- [ 'peptide' => 'semaglutide', 'dose_min' => 1.0, 'dose_max' => 1.0, 'dose_unit' => 'mg', 'route' => 'subq', 'frequency' => 'weekly', 'population' => 'clinical', 'evidence_strength' => 'rct-large', 'study_title' => 'SUSTAIN 6: CV outcomes with semaglutide', 'study_year' => 2016, 'citation_pubmed_id' => '27633186', 'indication' => 'Type 2 diabetes — maintenance' ],
- [ 'peptide' => 'semaglutide', 'dose_min' => 2.4, 'dose_max' => 2.4, 'dose_unit' => 'mg', 'route' => 'subq', 'frequency' => 'weekly', 'population' => 'clinical', 'evidence_strength' => 'rct-large', 'study_title' => 'STEP 1: Semaglutide 2.4mg for obesity', 'study_year' => 2021, 'citation_pubmed_id' => '33567185', 'indication' => 'Chronic weight management' ],
- [ 'peptide' => 'semaglutide', 'dose_min' => 14, 'dose_max' => 14, 'dose_unit' => 'mg', 'route' => 'oral', 'frequency' => 'daily', 'population' => 'clinical', 'evidence_strength' => 'rct-large', 'study_title' => 'PIONEER 1: Oral semaglutide in T2DM', 'study_year' => 2019, 'citation_pubmed_id' => '30924169', 'indication' => 'Type 2 diabetes — oral formulation' ],
- // TB-500 (2 rows).
- [ 'peptide' => 'tb-500', 'dose_min' => 2, 'dose_max' => 2.5, 'dose_unit' => 'mg', 'route' => 'subq', 'frequency' => 'twice weekly', 'population' => 'animal', 'evidence_strength' => 'preclinical', 'study_title' => 'Thymosin beta-4 promotes dermal healing in rats', 'study_year' => 2007, 'citation_pubmed_id' => '17584560' ],
- [ 'peptide' => 'tb-500', 'dose_min' => 5, 'dose_max' => 5, 'dose_unit' => 'mg', 'route' => 'subq', 'frequency' => 'weekly', 'population' => 'animal', 'evidence_strength' => 'preclinical', 'study_title' => 'TB4 cardiac repair in murine MI model', 'study_year' => 2012, 'citation_pubmed_id' => '22561753' ],
- ];
-
+ $repo = new PR_Core_Dosing_Repository();
+ $rows = PR_Core_Seed_Fixtures::get_dosing_rows();
$count = 0;
+
foreach ( $rows as $row ) {
$slug = $row['peptide'];
unset( $row['peptide'] );
@@ -153,10 +118,10 @@ class PR_Core_Seed_Data {
$row['peptide_id'] = $peptide_ids[ $slug ];
$row['source'] = 'manual';
- $row['added_by'] = get_current_user_id() ?: 1;
+ $row['added_by'] = get_current_user_id() !== 0 ? get_current_user_id() : 1;
if ( $repo->insert( $row ) > 0 ) {
- $count++;
+ ++$count;
}
}
@@ -164,22 +129,16 @@ class PR_Core_Seed_Data {
}
/**
- * Seed 5 legal cells across BPC-157 and Semaglutide.
+ * Seed 5 legal cells across BPC-157 and Semaglutide using fixture data.
*
* @param array $peptide_ids Map of slug => post ID.
* @return int Number of cells inserted.
*/
private static function seed_legal_cells( array $peptide_ids ): int {
$repo = new PR_Core_Legal_Repository();
- $cells = [
- [ 'peptide' => 'bpc-157', 'country_code' => 'US', 'status' => 'ruo', 'regulatory_framework' => 'Not FDA-approved; available as Research Use Only', 'notes' => 'Compounding pharmacies may dispense under FDA 503A/503B exemptions.' ],
- [ 'peptide' => 'bpc-157', 'country_code' => 'GB', 'status' => 'unclear', 'regulatory_framework' => 'Not listed by MHRA', 'notes' => 'No specific scheduling; import for personal use is a grey area.' ],
- [ 'peptide' => 'semaglutide', 'country_code' => 'US', 'status' => 'prescription', 'regulatory_framework' => 'FDA-approved (Ozempic, Wegovy, Rybelsus)', 'regulatory_text_url' => 'https://www.accessdata.fda.gov/drugsatfda_docs/label/2023/209637s020lbl.pdf', 'notes' => 'Schedule IV not applicable; requires valid prescription.' ],
- [ 'peptide' => 'semaglutide', 'country_code' => 'GB', 'status' => 'prescription', 'regulatory_framework' => 'MHRA-approved (Ozempic, Wegovy)', 'notes' => 'Available via NHS and private prescription.' ],
- [ 'peptide' => 'semaglutide', 'country_code' => 'AU', 'status' => 'prescription', 'regulatory_framework' => 'TGA-approved (Ozempic)', 'notes' => 'PBS-listed for type 2 diabetes; Wegovy approval pending as of 2025.' ],
- ];
-
+ $cells = PR_Core_Seed_Fixtures::get_legal_cells();
$count = 0;
+
foreach ( $cells as $cell ) {
$slug = $cell['peptide'];
unset( $cell['peptide'] );
@@ -189,10 +148,10 @@ class PR_Core_Seed_Data {
}
$cell['peptide_id'] = $peptide_ids[ $slug ];
- $cell['reviewer_id'] = get_current_user_id() ?: 1;
+ $cell['reviewer_id'] = get_current_user_id() !== 0 ? get_current_user_id() : 1;
if ( $repo->insert( $cell ) > 0 ) {
- $count++;
+ ++$count;
}
}
diff --git a/includes/class-pr-core-seed-fixtures.php b/includes/class-pr-core-seed-fixtures.php
new file mode 100644
index 0000000..863d229
--- /dev/null
+++ b/includes/class-pr-core-seed-fixtures.php
@@ -0,0 +1,265 @@
+>
+ */
+ public static function get_peptides(): array {
+ return array(
+ array(
+ 'title' => 'BPC-157',
+ 'slug' => 'bpc-157',
+ 'excerpt' => 'Body Protection Compound-157, a pentadecapeptide derived from human gastric juice.',
+ 'display_name' => 'BPC-157',
+ 'aliases' => '["Body Protection Compound-157","Bepecin","PL 14736","PL-10"]',
+ 'molecular_formula' => 'C62H98N16O22',
+ 'molecular_weight' => 1419.53,
+ 'cas_number' => '137525-51-0',
+ 'drugbank_id' => '',
+ 'chembl_id' => '',
+ 'evidence_strength' => 'observational',
+ ),
+ array(
+ 'title' => 'Semaglutide',
+ 'slug' => 'semaglutide',
+ 'excerpt' => 'A GLP-1 receptor agonist approved for type 2 diabetes and chronic weight management.',
+ 'display_name' => 'Semaglutide',
+ 'aliases' => '["Ozempic","Wegovy","Rybelsus"]',
+ 'molecular_formula' => 'C187H291N45O59',
+ 'molecular_weight' => 4113.58,
+ 'cas_number' => '910463-68-2',
+ 'drugbank_id' => 'DB13928',
+ 'chembl_id' => 'CHEMBL3137309',
+ 'evidence_strength' => 'meta-analysis',
+ ),
+ array(
+ 'title' => 'TB-500',
+ 'slug' => 'tb-500',
+ 'excerpt' => 'Thymosin Beta-4 fragment, a synthetic peptide used in tissue repair research.',
+ 'display_name' => 'TB-500',
+ 'aliases' => '["Thymosin Beta-4","T\u03b24"]',
+ 'molecular_formula' => 'C212H350N56O78S',
+ 'molecular_weight' => 4963.44,
+ 'cas_number' => '77591-33-4',
+ 'drugbank_id' => '',
+ 'chembl_id' => '',
+ 'evidence_strength' => 'preclinical',
+ ),
+ );
+ }
+
+ /**
+ * Return the canonical list of dosing row fixture definitions.
+ *
+ * @return array>
+ */
+ public static function get_dosing_rows(): array {
+ return array(
+ array(
+ 'peptide' => 'bpc-157',
+ 'dose_min' => 200,
+ 'dose_max' => 300,
+ 'dose_unit' => 'mcg',
+ 'route' => 'subq',
+ 'frequency' => 'twice daily',
+ 'population' => 'healthy',
+ 'evidence_strength' => 'observational',
+ 'study_title' => 'BPC 157 in wound healing review',
+ 'study_year' => 2021,
+ 'citation_pubmed_id' => '34537800',
+ ),
+ array(
+ 'peptide' => 'bpc-157',
+ 'dose_min' => 500,
+ 'dose_max' => 500,
+ 'dose_unit' => 'mcg',
+ 'route' => 'subq',
+ 'frequency' => 'daily',
+ 'population' => 'healthy',
+ 'evidence_strength' => 'case-series',
+ 'study_title' => 'Subcutaneous BPC-157 in tendon repair',
+ 'study_year' => 2019,
+ 'citation_pubmed_id' => '30915550',
+ ),
+ array(
+ 'peptide' => 'bpc-157',
+ 'dose_min' => 10,
+ 'dose_max' => 10,
+ 'dose_unit' => 'mcg',
+ 'route' => 'oral',
+ 'frequency' => 'daily',
+ 'population' => 'animal',
+ 'evidence_strength' => 'preclinical',
+ 'study_title' => 'Oral BPC-157 in rat gastric ulcer model',
+ 'study_year' => 2018,
+ 'citation_pubmed_id' => '29869189',
+ ),
+ array(
+ 'peptide' => 'bpc-157',
+ 'dose_min' => 250,
+ 'dose_max' => 750,
+ 'dose_unit' => 'mcg',
+ 'route' => 'im',
+ 'frequency' => 'daily',
+ 'population' => 'clinical',
+ 'evidence_strength' => 'case-series',
+ 'study_title' => 'Intramuscular BPC-157 for muscle injuries',
+ 'study_year' => 2020,
+ 'citation_pubmed_id' => '33312000',
+ ),
+ array(
+ 'peptide' => 'semaglutide',
+ 'dose_min' => 0.25,
+ 'dose_max' => 0.25,
+ 'dose_unit' => 'mg',
+ 'route' => 'subq',
+ 'frequency' => 'weekly',
+ 'population' => 'clinical',
+ 'evidence_strength' => 'rct-large',
+ 'study_title' => 'SUSTAIN 1: Semaglutide in T2DM',
+ 'study_year' => 2017,
+ 'citation_pubmed_id' => '28930514',
+ 'indication' => 'Type 2 diabetes initiation dose',
+ ),
+ array(
+ 'peptide' => 'semaglutide',
+ 'dose_min' => 1.0,
+ 'dose_max' => 1.0,
+ 'dose_unit' => 'mg',
+ 'route' => 'subq',
+ 'frequency' => 'weekly',
+ 'population' => 'clinical',
+ 'evidence_strength' => 'rct-large',
+ 'study_title' => 'SUSTAIN 6: CV outcomes with semaglutide',
+ 'study_year' => 2016,
+ 'citation_pubmed_id' => '27633186',
+ 'indication' => 'Type 2 diabetes maintenance',
+ ),
+ array(
+ 'peptide' => 'semaglutide',
+ 'dose_min' => 2.4,
+ 'dose_max' => 2.4,
+ 'dose_unit' => 'mg',
+ 'route' => 'subq',
+ 'frequency' => 'weekly',
+ 'population' => 'clinical',
+ 'evidence_strength' => 'rct-large',
+ 'study_title' => 'STEP 1: Semaglutide 2.4mg for obesity',
+ 'study_year' => 2021,
+ 'citation_pubmed_id' => '33567185',
+ 'indication' => 'Chronic weight management',
+ ),
+ array(
+ 'peptide' => 'semaglutide',
+ 'dose_min' => 14,
+ 'dose_max' => 14,
+ 'dose_unit' => 'mg',
+ 'route' => 'oral',
+ 'frequency' => 'daily',
+ 'population' => 'clinical',
+ 'evidence_strength' => 'rct-large',
+ 'study_title' => 'PIONEER 1: Oral semaglutide in T2DM',
+ 'study_year' => 2019,
+ 'citation_pubmed_id' => '30924169',
+ 'indication' => 'Type 2 diabetes oral formulation',
+ ),
+ array(
+ 'peptide' => 'tb-500',
+ 'dose_min' => 2,
+ 'dose_max' => 2.5,
+ 'dose_unit' => 'mg',
+ 'route' => 'subq',
+ 'frequency' => 'twice weekly',
+ 'population' => 'animal',
+ 'evidence_strength' => 'preclinical',
+ 'study_title' => 'Thymosin beta-4 promotes dermal healing in rats',
+ 'study_year' => 2007,
+ 'citation_pubmed_id' => '17584560',
+ ),
+ array(
+ 'peptide' => 'tb-500',
+ 'dose_min' => 5,
+ 'dose_max' => 5,
+ 'dose_unit' => 'mg',
+ 'route' => 'subq',
+ 'frequency' => 'weekly',
+ 'population' => 'animal',
+ 'evidence_strength' => 'preclinical',
+ 'study_title' => 'TB4 cardiac repair in murine MI model',
+ 'study_year' => 2012,
+ 'citation_pubmed_id' => '22561753',
+ ),
+ );
+ }
+
+ /**
+ * Return the canonical list of legal cell fixture definitions.
+ *
+ * @return array>
+ */
+ public static function get_legal_cells(): array {
+ return array(
+ array(
+ 'peptide' => 'bpc-157',
+ 'country_code' => 'US',
+ 'status' => 'ruo',
+ 'regulatory_framework' => 'Not FDA-approved; available as Research Use Only',
+ 'notes' => 'Compounding pharmacies may dispense under FDA 503A/503B exemptions.',
+ ),
+ array(
+ 'peptide' => 'bpc-157',
+ 'country_code' => 'GB',
+ 'status' => 'unclear',
+ 'regulatory_framework' => 'Not listed by MHRA',
+ 'notes' => 'No specific scheduling; import for personal use is a grey area.',
+ ),
+ array(
+ 'peptide' => 'semaglutide',
+ 'country_code' => 'US',
+ 'status' => 'prescription',
+ 'regulatory_framework' => 'FDA-approved (Ozempic, Wegovy, Rybelsus)',
+ 'regulatory_text_url' => 'https://www.accessdata.fda.gov/drugsatfda_docs/label/2023/209637s020lbl.pdf',
+ 'notes' => 'Schedule IV not applicable; requires valid prescription.',
+ ),
+ array(
+ 'peptide' => 'semaglutide',
+ 'country_code' => 'GB',
+ 'status' => 'prescription',
+ 'regulatory_framework' => 'MHRA-approved (Ozempic, Wegovy)',
+ 'notes' => 'Available via NHS and private prescription.',
+ ),
+ array(
+ 'peptide' => 'semaglutide',
+ 'country_code' => 'AU',
+ 'status' => 'prescription',
+ 'regulatory_framework' => 'TGA-approved (Ozempic)',
+ 'notes' => 'PBS-listed for type 2 diabetes; Wegovy approval pending as of 2025.',
+ ),
+ );
+ }
+}
diff --git a/includes/class-pr-core.php b/includes/class-pr-core.php
index f7b2315..dd70de1 100644
--- a/includes/class-pr-core.php
+++ b/includes/class-pr-core.php
@@ -1,6 +1,18 @@
register_hooks();
- // One-shot rewrite flush on in-place version bumps. Runs at the very
- // end of init (priority 999) so all CPTs/taxonomies — ours and
- // anyone else's — are registered first. Handles updates deployed
+ // One-shot rewrite flush on in-place version bumps. Runs at the very.
+ // end of init (priority 999) so all CPTs/taxonomies — ours and.
+ // anyone else's — are registered first. Handles updates deployed.
// without a deactivate/reactivate cycle (e.g., SCP/rsync pushes).
- add_action( 'init', [ PR_Core_Activator::class, 'maybe_flush_on_version_change' ], 999 );
+ add_action( 'init', array( PR_Core_Activator::class, 'maybe_flush_on_version_change' ), 999 );
// Verification scanner: register cron hook and schedule if not already scheduled.
if ( ! wp_next_scheduled( 'pr_core_verification_scan' ) ) {
wp_schedule_event( time(), get_option( 'pr_core_scan_cadence', 'weekly' ), 'pr_core_verification_scan' );
}
- add_action( 'pr_core_verification_scan', [ PR_Core_Verification_Scanner::class, 'run_scan' ] );
+ add_action( 'pr_core_verification_scan', array( PR_Core_Verification_Scanner::class, 'run_scan' ) );
- // Ajax handlers must be registered outside is_admin() — admin-ajax.php
+ // Ajax handlers must be registered outside is_admin() — admin-ajax.php.
// does not define WP_ADMIN, so is_admin() returns false for ajax requests.
- add_action( 'wp_ajax_pr_core_mark_verified', [ PR_Core_Ajax_Handlers::class, 'handle_mark_verified' ] );
- add_action( 'wp_ajax_pr_core_scan_now', [ PR_Core_Ajax_Handlers::class, 'handle_scan_now' ] );
+ add_action( 'wp_ajax_pr_core_mark_verified', array( PR_Core_Ajax_Handlers::class, 'handle_mark_verified' ) );
+ add_action( 'wp_ajax_pr_core_scan_now', array( PR_Core_Ajax_Handlers::class, 'handle_scan_now' ) );
// Admin UI (fires on admin_init, admin_menu).
if ( is_admin() ) {
- add_action( 'admin_menu', [ PR_Core_Settings::class, 'add_settings_page' ] );
- add_action( 'admin_init', [ PR_Core_Settings::class, 'register_settings' ] );
- add_action( 'wp_dashboard_setup', [ PR_Core_Verification_Widget::class, 'register' ] );
+ add_action( 'admin_menu', array( PR_Core_Settings::class, 'add_settings_page' ) );
+ add_action( 'admin_init', array( PR_Core_Settings::class, 'register_settings' ) );
+ add_action( 'wp_dashboard_setup', array( PR_Core_Verification_Widget::class, 'register' ) );
$admin = new PR_Core_Admin();
$admin->register_hooks();
@@ -86,7 +98,7 @@ class PR_Core {
$related_posts->register_hooks();
// Enqueue related-posts CSS on single peptide pages.
- add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_styles' ] );
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_styles' ) );
// REST API.
$rest = new PR_Core_Rest_Controller();
@@ -109,7 +121,7 @@ class PR_Core {
wp_enqueue_style(
'pr-core-related-posts',
PR_CORE_PLUGIN_URL . 'assets/css/related-posts.css',
- [],
+ array(),
PR_CORE_VERSION
);
}
@@ -125,9 +137,9 @@ class PR_Core {
* @return void
*/
private function register_public_filters(): void {
- add_filter( 'pr_core_get_indexable_corpus', [ $this, 'filter_indexable_corpus' ] );
- add_filter( 'pr_core_disclaimer_for_surface', [ $this, 'filter_disclaimer_for_surface' ], 10, 2 );
- add_filter( 'pr_core_evidence_strength_label', [ $this, 'filter_evidence_label' ], 10, 2 );
+ add_filter( 'pr_core_get_indexable_corpus', array( $this, 'filter_indexable_corpus' ) );
+ add_filter( 'pr_core_disclaimer_for_surface', array( $this, 'filter_disclaimer_for_surface' ), 10, 2 );
+ add_filter( 'pr_core_evidence_strength_label', array( $this, 'filter_evidence_label' ), 10, 2 );
}
/**
@@ -139,17 +151,17 @@ class PR_Core {
* @return array>
*/
public function filter_indexable_corpus( array $entries ): array {
- $repo = new PR_Core_Peptide_Repository();
- $peptides = $repo->find_all( [ 'status' => 'publish' ] );
+ $repo = new PR_Core_Peptide_Repository();
+ $peptides = $repo->find_all( array( 'status' => 'publish' ) );
foreach ( $peptides as $dto ) {
- $entries[] = [
+ $entries[] = array(
'id' => $dto->id,
'url' => get_permalink( $dto->id ),
'title' => $dto->title,
'body' => $dto->excerpt . "\n" . $dto->content,
'type' => 'peptide_monograph',
- ];
+ );
}
return $entries;
@@ -174,14 +186,14 @@ class PR_Core {
* @return string Localized label.
*/
public function filter_evidence_label( string $label, string $strength ): string {
- $map = [
- 'preclinical' => __( 'Preclinical', 'peptide-repo-core' ),
- 'case-series' => __( 'Case Series', 'peptide-repo-core' ),
- 'observational' => __( 'Observational', 'peptide-repo-core' ),
- 'rct-small' => __( 'Small RCT', 'peptide-repo-core' ),
- 'rct-large' => __( 'Large RCT', 'peptide-repo-core' ),
- 'meta-analysis' => __( 'Meta-Analysis', 'peptide-repo-core' ),
- ];
+ $map = array(
+ 'preclinical' => __( 'Preclinical', 'peptide-repo-core' ),
+ 'case-series' => __( 'Case Series', 'peptide-repo-core' ),
+ 'observational' => __( 'Observational', 'peptide-repo-core' ),
+ 'rct-small' => __( 'Small RCT', 'peptide-repo-core' ),
+ 'rct-large' => __( 'Large RCT', 'peptide-repo-core' ),
+ 'meta-analysis' => __( 'Meta-Analysis', 'peptide-repo-core' ),
+ );
return $map[ $strength ] ?? $strength;
}
diff --git a/includes/core/class-pr-core-internal-posts-provider.php b/includes/core/class-pr-core-internal-posts-provider.php
index dc7bb49..08653e5 100644
--- a/includes/core/class-pr-core-internal-posts-provider.php
+++ b/includes/core/class-pr-core-internal-posts-provider.php
@@ -1,6 +1,18 @@
post_type ) {
- return [];
+ return array();
}
$cache_key = 'pr_core_related_' . $peptide_id;
@@ -64,21 +76,23 @@ class PR_Core_Internal_Posts_Provider implements PR_Core_Related_Posts_Provider
* @return array
*/
private function query_by_taxonomy( string $peptide_slug, int $limit ): array {
- $query = new WP_Query( [
- 'post_type' => 'post',
- 'posts_per_page' => $limit,
- 'orderby' => 'date',
- 'order' => 'DESC',
- 'tax_query' => [
- [
- 'taxonomy' => 'peptide_topic',
- 'field' => 'slug',
- 'terms' => $peptide_slug,
- ],
- ],
- ] );
+ $query = new WP_Query(
+ array(
+ 'post_type' => 'post',
+ 'posts_per_page' => $limit,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'peptide_topic',
+ 'field' => 'slug',
+ 'terms' => $peptide_slug,
+ ),
+ ),
+ )
+ );
- return $query->posts ?: [];
+ return $query->posts ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
}
/**
@@ -89,14 +103,16 @@ class PR_Core_Internal_Posts_Provider implements PR_Core_Related_Posts_Provider
* @return array
*/
private function query_by_search( string $search_term, int $limit ): array {
- $query = new WP_Query( [
- 'post_type' => 'post',
- 'posts_per_page' => $limit,
- 'orderby' => 'date',
- 'order' => 'DESC',
- 's' => $search_term,
- ] );
+ $query = new WP_Query(
+ array(
+ 'post_type' => 'post',
+ 'posts_per_page' => $limit,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 's' => $search_term,
+ )
+ );
- return $query->posts ?: [];
+ return $query->posts ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
}
}
diff --git a/includes/core/class-pr-core-related-posts-provider.php b/includes/core/class-pr-core-related-posts-provider.php
index ef59707..869f4b2 100644
--- a/includes/core/class-pr-core-related-posts-provider.php
+++ b/includes/core/class-pr-core-related-posts-provider.php
@@ -1,6 +1,18 @@
$post ] );
+ get_template_part( 'template-parts/related-posts/card', null, array( 'post' => $post ) );
++$count;
}
wp_reset_postdata();
@@ -90,6 +106,7 @@ class PR_Core_Related_Posts_Section {
post_title )
)
diff --git a/includes/cpt/class-pr-core-peptide-cpt.php b/includes/cpt/class-pr-core-peptide-cpt.php
index d06b97a..13dd35c 100644
--- a/includes/cpt/class-pr-core-peptide-cpt.php
+++ b/includes/cpt/class-pr-core-peptide-cpt.php
@@ -1,6 +1,18 @@
>
*/
public static function get_meta_fields(): array {
- return [
- 'display_name' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- 'aliases' => [
- 'type' => 'string',
- 'default' => '[]',
- 'sanitize' => [ PR_Core_Schema_Sanitizers::class, 'sanitize_json_array' ],
- ],
- 'molecular_formula' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- 'molecular_weight' => [
- 'type' => 'number',
- 'default' => 0,
- 'sanitize' => 'floatval',
- ],
- 'cas_number' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- 'drugbank_id' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- 'chembl_id' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- 'evidence_strength' => [
- 'type' => 'string',
- 'default' => 'preclinical',
- 'sanitize' => [ __CLASS__, 'sanitize_evidence_strength' ],
- ],
- 'editorial_review_status' => [
- 'type' => 'string',
- 'default' => 'draft',
- 'sanitize' => [ __CLASS__, 'sanitize_review_status' ],
- ],
- 'last_editorial_review_at' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- 'medical_editor_id' => [
- 'type' => 'integer',
- 'default' => 0,
- 'sanitize' => 'absint',
- ],
- '_pr_last_source_verified' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- '_pr_last_reviewed' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- '_pr_next_review_by' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_text_field',
- ],
- '_pr_verification_velocity' => [
- 'type' => 'string',
- 'default' => 'medium',
- 'sanitize' => [ PR_Core_Verification_Sanitizers::class, 'sanitize_velocity' ],
- ],
- '_pr_verification_notes' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => 'sanitize_textarea_field',
- ],
- '_pr_verification_status' => [
- 'type' => 'string',
- 'default' => 'current',
- 'sanitize' => [ PR_Core_Verification_Sanitizers::class, 'sanitize_status' ],
- ],
- // Schema-input meta (v0.6.0): molecular data sourced from PSA psa_* keys via migration 0004.
- '_pr_molecular_formula' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => [ PR_Core_Schema_Sanitizers::class, 'sanitize_molecular_formula' ],
- ],
- '_pr_molecular_weight' => [
- 'type' => 'string',
- 'default' => '',
- 'sanitize' => [ PR_Core_Schema_Sanitizers::class, 'sanitize_molecular_weight_string' ],
- ],
- '_pr_aliases' => [
- 'type' => 'string',
- 'default' => '[]',
- 'sanitize' => [ PR_Core_Schema_Sanitizers::class, 'sanitize_json_array' ],
- ],
- // FAQ items (v0.6.0): [{question, answer}] JSON for FAQPage schema. Populated by CMO content sprint.
- '_pr_faq_items' => [
- 'type' => 'string',
- 'default' => '[]',
- 'sanitize' => [ PR_Core_Schema_Sanitizers::class, 'sanitize_faq_items' ],
- ],
- ];
+ return PR_Core_Peptide_Meta_Schema::get_fields();
}
/**
@@ -176,9 +92,9 @@ class PR_Core_Peptide_CPT {
* @return void
*/
public function register_hooks(): void {
- add_action( 'init', [ __CLASS__, 'register_peptide_post_type' ] );
- add_action( 'init', [ __CLASS__, 'register_taxonomies' ] );
- add_action( 'init', [ __CLASS__, 'register_meta_fields' ] );
+ add_action( 'init', array( __CLASS__, 'register_peptide_post_type' ) );
+ add_action( 'init', array( __CLASS__, 'register_taxonomies' ) );
+ add_action( 'init', array( __CLASS__, 'register_meta_fields' ) );
}
/**
@@ -193,7 +109,7 @@ class PR_Core_Peptide_CPT {
return;
}
- $labels = [
+ $labels = array(
'name' => __( 'Peptides', 'peptide-repo-core' ),
'singular_name' => __( 'Peptide', 'peptide-repo-core' ),
'add_new_item' => __( 'Add New Peptide', 'peptide-repo-core' ),
@@ -205,11 +121,11 @@ class PR_Core_Peptide_CPT {
'not_found_in_trash' => __( 'No peptides found in trash', 'peptide-repo-core' ),
'all_items' => __( 'All Peptides', 'peptide-repo-core' ),
'menu_name' => __( 'Peptides', 'peptide-repo-core' ),
- ];
+ );
// Args harmonized superset of PR Core + PSA for 89 existing posts.
// Supports union, capability/role perms preserved, slugs unchanged.
- $args = [
+ $args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
@@ -218,9 +134,9 @@ class PR_Core_Peptide_CPT {
'show_in_nav_menus' => true,
'show_in_rest' => true,
'rest_base' => 'peptides',
- // Note: 'rest_namespace' is deliberately omitted. Custom namespaces prevent
- // Gutenberg's block editor from loading posts for editing (it fetches the
- // hardcoded wp/v2 REST route). WordPress defaults to wp/v2 — appropriate for
+ // Note: 'rest_namespace' is deliberately omitted. Custom namespaces prevent.
+ // Gutenberg's block editor from loading posts for editing (it fetches the.
+ // hardcoded wp/v2 REST route). WordPress defaults to wp/v2 — appropriate for.
// this CPT — and keeps the REST endpoint at /wp-json/wp/v2/peptides/.
'menu_position' => 25,
'menu_icon' => 'dashicons-database',
@@ -228,9 +144,12 @@ class PR_Core_Peptide_CPT {
'map_meta_cap' => true,
'hierarchical' => false,
'has_archive' => true,
- 'rewrite' => [ 'slug' => 'peptides', 'with_front' => false ],
- 'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt', 'revisions', 'custom-fields' ],
- ];
+ 'rewrite' => array(
+ 'slug' => 'peptides',
+ 'with_front' => false,
+ ),
+ 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions', 'custom-fields' ),
+ );
register_post_type( self::POST_TYPE, $args );
}
@@ -246,19 +165,26 @@ class PR_Core_Peptide_CPT {
return;
}
- register_taxonomy( self::TAX_CATEGORY, self::POST_TYPE, [
- 'labels' => [
- 'name' => __( 'Peptide Categories', 'peptide-repo-core' ),
- 'singular_name' => __( 'Peptide Category', 'peptide-repo-core' ),
- ],
- 'public' => true,
- 'publicly_queryable' => true,
- 'show_in_rest' => true,
- 'show_ui' => true,
- 'show_admin_column' => true,
- 'hierarchical' => true,
- 'rewrite' => [ 'slug' => 'peptide-category', 'with_front' => false ],
- ] );
+ register_taxonomy(
+ self::TAX_CATEGORY,
+ self::POST_TYPE,
+ array(
+ 'labels' => array(
+ 'name' => __( 'Peptide Categories', 'peptide-repo-core' ),
+ 'singular_name' => __( 'Peptide Category', 'peptide-repo-core' ),
+ ),
+ 'public' => true,
+ 'publicly_queryable' => true,
+ 'show_in_rest' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'hierarchical' => true,
+ 'rewrite' => array(
+ 'slug' => 'peptide-category',
+ 'with_front' => false,
+ ),
+ )
+ );
}
/**
@@ -268,26 +194,40 @@ class PR_Core_Peptide_CPT {
*/
public static function register_meta_fields(): void {
foreach ( self::get_meta_fields() as $key => $config ) {
- register_post_meta( self::POST_TYPE, $key, [
- 'type' => $config['type'],
- 'single' => true,
- 'default' => $config['default'],
- 'show_in_rest' => true,
- 'sanitize_callback' => $config['sanitize'],
- 'auth_callback' => static function () {
- return current_user_can( self::CAPABILITY );
- },
- ] );
+ register_post_meta(
+ self::POST_TYPE,
+ $key,
+ array(
+ 'type' => $config['type'],
+ 'single' => true,
+ 'default' => $config['default'],
+ 'show_in_rest' => true,
+ 'sanitize_callback' => $config['sanitize'],
+ 'auth_callback' => static function () {
+ return current_user_can( self::CAPABILITY );
+ },
+ )
+ );
}
}
- /** Sanitize evidence_strength to allowed enum values. */
+ /**
+ * Sanitize evidence_strength to allowed enum values.
+ *
+ * @param mixed $value Raw input value.
+ * @return string
+ */
public static function sanitize_evidence_strength( $value ): string {
$value = sanitize_text_field( (string) $value );
return in_array( $value, self::EVIDENCE_STRENGTHS, true ) ? $value : 'preclinical';
}
- /** Sanitize editorial_review_status to allowed enum values. */
+ /**
+ * Sanitize editorial_review_status to allowed enum values.
+ *
+ * @param mixed $value Raw input value.
+ * @return string
+ */
public static function sanitize_review_status( $value ): string {
$value = sanitize_text_field( (string) $value );
return in_array( $value, self::REVIEW_STATUSES, true ) ? $value : 'draft';
diff --git a/includes/cpt/class-pr-core-peptide-meta-schema.php b/includes/cpt/class-pr-core-peptide-meta-schema.php
new file mode 100644
index 0000000..0d34b4c
--- /dev/null
+++ b/includes/cpt/class-pr-core-peptide-meta-schema.php
@@ -0,0 +1,144 @@
+>
+ */
+ public static function get_fields(): array {
+ return array(
+ 'display_name' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ 'aliases' => array(
+ 'type' => 'string',
+ 'default' => '[]',
+ 'sanitize' => array( PR_Core_Schema_Sanitizers::class, 'sanitize_json_array' ),
+ ),
+ 'molecular_formula' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ 'molecular_weight' => array(
+ 'type' => 'number',
+ 'default' => 0,
+ 'sanitize' => 'floatval',
+ ),
+ 'cas_number' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ 'drugbank_id' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ 'chembl_id' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ 'evidence_strength' => array(
+ 'type' => 'string',
+ 'default' => 'preclinical',
+ 'sanitize' => array( PR_Core_Peptide_CPT::class, 'sanitize_evidence_strength' ),
+ ),
+ 'editorial_review_status' => array(
+ 'type' => 'string',
+ 'default' => 'draft',
+ 'sanitize' => array( PR_Core_Peptide_CPT::class, 'sanitize_review_status' ),
+ ),
+ 'last_editorial_review_at' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ 'medical_editor_id' => array(
+ 'type' => 'integer',
+ 'default' => 0,
+ 'sanitize' => 'absint',
+ ),
+ '_pr_last_source_verified' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ '_pr_last_reviewed' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ '_pr_next_review_by' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ '_pr_verification_velocity' => array(
+ 'type' => 'string',
+ 'default' => 'medium',
+ 'sanitize' => array( PR_Core_Verification_Sanitizers::class, 'sanitize_velocity' ),
+ ),
+ '_pr_verification_notes' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => 'sanitize_textarea_field',
+ ),
+ '_pr_verification_status' => array(
+ 'type' => 'string',
+ 'default' => 'current',
+ 'sanitize' => array( PR_Core_Verification_Sanitizers::class, 'sanitize_status' ),
+ ),
+ '_pr_molecular_formula' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => array( PR_Core_Schema_Sanitizers::class, 'sanitize_molecular_formula' ),
+ ),
+ '_pr_molecular_weight' => array(
+ 'type' => 'string',
+ 'default' => '',
+ 'sanitize' => array( PR_Core_Schema_Sanitizers::class, 'sanitize_molecular_weight_string' ),
+ ),
+ '_pr_aliases' => array(
+ 'type' => 'string',
+ 'default' => '[]',
+ 'sanitize' => array( PR_Core_Schema_Sanitizers::class, 'sanitize_json_array' ),
+ ),
+ '_pr_faq_items' => array(
+ 'type' => 'string',
+ 'default' => '[]',
+ 'sanitize' => array( PR_Core_Schema_Sanitizers::class, 'sanitize_faq_items' ),
+ ),
+ );
+ }
+}
diff --git a/includes/cpt/class-pr-core-repo-daily-cpt.php b/includes/cpt/class-pr-core-repo-daily-cpt.php
index 2382520..6875ce2 100755
--- a/includes/cpt/class-pr-core-repo-daily-cpt.php
+++ b/includes/cpt/class-pr-core-repo-daily-cpt.php
@@ -1,6 +1,18 @@
__( 'Repo Daily', 'peptide-repo-core' ),
'singular_name' => __( 'Repo Daily', 'peptide-repo-core' ),
'add_new_item' => __( 'Add New Article', 'peptide-repo-core' ),
@@ -54,9 +70,9 @@ class PR_Core_Repo_Daily_CPT {
'not_found_in_trash' => __( 'No articles found in trash', 'peptide-repo-core' ),
'all_items' => __( 'All Articles', 'peptide-repo-core' ),
'menu_name' => __( 'Repo Daily', 'peptide-repo-core' ),
- ];
+ );
- $args = [
+ $args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
@@ -71,9 +87,12 @@ class PR_Core_Repo_Daily_CPT {
'map_meta_cap' => true,
'hierarchical' => false,
'has_archive' => true,
- 'rewrite' => [ 'slug' => 'daily', 'with_front' => false ],
- 'supports' => [ 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields' ],
- ];
+ 'rewrite' => array(
+ 'slug' => 'daily',
+ 'with_front' => false,
+ ),
+ 'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields' ),
+ );
register_post_type( self::POST_TYPE, $args );
}
diff --git a/includes/cpt/class-pr-core-schema-sanitizers.php b/includes/cpt/class-pr-core-schema-sanitizers.php
index 8d9b24f..865a1f1 100644
--- a/includes/cpt/class-pr-core-schema-sanitizers.php
+++ b/includes/cpt/class-pr-core-schema-sanitizers.php
@@ -1,6 +1,18 @@
$question,
'answer' => $answer,
- ];
+ );
}
}
- return wp_json_encode( $clean, JSON_UNESCAPED_UNICODE ) ?: '[]';
+ return wp_json_encode( $clean, JSON_UNESCAPED_UNICODE ) ?: '[]'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
}
}
diff --git a/includes/cpt/class-pr-core-topic-taxonomy.php b/includes/cpt/class-pr-core-topic-taxonomy.php
index 9dcf5f5..40f479f 100644
--- a/includes/cpt/class-pr-core-topic-taxonomy.php
+++ b/includes/cpt/class-pr-core-topic-taxonomy.php
@@ -1,6 +1,18 @@
[
- 'name' => __( 'Peptide Topics', 'peptide-repo-core' ),
- 'singular_name' => __( 'Peptide Topic', 'peptide-repo-core' ),
- ],
- 'public' => true,
- 'publicly_queryable' => true,
- 'show_in_rest' => true,
- 'show_ui' => true,
- 'show_admin_column' => true,
- 'hierarchical' => false,
- 'rewrite' => [ 'slug' => 'peptide-topic', 'with_front' => false ],
- ] );
+ register_taxonomy(
+ self::TAX_TOPIC,
+ 'post',
+ array(
+ 'labels' => array(
+ 'name' => __( 'Peptide Topics', 'peptide-repo-core' ),
+ 'singular_name' => __( 'Peptide Topic', 'peptide-repo-core' ),
+ ),
+ 'public' => true,
+ 'publicly_queryable' => true,
+ 'show_in_rest' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'hierarchical' => false,
+ 'rewrite' => array(
+ 'slug' => 'peptide-topic',
+ 'with_front' => false,
+ ),
+ )
+ );
}
}
diff --git a/includes/cpt/class-pr-core-verification-sanitizers.php b/includes/cpt/class-pr-core-verification-sanitizers.php
index 5b0a932..648a909 100644
--- a/includes/cpt/class-pr-core-verification-sanitizers.php
+++ b/includes/cpt/class-pr-core-verification-sanitizers.php
@@ -1,6 +1,18 @@
$data Associative array from database row.
*/
public function __construct( array $data ) {
@@ -74,7 +208,7 @@ class PR_Core_Candidate_DTO {
* @return array
*/
public function to_array(): array {
- return [
+ return array(
'id' => $this->id,
'peptide_id' => $this->peptide_id,
'dose_min' => $this->dose_min,
@@ -90,6 +224,6 @@ class PR_Core_Candidate_DTO {
'extraction_confidence' => $this->extraction_confidence,
'queue_status' => $this->queue_status,
'extracted_at' => $this->extracted_at,
- ];
+ );
}
}
diff --git a/includes/dto/class-pr-core-dosing-row-dto.php b/includes/dto/class-pr-core-dosing-row-dto.php
index 98724b5..b7548bd 100644
--- a/includes/dto/class-pr-core-dosing-row-dto.php
+++ b/includes/dto/class-pr-core-dosing-row-dto.php
@@ -1,6 +1,18 @@
$data Associative array from database row.
*/
public function __construct( array $data ) {
- $this->id = (int) ( $data['id'] ?? 0 );
- $this->peptide_id = (int) ( $data['peptide_id'] ?? 0 );
- $this->dose_min = isset( $data['dose_min'] ) ? (float) $data['dose_min'] : null;
- $this->dose_max = isset( $data['dose_max'] ) ? (float) $data['dose_max'] : null;
- $this->dose_unit = (string) ( $data['dose_unit'] ?? '' );
- $this->route = (string) ( $data['route'] ?? '' );
- $this->frequency = $data['frequency'] ?? null;
- $this->duration_value = isset( $data['duration_value'] ) ? (int) $data['duration_value'] : null;
- $this->duration_unit = $data['duration_unit'] ?? null;
- $this->population = (string) ( $data['population'] ?? '' );
- $this->indication = $data['indication'] ?? null;
- $this->evidence_strength = (string) ( $data['evidence_strength'] ?? 'preclinical' );
- $this->study_title = $data['study_title'] ?? null;
- $this->study_year = isset( $data['study_year'] ) ? (int) $data['study_year'] : null;
- $this->citation_pubmed_id = $data['citation_pubmed_id'] ?? null;
- $this->citation_doi = $data['citation_doi'] ?? null;
- $this->citation_url = $data['citation_url'] ?? null;
- $this->notes = $data['notes'] ?? null;
- $this->schema_version = (int) ( $data['schema_version'] ?? 1 );
- $this->source = (string) ( $data['source'] ?? 'manual' );
- $this->ai_candidate_id = isset( $data['ai_candidate_id'] ) ? (int) $data['ai_candidate_id'] : null;
- $this->added_by = (int) ( $data['added_by'] ?? 0 );
- $this->added_at = (string) ( $data['added_at'] ?? '' );
- $this->reviewed_by = isset( $data['reviewed_by'] ) ? (int) $data['reviewed_by'] : null;
- $this->reviewed_at = $data['reviewed_at'] ?? null;
- $this->superseded_by_id = isset( $data['superseded_by_id'] ) ? (int) $data['superseded_by_id'] : null;
+ $this->id = (int) ( $data['id'] ?? 0 );
+ $this->peptide_id = (int) ( $data['peptide_id'] ?? 0 );
+ $this->dose_min = isset( $data['dose_min'] ) ? (float) $data['dose_min'] : null;
+ $this->dose_max = isset( $data['dose_max'] ) ? (float) $data['dose_max'] : null;
+ $this->dose_unit = (string) ( $data['dose_unit'] ?? '' );
+ $this->route = (string) ( $data['route'] ?? '' );
+ $this->frequency = $data['frequency'] ?? null;
+ $this->duration_value = isset( $data['duration_value'] ) ? (int) $data['duration_value'] : null;
+ $this->duration_unit = $data['duration_unit'] ?? null;
+ $this->population = (string) ( $data['population'] ?? '' );
+ $this->indication = $data['indication'] ?? null;
+ $this->evidence_strength = (string) ( $data['evidence_strength'] ?? 'preclinical' );
+ $this->study_title = $data['study_title'] ?? null;
+ $this->study_year = isset( $data['study_year'] ) ? (int) $data['study_year'] : null;
+ $this->citation_pubmed_id = $data['citation_pubmed_id'] ?? null;
+ $this->citation_doi = $data['citation_doi'] ?? null;
+ $this->citation_url = $data['citation_url'] ?? null;
+ $this->notes = $data['notes'] ?? null;
+ $this->schema_version = (int) ( $data['schema_version'] ?? 1 );
+ $this->source = (string) ( $data['source'] ?? 'manual' );
+ $this->ai_candidate_id = isset( $data['ai_candidate_id'] ) ? (int) $data['ai_candidate_id'] : null;
+ $this->added_by = (int) ( $data['added_by'] ?? 0 );
+ $this->added_at = (string) ( $data['added_at'] ?? '' );
+ $this->reviewed_by = isset( $data['reviewed_by'] ) ? (int) $data['reviewed_by'] : null;
+ $this->reviewed_at = $data['reviewed_at'] ?? null;
+ $this->superseded_by_id = isset( $data['superseded_by_id'] ) ? (int) $data['superseded_by_id'] : null;
}
/**
@@ -78,27 +222,27 @@ class PR_Core_Dosing_Row_DTO {
* @return array
*/
public function to_array(): array {
- return [
- 'id' => $this->id,
- 'peptide_id' => $this->peptide_id,
- 'dose_min' => $this->dose_min,
- 'dose_max' => $this->dose_max,
- 'dose_unit' => $this->dose_unit,
- 'route' => $this->route,
- 'frequency' => $this->frequency,
- 'duration_value' => $this->duration_value,
- 'duration_unit' => $this->duration_unit,
- 'population' => $this->population,
- 'indication' => $this->indication,
- 'evidence_strength' => $this->evidence_strength,
- 'study_title' => $this->study_title,
- 'study_year' => $this->study_year,
- 'citation_pubmed_id' => $this->citation_pubmed_id,
- 'citation_doi' => $this->citation_doi,
- 'citation_url' => $this->citation_url,
- 'notes' => $this->notes,
- 'source' => $this->source,
- 'added_at' => $this->added_at,
- ];
+ return array(
+ 'id' => $this->id,
+ 'peptide_id' => $this->peptide_id,
+ 'dose_min' => $this->dose_min,
+ 'dose_max' => $this->dose_max,
+ 'dose_unit' => $this->dose_unit,
+ 'route' => $this->route,
+ 'frequency' => $this->frequency,
+ 'duration_value' => $this->duration_value,
+ 'duration_unit' => $this->duration_unit,
+ 'population' => $this->population,
+ 'indication' => $this->indication,
+ 'evidence_strength' => $this->evidence_strength,
+ 'study_title' => $this->study_title,
+ 'study_year' => $this->study_year,
+ 'citation_pubmed_id' => $this->citation_pubmed_id,
+ 'citation_doi' => $this->citation_doi,
+ 'citation_url' => $this->citation_url,
+ 'notes' => $this->notes,
+ 'source' => $this->source,
+ 'added_at' => $this->added_at,
+ );
}
}
diff --git a/includes/dto/class-pr-core-legal-cell-dto.php b/includes/dto/class-pr-core-legal-cell-dto.php
index ea0b8d0..9f2aea7 100644
--- a/includes/dto/class-pr-core-legal-cell-dto.php
+++ b/includes/dto/class-pr-core-legal-cell-dto.php
@@ -1,6 +1,18 @@
$data Associative array from database row.
*/
public function __construct( array $data ) {
@@ -59,7 +133,7 @@ class PR_Core_Legal_Cell_DTO {
* @return array
*/
public function to_array(): array {
- return [
+ return array(
'id' => $this->id,
'peptide_id' => $this->peptide_id,
'country_code' => $this->country_code,
@@ -70,6 +144,6 @@ class PR_Core_Legal_Cell_DTO {
'notes' => $this->notes,
'last_verified_at' => $this->last_verified_at,
'is_active' => $this->is_active(),
- ];
+ );
}
}
diff --git a/includes/dto/class-pr-core-peptide-dto.php b/includes/dto/class-pr-core-peptide-dto.php
index d8b02ed..047f123 100644
--- a/includes/dto/class-pr-core-peptide-dto.php
+++ b/includes/dto/class-pr-core-peptide-dto.php
@@ -1,6 +1,18 @@
$data Associative array of peptide fields.
*/
public function __construct( array $data ) {
@@ -47,7 +153,7 @@ class PR_Core_Peptide_DTO {
$this->excerpt = (string) ( $data['excerpt'] ?? '' );
$this->status = (string) ( $data['status'] ?? 'draft' );
$this->display_name = (string) ( $data['display_name'] ?? '' );
- $this->aliases = (array) ( $data['aliases'] ?? [] );
+ $this->aliases = (array) ( $data['aliases'] ?? array() );
$this->molecular_formula = (string) ( $data['molecular_formula'] ?? '' );
$this->molecular_weight = (float) ( $data['molecular_weight'] ?? 0 );
$this->cas_number = (string) ( $data['cas_number'] ?? '' );
@@ -57,8 +163,8 @@ class PR_Core_Peptide_DTO {
$this->editorial_review_status = (string) ( $data['editorial_review_status'] ?? 'draft' );
$this->last_editorial_review_at = (string) ( $data['last_editorial_review_at'] ?? '' );
$this->medical_editor_id = (int) ( $data['medical_editor_id'] ?? 0 );
- $this->categories = (array) ( $data['categories'] ?? [] );
- $this->families = (array) ( $data['families'] ?? [] );
+ $this->categories = (array) ( $data['categories'] ?? array() );
+ $this->families = (array) ( $data['families'] ?? array() );
}
/**
@@ -67,7 +173,7 @@ class PR_Core_Peptide_DTO {
* @return array
*/
public function to_array(): array {
- return [
+ return array(
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
@@ -86,6 +192,6 @@ class PR_Core_Peptide_DTO {
'categories' => $this->categories,
'families' => $this->families,
'url' => get_permalink( $this->id ),
- ];
+ );
}
}
diff --git a/includes/frontend/class-pr-core-disclaimer.php b/includes/frontend/class-pr-core-disclaimer.php
index e46f7b9..d24f8f7 100644
--- a/includes/frontend/class-pr-core-disclaimer.php
+++ b/includes/frontend/class-pr-core-disclaimer.php
@@ -1,6 +1,18 @@
*/
- private const DEFAULTS = [
- 'dosing' => 'The dosing information presented here is compiled from published peer-reviewed literature and is provided for educational and research purposes only. It does not constitute medical advice, diagnosis, or treatment recommendations. Always consult a licensed healthcare professional before making any decisions based on this information.',
- 'legal' => 'Legal status information is compiled from publicly available regulatory texts and is current as of the last-verified date shown. Regulatory frameworks change frequently. This information does not constitute legal advice. Verify current regulations with the relevant regulatory authority in your jurisdiction before taking any action.',
- 'reconstitution' => 'This calculator is provided for educational purposes only. Always verify your calculations independently and consult a licensed healthcare professional. The results are based on the inputs you provide and standard reconstitution mathematics.',
- 'ai-answer' => 'This response was generated by an AI system and may contain errors or omissions. All claims are grounded in the Peptide Repo corpus and cited sources where possible. This is not medical advice. Always verify information with primary sources and consult a licensed healthcare professional.',
- 'general' => 'The information on this site is for educational and research purposes only. It is not intended as medical advice, diagnosis, or treatment. Always consult a licensed healthcare professional.',
- ];
+ private const DEFAULTS = array(
+ 'dosing' => 'The dosing information presented here is compiled from published peer-reviewed literature and is provided for educational and research purposes only. It does not constitute medical advice, diagnosis, or treatment recommendations. Always consult a licensed healthcare professional before making any decisions based on this information.',
+ 'legal' => 'Legal status information is compiled from publicly available regulatory texts and is current as of the last-verified date shown. Regulatory frameworks change frequently. This information does not constitute legal advice. Verify current regulations with the relevant regulatory authority in your jurisdiction before taking any action.',
+ 'reconstitution' => 'This calculator is provided for educational purposes only. Always verify your calculations independently and consult a licensed healthcare professional. The results are based on the inputs you provide and standard reconstitution mathematics.',
+ 'ai-answer' => 'This response was generated by an AI system and may contain errors or omissions. All claims are grounded in the Peptide Repo corpus and cited sources where possible. This is not medical advice. Always verify information with primary sources and consult a licensed healthcare professional.',
+ 'general' => 'The information on this site is for educational and research purposes only. It is not intended as medical advice, diagnosis, or treatment. Always consult a licensed healthcare professional.',
+ );
/**
* Register shortcode and any frontend hooks.
@@ -42,7 +62,7 @@ class PR_Core_Disclaimer {
* @return void
*/
public function register_hooks(): void {
- add_shortcode( 'pr_disclaimer', [ $this, 'shortcode_handler' ] );
+ add_shortcode( 'pr_disclaimer', array( $this, 'shortcode_handler' ) );
}
/**
@@ -52,7 +72,7 @@ class PR_Core_Disclaimer {
* @return string Disclaimer HTML.
*/
public function shortcode_handler( $atts ): string {
- $atts = shortcode_atts( [ 'surface' => 'general' ], $atts, 'pr_disclaimer' );
+ $atts = shortcode_atts( array( 'surface' => 'general' ), $atts, 'pr_disclaimer' );
return self::render( sanitize_text_field( $atts['surface'] ) );
}
@@ -81,7 +101,7 @@ class PR_Core_Disclaimer {
*/
public static function get_disclaimer_text( string $surface ): string {
$custom = get_option( self::COPY_OPTION, '{}' );
- $copy = json_decode( $custom, true ) ?: [];
+ $copy = json_decode( $custom, true ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
if ( ! empty( $copy[ $surface ] ) ) {
return (string) $copy[ $surface ];
@@ -110,7 +130,7 @@ class PR_Core_Disclaimer {
*/
public static function update_disclaimer( string $surface, string $text ): void {
$custom = get_option( self::COPY_OPTION, '{}' );
- $copy = json_decode( $custom, true ) ?: [];
+ $copy = json_decode( $custom, true ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
$copy[ sanitize_text_field( $surface ) ] = sanitize_textarea_field( $text );
diff --git a/includes/frontend/class-pr-core-jsonld-drug.php b/includes/frontend/class-pr-core-jsonld-drug.php
index aad78d4..b5ef29e 100644
--- a/includes/frontend/class-pr-core-jsonld-drug.php
+++ b/includes/frontend/class-pr-core-jsonld-drug.php
@@ -1,6 +1,18 @@
id );
- $node = [
- '@type' => [ 'Drug', 'MolecularEntity' ],
+ $node = array(
+ '@type' => array( 'Drug', 'MolecularEntity' ),
'@id' => $permalink . '#drug',
- 'name' => $peptide->display_name ?: $peptide->title,
+ 'name' => $peptide->display_name ?: $peptide->title, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
'url' => $permalink,
- ];
+ );
if ( '' !== $peptide->excerpt ) {
$node['description'] = $peptide->excerpt;
@@ -64,11 +92,11 @@ class PR_Core_Jsonld_Drug {
// molecularWeight: prefer _pr_molecular_weight, fall back to DTO field.
$weight = $this->get_weight( $peptide );
if ( $weight > 0.0 ) {
- $node['molecularWeight'] = [
+ $node['molecularWeight'] = array(
'@type' => 'QuantitativeValue',
'value' => $weight,
'unitText' => self::UNIT_TEXT,
- ];
+ );
}
// External identifiers: CAS and DrugBank omitted in v0.6.0 (no source).
@@ -150,9 +178,9 @@ class PR_Core_Jsonld_Drug {
* @return array|null
*/
private function build_legal_status(): ?array {
- return [
+ return array(
'@type' => 'DrugLegalStatus',
'description' => esc_html__( 'Not approved for human therapeutic use; supplied for laboratory research purposes only.', 'peptide-repo-core' ),
- ];
+ );
}
}
diff --git a/includes/frontend/class-pr-core-jsonld-faq.php b/includes/frontend/class-pr-core-jsonld-faq.php
index baba40d..45c4c03 100644
--- a/includes/frontend/class-pr-core-jsonld-faq.php
+++ b/includes/frontend/class-pr-core-jsonld-faq.php
@@ -1,6 +1,18 @@
'FAQPage',
'@id' => $permalink . '#faq',
- 'mainEntity' => array_map( [ $this, 'build_question' ], $items ),
- ];
+ 'mainEntity' => array_map( array( $this, 'build_question' ), $items ),
+ );
}
/**
@@ -48,14 +64,14 @@ class PR_Core_Jsonld_Faq {
* @return array Schema.org Question node.
*/
private function build_question( array $item ): array {
- return [
- '@type' => 'Question',
- 'name' => esc_html( $item['question'] ),
- 'acceptedAnswer' => [
+ return array(
+ '@type' => 'Question',
+ 'name' => esc_html( $item['question'] ),
+ 'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => esc_html( $item['answer'] ),
- ],
- ];
+ ),
+ );
}
/**
@@ -71,16 +87,16 @@ class PR_Core_Jsonld_Faq {
$raw = get_post_meta( $post_id, self::META_FAQ_ITEMS, true );
if ( ! is_string( $raw ) || '' === $raw ) {
- return [];
+ return array();
}
$decoded = json_decode( $raw, true );
if ( ! is_array( $decoded ) || empty( $decoded ) ) {
- return [];
+ return array();
}
- $valid = [];
+ $valid = array();
foreach ( $decoded as $item ) {
if ( ! is_array( $item ) ) {
continue;
@@ -88,10 +104,10 @@ class PR_Core_Jsonld_Faq {
$question = sanitize_text_field( (string) ( $item['question'] ?? '' ) );
$answer = sanitize_textarea_field( (string) ( $item['answer'] ?? '' ) );
if ( '' !== $question && '' !== $answer ) {
- $valid[] = [
+ $valid[] = array(
'question' => $question,
'answer' => $answer,
- ];
+ );
}
}
diff --git a/includes/frontend/class-pr-core-jsonld-webpage.php b/includes/frontend/class-pr-core-jsonld-webpage.php
index 4b51e87..ca43eb8 100644
--- a/includes/frontend/class-pr-core-jsonld-webpage.php
+++ b/includes/frontend/class-pr-core-jsonld-webpage.php
@@ -1,6 +1,18 @@
$value ) {
@@ -97,16 +117,16 @@ class PR_Core_Jsonld_Webpage {
* @return array Enrichment fields to merge into the WebPage node.
*/
private function build_webpage_enrichments( int $post_id ): array {
- $enrichments = [];
+ $enrichments = array();
- // lastReviewed: prefer _pr_last_reviewed, fall back to _pr_last_source_verified,
+ // lastReviewed: prefer _pr_last_reviewed, fall back to _pr_last_source_verified,.
// then post modified date.
$last_reviewed = (string) get_post_meta( $post_id, self::META_LAST_REVIEWED, true );
if ( '' === $last_reviewed ) {
$last_reviewed = (string) get_post_meta( $post_id, self::META_LAST_VERIFIED, true );
}
if ( '' === $last_reviewed ) {
- $last_reviewed = get_post_modified_time( 'Y-m-d', false, $post_id ) ?: '';
+ $last_reviewed = get_post_modified_time( 'Y-m-d', false, $post_id ) ?: ''; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
}
if ( '' !== $last_reviewed ) {
@@ -118,27 +138,27 @@ class PR_Core_Jsonld_Webpage {
if ( $editor_id > 0 ) {
$editor = get_userdata( $editor_id );
if ( $editor ) {
- $enrichments['reviewedBy'] = [
+ $enrichments['reviewedBy'] = array(
'@type' => 'Person',
'name' => sanitize_text_field( $editor->display_name ),
'url' => get_author_posts_url( $editor_id ),
- ];
+ );
}
}
if ( ! isset( $enrichments['reviewedBy'] ) ) {
- $enrichments['reviewedBy'] = [
+ $enrichments['reviewedBy'] = array(
'@type' => 'Organization',
'name' => 'Peptide Repo',
'url' => home_url(),
- ];
+ );
}
// audience: MedicalAudience for researchers.
- $enrichments['audience'] = [
+ $enrichments['audience'] = array(
'@type' => 'MedicalAudience',
'audienceType' => 'Researcher',
- ];
+ );
return $enrichments;
}
diff --git a/includes/frontend/class-pr-core-jsonld.php b/includes/frontend/class-pr-core-jsonld.php
index c9ad16e..4bad099 100644
--- a/includes/frontend/class-pr-core-jsonld.php
+++ b/includes/frontend/class-pr-core-jsonld.php
@@ -1,11 +1,24 @@
MedicalWebPage on peptide singles.
- add_filter( 'wpseo_schema_webpage_type', [ $this->webpage_enricher, 'retype_to_medical_webpage' ] );
+ add_filter( 'wpseo_schema_webpage_type', array( $this->webpage_enricher, 'retype_to_medical_webpage' ) );
// wpseo_schema_graph: two callbacks at different priorities.
- // Priority 11 -- enrich_webpage_piece: merge lastReviewed/reviewedBy/audience into the
- // existing WebPage node that Yoast already emitted.
+ // Priority 11 -- enrich_webpage_piece: merge lastReviewed/reviewedBy/audience into the.
+ // existing WebPage node that Yoast already emitted.
// Priority 12 -- inject_graph_nodes: append Drug + FAQPage arrays to the graph.
- // Must run after enrich_webpage_piece so the WebPage node is already
- // enriched before Drug/FAQ are appended.
- add_filter( 'wpseo_schema_graph', [ $this->webpage_enricher, 'enrich_webpage_piece' ], 11, 2 );
- add_filter( 'wpseo_schema_graph', [ $this, 'inject_graph_nodes' ], 12, 2 );
+ // Must run after enrich_webpage_piece so the WebPage node is already.
+ // enriched before Drug/FAQ are appended.
+ add_filter( 'wpseo_schema_graph', array( $this->webpage_enricher, 'enrich_webpage_piece' ), 11, 2 );
+ add_filter( 'wpseo_schema_graph', array( $this, 'inject_graph_nodes' ), 12, 2 );
// Standalone fallback: emits only when Yoast is not producing output.
- add_action( 'wp_head', [ $this, 'emit_standalone_fallback' ], 99 );
+ add_action( 'wp_head', array( $this, 'emit_standalone_fallback' ), 99 );
}
/**
@@ -90,8 +115,8 @@ class PR_Core_Jsonld {
* Appending plain Drug/FAQ arrays here is safe -- unlike wpseo_schema_graph_pieces
* which calls get_class() and is_needed() on each item expecting objects.
*
- * @param array> $graph Assembled Yoast graph.
- * @param \Yoast\WP\SEO\Context\Meta_Tags_Context|null $context Yoast schema context.
+ * @param array> $graph Assembled Yoast graph.
+ * @param \Yoast\WP\SEO\Context\Meta_Tags_Context|null $context Yoast schema context.
* @return array> Graph with Drug (and optionally FAQ) appended.
*/
public function inject_graph_nodes( array $graph, $context ): array {
@@ -150,7 +175,7 @@ class PR_Core_Jsonld {
}
$permalink = get_permalink( (int) $post_id );
- $graph = [ $this->drug_builder->build( $peptide ) ];
+ $graph = array( $this->drug_builder->build( $peptide ) );
$faq_node = $this->faq_builder->build( (int) $post_id, $permalink );
if ( null !== $faq_node ) {
@@ -160,7 +185,10 @@ class PR_Core_Jsonld {
printf(
'' . "\n",
wp_json_encode(
- [ '@context' => 'https://schema.org', '@graph' => $graph ],
+ array(
+ '@context' => 'https://schema.org',
+ '@graph' => $graph,
+ ),
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
)
);
diff --git a/includes/frontend/class-pr-core-verification-display.php b/includes/frontend/class-pr-core-verification-display.php
index 57a1afd..30c56fe 100644
--- a/includes/frontend/class-pr-core-verification-display.php
+++ b/includes/frontend/class-pr-core-verification-display.php
@@ -1,6 +1,18 @@
format( get_option( 'date_format' ) );
- $html = '';
+ $html = '
';
$html .= esc_html__( 'Last verified: ', 'peptide-repo-core' );
$html .= sprintf(
'',
diff --git a/includes/interfaces/interface-related-posts-provider.php b/includes/interfaces/interface-related-posts-provider.php
index d999c1d..fb14e25 100644
--- a/includes/interfaces/interface-related-posts-provider.php
+++ b/includes/interfaces/interface-related-posts-provider.php
@@ -1,6 +1,18 @@
prefix . 'pr_legal_cells';
$charset = $wpdb->get_charset_collate();
- $sql = "CREATE TABLE {$table} (
+ $sql = "CREATE TABLE {$table} ( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
peptide_id BIGINT UNSIGNED NOT NULL,
country_code CHAR(2) NOT NULL,
@@ -61,7 +77,7 @@ class PR_Core_Migration_0002_Legal_Cells {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$index_exists = $wpdb->get_var(
$wpdb->prepare(
- "SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND INDEX_NAME = %s",
+ 'SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND INDEX_NAME = %s',
DB_NAME,
$table,
'uq_peptide_country_active'
@@ -71,7 +87,7 @@ class PR_Core_Migration_0002_Legal_Cells {
if ( '0' === $index_exists ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->query(
- "ALTER TABLE {$table} ADD UNIQUE KEY uq_peptide_country_active (peptide_id, country_code, superseded_by_id)"
+ "ALTER TABLE {$table} ADD UNIQUE KEY uq_peptide_country_active (peptide_id, country_code, superseded_by_id)" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
}
diff --git a/includes/migrations/class-pr-core-migration-0003-candidate-queue.php b/includes/migrations/class-pr-core-migration-0003-candidate-queue.php
index e8665ae..00f6c18 100644
--- a/includes/migrations/class-pr-core-migration-0003-candidate-queue.php
+++ b/includes/migrations/class-pr-core-migration-0003-candidate-queue.php
@@ -1,6 +1,18 @@
0, 'copied_psa' => 0, 'pubchem_ok' => 0, 'pubchem_skip' => 0 ];
+ $counts = array(
+ 'skipped' => 0,
+ 'copied_psa' => 0,
+ 'pubchem_ok' => 0,
+ 'pubchem_skip' => 0,
+ );
foreach ( $peptide_ids as $post_id ) {
$result = $this->backfill_post( $post_id );
$counts[ $result ] += 1;
}
- error_log( sprintf(
- '[PR Core Migration 0004] Complete. Skipped: %d, PSA-copied: %d, PubChem-ok: %d, PubChem-skip: %d',
- $counts['skipped'],
- $counts['copied_psa'],
- $counts['pubchem_ok'],
- $counts['pubchem_skip']
- ) );
+ error_log(
+ sprintf(
+ '[PR Core Migration 0004] Complete. Skipped: %d, PSA-copied: %d, PubChem-ok: %d, PubChem-skip: %d',
+ $counts['skipped'],
+ $counts['copied_psa'],
+ $counts['pubchem_ok'],
+ $counts['pubchem_skip']
+ )
+ );
}
/**
@@ -93,10 +110,10 @@ class PR_Core_Migration_0004_Backfill_Peptide_Meta {
$existing_weight = (string) get_post_meta( $post_id, self::META_WEIGHT, true );
$existing_aliases = (string) get_post_meta( $post_id, self::META_ALIASES, true );
- // Idempotency limitation: this guard is all-or-nothing. A peptide that legitimately
- // has no aliases (e.g., a novel compound) will never satisfy all three conditions and
- // the migration will re-run on every manual re-invoke. This is safe because
- // write_meta_from_psa() / write_meta_from_pubchem() only overwrite with non-empty
+ // Idempotency limitation: this guard is all-or-nothing. A peptide that legitimately.
+ // has no aliases (e.g., a novel compound) will never satisfy all three conditions and.
+ // the migration will re-run on every manual re-invoke. This is safe because.
+ // write_meta_from_psa() / write_meta_from_pubchem() only overwrite with non-empty.
// values, but callers should be aware the 'skipped' result requires all three keys set.
if ( '' !== $existing_formula && '' !== $existing_weight && '' !== $existing_aliases ) {
return 'skipped';
@@ -125,26 +142,30 @@ class PR_Core_Migration_0004_Backfill_Peptide_Meta {
}
if ( null === $data ) {
- error_log( sprintf(
- '[PR Core Migration 0004] Post ID %d (%s): PubChem returned no data — leaving meta empty.',
- $post_id,
- esc_html( $name )
- ) );
+ error_log(
+ sprintf(
+ '[PR Core Migration 0004] Post ID %d (%s): PubChem returned no data — leaving meta empty.',
+ $post_id,
+ esc_html( $name )
+ )
+ );
return 'pubchem_skip';
}
$formula = PR_Core_Schema_Sanitizers::sanitize_molecular_formula( $data['formula'] );
$weight = (float) $data['weight'];
- $aliases_json = $this->synonyms_to_json( $data['synonyms'] ?? [] );
+ $aliases_json = $this->synonyms_to_json( $data['synonyms'] ?? array() );
$this->write_pr_meta( $post_id, $formula, $weight, $aliases_json );
- error_log( sprintf(
- '[PR Core Migration 0004] Post ID %d (%s): PubChem-sourced formula=%s weight=%s',
- $post_id,
- esc_html( $name ),
- $formula,
- (string) $weight
- ) );
+ error_log(
+ sprintf(
+ '[PR Core Migration 0004] Post ID %d (%s): PubChem-sourced formula=%s weight=%s',
+ $post_id,
+ esc_html( $name ),
+ $formula,
+ (string) $weight
+ )
+ );
return 'pubchem_ok';
}
@@ -223,7 +244,7 @@ class PR_Core_Migration_0004_Backfill_Peptide_Meta {
return '[]';
}
- $clean = [];
+ $clean = array();
foreach ( explode( ',', $value ) as $part ) {
$alias = sanitize_text_field( trim( $part ) );
if ( '' !== $alias ) {
@@ -231,7 +252,7 @@ class PR_Core_Migration_0004_Backfill_Peptide_Meta {
}
}
- return wp_json_encode( $clean, JSON_UNESCAPED_UNICODE ) ?: '[]';
+ return wp_json_encode( $clean, JSON_UNESCAPED_UNICODE ) ?: '[]'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
}
/**
@@ -243,14 +264,14 @@ class PR_Core_Migration_0004_Backfill_Peptide_Meta {
* @return string JSON array of sanitized strings.
*/
private function synonyms_to_json( array $synonyms ): string {
- $clean = [];
+ $clean = array();
foreach ( $synonyms as $syn ) {
$alias = sanitize_text_field( (string) $syn );
if ( '' !== $alias ) {
$clean[] = $alias;
}
}
- return wp_json_encode( $clean, JSON_UNESCAPED_UNICODE ) ?: '[]';
+ return wp_json_encode( $clean, JSON_UNESCAPED_UNICODE ) ?: '[]'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
}
/**
@@ -259,12 +280,14 @@ class PR_Core_Migration_0004_Backfill_Peptide_Meta {
* @return int[]
*/
private function get_all_peptide_ids(): array {
- $posts = get_posts( [
- 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
- 'post_status' => 'publish',
- 'posts_per_page' => -1,
- 'fields' => 'ids',
- ] );
- return array_map( 'intval', is_array( $posts ) ? $posts : [] );
+ $posts = get_posts(
+ array(
+ 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ )
+ );
+ return array_map( 'intval', is_array( $posts ) ? $posts : array() );
}
}
diff --git a/includes/migrations/class-pr-core-migration-runner.php b/includes/migrations/class-pr-core-migration-runner.php
index b53e2e1..43ac052 100644
--- a/includes/migrations/class-pr-core-migration-runner.php
+++ b/includes/migrations/class-pr-core-migration-runner.php
@@ -1,6 +1,18 @@
migrations/class-pr-core-migration-0001-dosing-rows.php
*
- * @param string $class Fully-qualified class name.
+ * @param string $class_name Fully-qualified class name.
* @return string Absolute file path.
*/
- private function class_to_file( string $class ): string {
- $suffix = substr( $class, 8 ); // Strip 'PR_Core_'.
+ private function class_to_file( string $class_name ): string {
+ $suffix = substr( $class_name, 8 ); // Strip 'PR_Core_'.
$filename = 'class-pr-core-' . str_replace( '_', '-', strtolower( $suffix ) ) . '.php';
return PR_CORE_PLUGIN_DIR . 'includes/migrations/' . $filename;
diff --git a/includes/migrations/class-pr-core-pubchem-client.php b/includes/migrations/class-pr-core-pubchem-client.php
index 994bac0..cebe139 100644
--- a/includes/migrations/class-pr-core-pubchem-client.php
+++ b/includes/migrations/class-pr-core-pubchem-client.php
@@ -1,6 +1,18 @@
$formula,
'weight' => $weight,
'cid' => $cid,
- ];
+ );
}
/**
@@ -130,14 +158,14 @@ class PR_Core_Pubchem_Client {
$body = $this->get_with_retry( $url );
if ( null === $body ) {
- return [];
+ return array();
}
$data = json_decode( $body, true );
- $synonyms = $data['InformationList']['Information'][0]['Synonym'] ?? [];
+ $synonyms = $data['InformationList']['Information'][0]['Synonym'] ?? array();
if ( ! is_array( $synonyms ) ) {
- return [];
+ return array();
}
return array_slice( $synonyms, 0, self::MAX_SYNONYMS );
@@ -176,10 +204,13 @@ class PR_Core_Pubchem_Client {
*/
private function get_once( string $url ): ?string {
if ( function_exists( 'wp_remote_get' ) ) {
- $response = wp_remote_get( $url, [
- 'timeout' => self::TIMEOUT,
- 'headers' => [ 'Accept' => 'application/json' ],
- ] );
+ $response = wp_remote_get(
+ $url,
+ array(
+ 'timeout' => self::TIMEOUT,
+ 'headers' => array( 'Accept' => 'application/json' ),
+ )
+ );
if ( is_wp_error( $response ) ) {
return null;
@@ -194,13 +225,15 @@ class PR_Core_Pubchem_Client {
}
// WP-CLI / plain PHP fallback.
- $context = stream_context_create( [
- 'http' => [
- 'timeout' => self::TIMEOUT,
- 'header' => "Accept: application/json\r\n",
- 'method' => 'GET',
- ],
- ] );
+ $context = stream_context_create(
+ array(
+ 'http' => array(
+ 'timeout' => self::TIMEOUT,
+ 'header' => "Accept: application/json\r\n",
+ 'method' => 'GET',
+ ),
+ )
+ );
$body = @file_get_contents( $url, false, $context );
return ( false === $body ) ? null : $body;
diff --git a/includes/repositories/class-pr-core-candidate-queue-repository.php b/includes/repositories/class-pr-core-candidate-queue-repository.php
index b66beac..8135222 100644
--- a/includes/repositories/class-pr-core-candidate-queue-repository.php
+++ b/includes/repositories/class-pr-core-candidate-queue-repository.php
@@ -1,6 +1,18 @@
get_row(
- $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ),
+ $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
ARRAY_A
);
@@ -47,25 +59,25 @@ class PR_Core_Candidate_Queue_Repository {
global $wpdb;
$table = $wpdb->prefix . 'pr_ai_candidate_queue';
- $where = [ 'queue_status = %s' ];
- $params = [ sanitize_text_field( $status ) ];
+ $where = array( 'queue_status = %s' );
+ $params = array( sanitize_text_field( $status ) );
if ( $peptide_id > 0 ) {
$where[] = 'peptide_id = %d';
$params[] = $peptide_id;
}
- $params[] = $limit;
+ $params[] = $limit;
$where_sql = implode( ' AND ', $where );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$rows = $wpdb->get_results(
$wpdb->prepare(
- "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY extraction_confidence DESC LIMIT %d",
+ "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY extraction_confidence DESC LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
...$params
),
ARRAY_A
- ) ?: [];
+ ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
return array_map(
static fn( array $row ) => new PR_Core_Candidate_DTO( $row ),
@@ -85,7 +97,7 @@ class PR_Core_Candidate_Queue_Repository {
global $wpdb;
$table = $wpdb->prefix . 'pr_ai_candidate_queue';
- $row = [
+ $row = array(
'peptide_id' => absint( $data['peptide_id'] ?? 0 ),
'dose_min' => isset( $data['dose_min'] ) ? (float) $data['dose_min'] : null,
'dose_max' => isset( $data['dose_max'] ) ? (float) $data['dose_max'] : null,
@@ -106,7 +118,7 @@ class PR_Core_Candidate_Queue_Repository {
'extraction_confidence' => (float) ( $data['extraction_confidence'] ?? 0 ),
'queue_status' => 'pending',
'extracted_at' => current_time( 'mysql' ),
- ];
+ );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$result = $wpdb->insert( $table, $row );
@@ -134,30 +146,32 @@ class PR_Core_Candidate_Queue_Repository {
// Copy candidate data into a dosing row.
$dosing_repo = new PR_Core_Dosing_Repository();
- $dosing_id = $dosing_repo->insert( [
- 'peptide_id' => $candidate->peptide_id,
- 'dose_min' => $candidate->dose_min,
- 'dose_max' => $candidate->dose_max,
- 'dose_unit' => $candidate->dose_unit,
- 'route' => $candidate->route,
- 'frequency' => $candidate->frequency,
- 'duration_value' => $candidate->duration_value,
- 'duration_unit' => $candidate->duration_unit,
- 'population' => $candidate->population,
- 'indication' => $candidate->indication,
- 'evidence_strength' => $candidate->evidence_strength,
- 'study_title' => $candidate->study_title,
- 'study_year' => $candidate->study_year,
- 'citation_pubmed_id' => $candidate->citation_pubmed_id,
- 'citation_doi' => $candidate->citation_doi,
- 'citation_url' => $candidate->citation_url,
- 'notes' => $candidate->notes,
- 'source' => 'ai-candidate-approved',
- 'ai_candidate_id' => $candidate_id,
- 'added_by' => $reviewer_id,
- 'reviewed_by' => $reviewer_id,
- 'reviewed_at' => current_time( 'mysql' ),
- ] );
+ $dosing_id = $dosing_repo->insert(
+ array(
+ 'peptide_id' => $candidate->peptide_id,
+ 'dose_min' => $candidate->dose_min,
+ 'dose_max' => $candidate->dose_max,
+ 'dose_unit' => $candidate->dose_unit,
+ 'route' => $candidate->route,
+ 'frequency' => $candidate->frequency,
+ 'duration_value' => $candidate->duration_value,
+ 'duration_unit' => $candidate->duration_unit,
+ 'population' => $candidate->population,
+ 'indication' => $candidate->indication,
+ 'evidence_strength' => $candidate->evidence_strength,
+ 'study_title' => $candidate->study_title,
+ 'study_year' => $candidate->study_year,
+ 'citation_pubmed_id' => $candidate->citation_pubmed_id,
+ 'citation_doi' => $candidate->citation_doi,
+ 'citation_url' => $candidate->citation_url,
+ 'notes' => $candidate->notes,
+ 'source' => 'ai-candidate-approved',
+ 'ai_candidate_id' => $candidate_id,
+ 'added_by' => $reviewer_id,
+ 'reviewed_by' => $reviewer_id,
+ 'reviewed_at' => current_time( 'mysql' ),
+ )
+ );
if ( 0 === $dosing_id ) {
return 0;
@@ -204,7 +218,7 @@ class PR_Core_Candidate_Queue_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
return (int) $wpdb->get_var(
- $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE queue_status = %s", $status )
+ $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE queue_status = %s", $status ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
@@ -224,15 +238,15 @@ class PR_Core_Candidate_Queue_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$result = $wpdb->update(
$table,
- [
+ array(
'queue_status' => sanitize_text_field( $status ),
'reviewed_by' => $reviewer_id,
'reviewed_at' => current_time( 'mysql' ),
'reviewer_notes' => sanitize_textarea_field( $notes ),
- ],
- [ 'id' => $id ],
- [ '%s', '%d', '%s', '%s' ],
- [ '%d' ]
+ ),
+ array( 'id' => $id ),
+ array( '%s', '%d', '%s', '%s' ),
+ array( '%d' )
);
return false !== $result;
diff --git a/includes/repositories/class-pr-core-dosing-repository.php b/includes/repositories/class-pr-core-dosing-repository.php
index 1a9934e..853fe1e 100644
--- a/includes/repositories/class-pr-core-dosing-repository.php
+++ b/includes/repositories/class-pr-core-dosing-repository.php
@@ -1,6 +1,18 @@
get_row(
- $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ),
+ $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
ARRAY_A
);
@@ -39,12 +51,12 @@ class PR_Core_Dosing_Repository {
* @param array $filters Optional: route, population, evidence_strength.
* @return PR_Core_Dosing_Row_DTO[]
*/
- public function find_by_peptide( int $peptide_id, array $filters = [] ): array {
+ public function find_by_peptide( int $peptide_id, array $filters = array() ): array {
global $wpdb;
$table = $wpdb->prefix . 'pr_dosing_rows';
- $where = [ 'peptide_id = %d', 'superseded_by_id IS NULL' ];
- $params = [ $peptide_id ];
+ $where = array( 'peptide_id = %d', 'superseded_by_id IS NULL' );
+ $params = array( $peptide_id );
if ( ! empty( $filters['route'] ) ) {
$where[] = 'route = %s';
@@ -65,12 +77,12 @@ class PR_Core_Dosing_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$rows = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY evidence_strength DESC, study_year DESC",
+ $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- dynamic {$where_sql} is builder-constructed, not a placeholder
+ "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY evidence_strength DESC, study_year DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
...$params
),
ARRAY_A
- ) ?: [];
+ ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
return array_map(
static fn( array $row ) => new PR_Core_Dosing_Row_DTO( $row ),
@@ -92,11 +104,15 @@ class PR_Core_Dosing_Repository {
global $wpdb;
$table = $wpdb->prefix . 'pr_dosing_rows';
- $row = $this->sanitize_row( $data );
+ $row = $this->sanitize_row( $data );
$row['added_at'] = current_time( 'mysql' );
$row['schema_version'] = 1;
- /** @see PR_Core::register_public_filters() — Documented lifecycle hook. */
+ /**
+ * Property.
+ *
+ * @see PR_Core::register_public_filters() — Documented lifecycle hook.
+ */
do_action( 'pr_core_before_dosing_row_publish', $row );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
@@ -137,10 +153,10 @@ class PR_Core_Dosing_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->update(
$table,
- [ 'superseded_by_id' => $new_id ],
- [ 'id' => $old_id ],
- [ '%d' ],
- [ '%d' ]
+ array( 'superseded_by_id' => $new_id ),
+ array( 'id' => $old_id ),
+ array( '%d' ),
+ array( '%d' )
);
return $new_id;
@@ -159,7 +175,7 @@ class PR_Core_Dosing_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
return (int) $wpdb->get_var(
$wpdb->prepare(
- "SELECT COUNT(*) FROM {$table} WHERE peptide_id = %d AND superseded_by_id IS NULL",
+ "SELECT COUNT(*) FROM {$table} WHERE peptide_id = %d AND superseded_by_id IS NULL", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$peptide_id
)
);
@@ -172,7 +188,7 @@ class PR_Core_Dosing_Repository {
* @return array Sanitized row.
*/
private function sanitize_row( array $data ): array {
- return [
+ return array(
'peptide_id' => absint( $data['peptide_id'] ?? 0 ),
'dose_min' => isset( $data['dose_min'] ) ? (float) $data['dose_min'] : null,
'dose_max' => isset( $data['dose_max'] ) ? (float) $data['dose_max'] : null,
@@ -195,6 +211,6 @@ class PR_Core_Dosing_Repository {
'added_by' => absint( $data['added_by'] ?? get_current_user_id() ),
'reviewed_by' => isset( $data['reviewed_by'] ) ? absint( $data['reviewed_by'] ) : null,
'reviewed_at' => $data['reviewed_at'] ?? null,
- ];
+ );
}
}
diff --git a/includes/repositories/class-pr-core-legal-repository.php b/includes/repositories/class-pr-core-legal-repository.php
index f6abbbf..6ba7e35 100644
--- a/includes/repositories/class-pr-core-legal-repository.php
+++ b/includes/repositories/class-pr-core-legal-repository.php
@@ -1,6 +1,18 @@
get_row(
- $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ),
+ $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
ARRAY_A
);
@@ -48,11 +60,11 @@ class PR_Core_Legal_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$rows = $wpdb->get_results(
$wpdb->prepare(
- "SELECT * FROM {$table} WHERE peptide_id = %d AND superseded_by_id IS NULL ORDER BY country_code ASC",
+ "SELECT * FROM {$table} WHERE peptide_id = %d AND superseded_by_id IS NULL ORDER BY country_code ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$peptide_id
),
ARRAY_A
- ) ?: [];
+ ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
return array_map(
static fn( array $row ) => new PR_Core_Legal_Cell_DTO( $row ),
@@ -74,7 +86,7 @@ class PR_Core_Legal_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$row = $wpdb->get_row(
$wpdb->prepare(
- "SELECT * FROM {$table} WHERE peptide_id = %d AND country_code = %s AND superseded_by_id IS NULL",
+ "SELECT * FROM {$table} WHERE peptide_id = %d AND country_code = %s AND superseded_by_id IS NULL", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$peptide_id,
strtoupper( sanitize_text_field( $country_code ) )
),
@@ -97,11 +109,11 @@ class PR_Core_Legal_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$rows = $wpdb->get_results(
$wpdb->prepare(
- "SELECT * FROM {$table} WHERE country_code = %s AND superseded_by_id IS NULL ORDER BY peptide_id ASC",
+ "SELECT * FROM {$table} WHERE country_code = %s AND superseded_by_id IS NULL ORDER BY peptide_id ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
strtoupper( sanitize_text_field( $country_code ) )
),
ARRAY_A
- ) ?: [];
+ ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
return array_map(
static fn( array $row ) => new PR_Core_Legal_Cell_DTO( $row ),
@@ -161,10 +173,10 @@ class PR_Core_Legal_Repository {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->update(
$table,
- [ 'superseded_by_id' => $new_id ],
- [ 'id' => $old_id ],
- [ '%d' ],
- [ '%d' ]
+ array( 'superseded_by_id' => $new_id ),
+ array( 'id' => $old_id ),
+ array( '%d' ),
+ array( '%d' )
);
return $new_id;
@@ -184,7 +196,7 @@ class PR_Core_Legal_Repository {
$status = 'unclear';
}
- return [
+ return array(
'peptide_id' => absint( $data['peptide_id'] ?? 0 ),
'country_code' => strtoupper( sanitize_text_field( $data['country_code'] ?? '' ) ),
'status' => $status,
@@ -195,6 +207,6 @@ class PR_Core_Legal_Repository {
'last_verified_at' => sanitize_text_field( $data['last_verified_at'] ?? current_time( 'mysql' ) ),
'schema_version' => 1,
'reviewer_id' => absint( $data['reviewer_id'] ?? get_current_user_id() ),
- ];
+ );
}
}
diff --git a/includes/repositories/class-pr-core-peptide-repository.php b/includes/repositories/class-pr-core-peptide-repository.php
index c7903ba..5fb99e7 100644
--- a/includes/repositories/class-pr-core-peptide-repository.php
+++ b/includes/repositories/class-pr-core-peptide-repository.php
@@ -1,6 +1,18 @@
PR_Core_Peptide_CPT::POST_TYPE,
- 'name' => sanitize_title( $slug ),
- 'posts_per_page' => 1,
- 'post_status' => 'any',
- ] );
+ $posts = get_posts(
+ array(
+ 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
+ 'name' => sanitize_title( $slug ),
+ 'posts_per_page' => 1,
+ 'post_status' => 'any',
+ )
+ );
return ! empty( $posts ) ? $this->post_to_dto( $posts[0] ) : null;
}
@@ -56,14 +70,16 @@ class PR_Core_Peptide_Repository {
public function search( string $query, int $limit = 20 ): array {
$query = sanitize_text_field( $query );
- $posts = get_posts( [
- 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
- 's' => $query,
- 'posts_per_page' => $limit,
- 'post_status' => 'publish',
- ] );
+ $posts = get_posts(
+ array(
+ 'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
+ 's' => $query,
+ 'posts_per_page' => $limit,
+ 'post_status' => 'publish',
+ )
+ );
- return array_map( [ $this, 'post_to_dto' ], $posts );
+ return array_map( array( $this, 'post_to_dto' ), $posts );
}
/**
@@ -73,40 +89,44 @@ class PR_Core_Peptide_Repository {
* evidence_strength, per_page, page.
* @return PR_Core_Peptide_DTO[]
*/
- public function find_all( array $filters = [] ): array {
- $args = [
+ public function find_all( array $filters = array() ): array {
+ $args = array(
'post_type' => PR_Core_Peptide_CPT::POST_TYPE,
'posts_per_page' => (int) ( $filters['per_page'] ?? 100 ),
'paged' => (int) ( $filters['page'] ?? 1 ),
'orderby' => 'title',
'order' => 'ASC',
- ];
+ );
$args['post_status'] = $filters['status'] ?? 'publish';
if ( ! empty( $filters['category'] ) ) {
- $args['tax_query'] = [ [
- 'taxonomy' => PR_Core_Peptide_CPT::TAX_CATEGORY,
- 'field' => 'slug',
- 'terms' => sanitize_text_field( $filters['category'] ),
- ] ];
+ $args['tax_query'] = array(
+ array(
+ 'taxonomy' => PR_Core_Peptide_CPT::TAX_CATEGORY,
+ 'field' => 'slug',
+ 'terms' => sanitize_text_field( $filters['category'] ),
+ ),
+ );
}
- // v0.2.0: `family` filter silently ignored — `pr_peptide_family` taxonomy
- // was removed with the CPT consolidation. Callers passing `family` get
- // all-category results rather than a fatal. Remove this key from the
+ // v0.2.0: `family` filter silently ignored — `pr_peptide_family` taxonomy.
+ // was removed with the CPT consolidation. Callers passing `family` get.
+ // all-category results rather than a fatal. Remove this key from the.
// REST schema in a future minor bump.
if ( ! empty( $filters['evidence_strength'] ) ) {
- $args['meta_query'] = [ [
- 'key' => 'evidence_strength',
- 'value' => sanitize_text_field( $filters['evidence_strength'] ),
- ] ];
+ $args['meta_query'] = array(
+ array(
+ 'key' => 'evidence_strength',
+ 'value' => sanitize_text_field( $filters['evidence_strength'] ),
+ ),
+ );
}
$posts = get_posts( $args );
- return array_map( [ $this, 'post_to_dto' ], $posts );
+ return array_map( array( $this, 'post_to_dto' ), $posts );
}
/**
@@ -128,34 +148,36 @@ class PR_Core_Peptide_Repository {
*/
private function post_to_dto( \WP_Post $post ): PR_Core_Peptide_DTO {
$aliases_raw = get_post_meta( $post->ID, 'aliases', true );
- $aliases = json_decode( $aliases_raw ?: '[]', true ) ?: [];
+ $aliases = json_decode( $aliases_raw ?: '[]', true ) ?: array(); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
- $categories = wp_get_post_terms( $post->ID, PR_Core_Peptide_CPT::TAX_CATEGORY, [ 'fields' => 'names' ] );
- // v0.2.0: `pr_peptide_family` taxonomy removed. DTO `families` field
- // preserved as empty array to keep the REST response shape stable for
+ $categories = wp_get_post_terms( $post->ID, PR_Core_Peptide_CPT::TAX_CATEGORY, array( 'fields' => 'names' ) );
+ // v0.2.0: `pr_peptide_family` taxonomy removed. DTO `families` field.
+ // preserved as empty array to keep the REST response shape stable for.
// clients; will be dropped in a future minor bump with a release note.
- $families = [];
+ $families = array();
- return new PR_Core_Peptide_DTO( [
- 'id' => $post->ID,
- 'title' => $post->post_title,
- 'slug' => $post->post_name,
- 'content' => $post->post_content,
- 'excerpt' => $post->post_excerpt,
- 'status' => $post->post_status,
- 'display_name' => get_post_meta( $post->ID, 'display_name', true ) ?: '',
- 'aliases' => $aliases,
- 'molecular_formula' => get_post_meta( $post->ID, 'molecular_formula', true ) ?: '',
- 'molecular_weight' => (float) get_post_meta( $post->ID, 'molecular_weight', true ),
- 'cas_number' => get_post_meta( $post->ID, 'cas_number', true ) ?: '',
- 'drugbank_id' => get_post_meta( $post->ID, 'drugbank_id', true ) ?: '',
- 'chembl_id' => get_post_meta( $post->ID, 'chembl_id', true ) ?: '',
- 'evidence_strength' => get_post_meta( $post->ID, 'evidence_strength', true ) ?: 'preclinical',
- 'editorial_review_status' => get_post_meta( $post->ID, 'editorial_review_status', true ) ?: 'draft',
- 'last_editorial_review_at' => get_post_meta( $post->ID, 'last_editorial_review_at', true ) ?: '',
- 'medical_editor_id' => (int) get_post_meta( $post->ID, 'medical_editor_id', true ),
- 'categories' => is_array( $categories ) ? $categories : [],
- 'families' => $families,
- ] );
+ return new PR_Core_Peptide_DTO(
+ array(
+ 'id' => $post->ID,
+ 'title' => $post->post_title,
+ 'slug' => $post->post_name,
+ 'content' => $post->post_content,
+ 'excerpt' => $post->post_excerpt,
+ 'status' => $post->post_status,
+ 'display_name' => get_post_meta( $post->ID, 'display_name', true ) ?: '', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'aliases' => $aliases,
+ 'molecular_formula' => get_post_meta( $post->ID, 'molecular_formula', true ) ?: '', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'molecular_weight' => (float) get_post_meta( $post->ID, 'molecular_weight', true ),
+ 'cas_number' => get_post_meta( $post->ID, 'cas_number', true ) ?: '', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'drugbank_id' => get_post_meta( $post->ID, 'drugbank_id', true ) ?: '', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'chembl_id' => get_post_meta( $post->ID, 'chembl_id', true ) ?: '', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'evidence_strength' => get_post_meta( $post->ID, 'evidence_strength', true ) ?: 'preclinical', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'editorial_review_status' => get_post_meta( $post->ID, 'editorial_review_status', true ) ?: 'draft', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'last_editorial_review_at' => get_post_meta( $post->ID, 'last_editorial_review_at', true ) ?: '', // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ 'medical_editor_id' => (int) get_post_meta( $post->ID, 'medical_editor_id', true ),
+ 'categories' => is_array( $categories ) ? $categories : array(),
+ 'families' => $families,
+ )
+ );
}
}
diff --git a/includes/scanner/class-pr-core-verification-scanner.php b/includes/scanner/class-pr-core-verification-scanner.php
index 6c9201b..2d69562 100644
--- a/includes/scanner/class-pr-core-verification-scanner.php
+++ b/includes/scanner/class-pr-core-verification-scanner.php
@@ -1,6 +1,18 @@
find_all( [ 'status' => 'publish' ] );
+ $repo = new PR_Core_Peptide_Repository();
+ $peptides = $repo->find_all( array( 'status' => 'publish' ) );
- $due_peptides = [];
- $overdue_peptides = [];
+ $due_peptides = array();
+ $overdue_peptides = array();
foreach ( $peptides as $dto ) {
$last_verified = get_post_meta( $dto->id, '_pr_last_source_verified', true );
- $velocity = get_post_meta( $dto->id, '_pr_verification_velocity', true ) ?: 'medium';
+ $velocity = get_post_meta( $dto->id, '_pr_verification_velocity', true ) ?: 'medium'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
// Empty verified date = treat as overdue.
if ( empty( $last_verified ) ) {
@@ -102,17 +114,17 @@ class PR_Core_Verification_Scanner {
* @return void
*/
private static function log_scan( int $total, int $due, int $overdue ): void {
- $log = get_option( self::SCAN_LOG_OPTION_KEY, [] );
+ $log = get_option( self::SCAN_LOG_OPTION_KEY, array() );
if ( ! is_array( $log ) ) {
- $log = [];
+ $log = array();
}
- $log[] = [
+ $log[] = array(
'timestamp' => current_time( 'mysql' ),
'total' => $total,
'due' => $due,
'overdue' => $overdue,
- ];
+ );
// Keep only the last MAX_LOG_ENTRIES.
if ( count( $log ) > self::MAX_LOG_ENTRIES ) {
@@ -137,9 +149,12 @@ class PR_Core_Verification_Scanner {
// Parse comma-separated emails.
$emails = array_map( 'trim', explode( ',', $recipients ) );
- $emails = array_filter( $emails, static function ( $email ) {
- return is_email( $email );
- } );
+ $emails = array_filter(
+ $emails,
+ static function ( $email ) {
+ return is_email( $email );
+ }
+ );
if ( empty( $emails ) ) {
return;
@@ -153,7 +168,7 @@ class PR_Core_Verification_Scanner {
$body = self::build_email_body( $due_peptides, $overdue_peptides );
- wp_mail( $emails, $subject, $body, [ 'Content-Type: text/html; charset=UTF-8' ] );
+ wp_mail( $emails, $subject, $body, array( 'Content-Type: text/html; charset=UTF-8' ) );
}
/**
@@ -170,7 +185,7 @@ class PR_Core_Verification_Scanner {
$body .= 'Overdue for review (' . count( $overdue_peptides ) . ')
';
foreach ( $overdue_peptides as $dto ) {
$edit_url = get_edit_post_link( $dto->id );
- $body .= sprintf(
+ $body .= sprintf(
'- %s
',
esc_url( $edit_url ),
esc_html( $dto->title )
@@ -183,7 +198,7 @@ class PR_Core_Verification_Scanner {
$body .= 'Due for review (' . count( $due_peptides ) . ')
';
foreach ( $due_peptides as $dto ) {
$edit_url = get_edit_post_link( $dto->id );
- $body .= sprintf(
+ $body .= sprintf(
'- %s
',
esc_url( $edit_url ),
esc_html( $dto->title )
diff --git a/includes/taxonomies/class-pr-core-repo-daily-taxonomy.php b/includes/taxonomies/class-pr-core-repo-daily-taxonomy.php
index c7397d5..0398685 100755
--- a/includes/taxonomies/class-pr-core-repo-daily-taxonomy.php
+++ b/includes/taxonomies/class-pr-core-repo-daily-taxonomy.php
@@ -1,6 +1,18 @@
[
- 'name' => __( 'Category', 'peptide-repo-core' ),
- 'singular_name' => __( 'Category', 'peptide-repo-core' ),
- ],
- 'public' => true,
- 'publicly_queryable' => true,
- 'show_in_rest' => true,
- 'show_ui' => true,
- 'show_admin_column' => true,
- 'hierarchical' => false,
- 'rewrite' => [ 'slug' => 'daily/category', 'with_front' => false ],
- ] );
+ register_taxonomy(
+ self::TAX_CATEGORY,
+ PR_Core_Repo_Daily_CPT::POST_TYPE,
+ array(
+ 'labels' => array(
+ 'name' => __( 'Category', 'peptide-repo-core' ),
+ 'singular_name' => __( 'Category', 'peptide-repo-core' ),
+ ),
+ 'public' => true,
+ 'publicly_queryable' => true,
+ 'show_in_rest' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'hierarchical' => false,
+ 'rewrite' => array(
+ 'slug' => 'daily/category',
+ 'with_front' => false,
+ ),
+ )
+ );
}
/**
@@ -67,7 +90,7 @@ class PR_Core_Repo_Daily_Taxonomy {
* @return void
*/
public static function seed_terms(): void {
- $terms = [ 'article', 'guide', 'comparison', 'news' ];
+ $terms = array( 'article', 'guide', 'comparison', 'news' );
foreach ( $terms as $term ) {
if ( ! term_exists( $term, self::TAX_CATEGORY ) ) {
diff --git a/peptide-repo-core.php b/peptide-repo-core.php
index 15e72b3..434e259 100644
--- a/peptide-repo-core.php
+++ b/peptide-repo-core.php
@@ -1,9 +1,15 @@
init();
-} );
+add_action(
+ 'plugins_loaded',
+ static function (): void {
+ $plugin = new PR_Core();
+ $plugin->init();
+ }
+);
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 31dcb0a..b5fab5c 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -13,5 +13,31 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..6cf6d35
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,20 @@
+
+
+
+
+ ./tests/unit
+
+
+
+
+
+ ./includes
+
+
+
diff --git a/template-parts/related-posts/card.php b/template-parts/related-posts/card.php
index 57e24d5..93a1275 100644
--- a/template-parts/related-posts/card.php
+++ b/template-parts/related-posts/card.php
@@ -1,4 +1,10 @@
'pr-related-posts__image-element',
'loading' => 'lazy',
'decoding' => 'async',
- ]
+ )
)
);
?>
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index c3f1913..a2a416e 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,165 +1,417 @@
[],
+ 'existing_taxonomies' => [],
+ 'registered_post_types' => [],
+ 'registered_taxonomies' => [],
+ 'registered_meta' => [],
+ 'added_actions' => [],
+ 'added_filters' => [],
+];
+$GLOBALS['pr_core_options'] = [];
+$GLOBALS['pr_core_cron_calls'] = [];
+$GLOBALS['pr_test_postmeta'] = [];
+$GLOBALS['pr_test_posts'] = [];
+$GLOBALS['pr_test_updated_meta'] = [];
+$GLOBALS['pr_test_is_singular'] = false;
+$GLOBALS['pr_test_singular_type'] = '';
+$GLOBALS['pr_test_the_id'] = 0;
+$GLOBALS['pr_test_peptide_dto'] = null;
+
+// ── WordPress function stubs ──────────────────────────────────────────.
if ( ! function_exists( 'trailingslashit' ) ) {
function trailingslashit( string $path ): string {
return rtrim( $path, '/\\' ) . '/';
}
}
-
-/**
- * Global harness state. Tests reset this between cases.
- *
- * @var array
- */
-$GLOBALS['pr_core_test_state'] = [
- 'existing_post_types' => [],
- 'existing_taxonomies' => [],
- 'registered_post_types' => [],
- 'registered_taxonomies' => [],
- 'registered_meta' => [],
- 'added_actions' => [],
-];
-
-function pr_core_test_reset(): void {
- $GLOBALS['pr_core_test_state'] = [
- 'existing_post_types' => [],
- 'existing_taxonomies' => [],
- 'registered_post_types' => [],
- 'registered_taxonomies' => [],
- 'registered_meta' => [],
- 'added_actions' => [],
- ];
+if ( ! function_exists( 'plugin_dir_path' ) ) {
+ function plugin_dir_path( string $file ): string {
+ return trailingslashit( dirname( $file ) );
+ }
}
-
-function post_type_exists( string $post_type ): bool {
- return in_array( $post_type, $GLOBALS['pr_core_test_state']['existing_post_types'], true );
+if ( ! function_exists( 'plugin_dir_url' ) ) {
+ function plugin_dir_url( string $file ): string {
+ return 'https://example.com/wp-content/plugins/' . basename( dirname( $file ) ) . '/';
+ }
}
-
-function taxonomy_exists( string $taxonomy ): bool {
- return in_array( $taxonomy, $GLOBALS['pr_core_test_state']['existing_taxonomies'], true );
+if ( ! function_exists( 'plugin_basename' ) ) {
+ function plugin_basename( string $file ): string {
+ return basename( dirname( $file ) ) . '/' . basename( $file );
+ }
}
-
-function register_post_type( string $post_type, array $args = [] ) {
- $GLOBALS['pr_core_test_state']['registered_post_types'][ $post_type ] = $args;
- $GLOBALS['pr_core_test_state']['existing_post_types'][] = $post_type;
- return (object) [ 'name' => $post_type, 'args' => $args ];
+if ( ! function_exists( '__' ) ) {
+ function __( string $text, string $domain = 'default' ): string {
+ return $text;
+ }
}
-
-function register_taxonomy( string $taxonomy, $object_type, array $args = [] ): void {
- $GLOBALS['pr_core_test_state']['registered_taxonomies'][ $taxonomy ] = [
- 'object_type' => $object_type,
- 'args' => $args,
- ];
- $GLOBALS['pr_core_test_state']['existing_taxonomies'][] = $taxonomy;
+if ( ! function_exists( 'esc_html' ) ) {
+ function esc_html( string $text ): string {
+ return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
+ }
}
-
-function register_post_meta( string $post_type, string $key, array $args = [] ): bool {
- $GLOBALS['pr_core_test_state']['registered_meta'][ $post_type ][ $key ] = $args;
- return true;
+if ( ! function_exists( 'esc_html__' ) ) {
+ function esc_html__( string $text, string $domain = 'default' ): string {
+ return esc_html( $text );
+ }
}
-
-function add_action( string $hook, $callback, int $priority = 10, int $accepted_args = 1 ): bool {
- $GLOBALS['pr_core_test_state']['added_actions'][] = [
- 'hook' => $hook,
- 'callback' => $callback,
- 'priority' => $priority,
- ];
- return true;
+if ( ! function_exists( 'esc_attr' ) ) {
+ function esc_attr( string $text ): string {
+ return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
+ }
}
-
-function __( string $text, string $domain = 'default' ): string {
- return $text;
+if ( ! function_exists( 'esc_url_raw' ) ) {
+ function esc_url_raw( string $url ): string {
+ return filter_var( $url, FILTER_SANITIZE_URL ) ?: '';
+ }
}
-
-function current_user_can( string $capability ): bool {
- return true;
+if ( ! function_exists( 'sanitize_text_field' ) ) {
+ function sanitize_text_field( $value ): string {
+ return is_scalar( $value ) ? trim( strip_tags( (string) $value ) ) : '';
+ }
}
-
-function sanitize_text_field( $value ): string {
- return is_scalar( $value ) ? trim( (string) $value ) : '';
+if ( ! function_exists( 'sanitize_textarea_field' ) ) {
+ function sanitize_textarea_field( $value ): string {
+ return is_scalar( $value ) ? trim( strip_tags( (string) $value ) ) : '';
+ }
+}
+if ( ! function_exists( 'wp_strip_all_tags' ) ) {
+ function wp_strip_all_tags( $value ): string {
+ return strip_tags( (string) $value );
+ }
+}
+if ( ! function_exists( 'absint' ) ) {
+ function absint( $value ): int {
+ return abs( (int) $value );
+ }
}
-
if ( ! function_exists( 'wp_json_encode' ) ) {
- function wp_json_encode( $data, int $flags = 0 ): string {
- return json_encode( $data, $flags ) ?: '[]';
+ function wp_json_encode( $data, int $flags = 0, int $depth = 512 ): string {
+ return json_encode( $data, $flags, $depth ) ?: '[]';
+ }
+}
+if ( ! function_exists( 'is_wp_error' ) ) {
+ function is_wp_error( $thing ): bool {
+ return $thing instanceof WP_Error;
+ }
+}
+if ( ! function_exists( 'apply_filters' ) ) {
+ function apply_filters( string $tag, $value ) {
+ return $value;
+ }
+}
+if ( ! function_exists( 'current_user_can' ) ) {
+ function current_user_can( string $capability ): bool {
+ return true;
+ }
+}
+if ( ! function_exists( 'home_url' ) ) {
+ function home_url( string $path = '' ): string {
+ return 'https://peptiderepo.com' . $path;
+ }
+}
+if ( ! function_exists( 'error_log' ) ) {
+ function error_log( string $msg ): bool {
+ return true;
+ }
+}
+if ( ! function_exists( 'is_email' ) ) {
+ function is_email( string $email ): bool {
+ return (bool) filter_var( $email, FILTER_VALIDATE_EMAIL );
}
}
-function absint( $value ): int {
- return abs( (int) $value );
-}
+// ── Option / cron stubs ───────────────────────────────────────────────.
-
-/* ── Test assertion helpers ─────────────────────────────────────────── */
-
-$GLOBALS['pr_core_test_report'] = [ 'pass' => 0, 'fail' => 0, 'failures' => [] ];
-
-function pr_assert( bool $condition, string $label ): void {
- if ( $condition ) {
- $GLOBALS['pr_core_test_report']['pass']++;
- echo " PASS: {$label}\n";
- } else {
- $GLOBALS['pr_core_test_report']['fail']++;
- $GLOBALS['pr_core_test_report']['failures'][] = $label;
- echo " FAIL: {$label}\n";
- }
-}
-
-function pr_assert_equals( $expected, $actual, string $label ): void {
- $pass = ( $expected === $actual );
- pr_assert( $pass, $label . ( $pass ? '' : ' — expected ' . var_export( $expected, true ) . ', got ' . var_export( $actual, true ) ) );
-}
-
-function pr_test_summary(): int {
- $r = $GLOBALS['pr_core_test_report'];
- echo "\n---\n";
- echo "Totals: {$r['pass']} passed, {$r['fail']} failed\n";
- if ( $r['fail'] > 0 ) {
- echo "Failures:\n";
- foreach ( $r['failures'] as $f ) {
- echo " - {$f}\n";
+if ( ! function_exists( 'get_option' ) ) {
+ function get_option( string $key, $default = false ) {
+ if ( isset( $GLOBALS['pr_core_options'][ $key ] ) ) {
+ return $GLOBALS['pr_core_options'][ $key ];
}
- return 1;
+ return $default;
}
- return 0;
+}
+if ( ! function_exists( 'update_option' ) ) {
+ function update_option( string $key, $value ): bool {
+ $GLOBALS['pr_core_options'][ $key ] = $value;
+ return true;
+ }
+}
+if ( ! function_exists( 'wp_clear_scheduled_hook' ) ) {
+ function wp_clear_scheduled_hook( string $hook ): void {
+ $GLOBALS['pr_core_cron_calls'][] = [ 'action' => 'clear', 'hook' => $hook ];
+ }
+}
+if ( ! function_exists( 'wp_schedule_event' ) ) {
+ function wp_schedule_event( int $timestamp, string $recurrence, string $hook ): void {
+ $GLOBALS['pr_core_cron_calls'][] = [
+ 'action' => 'schedule',
+ 'hook' => $hook,
+ 'recurrence' => $recurrence,
+ ];
+ }
+}
+if ( ! function_exists( 'register_setting' ) ) {
+ function register_setting( string $group, string $option, array $args = [] ): void {}
+}
+if ( ! function_exists( 'add_settings_section' ) ) {
+ function add_settings_section( string $id, string $title, $cb, string $page ): void {}
+}
+if ( ! function_exists( 'add_settings_field' ) ) {
+ function add_settings_field( string $id, string $title, $cb, string $page, string $section = '' ): void {}
}
-/* ── Load the plugin's autoloader + required classes ─────────────────── */
+// ── Post meta stubs ───────────────────────────────────────────────────.
-require_once __DIR__ . '/../includes/class-pr-core-autoloader.php';
+if ( ! function_exists( 'get_post_meta' ) ) {
+ function get_post_meta( $post_id, string $key = '', bool $single = false ) {
+ if ( $single ) {
+ return $GLOBALS['pr_test_postmeta'][ (int) $post_id ][ $key ] ?? '';
+ }
+ return array_values( $GLOBALS['pr_test_postmeta'][ (int) $post_id ] ?? [] );
+ }
+}
+if ( ! function_exists( 'update_post_meta' ) ) {
+ function update_post_meta( int $post_id, string $key, $value ): bool {
+ $GLOBALS['pr_test_postmeta'][ $post_id ][ $key ] = $value;
+ $GLOBALS['pr_test_updated_meta'][] = [
+ 'post_id' => $post_id,
+ 'key' => $key,
+ 'value' => $value,
+ ];
+ return true;
+ }
+}
+if ( ! function_exists( 'get_post' ) ) {
+ function get_post( int $id ) {
+ return $GLOBALS['pr_test_posts'][ $id ] ?? null;
+ }
+}
+if ( ! function_exists( 'get_posts' ) ) {
+ function get_posts( array $args = [] ): array {
+ return $GLOBALS['pr_test_all_post_ids'] ?? [];
+ }
+}
+
+// ── CPT / taxonomy stubs ──────────────────────────────────────────────.
+
+if ( ! function_exists( 'post_type_exists' ) ) {
+ function post_type_exists( string $post_type ): bool {
+ return in_array( $post_type, $GLOBALS['pr_core_test_state']['existing_post_types'], true );
+ }
+}
+if ( ! function_exists( 'taxonomy_exists' ) ) {
+ function taxonomy_exists( string $taxonomy ): bool {
+ return in_array( $taxonomy, $GLOBALS['pr_core_test_state']['existing_taxonomies'], true );
+ }
+}
+if ( ! function_exists( 'register_post_type' ) ) {
+ function register_post_type( string $post_type, array $args = [] ) {
+ $GLOBALS['pr_core_test_state']['registered_post_types'][ $post_type ] = $args;
+ $GLOBALS['pr_core_test_state']['existing_post_types'][] = $post_type;
+ return (object) [ 'name' => $post_type, 'args' => $args ];
+ }
+}
+if ( ! function_exists( 'register_taxonomy' ) ) {
+ function register_taxonomy( string $taxonomy, $object_type, array $args = [] ): void {
+ $GLOBALS['pr_core_test_state']['registered_taxonomies'][ $taxonomy ] = [
+ 'object_type' => $object_type,
+ 'args' => $args,
+ ];
+ $GLOBALS['pr_core_test_state']['existing_taxonomies'][] = $taxonomy;
+ }
+}
+if ( ! function_exists( 'register_post_meta' ) ) {
+ function register_post_meta( string $post_type, string $key, array $args = [] ): bool {
+ $GLOBALS['pr_core_test_state']['registered_meta'][ $post_type ][ $key ] = $args;
+ return true;
+ }
+}
+if ( ! function_exists( 'add_action' ) ) {
+ function add_action( string $hook, $callback, int $priority = 10, int $accepted_args = 1 ): bool {
+ $GLOBALS['pr_core_test_state']['added_actions'][] = [
+ 'hook' => $hook,
+ 'callback' => $callback,
+ 'priority' => $priority,
+ ];
+ return true;
+ }
+}
+if ( ! function_exists( 'add_filter' ) ) {
+ function add_filter( string $hook, $callback, int $priority = 10, int $accepted_args = 1 ): bool {
+ $GLOBALS['pr_core_test_state']['added_filters'][] = [
+ 'hook' => $hook,
+ 'callback' => $callback,
+ 'priority' => $priority,
+ ];
+ return true;
+ }
+}
+
+// ── Singular / permalink stubs ────────────────────────────────────────.
+
+if ( ! function_exists( 'is_singular' ) ) {
+ function is_singular( $type = '' ): bool {
+ if ( '' === $type ) {
+ return $GLOBALS['pr_test_is_singular'];
+ }
+ return $GLOBALS['pr_test_is_singular']
+ && ( $GLOBALS['pr_test_singular_type'] === $type );
+ }
+}
+if ( ! function_exists( 'get_the_ID' ) ) {
+ function get_the_ID(): int {
+ return $GLOBALS['pr_test_the_id'];
+ }
+}
+if ( ! function_exists( 'get_permalink' ) ) {
+ function get_permalink( $post_id = null ): string {
+ return 'https://peptiderepo.com/peptides/bpc-157/';
+ }
+}
+if ( ! function_exists( 'get_post_modified_time' ) ) {
+ function get_post_modified_time( $format, $gmt = false, $post = null ) {
+ return '2026-04-25';
+ }
+}
+if ( ! function_exists( 'get_userdata' ) ) {
+ function get_userdata( int $id ) {
+ return false;
+ }
+}
+if ( ! function_exists( 'get_author_posts_url' ) ) {
+ function get_author_posts_url( int $id ): string {
+ return 'https://peptiderepo.com/author/' . $id;
+ }
+}
+
+// ── WP_Error stub ─────────────────────────────────────────────────────.
+
+if ( ! class_exists( 'WP_Error' ) ) {
+ class WP_Error {
+ /** @var array */
+ public array $errors = [];
+ /** @var array */
+ public array $error_data = [];
+
+ public function __construct( string $code = '', string $message = '', $data = '' ) {
+ if ( $code ) {
+ $this->errors[ $code ][] = $message;
+ if ( $data ) {
+ $this->error_data[ $code ] = $data;
+ }
+ }
+ }
+
+ public function get_error_code(): string {
+ $codes = array_keys( $this->errors );
+ return $codes ? $codes[0] : '';
+ }
+
+ public function get_error_message( string $code = '' ): string {
+ if ( ! $code ) {
+ $code = $this->get_error_code();
+ }
+ return isset( $this->errors[ $code ] ) ? $this->errors[ $code ][0] : '';
+ }
+ }
+}
+
+
+// ── WP_Post stub ──────────────────────────────────────────────────────.
+
+if ( ! class_exists( 'WP_Post' ) ) {
+ class WP_Post {
+ public int $ID = 0;
+ public string $post_author = '0';
+ public string $post_date = '';
+ public string $post_title = '';
+ public string $post_content = '';
+ public string $post_excerpt = '';
+ public string $post_status = 'publish';
+ public string $post_name = '';
+ public string $post_type = 'post';
+ public string $post_modified = '';
+
+ /**
+ * @param array|object $data
+ */
+ public function __construct( $data = [] ) {
+ foreach ( (array) $data as $key => $value ) {
+ $this->$key = $value;
+ }
+ }
+ }
+}
+
+if ( ! function_exists( 'wp_get_post_terms' ) ) {
+ function wp_get_post_terms( int $post_id, string $taxonomy, array $args = [] ): array {
+ return [];
+ }
+}
+
+if ( ! function_exists( 'sanitize_title' ) ) {
+ function sanitize_title( string $title ): string {
+ return strtolower( trim( preg_replace( '/[^a-z0-9-]/', '-', strtolower( $title ) ) ?: '', '-' ) );
+ }
+}
+
+if ( ! function_exists( 'wp_count_posts' ) ) {
+ function wp_count_posts( string $type = 'post' ) {
+ return (object) [ 'publish' => 0 ];
+ }
+}
+
+// ── Load the plugin's autoloader ──────────────────────────────────────.
+
+require_once PR_CORE_PLUGIN_DIR . 'includes/class-pr-core-autoloader.php';
PR_Core_Autoloader::register();
-require_once __DIR__ . '/../includes/cpt/class-pr-core-peptide-cpt.php';
diff --git a/tests/unit/JsonldPeptidePieceTest.php b/tests/unit/JsonldPeptidePieceTest.php
new file mode 100644
index 0000000..5184eeb
--- /dev/null
+++ b/tests/unit/JsonldPeptidePieceTest.php
@@ -0,0 +1,285 @@
+ [],
+ 'existing_taxonomies' => [],
+ 'registered_post_types' => [],
+ 'registered_taxonomies' => [],
+ 'registered_meta' => [],
+ 'added_actions' => [],
+ 'added_filters' => [],
+ ];
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/dto/class-pr-core-peptide-dto.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-drug.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────.
+
+ /**
+ * @param array $overrides
+ */
+ private function make_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
+ return new PR_Core_Peptide_DTO( array_merge( [
+ 'id' => 36,
+ 'title' => 'BPC-157',
+ 'slug' => 'bpc-157',
+ 'content' => '',
+ 'excerpt' => 'A synthetic pentadecapeptide.',
+ 'status' => 'publish',
+ 'display_name' => 'BPC-157',
+ 'aliases' => [],
+ 'molecular_formula' => '',
+ 'molecular_weight' => 0.0,
+ 'cas_number' => '',
+ 'drugbank_id' => '',
+ 'chembl_id' => '',
+ 'evidence_strength' => 'preclinical',
+ 'editorial_review_status' => 'published',
+ 'last_editorial_review_at' => '',
+ 'medical_editor_id' => 0,
+ 'categories' => [],
+ 'families' => [],
+ ], $overrides ) );
+ }
+
+ /**
+ * @return array>
+ */
+ private function make_existing_graph(): array {
+ return [
+ [
+ '@type' => 'MedicalWebPage',
+ '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage',
+ 'name' => 'BPC-157',
+ ],
+ [
+ '@type' => 'BreadcrumbList',
+ '@id' => 'https://peptiderepo.com/peptides/bpc-157/#breadcrumb',
+ ],
+ ];
+ }
+
+ private function set_peptide_context( int $post_id, array $meta = [] ): void {
+ $GLOBALS['pr_test_the_id'] = $post_id;
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = PR_Core_Peptide_CPT::POST_TYPE;
+ $GLOBALS['pr_test_postmeta'][ $post_id ] = $meta;
+ $GLOBALS['pr_test_peptide_dto'] = $this->make_dto( [ 'id' => $post_id ] );
+ // Supply a WP_Post object so PR_Core_Peptide_Repository::find_by_id() passes.
+ // the post_type check and post_to_dto() type hint (our get_post() stub reads.
+ // from pr_test_posts).
+ $post = new WP_Post();
+ $post->ID = $post_id;
+ $post->post_type = PR_Core_Peptide_CPT::POST_TYPE;
+ $post->post_title = 'BPC-157';
+ $post->post_name = 'bpc-157';
+ $post->post_content = '';
+ $post->post_excerpt = '';
+ $post->post_status = 'publish';
+ $GLOBALS['pr_test_posts'][ $post_id ] = $post;
+ }
+
+ // ── Regression: get_class(array) throws ───────────────────────────.
+
+ public function test_get_class_on_plain_array_throws(): void {
+ $GLOBALS['pr_test_postmeta'][36] = [];
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $this->make_dto() );
+ $caught = false;
+ try {
+ get_class( $drug_node );
+ } catch ( TypeError $e ) {
+ $caught = true;
+ } catch ( Error $e ) {
+ $caught = true;
+ }
+ $this->assertTrue( $caught, 'get_class(array) must throw — confirms the v0.6.1 fatal' );
+ }
+
+ // ── inject_graph_nodes does not throw ─────────────────────────────.
+
+ public function test_inject_graph_nodes_does_not_throw(): void {
+ $this->set_peptide_context( 36, [
+ '_pr_molecular_formula' => 'C62H98N16O22',
+ '_pr_molecular_weight' => '1419.5',
+ '_pr_aliases' => '["Body Protection Compound"]',
+ ] );
+ $jsonld = new PR_Core_Jsonld();
+ $threw = false;
+ try {
+ $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ } catch ( Throwable $e ) {
+ $threw = true;
+ }
+ $this->assertFalse( $threw );
+ }
+
+ public function test_inject_graph_nodes_appends_drug_node(): void {
+ $this->set_peptide_context( 36, [
+ '_pr_molecular_formula' => 'C62H98N16O22',
+ '_pr_molecular_weight' => '1419.5',
+ '_pr_aliases' => '["Body Protection Compound"]',
+ ] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $this->assertGreaterThan( count( $this->make_existing_graph() ), count( $result ) );
+ }
+
+ public function test_inject_graph_nodes_exactly_one_drug_node(): void {
+ $this->set_peptide_context( 36, [
+ '_pr_molecular_formula' => 'C62H98N16O22',
+ '_pr_molecular_weight' => '1419.5',
+ '_pr_aliases' => '["Body Protection Compound"]',
+ ] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $drug_nodes = array_filter( $result, static function ( $n ) {
+ $types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
+ return in_array( 'Drug', $types, true );
+ } );
+ $this->assertCount( 1, $drug_nodes );
+ }
+
+ public function test_inject_graph_nodes_drug_id_format(): void {
+ $this->set_peptide_context( 36, [
+ '_pr_molecular_formula' => 'C62H98N16O22',
+ '_pr_molecular_weight' => '1419.5',
+ '_pr_aliases' => '[]',
+ ] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $drug_nodes = array_values( array_filter( $result, static function ( $n ) {
+ $types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
+ return in_array( 'Drug', $types, true );
+ } ) );
+ $this->assertSame( 'https://peptiderepo.com/peptides/bpc-157/#drug', $drug_nodes[0]['@id'] );
+ }
+
+ public function test_inject_graph_nodes_drug_has_molecular_formula(): void {
+ $this->set_peptide_context( 36, [
+ '_pr_molecular_formula' => 'C62H98N16O22',
+ '_pr_molecular_weight' => '1419.5',
+ '_pr_aliases' => '[]',
+ ] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $drug_nodes = array_values( array_filter( $result, static function ( $n ) {
+ $types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
+ return in_array( 'Drug', $types, true );
+ } ) );
+ $this->assertArrayHasKey( 'molecularFormula', $drug_nodes[0] );
+ }
+
+ public function test_inject_graph_nodes_drug_molecular_weight_quantitative_value(): void {
+ $this->set_peptide_context( 36, [ '_pr_molecular_weight' => '1419.5' ] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $drug_nodes = array_values( array_filter( $result, static function ( $n ) {
+ $types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
+ return in_array( 'Drug', $types, true );
+ } ) );
+ $this->assertSame( 'QuantitativeValue', $drug_nodes[0]['molecularWeight']['@type'] );
+ $this->assertSame( 1419.5, $drug_nodes[0]['molecularWeight']['value'] );
+ $this->assertSame( 'g/mol', $drug_nodes[0]['molecularWeight']['unitText'] );
+ }
+
+ public function test_inject_graph_nodes_preserves_webpage_and_breadcrumb(): void {
+ $this->set_peptide_context( 36, [] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $webpage = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'MedicalWebPage' );
+ $breadcrumb = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'BreadcrumbList' );
+ $this->assertCount( 1, $webpage );
+ $this->assertCount( 1, $breadcrumb );
+ }
+
+ // ── FAQ injection via inject_graph_nodes ──────────────────────────.
+
+ public function test_inject_graph_nodes_faq_present_when_items_exist(): void {
+ $this->set_peptide_context( 36, [
+ '_pr_faq_items' => wp_json_encode( [
+ [ 'question' => 'What is BPC-157?', 'answer' => 'A synthetic pentadecapeptide.' ],
+ [ 'question' => 'Is it safe?', 'answer' => 'Research use only.' ],
+ ] ),
+ ] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $faq_nodes = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'FAQPage' );
+ $this->assertCount( 1, $faq_nodes );
+ $faq_node = array_values( $faq_nodes )[0];
+ $this->assertSame( 'https://peptiderepo.com/peptides/bpc-157/#faq', $faq_node['@id'] );
+ $this->assertCount( 2, $faq_node['mainEntity'] );
+ }
+
+ public function test_inject_graph_nodes_faq_absent_when_items_empty(): void {
+ $this->set_peptide_context( 36, [ '_pr_faq_items' => '[]' ] );
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
+ $faq_nodes = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'FAQPage' );
+ $this->assertCount( 0, $faq_nodes );
+ }
+
+ // ── Non-peptide page: no injection ────────────────────────────────.
+
+ public function test_inject_graph_nodes_noop_on_non_peptide_page(): void {
+ $GLOBALS['pr_test_is_singular'] = false;
+ $GLOBALS['pr_test_singular_type'] = '';
+ $GLOBALS['pr_test_the_id'] = 0;
+ $non_peptide_graph = [ [ '@type' => 'WebPage', '@id' => 'https://peptiderepo.com/about/#webpage' ] ];
+ $jsonld = new PR_Core_Jsonld();
+ $result = $jsonld->inject_graph_nodes( $non_peptide_graph, null );
+ $this->assertCount( 1, $result );
+ }
+
+ // ── register_hooks: wpseo_schema_graph_pieces must NOT be hooked ──.
+
+ public function test_jsonld_does_not_hook_wpseo_schema_graph_pieces(): void {
+ $src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
+ $this->assertStringNotContainsString(
+ "'wpseo_schema_graph_pieces'",
+ $src,
+ 'class-pr-core-jsonld.php must NOT hook wpseo_schema_graph_pieces (v0.6.2 fix)'
+ );
+ }
+
+ public function test_jsonld_references_inject_graph_nodes(): void {
+ $src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
+ $this->assertStringContainsString( 'inject_graph_nodes', $src );
+ }
+
+ public function test_jsonld_hooks_wpseo_schema_graph(): void {
+ $src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
+ $this->assertStringContainsString( "'wpseo_schema_graph'", $src );
+ }
+}
diff --git a/tests/unit/JsonldTest.php b/tests/unit/JsonldTest.php
new file mode 100644
index 0000000..a9bc3d9
--- /dev/null
+++ b/tests/unit/JsonldTest.php
@@ -0,0 +1,297 @@
+ [],
+ 'existing_taxonomies' => [],
+ 'registered_post_types' => [],
+ 'registered_taxonomies' => [],
+ 'registered_meta' => [],
+ 'added_actions' => [],
+ 'added_filters' => [],
+ ];
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/dto/class-pr-core-peptide-dto.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-drug.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
+ }
+
+ // ── Helper ────────────────────────────────────────────────────────.
+
+ /**
+ * @param array $overrides
+ */
+ private function make_peptide_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
+ return new PR_Core_Peptide_DTO( array_merge( [
+ 'id' => 1,
+ 'title' => 'BPC-157',
+ 'slug' => 'bpc-157',
+ 'content' => '',
+ 'excerpt' => 'A synthetic peptide.',
+ 'status' => 'publish',
+ 'display_name' => 'BPC-157',
+ 'aliases' => [],
+ 'molecular_formula' => '',
+ 'molecular_weight' => 0.0,
+ 'cas_number' => '',
+ 'drugbank_id' => '',
+ 'chembl_id' => '',
+ 'evidence_strength' => 'preclinical',
+ 'editorial_review_status' => 'published',
+ 'last_editorial_review_at' => '',
+ 'medical_editor_id' => 0,
+ 'categories' => [],
+ 'families' => [],
+ ], $overrides ) );
+ }
+
+ // ── Drug @id ─────────────────────────────────────────────────────.
+
+ public function test_drug_node_has_id(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertArrayHasKey( '@id', $drug_node );
+ }
+
+ public function test_drug_node_id_format(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertSame( 'https://peptiderepo.com/peptides/bpc-157/#drug', $drug_node['@id'] );
+ }
+
+ // ── Drug @type ────────────────────────────────────────────────────.
+
+ public function test_drug_type_is_array(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertIsArray( $drug_node['@type'] );
+ }
+
+ public function test_drug_type_includes_drug(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertContains( 'Drug', $drug_node['@type'] );
+ }
+
+ public function test_drug_type_includes_molecular_entity(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertContains( 'MolecularEntity', $drug_node['@type'] );
+ }
+
+ // ── Drug fields present when meta set ────────────────────────────.
+
+ public function test_molecular_formula_present_when_meta_set(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [ '_pr_molecular_formula' => 'C62H98N16O22', '_pr_molecular_weight' => '1419.5', '_pr_aliases' => '[]' ];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertArrayHasKey( 'molecularFormula', $drug_node );
+ $this->assertSame( 'C62H98N16O22', $drug_node['molecularFormula'] );
+ }
+
+ public function test_molecular_weight_present_when_meta_set(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [ '_pr_molecular_formula' => '', '_pr_molecular_weight' => '1419.5', '_pr_aliases' => '[]' ];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertArrayHasKey( 'molecularWeight', $drug_node );
+ $this->assertSame( 'QuantitativeValue', $drug_node['molecularWeight']['@type'] );
+ $this->assertSame( 1419.5, $drug_node['molecularWeight']['value'] );
+ $this->assertSame( 'g/mol', $drug_node['molecularWeight']['unitText'] );
+ }
+
+ public function test_alternate_name_present_when_aliases_set(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [ '_pr_molecular_formula' => '', '_pr_molecular_weight' => '0', '_pr_aliases' => '["Body Protection Compound-157","PL 14736"]' ];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertArrayHasKey( 'alternateName', $drug_node );
+ $this->assertCount( 2, $drug_node['alternateName'] );
+ }
+
+ // ── Drug fields omitted when meta empty ──────────────────────────.
+
+ public function test_molecular_formula_omitted_when_meta_empty(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertArrayNotHasKey( 'molecularFormula', $drug_node );
+ }
+
+ public function test_molecular_weight_omitted_when_meta_empty(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertArrayNotHasKey( 'molecularWeight', $drug_node );
+ }
+
+ public function test_alternate_name_omitted_when_aliases_empty(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [ '_pr_aliases' => '[]' ];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $this->assertArrayNotHasKey( 'alternateName', $drug_node );
+ }
+
+ // ── FAQPage emission ──────────────────────────────────────────────.
+
+ public function test_faq_piece_not_null_when_items_present(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_faq_items' => wp_json_encode( [
+ [ 'question' => 'What is BPC-157?', 'answer' => 'A synthetic peptide.' ],
+ [ 'question' => 'Is it legal?', 'answer' => 'Research use only.' ],
+ ] ),
+ ];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
+ $this->assertNotNull( $faq_piece );
+ }
+
+ public function test_faq_piece_type_is_faq_page(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
+ ];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
+ $this->assertSame( 'FAQPage', $faq_piece['@type'] );
+ }
+
+ public function test_faq_piece_id_format(): void {
+ $permalink = 'https://peptiderepo.com/peptides/bpc-157/';
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
+ ];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, $permalink );
+ $this->assertSame( $permalink . '#faq', $faq_piece['@id'] );
+ }
+
+ public function test_faq_piece_has_main_entity(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_faq_items' => wp_json_encode( [
+ [ 'question' => 'What is BPC-157?', 'answer' => 'A synthetic peptide.' ],
+ [ 'question' => 'Is it legal?', 'answer' => 'Research use only.' ],
+ ] ),
+ ];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
+ $this->assertArrayHasKey( 'mainEntity', $faq_piece );
+ $this->assertCount( 2, $faq_piece['mainEntity'] );
+ }
+
+ public function test_faq_main_entity_question_type(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
+ ];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
+ $this->assertSame( 'Question', $faq_piece['mainEntity'][0]['@type'] );
+ }
+
+ public function test_faq_main_entity_has_accepted_answer(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
+ ];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
+ $this->assertArrayHasKey( 'acceptedAnswer', $faq_piece['mainEntity'][0] );
+ }
+
+ public function test_faq_piece_null_when_items_json_empty(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [ '_pr_faq_items' => '[]' ];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
+ $this->assertNull( $faq_piece );
+ }
+
+ public function test_faq_piece_null_when_items_absent(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
+ $this->assertNull( $faq_piece );
+ }
+
+ // ── MedicalWebPage retype ─────────────────────────────────────────.
+
+ public function test_retype_returns_medical_webpage_on_peptide_singular(): void {
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = 'peptide';
+ $result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( 'WebPage' );
+ $this->assertSame( 'MedicalWebPage', $result );
+ }
+
+ public function test_retype_passes_through_on_non_peptide_page(): void {
+ $GLOBALS['pr_test_is_singular'] = false;
+ $GLOBALS['pr_test_singular_type'] = '';
+ $result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( 'WebPage' );
+ $this->assertSame( 'WebPage', $result );
+ }
+
+ // ── No duplicate nodes from our builders ──────────────────────────.
+
+ public function test_builders_do_not_emit_webpage_type(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_molecular_formula' => 'C62H98N16O22',
+ '_pr_molecular_weight' => '1419.5',
+ '_pr_aliases' => '["Body Protection Compound-157","PL 14736"]',
+ ];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $types = is_array( $drug_node['@type'] ) ? $drug_node['@type'] : [ $drug_node['@type'] ];
+ $this->assertNotContains( 'WebPage', $types );
+ $this->assertNotContains( 'MedicalWebPage', $types );
+ }
+
+ public function test_builders_do_not_emit_breadcrumb_type(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [];
+ $peptide = $this->make_peptide_dto();
+ $drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
+ $types = is_array( $drug_node['@type'] ) ? $drug_node['@type'] : [ $drug_node['@type'] ];
+ $this->assertNotContains( 'BreadcrumbList', $types );
+ }
+
+ // ── sanitize_faq_items() ──────────────────────────────────────────.
+
+ public function test_sanitize_faq_items_valid_input(): void {
+ $result = PR_Core_Schema_Sanitizers::sanitize_faq_items( wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ) );
+ $decoded = json_decode( $result, true );
+ $this->assertIsArray( $decoded );
+ $this->assertCount( 1, $decoded );
+ }
+
+ public function test_sanitize_faq_items_empty_json_array(): void {
+ $this->assertSame( '[]', PR_Core_Schema_Sanitizers::sanitize_faq_items( '[]' ) );
+ }
+
+ public function test_sanitize_faq_items_malformed_json(): void {
+ $this->assertSame( '[]', PR_Core_Schema_Sanitizers::sanitize_faq_items( 'not-json' ) );
+ }
+
+ public function test_sanitize_faq_items_drops_item_missing_answer(): void {
+ $result = PR_Core_Schema_Sanitizers::sanitize_faq_items( wp_json_encode( [ [ 'question' => 'No answer here?' ] ] ) );
+ $decoded = json_decode( $result, true );
+ $this->assertCount( 0, $decoded );
+ }
+}
diff --git a/tests/unit/JsonldWebpageTest.php b/tests/unit/JsonldWebpageTest.php
new file mode 100644
index 0000000..b08c785
--- /dev/null
+++ b/tests/unit/JsonldWebpageTest.php
@@ -0,0 +1,112 @@
+retype_to_medical_webpage( [ 'WebPage' ] );
+ $this->assertSame( [ 'WebPage' ], $result );
+ }
+
+ public function test_retype_peptide_singular_array_returns_medical_webpage_string(): void {
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = 'peptide';
+ $result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( [ 'WebPage' ] );
+ $this->assertSame( 'MedicalWebPage', $result );
+ }
+
+ public function test_retype_string_input_non_singular_passthrough(): void {
+ $GLOBALS['pr_test_is_singular'] = false;
+ $GLOBALS['pr_test_singular_type'] = '';
+ $result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( 'CollectionPage' );
+ $this->assertSame( 'CollectionPage', $result );
+ }
+
+ // ── enrich_webpage_piece: @type as array injects enrichments ──────.
+
+ public function test_enrich_webpage_piece_array_type_injects_last_reviewed(): void {
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = 'peptide';
+ $GLOBALS['pr_test_the_id'] = 42;
+ $GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
+
+ $graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage', 'name' => 'BPC-157' ] ];
+ $result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
+ $piece = $result_graph[0] ?? [];
+ $this->assertArrayHasKey( 'lastReviewed', $piece );
+ $this->assertSame( '2026-06-01', $piece['lastReviewed'] );
+ }
+
+ public function test_enrich_webpage_piece_array_type_injects_reviewed_by(): void {
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = 'peptide';
+ $GLOBALS['pr_test_the_id'] = 42;
+ $GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
+
+ $graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage' ] ];
+ $result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
+ $this->assertArrayHasKey( 'reviewedBy', $result_graph[0] ?? [] );
+ }
+
+ public function test_enrich_webpage_piece_array_type_injects_audience(): void {
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = 'peptide';
+ $GLOBALS['pr_test_the_id'] = 42;
+ $GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
+
+ $graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage' ] ];
+ $result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
+ $this->assertArrayHasKey( 'audience', $result_graph[0] ?? [] );
+ }
+
+ public function test_enrich_webpage_piece_string_medical_webpage_also_enriches(): void {
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = 'peptide';
+ $GLOBALS['pr_test_the_id'] = 42;
+ $GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
+
+ $graph = [ [ '@type' => 'MedicalWebPage', '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage' ] ];
+ $result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
+ $this->assertArrayHasKey( 'lastReviewed', $result_graph[0] ?? [] );
+ }
+
+ public function test_enrich_webpage_piece_non_peptide_does_not_inject(): void {
+ $GLOBALS['pr_test_is_singular'] = true;
+ $GLOBALS['pr_test_singular_type'] = 'page';
+ $GLOBALS['pr_test_the_id'] = 50;
+ $GLOBALS['pr_test_postmeta'][50] = [];
+
+ $graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/about/#webpage' ] ];
+ $result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
+ $this->assertArrayNotHasKey( 'lastReviewed', $result_graph[0] ?? [] );
+ }
+}
diff --git a/tests/unit/Migration0004Test.php b/tests/unit/Migration0004Test.php
new file mode 100644
index 0000000..ba8135f
--- /dev/null
+++ b/tests/unit/Migration0004Test.php
@@ -0,0 +1,239 @@
+ [],
+ 'existing_taxonomies' => [],
+ 'registered_post_types' => [],
+ 'registered_taxonomies' => [],
+ 'registered_meta' => [],
+ 'added_actions' => [],
+ 'added_filters' => [],
+ ];
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-pubchem-client.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-migration-0004-backfill-peptide-meta.php';
+ $this->migration = new PR_Core_Migration_0004_Backfill_Peptide_Meta();
+ }
+
+ // ── Helper: call private method via reflection ────────────────────.
+
+ /**
+ * @param mixed[] $args
+ * @return mixed
+ */
+ private function call_private( object $obj, string $method, array $args = [] ) {
+ $ref = new ReflectionMethod( $obj, $method );
+ $ref->setAccessible( true );
+ return $ref->invokeArgs( $obj, $args );
+ }
+
+ // ── parse_weight() ────────────────────────────────────────────────.
+
+ public function test_parse_weight_strips_da_suffix(): void {
+ $this->assertSame( 1419.5, $this->call_private( $this->migration, 'parse_weight', [ '1419.5 Da' ] ) );
+ }
+
+ public function test_parse_weight_strips_uppercase_da(): void {
+ $this->assertSame( 1419.5, $this->call_private( $this->migration, 'parse_weight', [ '1419.5 DA' ] ) );
+ }
+
+ public function test_parse_weight_plain_float(): void {
+ $this->assertSame( 1419.5, $this->call_private( $this->migration, 'parse_weight', [ '1419.5' ] ) );
+ }
+
+ public function test_parse_weight_empty_returns_zero(): void {
+ $this->assertSame( 0.0, $this->call_private( $this->migration, 'parse_weight', [ '' ] ) );
+ }
+
+ public function test_parse_weight_suffix_only_returns_zero(): void {
+ $this->assertSame( 0.0, $this->call_private( $this->migration, 'parse_weight', [ ' Da ' ] ) );
+ }
+
+ public function test_parse_weight_lowercase_da_with_whitespace(): void {
+ $this->assertSame( 800.123, $this->call_private( $this->migration, 'parse_weight', [ ' 800.123 da ' ] ) );
+ }
+
+ // ── parse_aliases_string() ────────────────────────────────────────.
+
+ public function test_parse_aliases_string_splits_csv(): void {
+ $result = json_decode(
+ $this->call_private( $this->migration, 'parse_aliases_string', [ 'Body Protection Compound-157, PL 14736, PL-10, Bepecin' ] ),
+ true
+ );
+ $this->assertIsArray( $result );
+ $this->assertCount( 4, $result );
+ $this->assertSame( 'Body Protection Compound-157', $result[0] );
+ $this->assertSame( 'PL 14736', $result[1] );
+ }
+
+ public function test_parse_aliases_string_empty_returns_json_empty_array(): void {
+ $this->assertSame( '[]', $this->call_private( $this->migration, 'parse_aliases_string', [ '' ] ) );
+ }
+
+ public function test_parse_aliases_string_whitespace_only_returns_json_empty_array(): void {
+ $this->assertSame( '[]', $this->call_private( $this->migration, 'parse_aliases_string', [ ' , , ' ] ) );
+ }
+
+ public function test_parse_aliases_string_single_alias(): void {
+ $result = json_decode( $this->call_private( $this->migration, 'parse_aliases_string', [ 'Only One' ] ), true );
+ $this->assertCount( 1, $result );
+ }
+
+ // ── sanitize_molecular_formula() ──────────────────────────────────.
+
+ public function test_sanitize_formula_valid_passes_unchanged(): void {
+ $this->assertSame( 'C62H98N16O22', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( 'C62H98N16O22' ) );
+ }
+
+ public function test_sanitize_formula_strips_html_tags(): void {
+ $this->assertSame( 'C62H98N16O22', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( 'C62H98N16O22' ) );
+ }
+
+ public function test_sanitize_formula_preserves_parentheses(): void {
+ $this->assertSame( 'C2H5(OH)', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( 'C2H5(OH)' ) );
+ }
+
+ public function test_sanitize_formula_strips_non_formula_chars(): void {
+ $this->assertSame( 'C10H12', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( "C10H12\ninjection" ) );
+ }
+
+ public function test_sanitize_formula_fully_invalid_returns_empty(): void {
+ $this->assertSame( '', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( '' ) );
+ }
+
+ public function test_sanitize_formula_empty_returns_empty(): void {
+ $this->assertSame( '', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( '' ) );
+ }
+
+ public function test_sanitize_formula_whitespace_only_returns_empty(): void {
+ $this->assertSame( '', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( ' ' ) );
+ }
+
+ // ── Idempotency ───────────────────────────────────────────────────.
+
+ public function test_backfill_post_skipped_when_all_keys_populated(): void {
+ $GLOBALS['pr_test_postmeta'][1] = [
+ '_pr_molecular_formula' => 'C62H98N16O22',
+ '_pr_molecular_weight' => '1419.5',
+ '_pr_aliases' => '["Alias One"]',
+ ];
+ $result = $this->call_private( $this->migration, 'backfill_post', [ 1 ] );
+ $this->assertSame( 'skipped', $result );
+ $this->assertEmpty( $GLOBALS['pr_test_updated_meta'] );
+ }
+
+ // ── PSA source path ───────────────────────────────────────────────.
+
+ public function test_backfill_post_copies_psa_data(): void {
+ $GLOBALS['pr_test_postmeta'][2] = [
+ 'psa_molecular_formula' => 'C62H98N16O22',
+ 'psa_molecular_weight' => '1419.5 Da',
+ 'psa_aliases' => 'Body Protection Compound-157, PL 14736',
+ ];
+ $GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
+ $result = $this->call_private( $this->migration, 'backfill_post', [ 2 ] );
+ $this->assertSame( 'copied_psa', $result );
+ }
+
+ public function test_backfill_post_psa_writes_formula_correctly(): void {
+ $GLOBALS['pr_test_postmeta'][2] = [
+ 'psa_molecular_formula' => 'C62H98N16O22',
+ 'psa_molecular_weight' => '1419.5 Da',
+ 'psa_aliases' => 'Body Protection Compound-157',
+ ];
+ $GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
+ $this->call_private( $this->migration, 'backfill_post', [ 2 ] );
+ $written = null;
+ foreach ( $GLOBALS['pr_test_updated_meta'] as $entry ) {
+ if ( '_pr_molecular_formula' === $entry['key'] ) {
+ $written = $entry['value'];
+ }
+ }
+ $this->assertSame( 'C62H98N16O22', $written );
+ }
+
+ public function test_backfill_post_psa_strips_da_from_weight(): void {
+ $GLOBALS['pr_test_postmeta'][2] = [
+ 'psa_molecular_formula' => 'C62H98N16O22',
+ 'psa_molecular_weight' => '1419.5 Da',
+ 'psa_aliases' => 'Body Protection Compound-157',
+ ];
+ $GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
+ $this->call_private( $this->migration, 'backfill_post', [ 2 ] );
+ $written = null;
+ foreach ( $GLOBALS['pr_test_updated_meta'] as $entry ) {
+ if ( '_pr_molecular_weight' === $entry['key'] ) {
+ $written = $entry['value'];
+ }
+ }
+ $this->assertSame( '1419.5', $written );
+ }
+
+ public function test_backfill_post_psa_writes_aliases_as_json_array(): void {
+ $GLOBALS['pr_test_postmeta'][2] = [
+ 'psa_molecular_formula' => 'C62H98N16O22',
+ 'psa_molecular_weight' => '1419.5 Da',
+ 'psa_aliases' => 'Body Protection Compound-157, PL 14736',
+ ];
+ $GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
+ $this->call_private( $this->migration, 'backfill_post', [ 2 ] );
+ $written = null;
+ foreach ( $GLOBALS['pr_test_updated_meta'] as $entry ) {
+ if ( '_pr_aliases' === $entry['key'] ) {
+ $written = $entry['value'];
+ }
+ }
+ $aliases = json_decode( $written, true );
+ $this->assertIsArray( $aliases );
+ $this->assertCount( 2, $aliases );
+ }
+
+ // ── PubChem skip path ─────────────────────────────────────────────.
+
+ public function test_backfill_post_returns_pubchem_skip_when_network_fails(): void {
+ $mock_pubchem = new class extends PR_Core_Pubchem_Client {
+ public function fetch_by_cid( string $cid ): ?array { return null; }
+ public function fetch_by_name( string $name ): ?array { return null; }
+ };
+ $mock_migration = new PR_Core_Migration_0004_Backfill_Peptide_Meta( $mock_pubchem );
+
+ $GLOBALS['pr_test_postmeta'][10] = [];
+ $GLOBALS['pr_test_posts'][10] = (object) [ 'post_title' => 'IGF-1 LR3', 'ID' => 10 ];
+
+ $result = $this->call_private( $mock_migration, 'backfill_post', [ 10 ] );
+ $this->assertSame( 'pubchem_skip', $result );
+ $this->assertEmpty( $GLOBALS['pr_test_updated_meta'] );
+ }
+}
diff --git a/tests/unit/PeptideCptTest.php b/tests/unit/PeptideCptTest.php
new file mode 100644
index 0000000..0214413
--- /dev/null
+++ b/tests/unit/PeptideCptTest.php
@@ -0,0 +1,247 @@
+ [],
+ 'existing_taxonomies' => [],
+ 'registered_post_types' => [],
+ 'registered_taxonomies' => [],
+ 'registered_meta' => [],
+ 'added_actions' => [],
+ 'added_filters' => [],
+ ];
+ // Ensure CPT class is loaded.
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
+ }
+
+ // ── Constants ─────────────────────────────────────────────────────.
+
+ public function test_post_type_constant(): void {
+ $this->assertSame( 'peptide', PR_Core_Peptide_CPT::POST_TYPE );
+ }
+
+ public function test_tax_category_constant(): void {
+ $this->assertSame( 'peptide_category', PR_Core_Peptide_CPT::TAX_CATEGORY );
+ }
+
+ public function test_capability_constant(): void {
+ $this->assertSame( 'manage_peptide_content', PR_Core_Peptide_CPT::CAPABILITY );
+ }
+
+ public function test_tax_family_constant_removed(): void {
+ $reflection = new ReflectionClass( PR_Core_Peptide_CPT::class );
+ $this->assertFalse(
+ $reflection->hasConstant( 'TAX_FAMILY' ),
+ 'TAX_FAMILY constant should have been removed in v0.2.0'
+ );
+ }
+
+ // ── CPT registration guard ────────────────────────────────────────.
+
+ public function test_register_post_type_noop_when_already_exists(): void {
+ $GLOBALS['pr_core_test_state']['existing_post_types'] = [ 'peptide' ];
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $this->assertEmpty(
+ $GLOBALS['pr_core_test_state']['registered_post_types'],
+ 'register_peptide_post_type() must no-op when post_type_exists("peptide") is true'
+ );
+ }
+
+ // ── CPT registration payload ──────────────────────────────────────.
+
+ public function test_register_post_type_registers_peptide_slug(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $this->assertArrayHasKey(
+ 'peptide',
+ $GLOBALS['pr_core_test_state']['registered_post_types']
+ );
+ }
+
+ public function test_register_post_type_args_public(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertTrue( $args['public'] ?? null );
+ }
+
+ public function test_register_post_type_args_publicly_queryable(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertTrue( $args['publicly_queryable'] ?? null );
+ }
+
+ public function test_register_post_type_args_show_in_rest(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertTrue( $args['show_in_rest'] ?? null );
+ }
+
+ public function test_register_post_type_args_rest_base(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertSame( 'peptides', $args['rest_base'] ?? null );
+ }
+
+ public function test_register_post_type_args_no_rest_namespace(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertArrayNotHasKey(
+ 'rest_namespace',
+ $args,
+ 'rest_namespace must be absent — defaults to wp/v2 for Gutenberg compatibility'
+ );
+ }
+
+ public function test_register_post_type_args_capability_type(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertSame( 'post', $args['capability_type'] ?? null );
+ }
+
+ public function test_register_post_type_args_map_meta_cap(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertTrue( $args['map_meta_cap'] ?? null );
+ }
+
+ public function test_register_post_type_args_not_hierarchical(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertFalse( $args['hierarchical'] ?? null );
+ }
+
+ public function test_register_post_type_args_has_archive(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertTrue( $args['has_archive'] ?? null );
+ }
+
+ public function test_register_post_type_args_rewrite_slug(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertSame( 'peptides', $args['rewrite']['slug'] ?? null );
+ }
+
+ public function test_register_post_type_args_rewrite_with_front(): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $this->assertFalse( $args['rewrite']['with_front'] ?? null );
+ }
+
+ /**
+ * @dataProvider supports_provider
+ */
+ public function test_register_post_type_supports_feature( string $feature ): void {
+ PR_Core_Peptide_CPT::register_peptide_post_type();
+ $args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
+ $supports = $args['supports'] ?? [];
+ $this->assertContains( $feature, $supports );
+ }
+
+ /**
+ * @return array>
+ */
+ public static function supports_provider(): array {
+ return [
+ [ 'title' ],
+ [ 'editor' ],
+ [ 'thumbnail' ],
+ [ 'excerpt' ],
+ [ 'revisions' ],
+ [ 'custom-fields' ],
+ ];
+ }
+
+ // ── Taxonomy guard ────────────────────────────────────────────────.
+
+ public function test_register_taxonomies_noop_when_already_exists(): void {
+ $GLOBALS['pr_core_test_state']['existing_taxonomies'] = [ 'peptide_category' ];
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $this->assertEmpty( $GLOBALS['pr_core_test_state']['registered_taxonomies'] );
+ }
+
+ // ── Taxonomy registration payload ─────────────────────────────────.
+
+ public function test_register_taxonomies_registers_peptide_category(): void {
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $this->assertArrayHasKey(
+ 'peptide_category',
+ $GLOBALS['pr_core_test_state']['registered_taxonomies']
+ );
+ }
+
+ public function test_register_taxonomies_does_not_register_pr_peptide_family(): void {
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $this->assertArrayNotHasKey(
+ 'pr_peptide_family',
+ $GLOBALS['pr_core_test_state']['registered_taxonomies'],
+ 'pr_peptide_family taxonomy must not be registered (removed in v0.2.0)'
+ );
+ }
+
+ public function test_register_taxonomies_does_not_register_peptide_family(): void {
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $this->assertArrayNotHasKey(
+ 'peptide_family',
+ $GLOBALS['pr_core_test_state']['registered_taxonomies']
+ );
+ }
+
+ public function test_register_taxonomies_attached_to_peptide_cpt(): void {
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
+ $this->assertSame( 'peptide', $tax['object_type'] ?? null );
+ }
+
+ public function test_register_taxonomies_is_hierarchical(): void {
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
+ $this->assertTrue( $tax['args']['hierarchical'] ?? null );
+ }
+
+ public function test_register_taxonomies_show_in_rest(): void {
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
+ $this->assertTrue( $tax['args']['show_in_rest'] ?? null );
+ }
+
+ public function test_register_taxonomies_rewrite_slug(): void {
+ PR_Core_Peptide_CPT::register_taxonomies();
+ $tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
+ $this->assertSame( 'peptide-category', $tax['args']['rewrite']['slug'] ?? null );
+ }
+
+ // ── Hook registration ─────────────────────────────────────────────.
+
+ public function test_register_hooks_adds_init_action(): void {
+ $cpt = new PR_Core_Peptide_CPT();
+ $cpt->register_hooks();
+ $hooks = array_column( $GLOBALS['pr_core_test_state']['added_actions'], 'hook' );
+ $this->assertContains( 'init', $hooks );
+ }
+}
diff --git a/tests/unit/VerificationScannerTest.php b/tests/unit/VerificationScannerTest.php
new file mode 100644
index 0000000..19c8a11
--- /dev/null
+++ b/tests/unit/VerificationScannerTest.php
@@ -0,0 +1,126 @@
+= 90% and < 100% → due.
+ * - Days >= threshold → overdue.
+ * - Boundary at exactly 90% → due.
+ * - high velocity applies 60-day threshold from option.
+ * - low velocity applies 365-day constant (ignores option).
+ * - medium/default applies 180-day threshold from option.
+ * - Unknown velocity falls to medium/default.
+ *
+ * @package PeptideRepoCore\Tests
+ */
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests for PR_Core_Verification_Scanner::compute_status().
+ */
+class VerificationScannerTest extends TestCase {
+
+ protected function setUp(): void {
+ $GLOBALS['pr_core_options'] = [];
+ require_once PR_CORE_PLUGIN_DIR . 'includes/scanner/class-pr-core-verification-scanner.php';
+ }
+
+ // ── Helper ────────────────────────────────────────────────────────.
+
+ private function days_ago( int $days ): string {
+ return gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
+ }
+
+ // ── Empty last_verified ───────────────────────────────────────────.
+
+ public function test_empty_last_verified_returns_overdue(): void {
+ $this->assertSame(
+ 'overdue',
+ PR_Core_Verification_Scanner::compute_status( '', 'medium' )
+ );
+ }
+
+ // ── Medium velocity (default 180-day threshold) ───────────────────.
+
+ public function test_medium_160_days_is_current(): void {
+ $this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 160 ), 'medium' ) );
+ }
+
+ public function test_medium_163_days_is_due(): void {
+ $this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 163 ), 'medium' ) );
+ }
+
+ public function test_medium_180_days_is_overdue(): void {
+ $this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 180 ), 'medium' ) );
+ }
+
+ public function test_medium_200_days_is_overdue(): void {
+ $this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 200 ), 'medium' ) );
+ }
+
+ // ── 90% boundary exactness ────────────────────────────────────────.
+
+ public function test_boundary_89_days_threshold_100_is_current(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
+ $this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 89 ), 'medium' ) );
+ }
+
+ public function test_boundary_90_days_threshold_100_is_due(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
+ $this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 90 ), 'medium' ) );
+ }
+
+ public function test_boundary_100_days_threshold_100_is_overdue(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
+ $this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 100 ), 'medium' ) );
+ }
+
+ // ── High velocity (60-day threshold from option) ──────────────────.
+
+ public function test_high_50_days_is_current(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
+ $this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 50 ), 'high' ) );
+ }
+
+ public function test_high_55_days_is_due(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
+ $this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 55 ), 'high' ) );
+ }
+
+ public function test_high_61_days_is_overdue(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
+ $this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 61 ), 'high' ) );
+ }
+
+ // ── Low velocity (365-day constant — ignores option) ──────────────.
+
+ public function test_low_300_days_is_current(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ];
+ $this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 300 ), 'low' ) );
+ }
+
+ public function test_low_340_days_is_due(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ];
+ $this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 340 ), 'low' ) );
+ }
+
+ public function test_low_366_days_is_overdue(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ];
+ $this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 366 ), 'low' ) );
+ }
+
+ // ── Unknown velocity falls to medium/default ──────────────────────.
+
+ public function test_unknown_velocity_uses_default_threshold(): void {
+ $GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 180 ];
+ $this->assertSame(
+ 'current',
+ PR_Core_Verification_Scanner::compute_status( $this->days_ago( 100 ), 'unknown' )
+ );
+ }
+}
diff --git a/tests/unit/VerificationSettingsTest.php b/tests/unit/VerificationSettingsTest.php
new file mode 100644
index 0000000..c8c20f8
--- /dev/null
+++ b/tests/unit/VerificationSettingsTest.php
@@ -0,0 +1,144 @@
+ [],
+ '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';
+ }
+
+ // ── sanitize_cadence() ────────────────────────────────────────────.
+
+ public function test_sanitize_cadence_daily(): void {
+ $this->assertSame( 'daily', PR_Core_Settings::sanitize_cadence( 'daily' ) );
+ }
+
+ public function test_sanitize_cadence_weekly(): void {
+ $this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( 'weekly' ) );
+ }
+
+ public function test_sanitize_cadence_monthly(): void {
+ $this->assertSame( 'monthly', PR_Core_Settings::sanitize_cadence( 'monthly' ) );
+ }
+
+ public function test_sanitize_cadence_invalid_defaults_to_weekly(): void {
+ $this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( 'hourly' ) );
+ }
+
+ public function test_sanitize_cadence_empty_defaults_to_weekly(): void {
+ $this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( '' ) );
+ }
+
+ public function test_sanitize_cadence_uppercase_defaults_to_weekly(): void {
+ $this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( 'DAILY' ) );
+ }
+
+ // ── sanitize_emails() ─────────────────────────────────────────────.
+
+ public function test_sanitize_emails_empty_returns_empty(): void {
+ $this->assertSame( '', PR_Core_Settings::sanitize_emails( '' ) );
+ }
+
+ public function test_sanitize_emails_valid_single_kept(): void {
+ $this->assertSame( 'admin@example.com', PR_Core_Settings::sanitize_emails( 'admin@example.com' ) );
+ }
+
+ public function test_sanitize_emails_invalid_single_returns_empty(): void {
+ $this->assertSame( '', PR_Core_Settings::sanitize_emails( 'not-an-email' ) );
+ }
+
+ public function test_sanitize_emails_valid_pair_both_kept(): void {
+ $this->assertSame( 'a@test.com, b@test.com', PR_Core_Settings::sanitize_emails( 'a@test.com, b@test.com' ) );
+ }
+
+ public function test_sanitize_emails_mixed_only_valid_kept(): void {
+ $this->assertSame( 'good@test.com', PR_Core_Settings::sanitize_emails( 'good@test.com, bad-email' ) );
+ }
+
+ public function test_sanitize_emails_whitespace_trimmed(): void {
+ $this->assertSame( 'trimmed@test.com', PR_Core_Settings::sanitize_emails( ' trimmed@test.com ' ) );
+ }
+
+ // ── reschedule_cron(): no-op when same value ──────────────────────.
+
+ public function test_reschedule_cron_noop_when_value_unchanged(): void {
+ PR_Core_Settings::reschedule_cron( 'weekly', 'weekly' );
+ $this->assertEmpty( $GLOBALS['pr_core_cron_calls'] );
+ }
+
+ // ── reschedule_cron(): clears + reschedules on change ─────────────.
+
+ public function test_reschedule_cron_calls_clear_when_changed(): void {
+ PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
+ $actions = array_column( $GLOBALS['pr_core_cron_calls'], 'action' );
+ $this->assertContains( 'clear', $actions );
+ }
+
+ public function test_reschedule_cron_calls_schedule_when_changed(): void {
+ PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
+ $actions = array_column( $GLOBALS['pr_core_cron_calls'], 'action' );
+ $this->assertContains( 'schedule', $actions );
+ }
+
+ public function test_reschedule_cron_schedules_correct_hook(): void {
+ PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
+ $schedule_call = null;
+ foreach ( $GLOBALS['pr_core_cron_calls'] as $call ) {
+ if ( 'schedule' === $call['action'] ) {
+ $schedule_call = $call;
+ break;
+ }
+ }
+ $this->assertNotNull( $schedule_call );
+ $this->assertSame( 'pr_core_verification_scan', $schedule_call['hook'] ?? '' );
+ }
+
+ public function test_reschedule_cron_schedules_new_cadence(): void {
+ PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
+ $schedule_call = null;
+ foreach ( $GLOBALS['pr_core_cron_calls'] as $call ) {
+ if ( 'schedule' === $call['action'] ) {
+ $schedule_call = $call;
+ break;
+ }
+ }
+ $this->assertSame( 'daily', $schedule_call['recurrence'] ?? '' );
+ }
+
+ public function test_reschedule_cron_clear_fires_before_schedule(): void {
+ PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
+ $actions = array_column( $GLOBALS['pr_core_cron_calls'], 'action' );
+ $clear_idx = array_search( 'clear', $actions, true );
+ $schedule_idx = array_search( 'schedule', $actions, true );
+ $this->assertLessThan( $schedule_idx, $clear_idx );
+ }
+}
diff --git a/tests/unit/test-jsonld-peptide-piece.php b/tests/unit/test-jsonld-peptide-piece.php
index b7668d3..e35f6c7 100644
--- a/tests/unit/test-jsonld-peptide-piece.php
+++ b/tests/unit/test-jsonld-peptide-piece.php
@@ -31,7 +31,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../bootstrap.php';
-// ── Additional stubs ─────────────────────────────────────────────────────
+// ── Additional stubs ─────────────────────────────────────────────────────.
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
@@ -129,7 +129,7 @@ if ( ! function_exists( 'sanitize_textarea_field' ) ) {
}
}
-// ── Stub PR_Core_Peptide_Repository ──────────────────────────────────────
+// ── Stub PR_Core_Peptide_Repository ──────────────────────────────────────.
/**
* Minimal repository stub — returns whatever is in $GLOBALS['pr_test_peptide_dto'].
@@ -140,7 +140,7 @@ class PR_Core_Peptide_Repository {
}
}
-// ── Load classes under test ───────────────────────────────────────────────
+// ── Load classes under test ───────────────────────────────────────────────.
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
@@ -150,7 +150,7 @@ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.ph
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
-// ── Helper ────────────────────────────────────────────────────────────────
+// ── Helper ────────────────────────────────────────────────────────────────.
/**
* Create a minimal PR_Core_Peptide_DTO for testing.
@@ -184,7 +184,7 @@ function make_peptide_piece_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
echo "== JSON-LD peptide-piece regression tests (v0.6.2) ==\n\n";
-// ── Setup ─────────────────────────────────────────────────────────────────
+// ── Setup ─────────────────────────────────────────────────────────────────.
$GLOBALS['pr_test_the_id'] = 36;
$GLOBALS['pr_test_is_singular'] = true;
@@ -198,11 +198,11 @@ $GLOBALS['pr_test_peptide_dto'] = make_peptide_piece_dto();
$jsonld = new PR_Core_Jsonld();
-// ── Regression: inject_graph_pieces fatal reproduced against old contract ─
+// ── Regression: inject_graph_pieces fatal reproduced against old contract ─.
echo "Regression: inject_graph_pieces (OLD hook) with plain array fatal:\n";
-// Simulate what Yoast's filter_graph_pieces_to_generate does:
+// Simulate what Yoast's filter_graph_pieces_to_generate does:.
// it calls get_class($piece) on every item. A plain array causes a fatal.
$drug_builder = new PR_Core_Jsonld_Drug();
$drug_array = $drug_builder->build( $GLOBALS['pr_test_peptide_dto'] );
@@ -210,8 +210,8 @@ $drug_array = $drug_builder->build( $GLOBALS['pr_test_peptide_dto'] );
$caught_fatal = false;
try {
// This is the exact operation Yoast does in filter_graph_pieces_to_generate().
- // Against the old v0.6.1 code (inject via wpseo_schema_graph_pieces), this would
- // produce: "get_class(): Argument #1 ($object) must be of type object, array given"
+ // Against the old v0.6.1 code (inject via wpseo_schema_graph_pieces), this would.
+ // produce: "get_class(): Argument #1 ($object) must be of type object, array given".
$class_result = get_class( $drug_array );
} catch ( TypeError $e ) {
$caught_fatal = true;
@@ -220,7 +220,7 @@ try {
}
pr_assert( $caught_fatal, 'get_class(array) throws TypeError/Error — confirms the v0.6.1 fatal' );
-// ── Fix: inject_graph_nodes (NEW hook) works without fatal ────────────────
+// ── Fix: inject_graph_nodes (NEW hook) works without fatal ────────────────.
echo "\nFix: inject_graph_nodes via wpseo_schema_graph (plain array graph):\n";
@@ -279,7 +279,7 @@ pr_assert( count( $webpage_nodes ) === 1, 'MedicalWebPage node preserved (no dup
$breadcrumb_nodes = array_filter( $result_graph, static fn( $n ) => ( $n['@type'] ?? '' ) === 'BreadcrumbList' );
pr_assert( count( $breadcrumb_nodes ) === 1, 'BreadcrumbList node preserved (no duplicate)' );
-// ── FAQ: present when _pr_faq_items has items ─────────────────────────────
+// ── FAQ: present when _pr_faq_items has items ─────────────────────────────.
echo "\nFAQ injection via inject_graph_nodes:\n";
@@ -301,7 +301,7 @@ pr_assert_equals( 'https://peptiderepo.com/peptides/bpc-157/#faq', $faq_node['@i
pr_assert( isset( $faq_node['mainEntity'] ), 'FAQPage has mainEntity' );
pr_assert_equals( 2, count( $faq_node['mainEntity'] ), 'FAQPage has 2 questions' );
-// ── FAQ: absent when _pr_faq_items empty ─────────────────────────────────
+// ── FAQ: absent when _pr_faq_items empty ─────────────────────────────────.
$GLOBALS['pr_test_postmeta'][36]['_pr_faq_items'] = '[]';
@@ -312,7 +312,7 @@ $faq_nodes_empty = array_filter(
);
pr_assert( count( $faq_nodes_empty ) === 0, 'FAQPage NOT injected when _pr_faq_items is empty array' );
-// ── Non-peptide page: no injection ───────────────────────────────────────
+// ── Non-peptide page: no injection ───────────────────────────────────────.
echo "\nNon-peptide page: no injection:\n";
@@ -332,7 +332,7 @@ pr_assert( count( $result_non_peptide ) === 1, 'inject_graph_nodes no-ops on non
$drug_nodes_np = array_filter( $result_non_peptide, static fn( $n ) => ( is_array( $n['@type'] ?? null ) ? in_array( 'Drug', $n['@type'], true ) : ( $n['@type'] ?? '' ) === 'Drug' ) );
pr_assert( count( $drug_nodes_np ) === 0, 'Drug NOT injected on non-peptide page' );
-// ── register_hooks: does NOT hook wpseo_schema_graph_pieces ──────────────
+// ── register_hooks: does NOT hook wpseo_schema_graph_pieces ──────────────.
echo "\nregister_hooks: wpseo_schema_graph_pieces must NOT be hooked:\n";
@@ -341,11 +341,11 @@ $GLOBALS['pr_core_test_state']['added_filters'] = [];
$filters_added = [];
// Override add_filter in the bootstrap to capture calls.
-// (bootstrap only stubs add_action; we check via a fresh parse of register_hooks.)
+// (bootstrap only stubs add_action; we check via a fresh parse of register_hooks.).
$jsonld2 = new PR_Core_Jsonld();
// Use Reflection to inspect what hooks register_hooks would call.
-// Instead: inspect the class source for the string — if inject_graph_pieces is
+// Instead: inspect the class source for the string — if inject_graph_pieces is.
// still registered on wpseo_schema_graph_pieces, the bug is NOT fixed.
$src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
pr_assert(
@@ -361,6 +361,6 @@ pr_assert(
'class-pr-core-jsonld.php hooks wpseo_schema_graph for injection'
);
-// ── Summary ───────────────────────────────────────────────────────────────
+// ── Summary ───────────────────────────────────────────────────────────────.
exit( pr_test_summary() );
diff --git a/tests/unit/test-jsonld-webpage.php b/tests/unit/test-jsonld-webpage.php
index 1539792..1fb8212 100644
--- a/tests/unit/test-jsonld-webpage.php
+++ b/tests/unit/test-jsonld-webpage.php
@@ -21,7 +21,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../bootstrap.php';
-// ── Additional stubs ─────────────────────────────────────────────────────
+// ── Additional stubs ─────────────────────────────────────────────────────.
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
@@ -81,7 +81,7 @@ echo "== JSON-LD WebPage v0.6.1 regression tests ==\n\n";
$enricher = new PR_Core_Jsonld_Webpage();
-// ── retype_to_medical_webpage: array input, non-peptide singular ──────────
+// ── retype_to_medical_webpage: array input, non-peptide singular ──────────.
echo "retype_to_medical_webpage — array input:\n";
@@ -106,7 +106,7 @@ $GLOBALS['pr_test_singular_type'] = '';
$result = $enricher->retype_to_medical_webpage( 'CollectionPage' );
pr_assert_equals( 'CollectionPage', $result, 'non-singular string: passes through unchanged' );
-// ── enrich_webpage_piece: @type as array injects enrichments ─────────────
+// ── enrich_webpage_piece: @type as array injects enrichments ─────────────.
echo "\nenrich_webpage_piece — @type as array:\n";
@@ -160,6 +160,6 @@ $graph_non_peptide = [
$result_non_peptide = $enricher->enrich_webpage_piece( $graph_non_peptide, new stdClass() );
pr_assert( ! isset( $result_non_peptide[0]['lastReviewed'] ), 'non-peptide: lastReviewed NOT injected' );
-// ── Summary ──────────────────────────────────────────────────────────────
+// ── Summary ──────────────────────────────────────────────────────────────.
exit( pr_test_summary() );
diff --git a/tests/unit/test-jsonld.php b/tests/unit/test-jsonld.php
index 978d7dd..951d0c1 100644
--- a/tests/unit/test-jsonld.php
+++ b/tests/unit/test-jsonld.php
@@ -29,7 +29,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../bootstrap.php';
-// ── Additional stubs ─────────────────────────────────────────────────────
+// ── Additional stubs ─────────────────────────────────────────────────────.
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
@@ -111,7 +111,7 @@ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.ph
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
-// ── Helpers ──────────────────────────────────────────────────────────────
+// ── Helpers ──────────────────────────────────────────────────────────────.
/**
* Create a minimal PR_Core_Peptide_DTO for testing.
@@ -145,7 +145,7 @@ function make_peptide_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
echo "== JSON-LD v0.6.0 unit tests ==\n\n";
-// ── Drug @id ─────────────────────────────────────────────────────────────
+// ── Drug @id ─────────────────────────────────────────────────────────────.
echo "Drug @id:\n";
@@ -160,14 +160,14 @@ pr_assert_equals(
'Drug @id = {permalink}#drug'
);
-// ── Drug @type ───────────────────────────────────────────────────────────
+// ── Drug @type ───────────────────────────────────────────────────────────.
echo "\nDrug @type:\n";
pr_assert( is_array( $drug_node['@type'] ), 'Drug @type is an array' );
pr_assert( in_array( 'Drug', $drug_node['@type'], true ), '@type includes "Drug"' );
pr_assert( in_array( 'MolecularEntity', $drug_node['@type'], true ), '@type includes "MolecularEntity"' );
-// ── Fields present when meta set ─────────────────────────────────────────
+// ── Fields present when meta set ─────────────────────────────────────────.
echo "\nFields present when _pr_* meta set:\n";
@@ -190,7 +190,7 @@ pr_assert_equals( 'g/mol', $drug_node_populated['molecularWeight']['unitText'],
pr_assert( isset( $drug_node_populated['alternateName'] ), 'alternateName present when _pr_aliases set' );
pr_assert_equals( 2, count( $drug_node_populated['alternateName'] ), 'alternateName has correct count' );
-// ── Fields omitted when meta empty ───────────────────────────────────────
+// ── Fields omitted when meta empty ───────────────────────────────────────.
echo "\nFields OMITTED when meta empty:\n";
@@ -201,7 +201,7 @@ pr_assert( ! isset( $drug_node_empty['molecularFormula'] ), 'molecularFormula OM
pr_assert( ! isset( $drug_node_empty['molecularWeight'] ), 'molecularWeight OMITTED when meta empty' );
pr_assert( ! isset( $drug_node_empty['alternateName'] ), 'alternateName OMITTED when _pr_aliases empty' );
-// ── FAQPage present when _pr_faq_items has items ─────────────────────────
+// ── FAQPage present when _pr_faq_items has items ─────────────────────────.
echo "\nFAQPage emission:\n";
@@ -224,7 +224,7 @@ pr_assert_equals( 2, count( $faq_piece['mainEntity'] ), 'FAQPage has 2 questions
pr_assert_equals( 'Question', $faq_piece['mainEntity'][0]['@type'], 'Question @type correct' );
pr_assert( isset( $faq_piece['mainEntity'][0]['acceptedAnswer'] ), 'Question has acceptedAnswer' );
-// ── FAQPage absent when _pr_faq_items empty ──────────────────────────────
+// ── FAQPage absent when _pr_faq_items empty ──────────────────────────────.
$GLOBALS['pr_test_postmeta'][1] = [ '_pr_faq_items' => '[]' ];
$faq_empty = $faq_builder->build( 1, $permalink );
@@ -234,7 +234,7 @@ $GLOBALS['pr_test_postmeta'][1] = [];
$faq_absent = $faq_builder->build( 1, $permalink );
pr_assert( null === $faq_absent, 'FAQPage piece IS null when _pr_faq_items absent' );
-// ── MedicalWebPage retype ────────────────────────────────────────────────
+// ── MedicalWebPage retype ────────────────────────────────────────────────.
echo "\nMedicalWebPage retype:\n";
@@ -250,7 +250,7 @@ $GLOBALS['pr_test_singular_type'] = '';
$result = $webpage_enricher->retype_to_medical_webpage( 'WebPage' );
pr_assert_equals( 'WebPage', $result, 'passes through unchanged on non-peptide pages' );
-// ── No duplicate WebPage / BreadcrumbList nodes ───────────────────────────
+// ── No duplicate WebPage / BreadcrumbList nodes ───────────────────────────.
echo "\nNo duplicate WebPage/BreadcrumbList nodes:\n";
@@ -270,7 +270,7 @@ pr_assert( ! in_array( 'BreadcrumbList', $types_from_builders, true ), 'Breadcru
pr_assert( in_array( 'Drug', $types_from_builders, true ), 'Drug IS in our emitted pieces' );
pr_assert( in_array( 'FAQPage', $types_from_builders, true ), 'FAQPage IS in our emitted pieces when items present' );
-// ── PR_Core_Schema_Sanitizers::sanitize_faq_items() ──────────────────────
+// ── PR_Core_Schema_Sanitizers::sanitize_faq_items() ──────────────────────.
echo "\nPR_Core_Schema_Sanitizers::sanitize_faq_items():\n";
@@ -294,6 +294,6 @@ $result_incomplete = PR_Core_Schema_Sanitizers::sanitize_faq_items( $incomplete_
$decoded_incomplete = json_decode( $result_incomplete, true );
pr_assert_equals( 0, count( $decoded_incomplete ), 'item without answer field dropped' );
-// ── Summary ──────────────────────────────────────────────────────────────
+// ── Summary ──────────────────────────────────────────────────────────────.
exit( pr_test_summary() );
diff --git a/tests/unit/test-migration-0004.php b/tests/unit/test-migration-0004.php
index 82562ee..7ad5627 100644
--- a/tests/unit/test-migration-0004.php
+++ b/tests/unit/test-migration-0004.php
@@ -27,7 +27,7 @@ declare(strict_types=1);
// Bootstrap the lightweight WP stubs.
require_once __DIR__ . '/../bootstrap.php';
-// ── Additional stubs needed for migration 0004 ──────────────────────────
+// ── Additional stubs needed for migration 0004 ──────────────────────────.
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
@@ -85,7 +85,7 @@ require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-pubchem-cli
// Load migration class under test.
require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-migration-0004-backfill-peptide-meta.php';
-// ── Expose private methods via reflection ────────────────────────────────
+// ── Expose private methods via reflection ────────────────────────────────.
/**
* Call a private/protected method on an object via reflection.
@@ -105,7 +105,7 @@ $migration = new PR_Core_Migration_0004_Backfill_Peptide_Meta();
echo "== PR_Core_Migration_0004 unit tests ==\n\n";
-// ── parse_weight() ───────────────────────────────────────────────────────
+// ── parse_weight() ───────────────────────────────────────────────────────.
echo "parse_weight():\n";
@@ -145,7 +145,7 @@ pr_assert_equals(
'strips whitespace and lowercase "da"'
);
-// ── parse_aliases_string() ───────────────────────────────────────────────
+// ── parse_aliases_string() ───────────────────────────────────────────────.
echo "\nparse_aliases_string():\n";
@@ -167,8 +167,8 @@ pr_assert_equals( '[]', $whitespace_result, 'whitespace-only segments dropped, r
$single = json_decode( $migration->parse_aliases_string( 'Only One' ), true );
pr_assert_equals( 1, count( $single ), 'single alias produces array of 1' );
-// ── sanitize_formula() ───────────────────────────────────────────────────
-// Five required cases (Option B — validate, don't allowlist):
+// ── sanitize_formula() ───────────────────────────────────────────────────.
+// Five required cases (Option B — validate, don't allowlist):.
// 1. Valid formula unchanged.
// 2. Parenthesised formula preserved.
// 3. Newline-injected payload — "injection" stripped, formula kept.
@@ -219,7 +219,7 @@ pr_assert_equals(
'whitespace-only returns empty string'
);
-// ── Idempotency: skips when all three PR Core keys populated ─────────────
+// ── Idempotency: skips when all three PR Core keys populated ─────────────.
echo "\nIdempotency (all _pr_* keys populated):\n";
@@ -235,7 +235,7 @@ $result = call_private( $migration, 'backfill_post', [ 1 ] );
pr_assert_equals( 'skipped', $result, 'returns "skipped" when all three keys non-empty' );
pr_assert( empty( $GLOBALS['pr_test_updated_meta'] ), 'no update_post_meta calls when skipped' );
-// ── PSA source path ──────────────────────────────────────────────────────
+// ── PSA source path ──────────────────────────────────────────────────────.
echo "\nPSA source path:\n";
@@ -277,7 +277,7 @@ pr_assert( $formula_written, 'molecular formula meta was written' );
pr_assert( $weight_written, 'molecular weight meta was written' );
pr_assert( $aliases_written, 'aliases meta was written' );
-// ── Partial re-run: existing values preserved ────────────────────────────
+// ── Partial re-run: existing values preserved ────────────────────────────.
echo "\nIdempotency: only some keys populated (partial re-run):\n";
@@ -297,7 +297,7 @@ $GLOBALS['pr_test_posts'][3] = (object) [ 'post_title' => 'BPC-157', 'ID' => 3 ]
$result = call_private( $migration, 'backfill_post', [ 3 ] );
pr_assert_equals( 'copied_psa', $result, 'runs PSA copy even when only some keys populated' );
-// ── PubChem skip path: no PSA data, no network ──────────────────────────
+// ── PubChem skip path: no PSA data, no network ──────────────────────────.
echo "\nPubChem skip path (no PSA data, no network in unit tests):\n";
@@ -317,6 +317,6 @@ $result = call_private( $mock_migration, 'backfill_post', [ 10 ] );
pr_assert_equals( 'pubchem_skip', $result, 'returns "pubchem_skip" when PubChem network fails' );
pr_assert( empty( $GLOBALS['pr_test_updated_meta'] ), 'no meta written on pubchem_skip' );
-// ── Summary ──────────────────────────────────────────────────────────────
+// ── Summary ──────────────────────────────────────────────────────────────.
exit( pr_test_summary() );
diff --git a/tests/unit/test-verification-scanner.php b/tests/unit/test-verification-scanner.php
index 369dd67..4127b4f 100644
--- a/tests/unit/test-verification-scanner.php
+++ b/tests/unit/test-verification-scanner.php
@@ -22,7 +22,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../bootstrap.php';
-// ── Additional stubs needed by the scanner ──────────────────────────────
+// ── Additional stubs needed by the scanner ──────────────────────────────.
if ( ! defined( 'DAY_IN_SECONDS' ) ) {
define( 'DAY_IN_SECONDS', 86400 );
@@ -37,11 +37,11 @@ if ( ! function_exists( 'get_option' ) ) {
}
}
-// ── Load the scanner class ──────────────────────────────────────────────
+// ── Load the scanner class ──────────────────────────────────────────────.
require_once __DIR__ . '/../../includes/scanner/class-pr-core-verification-scanner.php';
-// ── Helper: build a date N days ago ────────────────────────────────────
+// ── Helper: build a date N days ago ────────────────────────────────────.
function days_ago( int $days ): string {
return gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
@@ -49,7 +49,7 @@ function days_ago( int $days ): string {
echo "== PR_Core_Verification_Scanner::compute_status() unit tests ==\n\n";
-// ── Empty last_verified ─────────────────────────────────────────────────
+// ── Empty last_verified ─────────────────────────────────────────────────.
echo "Empty last_verified:\n";
pr_assert_equals(
@@ -58,7 +58,7 @@ pr_assert_equals(
'empty string → overdue'
);
-// ── Medium velocity (default 180-day threshold) ─────────────────────────
+// ── Medium velocity (default 180-day threshold) ─────────────────────────.
echo "\nMedium velocity (threshold=180, option default):\n";
$GLOBALS['pr_core_options'] = [];
@@ -75,7 +75,7 @@ pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_
// 200 days ago → overdue.
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 200 ), 'medium' ), '200 days → overdue' );
-// ── 90% boundary exactness ──────────────────────────────────────────────
+// ── 90% boundary exactness ──────────────────────────────────────────────.
echo "\n90% boundary (threshold=100):\n";
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
@@ -89,7 +89,7 @@ pr_assert_equals( 'due', PR_Core_Verification_Scanner::compute_status( days_ago(
// 100 days → 100 >= 100 → overdue.
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 100 ), 'medium' ), '100 days (threshold 100) → overdue' );
-// ── High velocity (60-day threshold from option) ────────────────────────
+// ── High velocity (60-day threshold from option) ────────────────────────.
echo "\nHigh velocity (threshold=60 from option):\n";
$GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
@@ -103,7 +103,7 @@ pr_assert_equals( 'due', PR_Core_Verification_Scanner::compute_status( days_ago(
// 61 days → overdue.
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 61 ), 'high' ), 'high: 61 days → overdue' );
-// ── Low velocity (365-day constant — ignores option) ────────────────────
+// ── Low velocity (365-day constant — ignores option) ────────────────────.
echo "\nLow velocity (threshold=365, constant, ignores option):\n";
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ]; // Should be ignored for low.
@@ -117,12 +117,12 @@ pr_assert_equals( 'due', PR_Core_Verification_Scanner::compute_status( days_ago(
// 366 days → overdue.
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 366 ), 'low' ), 'low: 366 days → overdue' );
-// ── Unknown velocity falls through to medium/default ────────────────────
+// ── Unknown velocity falls through to medium/default ────────────────────.
echo "\nUnknown velocity (falls to default/medium):\n";
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 180 ];
pr_assert_equals( 'current', PR_Core_Verification_Scanner::compute_status( days_ago( 100 ), 'unknown' ), 'unknown velocity: 100 days → current (uses default 180)' );
-// ── Summary ─────────────────────────────────────────────────────────────
+// ── Summary ─────────────────────────────────────────────────────────────.
exit( pr_test_summary() );
diff --git a/tests/unit/test-verification-settings.php b/tests/unit/test-verification-settings.php
index b3cf0f6..9a1a9c7 100644
--- a/tests/unit/test-verification-settings.php
+++ b/tests/unit/test-verification-settings.php
@@ -18,7 +18,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../bootstrap.php';
-// ── Additional stubs needed by the settings class ───────────────────────
+// ── Additional stubs needed by the settings class ───────────────────────.
$GLOBALS['pr_core_options'] = [];
$GLOBALS['pr_core_cron_calls'] = [];
@@ -66,14 +66,14 @@ if ( ! function_exists( 'wp_schedule_event' ) ) {
}
}
-// ── Load the settings class ─────────────────────────────────────────────
+// ── Load the settings class ─────────────────────────────────────────────.
require_once __DIR__ . '/../../includes/cpt/class-pr-core-peptide-cpt.php';
require_once __DIR__ . '/../../includes/admin/class-pr-core-settings.php';
echo "== PR_Core_Settings verification methods unit tests ==\n\n";
-// ── sanitize_cadence() ──────────────────────────────────────────────────
+// ── sanitize_cadence() ──────────────────────────────────────────────────.
echo "sanitize_cadence():\n";
pr_assert_equals( 'daily', PR_Core_Settings::sanitize_cadence( 'daily' ), 'daily → daily' );
@@ -83,7 +83,7 @@ pr_assert_equals( 'weekly', PR_Core_Settings::sanitize_cadence( 'hourly' ), 'h
pr_assert_equals( 'weekly', PR_Core_Settings::sanitize_cadence( '' ), 'empty → weekly default' );
pr_assert_equals( 'weekly', PR_Core_Settings::sanitize_cadence( 'DAILY' ), 'uppercase → weekly (case-sensitive)' );
-// ── sanitize_emails() ───────────────────────────────────────────────────
+// ── sanitize_emails() ───────────────────────────────────────────────────.
echo "\nsanitize_emails():\n";
pr_assert_equals( '', PR_Core_Settings::sanitize_emails( '' ), 'empty → empty' );
@@ -93,14 +93,14 @@ pr_assert_equals( 'a@test.com, b@test.com', PR_Core_Settings::sanitize_emails( '
pr_assert_equals( 'good@test.com', PR_Core_Settings::sanitize_emails( 'good@test.com, bad-email' ), 'mixed → only valid kept' );
pr_assert_equals( 'trimmed@test.com', PR_Core_Settings::sanitize_emails( ' trimmed@test.com ' ), 'whitespace trimmed around valid email' );
-// ── reschedule_cron(): no-op when same value ────────────────────────────
+// ── reschedule_cron(): no-op when same value ────────────────────────────.
echo "\nreschedule_cron() — no-op when value unchanged:\n";
$GLOBALS['pr_core_cron_calls'] = [];
PR_Core_Settings::reschedule_cron( 'weekly', 'weekly' );
pr_assert( empty( $GLOBALS['pr_core_cron_calls'] ), 'no cron calls when old === new' );
-// ── reschedule_cron(): clears + reschedules on change ───────────────────
+// ── reschedule_cron(): clears + reschedules on change ───────────────────.
echo "\nreschedule_cron() — reschedules when value changes:\n";
$GLOBALS['pr_core_cron_calls'] = [];
@@ -126,6 +126,6 @@ $clear_idx = array_search( 'clear', $actions, true );
$schedule_idx = array_search( 'schedule', $actions, true );
pr_assert( $clear_idx < $schedule_idx, 'clear fires before schedule' );
-// ── Summary ─────────────────────────────────────────────────────────────
+// ── Summary ─────────────────────────────────────────────────────────────.
exit( pr_test_summary() );
diff --git a/uninstall.php b/uninstall.php
index 2a313fb..ea12ebc 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -8,6 +8,7 @@
* peptide_category taxonomy terms) is preserved.
*
* @see ARCHITECTURE.md — §2.9 Uninstall specification.
+ * @package Peptide_Repo_Core
*/
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
@@ -18,15 +19,16 @@ global $wpdb;
/* ── 1. Drop custom tables ────────────────────────────────────────────── */
-$tables = [
+$tables = array(
$wpdb->prefix . 'pr_dosing_rows',
$wpdb->prefix . 'pr_legal_cells',
$wpdb->prefix . 'pr_ai_candidate_queue',
-];
+);
foreach ( $tables as $table ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange
- $wpdb->query( "DROP TABLE IF EXISTS {$table}" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.DirectQuery
+ $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/* ── 2. Delete only peptide posts authored through PR Core's UI ───────── */
@@ -44,7 +46,7 @@ foreach ( $tables as $table ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$post_ids = $wpdb->get_col(
$wpdb->prepare(
- "SELECT p.ID FROM {$wpdb->posts} p
+ "SELECT p.ID FROM {$wpdb->posts} p // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
INNER JOIN {$wpdb->postmeta} pm
ON p.ID = pm.post_id AND pm.meta_key = %s AND pm.meta_value = %s
WHERE p.post_type = %s",
@@ -54,7 +56,7 @@ $post_ids = $wpdb->get_col(
)
);
-foreach ( $post_ids as $post_id ) {
+foreach ( $post_ids as $post_id ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
wp_delete_post( (int) $post_id, true );
}
@@ -72,8 +74,9 @@ foreach ( $post_ids as $post_id ) {
/* ── 4. Delete all pr_core_ prefixed options ──────────────────────────── */
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
+// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
- "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'pr\_core\_%'"
+ "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'pr\_core\_%'" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
/* ── 4b. Delete v0.6.0 schema-input meta keys from all peptide posts ──── */
@@ -85,19 +88,19 @@ $wpdb->query(
*
* @see ARCHITECTURE.md — §2.9 Uninstall specification.
*/
-$schema_meta_keys = [
+$schema_meta_keys = array(
'_pr_molecular_formula',
'_pr_molecular_weight',
'_pr_aliases',
'_pr_faq_items',
-];
+);
foreach ( $schema_meta_keys as $meta_key ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->delete(
$wpdb->postmeta,
- [ 'meta_key' => $meta_key ],
- [ '%s' ]
+ array( 'meta_key' => $meta_key ),
+ array( '%s' )
);
}
@@ -106,7 +109,7 @@ foreach ( $schema_meta_keys as $meta_key ) {
$editable_roles = wp_roles()->roles;
foreach ( array_keys( $editable_roles ) as $role_name ) {
- $role = get_role( $role_name );
+ $role = get_role( $role_name ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
if ( $role && $role->has_cap( 'manage_peptide_content' ) ) {
$role->remove_cap( 'manage_peptide_content' );
}