declare(strict_types=1); namespace App\Modules\Products\Projection\Discovery; use PDO; final readonly class TropeInferer { public function __construct( private PDO $pdo, ) {} public function inferForProduct(int $productId): array { $sql = 'WITH product_target AS ( SELECT :product_id AS product_id ), haystack AS ( SELECT pt.product_id, 'title' AS source, 0 AS is_main_subject, LOWER(CONCAT(' ', COALESCE(p.title, ''), ' ')) AS content FROM product_target pt JOIN products p ON p.id = pt.product_id UNION ALL SELECT pt.product_id, 'subtitle' AS source, 0 AS is_main_subject, LOWER(CONCAT(' ', COALESCE(p.subtitle, ''), ' ')) AS content FROM product_target pt JOIN products p ON p.id = pt.product_id UNION ALL SELECT pt.product_id, CASE WHEN ps.is_main_subject = 1 THEN 'main_subject' ELSE 'subject' END AS source, ps.is_main_subject, LOWER(CONCAT(' ', COALESCE(s.heading_text, ''), ' ')) AS content FROM product_target pt JOIN product_subjects ps ON ps.product_id = pt.product_id JOIN subjects s ON s.id = ps.subject_id UNION ALL SELECT pt.product_id, 'text_11' AS source, 0 AS is_main_subject, LOWER(CONCAT(' ', COALESCE(ptx.content, ''), ' ')) AS content FROM product_target pt JOIN product_texts ptx ON ptx.product_id = pt.product_id WHERE ptx.text_type = '11' UNION ALL SELECT pt.product_id, 'text_03' AS source, 0 AS is_main_subject, LOWER(CONCAT(' ', COALESCE(ptx.content, ''), ' ')) AS content FROM product_target pt JOIN product_texts ptx ON ptx.product_id = pt.product_id WHERE ptx.text_type = '03' UNION ALL SELECT pt.product_id, 'product_description' AS source, 0 AS is_main_subject, LOWER(CONCAT(' ', COALESCE(p.description, ''), ' ')) AS content FROM product_target pt JOIN products p ON p.id = pt.product_id UNION ALL SELECT pt.product_id, 'product_marketing_text' AS source, 0 AS is_main_subject, LOWER(CONCAT(' ', COALESCE(p.marketing_text, ''), ' ')) AS content FROM product_target pt JOIN products p ON p.id = pt.product_id UNION ALL SELECT pt.product_id, 'collection' AS source, 0 AS is_main_subject, LOWER(CONCAT(' ', COALESCE(c.title, ''), ' ', COALESCE(c.subtitle, ''), ' ')) AS content FROM product_target pt JOIN product_collections pc ON pc.product_id = pt.product_id JOIN collections c ON c.id = pc.collection_id ), normalized_haystack AS ( SELECT product_id, source, is_main_subject, CONCAT( ' ', TRIM( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( content, '<', ' '), '>', ' '), '/', ' '), '-', ' '), '_', ' '), '&', ' '), ':', ' '), ';', ' '), ',', ' '), '.', ' '), '(', ' '), ')', ' '), '[', ' '), ']', ' '), '{', ' '), '}', ' '), '"', ' '), '\'', ' '), '?', ' '), '!', ' ') ), ' ' ) AS norm_content FROM haystack ), source_rank AS ( SELECT 'main_subject' AS source_type, 1 AS rank_order UNION ALL SELECT 'subject', 2 UNION ALL SELECT 'text_11', 3 UNION ALL SELECT 'title', 4 UNION ALL SELECT 'subtitle', 5 UNION ALL SELECT 'collection', 6 UNION ALL SELECT 'text_03', 7 UNION ALL SELECT 'product_description', 8 UNION ALL SELECT 'product_marketing_text', 9 ), source_weights AS ( SELECT 'primary' AS relevance, 30 AS points UNION ALL SELECT 'secondary', 15 ), direct_trope_terms AS ( SELECT tr.id AS trope_id, tr.slug AS trope_slug, tr.name AS trope_name, 'trope_name' AS match_type, tr.name AS match_term FROM discovery_tropes tr WHERE tr.is_active = 1 UNION ALL SELECT tr.id, tr.slug, tr.name, 'trope_alias' AS match_type, dta.alias AS match_term FROM discovery_trope_aliases dta JOIN discovery_tropes tr ON tr.id = dta.trope_id WHERE dta.is_active = 1 AND tr.is_active = 1 ), normalized_direct_trope_terms AS ( SELECT trope_id, trope_slug, trope_name, match_type, match_term, CONCAT( ' ', TRIM( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( LOWER(match_term), '<', ' '), '>', ' '), '/', ' '), '-', ' '), '_', ' '), '&', ' '), ':', ' '), ';', ' '), ',', ' '), '.', ' '), '(', ' '), ')', ' '), '[', ' '), ']', ' '), '{', ' '), '}', ' '), '"', ' '), '\'', ' '), '?', ' '), '!', ' ') ), ' ' ) AS norm_term FROM direct_trope_terms ), direct_matches_raw AS ( SELECT ndt.trope_id, ndt.trope_slug, ndt.trope_name, nh.source, ndt.match_type, ndt.match_term, sr.rank_order FROM normalized_haystack nh JOIN normalized_direct_trope_terms ndt ON nh.norm_content LIKE CONCAT('%', ndt.norm_term, '%') LEFT JOIN source_rank sr ON sr.source_type = nh.source WHERE LENGTH(TRIM(ndt.norm_term)) >= 4 ), direct_matches AS ( SELECT trope_id, trope_slug, trope_name, CASE WHEN SUM(match_type = 'trope_name') > 0 THEN 'trope_name' ELSE 'trope_alias' END AS direct_match_type, 1 AS direct_match_count, SUBSTRING_INDEX( GROUP_CONCAT( DISTINCT source ORDER BY rank_order ASC SEPARATOR '||' ), '||', 1 ) AS direct_sources FROM direct_matches_raw GROUP BY trope_id, trope_slug, trope_name ), normalized_tags AS ( SELECT dt.id AS tag_id, dt.slug AS tag_slug, dt.name AS tag_name, CONCAT(' ', TRIM( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( LOWER(dt.slug), '<',' '), '>',' '), '/',' '), '-',' '), '_',' '), '&',' '), ':',' '), ';',' '), ',',' '), '.',' '), '(',' '), ')',' '), '[',' '), ']',' '), '{',' '), '}',' '), '"',' '), '\'', ' '), '?',' '), '!',' ') ), ' ') AS norm_slug, CONCAT(' ', TRIM( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( LOWER(dt.name), '<',' '), '>',' '), '/',' '), '-',' '), '_',' '), '&',' '), ':',' '), ';',' '), ',',' '), '.',' '), '(',' '), ')',' '), '[',' '), ']',' '), '{',' '), '}',' '), '"',' '), '\'', ' '), '?',' '), '!',' ') ), ' ') AS norm_name FROM discovery_tags dt WHERE dt.is_active = 1 AND NOT EXISTS ( SELECT 1 FROM discovery_genres dg WHERE dg.is_active = 1 AND ( LOWER(dg.slug) = LOWER(dt.slug) OR LOWER(dg.name) = LOWER(dt.name) OR LOWER(dg.slug) = LOWER(dt.name) OR LOWER(dg.name) = LOWER(dt.slug) ) ) AND NOT EXISTS ( SELECT 1 FROM genre_aliases ga WHERE LOWER(ga.alias) = LOWER(dt.name) OR LOWER(ga.alias) = LOWER(dt.slug) ) ), tag_hits_raw AS ( SELECT dtt.trope_id, tr.slug AS trope_slug, tr.name AS trope_name, nt.tag_id, nt.tag_slug, nt.tag_name, dtt.relevance, dtg.id AS group_id, dtg.slug AS group_slug, dtg.name AS group_name, nh.source, sw.points, COALESCE(sr.rank_order, 99) AS rank_order FROM normalized_haystack nh JOIN normalized_tags nt ON ( nh.norm_content LIKE CONCAT('%', nt.norm_slug, '%') OR nh.norm_content LIKE CONCAT('%', nt.norm_name, '%') ) JOIN discovery_trope_tags dtt ON dtt.tag_id = nt.tag_id AND dtt.active = 1 JOIN discovery_tropes tr ON tr.id = dtt.trope_id AND tr.is_active = 1 JOIN source_weights sw ON sw.relevance = dtt.relevance LEFT JOIN normalized_direct_trope_terms ndt_exclude ON ndt_exclude.trope_id = dtt.trope_id AND ( nt.norm_slug = ndt_exclude.norm_term OR nt.norm_name = ndt_exclude.norm_term ) LEFT JOIN source_rank sr ON sr.source_type = nh.source LEFT JOIN discovery_tag_group_tags dgt ON dgt.tag_id = nt.tag_id LEFT JOIN discovery_tag_groups dtg ON dtg.id = dgt.group_id WHERE ( LENGTH(TRIM(nt.norm_slug)) >= 4 OR LENGTH(TRIM(nt.norm_name)) >= 4 ) AND ndt_exclude.trope_id IS NULL ), best_tag_hits AS ( SELECT trope_id, trope_slug, trope_name, tag_id, tag_slug, tag_name, relevance, MAX(points) AS points, MIN(rank_order) AS best_rank, SUBSTRING_INDEX( GROUP_CONCAT(source ORDER BY points DESC, rank_order ASC SEPARATOR '||'), '||', 1 ) AS best_source FROM tag_hits_raw WHERE points > 0 GROUP BY trope_id, trope_slug, trope_name, tag_id, tag_slug, tag_name, relevance ), best_group_hits AS ( SELECT bth.trope_id, bth.trope_slug, bth.trope_name, dgt.group_id, dtg.slug AS group_slug, dtg.name AS group_name, MAX(bth.points) AS group_points, SUBSTRING_INDEX( GROUP_CONCAT( CONCAT( bth.tag_name, ' (', bth.relevance, ' • ', bth.best_source, ' • ', bth.points, ' Punkte)' ) ORDER BY bth.points DESC, bth.best_rank ASC SEPARATOR '||' ), '||', 1 ) AS group_evidence FROM best_tag_hits bth JOIN discovery_tag_group_tags dgt ON dgt.tag_id = bth.tag_id JOIN discovery_tag_groups dtg ON dtg.id = dgt.group_id AND dtg.is_active = 1 GROUP BY bth.trope_id, bth.trope_slug, bth.trope_name, dgt.group_id, dtg.slug, dtg.name ), trope_group_max AS ( SELECT dtt.trope_id, dgt.group_id, SUM(dtt.relevance = 'primary') AS primary_tag_count, SUM(dtt.relevance = 'secondary') AS secondary_tag_count, ( SUM(dtt.relevance = 'primary') + SUM(dtt.relevance = 'secondary') * 0.25 ) AS core_score, MAX( CASE dtt.relevance WHEN 'primary' THEN 30 ELSE 15 END ) AS max_group_points FROM discovery_trope_tags dtt JOIN discovery_tag_group_tags dgt ON dgt.tag_id = dtt.tag_id JOIN discovery_tag_groups dtg ON dtg.id = dgt.group_id AND dtg.is_active = 1 AND dtg.slug NOT IN ('genres', 'marketing') WHERE dtt.active = 1 GROUP BY dtt.trope_id, dgt.group_id ), trope_group_roles AS ( SELECT tgm.*, CASE WHEN tgm.primary_tag_count >= 2 THEN 'core' WHEN tgm.primary_tag_count >= 1 AND tgm.core_score >= 1.25 THEN 'core' WHEN tgm.primary_tag_count >= 1 THEN 'support' WHEN tgm.secondary_tag_count >= 2 THEN 'support' ELSE 'optional' END AS group_role, CASE WHEN tgm.primary_tag_count >= 2 THEN 1.00 WHEN tgm.primary_tag_count >= 1 AND tgm.core_score >= 1.25 THEN 1.00 WHEN tgm.primary_tag_count >= 1 THEN 0.50 WHEN tgm.secondary_tag_count >= 2 THEN 0.50 ELSE 0.00 END AS denominator_weight FROM trope_group_max tgm ), trope_group_score AS ( SELECT tgr.trope_id, COUNT(*) AS trope_group_count, SUM(tgr.max_group_points * tgr.denominator_weight) AS maximum_possible_score, COUNT(bgh.group_id) AS matched_group_count, COALESCE( SUM( COALESCE(bgh.group_points, 0) * tgr.denominator_weight ), 0 ) AS evidence_score, ROUND( 100 * COUNT(bgh.group_id) / NULLIF(COUNT(*), 0), 1 ) AS coverage_percent, ROUND( 100 * COALESCE( SUM( COALESCE(bgh.group_points, 0) * tgr.denominator_weight ), 0 ) / NULLIF( SUM(tgr.max_group_points * tgr.denominator_weight), 0 ), 1 ) AS confidence_percent, GROUP_CONCAT( CONCAT( bgh.group_evidence, ' [', tgr.group_role, ']' ) ORDER BY bgh.group_points DESC SEPARATOR ' | ' ) AS tag_evidence FROM trope_group_roles tgr LEFT JOIN best_group_hits bgh ON bgh.trope_id = tgr.trope_id AND bgh.group_id = tgr.group_id GROUP BY tgr.trope_id ), tag_trope_scores AS ( SELECT trope_id, COUNT(DISTINCT tag_id) AS unique_tags, SUM(relevance = 'primary') AS primary_hits, SUM(relevance = 'secondary') AS secondary_hits FROM best_tag_hits GROUP BY trope_id ), final_ranking AS ( SELECT tr.id AS trope_id, tr.slug AS trope_slug, tr.name AS trope_name, COALESCE(tgs.evidence_score,0) AS evidence_score, CASE WHEN dm.trope_id IS NOT NULL THEN 1 ELSE 0 END AS has_direct_match, dm.direct_match_type AS direct_match_type, COALESCE(ts.unique_tags,0) AS unique_tags, COALESCE(ts.primary_hits,0) AS primary_hits, COALESCE(ts.secondary_hits,0) AS secondary_hits, COALESCE(dm.direct_match_count,0) AS direct_match_count, tgs.maximum_possible_score, COALESCE(tgs.trope_group_count, 0) AS trope_group_count, COALESCE(tgs.matched_group_count, 0) AS matched_group_count, COALESCE(tgs.coverage_percent, 0) AS coverage_percent, COALESCE(tgs.confidence_percent, 0) AS confidence_percent, dm.direct_sources AS direct_sources, tgs.tag_evidence AS tag_evidence FROM discovery_tropes tr LEFT JOIN direct_matches dm ON dm.trope_id = tr.id LEFT JOIN tag_trope_scores ts ON ts.trope_id = tr.id LEFT JOIN trope_group_score tgs ON tgs.trope_id = tr.id WHERE dm.trope_id IS NOT NULL OR tgs.trope_id IS NOT NULL ) SELECT pt.product_id, fr.trope_id, 0 AS is_primary, 0 AS sort_order, CASE WHEN fr.has_direct_match = 1 THEN 'direct_match' WHEN fr.primary_hits >= 3 AND fr.matched_group_count >= 3 AND fr.coverage_percent >= 60 THEN 'inferred_strong' WHEN fr.primary_hits >= 2 AND fr.matched_group_count >= 2 AND fr.coverage_percent >= 50 THEN 'inferred_probable' WHEN fr.primary_hits >= 2 AND fr.matched_group_count >= 2 AND fr.confidence_percent >= 35 THEN 'inferred_possible' ELSE 'inferred_weak' END AS source, CASE WHEN fr.has_direct_match = 1 THEN 1.00 ELSE ROUND(fr.confidence_percent / 100, 2) END AS confidence, JSON_OBJECT( 'trope_name', fr.trope_name, 'confidence_percent', fr.confidence_percent, 'evidence_score', fr.evidence_score, 'coverage_percent', fr.coverage_percent, 'matched_group_count', fr.matched_group_count, 'trope_group_count', fr.trope_group_count, 'unique_tags', fr.unique_tags, 'primary_hits', fr.primary_hits, 'secondary_hits', fr.secondary_hits, 'has_direct_match', fr.has_direct_match, 'direct_match_type', fr.direct_match_type, 'direct_sources', fr.direct_sources, 'tag_evidence', fr.tag_evidence ) AS evidence FROM final_ranking fr CROSS JOIN product_target pt WHERE fr.has_direct_match = 1 OR ( fr.primary_hits >= 3 AND fr.matched_group_count >= 3 AND fr.coverage_percent >= 60 ) OR ( fr.primary_hits >= 2 AND fr.matched_group_count >= 2 AND fr.coverage_percent >= 50 ) OR ( fr.primary_hits >= 2 AND fr.matched_group_count >= 2 AND fr.confidence_percent >= 35 );'; $statement = $this->pdo->prepare($sql); $statement->execute([ 'product_id' => $productId, ]); return $statement->fetchAll(PDO::FETCH_ASSOC); } }
Fatal error: Uncaught Error: Class "App\Modules\Products\Projection\Discovery\TropeInferer" not found in /home/u504091607/domains/romancemagazin.de/public_html/app/Providers/ProductsPersistenceServiceProvider.php:677 Stack trace: #0 /home/u504091607/domains/romancemagazin.de/public_html/app/bootstrap.php(62): App\Providers\ProductsPersistenceServiceProvider->register() #1 /home/u504091607/domains/romancemagazin.de/public_html/wp-content/plugins/product-catalog/product-catalog.php(38): require_once('/home/u50409160...') #2 /home/u504091607/domains/romancemagazin.de/public_html/wp-settings.php(589): include_once('/home/u50409160...') #3 /home/u504091607/domains/romancemagazin.de/public_html/wp-config.php(109): require_once('/home/u50409160...') #4 /home/u504091607/domains/romancemagazin.de/public_html/wp-load.php(50): require_once('/home/u50409160...') #5 /home/u504091607/domains/romancemagazin.de/public_html/wp-blog-header.php(13): require_once('/home/u50409160...') #6 /home/u504091607/domains/romancemagazin.de/public_html/index.php(17): require('/home/u50409160...') #7 {main} thrown in /home/u504091607/domains/romancemagazin.de/public_html/app/Providers/ProductsPersistenceServiceProvider.php on line 677