$overrides Field overrides. * @return PR_Core_Peptide_DTO */ function make_peptide_piece_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 ) ); } echo "== JSON-LD peptide-piece regression tests (v0.6.2) ==\n\n"; // ── Setup ─────────────────────────────────────────────────────────────────. $GLOBALS['pr_test_the_id'] = 36; $GLOBALS['pr_test_is_singular'] = true; $GLOBALS['pr_test_singular_type'] = PR_Core_Peptide_CPT::POST_TYPE; $GLOBALS['pr_test_postmeta'][36] = [ '_pr_molecular_formula' => 'C62H98N16O22', '_pr_molecular_weight' => '1419.5', '_pr_aliases' => '["Body Protection Compound"]', ]; $GLOBALS['pr_test_peptide_dto'] = make_peptide_piece_dto(); $jsonld = new PR_Core_Jsonld(); // ── 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:. // 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'] ); $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". $class_result = get_class( $drug_array ); } catch ( TypeError $e ) { $caught_fatal = true; } catch ( Error $e ) { $caught_fatal = true; } 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 ────────────────. echo "\nFix: inject_graph_nodes via wpseo_schema_graph (plain array graph):\n"; // The new inject_graph_nodes method receives an already-built graph (array of arrays). // We simulate the wpseo_schema_graph filter call: $graph = array of plain arrays. $existing_graph = [ [ '@type' => 'MedicalWebPage', '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage', 'name' => 'BPC-157', ], [ '@type' => 'BreadcrumbList', '@id' => 'https://peptiderepo.com/peptides/bpc-157/#breadcrumb', ], ]; // This must NOT throw and must append Drug node. $threw = false; try { $result_graph = $jsonld->inject_graph_nodes( $existing_graph, null ); } catch ( Throwable $e ) { $threw = true; } pr_assert( ! $threw, 'inject_graph_nodes does NOT throw (no get_class/is_needed calls)' ); pr_assert( count( $result_graph ) > count( $existing_graph ), 'inject_graph_nodes appended at least one node' ); // Drug node must be present. $drug_nodes = array_filter( $result_graph, static function ( $node ) { $types = is_array( $node['@type'] ?? null ) ? $node['@type'] : [ $node['@type'] ?? '' ]; return in_array( 'Drug', $types, true ); } ); pr_assert( count( $drug_nodes ) === 1, 'exactly one Drug node in graph after inject' ); $drug_node = array_values( $drug_nodes )[0]; pr_assert_equals( 'https://peptiderepo.com/peptides/bpc-157/#drug', $drug_node['@id'], 'Drug @id = {permalink}#drug' ); pr_assert( isset( $drug_node['molecularFormula'] ), 'Drug has molecularFormula' ); pr_assert( isset( $drug_node['molecularWeight'] ), 'Drug has molecularWeight (QuantitativeValue)' ); pr_assert_equals( 'QuantitativeValue', $drug_node['molecularWeight']['@type'], 'molecularWeight @type is QuantitativeValue' ); pr_assert_equals( 1419.5, $drug_node['molecularWeight']['value'], 'molecularWeight value correct' ); pr_assert_equals( 'g/mol', $drug_node['molecularWeight']['unitText'], 'molecularWeight unitText = g/mol' ); pr_assert( isset( $drug_node['alternateName'] ), 'Drug has alternateName' ); // Original WebPage + BreadcrumbList nodes must be preserved. $webpage_nodes = array_filter( $result_graph, static fn( $n ) => ( $n['@type'] ?? '' ) === 'MedicalWebPage' ); pr_assert( count( $webpage_nodes ) === 1, 'MedicalWebPage node preserved (no duplicate)' ); $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 ─────────────────────────────. echo "\nFAQ injection via inject_graph_nodes:\n"; $GLOBALS['pr_test_postmeta'][36]['_pr_faq_items'] = wp_json_encode( [ [ 'question' => 'What is BPC-157?', 'answer' => 'A synthetic pentadecapeptide.' ], [ 'question' => 'Is it safe?', 'answer' => 'Research use only; not approved for human therapy.' ], ] ); $result_with_faq = $jsonld->inject_graph_nodes( $existing_graph, null ); $faq_nodes = array_filter( $result_with_faq, static fn( $n ) => ( $n['@type'] ?? '' ) === 'FAQPage' ); pr_assert( count( $faq_nodes ) === 1, 'FAQPage node injected when _pr_faq_items has items' ); $faq_node = array_values( $faq_nodes )[0]; pr_assert_equals( 'https://peptiderepo.com/peptides/bpc-157/#faq', $faq_node['@id'], 'FAQPage @id = {permalink}#faq' ); 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 ─────────────────────────────────. $GLOBALS['pr_test_postmeta'][36]['_pr_faq_items'] = '[]'; $result_no_faq = $jsonld->inject_graph_nodes( $existing_graph, null ); $faq_nodes_empty = array_filter( $result_no_faq, static fn( $n ) => ( $n['@type'] ?? '' ) === 'FAQPage' ); pr_assert( count( $faq_nodes_empty ) === 0, 'FAQPage NOT injected when _pr_faq_items is empty array' ); // ── Non-peptide page: no injection ───────────────────────────────────────. echo "\nNon-peptide page: no injection:\n"; $GLOBALS['pr_test_is_singular'] = false; $GLOBALS['pr_test_singular_type'] = ''; $non_peptide_graph = [ [ '@type' => 'WebPage', '@id' => 'https://peptiderepo.com/about/#webpage', ], ]; $result_non_peptide = $jsonld->inject_graph_nodes( $non_peptide_graph, null ); pr_assert( count( $result_non_peptide ) === 1, 'inject_graph_nodes no-ops on non-peptide pages (graph unchanged)' ); $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 ──────────────. echo "\nregister_hooks: wpseo_schema_graph_pieces must NOT be hooked:\n"; $GLOBALS['pr_core_test_state']['added_actions'] = []; $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.). $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. // 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( strpos( $src, "'wpseo_schema_graph_pieces'" ) === false, 'class-pr-core-jsonld.php does NOT add_filter wpseo_schema_graph_pieces (correct hook removed)' ); pr_assert( strpos( $src, 'inject_graph_nodes' ) !== false, 'class-pr-core-jsonld.php references inject_graph_nodes (new method present)' ); pr_assert( strpos( $src, "'wpseo_schema_graph'" ) !== false, 'class-pr-core-jsonld.php hooks wpseo_schema_graph for injection' ); // ── Summary ───────────────────────────────────────────────────────────────. exit( pr_test_summary() );