<?php declare(strict_types=1);
namespace App\StartPlatz\Bundle\MentorsBundle\Controller;
use App\StartPlatz\Bundle\MentorsBundle\Service\MentorService;
use App\StartPlatz\Bundle\MentorsBundle\Service\WordPressMenuService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* PublicController
*
* Handles public-facing mentor and expert pages
*/
class PublicController extends AbstractController
{
private MentorService $mentorService;
private WordPressMenuService $menuService;
private EntityManagerInterface $entityManager;
public function __construct(
MentorService $mentorService,
WordPressMenuService $menuService,
EntityManagerInterface $entityManager
) {
$this->mentorService = $mentorService;
$this->menuService = $menuService;
$this->entityManager = $entityManager;
}
/**
* STARTPLATZ Mentors page
*/
#[Route('/mentors/startplatz', name: 'mentors_startplatz')]
#[Route('/en/mentors/startplatz', name: 'mentors_startplatz_en')]
public function startplatzMentorsAction(Request $request): Response
{
$locale = $request->getLocale();
$isEnglish = str_contains($request->getPathInfo(), '/en/');
// Get all mentors (no pagination for section-based display)
$allMentors = $this->mentorService->getMentorsByCategory(
MentorService::CATEGORY_STARTPLATZ,
[]
);
// Define categories in the order they should appear (matching original page)
$categoryOrder = [
'apps-it' => 'Apps und IT',
'marketing' => 'Marketing',
'finanzierung' => 'Finanzierung',
'gruendung-fuehrung' => 'Gründung und Führung',
'recht' => 'Recht',
'design' => 'Design',
'wirtschaftspruefung-steuerberatung' => 'Wirtschaftsprüfung und Steuerberatung',
'versicherung' => 'Versicherung'
];
// Group mentors by category
$mentorsByCategory = [];
// Initialize all categories even if empty
foreach ($categoryOrder as $categoryKey => $categoryLabel) {
$mentorsByCategory[$categoryKey] = [
'label' => $categoryLabel,
'mentors' => []
];
}
// Process each mentor
foreach ($allMentors as $mentor) {
$metadata = $mentor->getMentorMetadata() ?? [];
// Get mentor's categories from mentorMetadata ONLY
// If no categories defined, assign to default category
$mentorCategories = $metadata['categories'] ?? ['gruendung-fuehrung'];
if (!is_array($mentorCategories)) {
$mentorCategories = [$mentorCategories];
}
// Prepare mentor data - use existing member fields
$mentorData = [
'id' => $mentor->getId(),
'name' => trim($mentor->getFirstName() . ' ' . $mentor->getLastName()),
'firstName' => $mentor->getFirstName(),
'lastName' => $mentor->getLastName(),
'email' => $mentor->getEmail(),
'title' => $mentor->getJobTitle() ?? '',
'academicTitle' => $mentor->getTitle() ?? '',
'company' => $mentor->getMentorFirma() ?? $mentor->getCompany() ?? '',
'description' => $mentor->getMentorProfile() ?? $mentor->getDescription() ?? '',
'image' => $mentor->getImageLink() ?? 'https://res.cloudinary.com/startplatz/image/upload/v1637360496/Wordpress/Sprechstunden/Sprechstunden_Platzhalter.jpg',
'skills' => [], // Skills werden aus mentor_profile extrahiert in der Template
'slug' => strtolower(str_replace(' ', '-', trim($mentor->getFirstName() . '-' . $mentor->getLastName())))
];
// Add to appropriate categories (or to first category if none specified)
if (empty($mentorCategories)) {
// If no category specified, add to first category
$firstKey = array_key_first($categoryOrder);
$mentorsByCategory[$firstKey]['mentors'][] = $mentorData;
} else {
// Add to all specified categories
foreach ($mentorCategories as $category) {
if (isset($mentorsByCategory[$category])) {
$mentorsByCategory[$category]['mentors'][] = $mentorData;
}
}
}
}
// Remove empty categories if needed (optional)
// $mentorsByCategory = array_filter($mentorsByCategory, function($category) {
// return !empty($category['mentors']);
// });
return $this->render('@StartPlatzMentors/Public/startplatz-mentors.html.twig', [
'mentorsByCategory' => $mentorsByCategory,
'isEnglish' => $isEnglish
]);
}
/**
* Determine categories based on mentor's skills
* This is a fallback when mentorMetadata doesn't have categories defined
* Uses the first 3 skills to determine the best matching category
*/
private function determineCategoriesFromMentor($mentor): array
{
// Get mentorMetadata to check for skills
$metadata = $mentor->getMentorMetadata() ?? [];
$skills = $metadata['skills'] ?? [];
// If no skills in metadata, we can't categorize properly
// Default to 'gruendung-fuehrung' as the most general category
if (empty($skills)) {
return ['gruendung-fuehrung'];
}
// Convert skills to lowercase for matching
$skillsLower = array_map('strtolower', array_slice($skills, 0, 3)); // Use first 3 skills
$categories = [];
// Keywords for each category based on skills
$categorySkillKeywords = [
'apps-it' => ['software', 'app', 'tech', 'digital', 'entwicklung', 'programmier', 'web', 'mobile', 'cyber', 'it', 'code', 'system'],
'marketing' => ['marketing', 'brand', 'pr', 'social media', 'content', 'seo', 'kommunikation', 'werbung', 'sales', 'vertrieb'],
'finanzierung' => ['finanz', 'invest', 'venture capital', 'capital', 'funding', 'förder', 'angel', 'seed', 'fundraising'],
'gruendung-fuehrung' => ['gründ', 'startup', 'führung', 'management', 'strateg', 'business', 'unternehm', 'leadership'],
'recht' => ['recht', 'anwalt', 'jurist', 'legal', 'vertrag', 'gesetz', 'compliance', 'datenschutz', 'gesellschaftsrecht'],
'design' => ['design', 'ux', 'ui', 'grafik', 'kreativ', 'visual', 'prototype', 'user experience'],
'wirtschaftspruefung-steuerberatung' => ['steuer', 'wirtschaftsprüf', 'tax', 'audit', 'buchführung', 'bilanz', 'accounting'],
'versicherung' => ['versicher', 'vorsorge', 'risiko', 'haftpflicht', 'kranken', 'altersvorsorge']
];
// Check each skill against category keywords
foreach ($skillsLower as $skill) {
foreach ($categorySkillKeywords as $category => $keywords) {
foreach ($keywords as $keyword) {
if (str_contains($skill, $keyword)) {
if (!in_array($category, $categories)) {
$categories[] = $category;
}
break; // Found match for this category in this skill
}
}
}
}
// If no category found from skills, default to 'gruendung-fuehrung'
if (empty($categories)) {
$categories[] = 'gruendung-fuehrung';
}
// Return only the first matched category to avoid appearing in all categories
return [array_shift($categories)];
}
/**
* Extract skills from description text
* This is a fallback when mentorMetadata doesn't have skills defined
*/
private function extractSkillsFromDescription(?string $description): array
{
if (empty($description)) {
return [];
}
// Simple extraction based on common keywords
$skills = [];
// Common skill patterns in German mentor descriptions
$patterns = [
'/(?:Beratung|Expertise|Schwerpunkt|Fokus|Spezialisierung)(?:\s+(?:in|auf|für))?\s*:?\s*([^.]+)/i',
'/(?:berate|unterstütze|helfe)(?:\s+bei)?\s*:?\s*([^.]+)/i'
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $description, $matches)) {
$skillText = $matches[1];
// Split by common delimiters
$potentialSkills = preg_split('/[,;]/', $skillText);
foreach ($potentialSkills as $skill) {
$skill = trim($skill);
if (strlen($skill) > 3 && strlen($skill) < 50) {
$skills[] = $skill;
if (count($skills) >= 3) {
return $skills;
}
}
}
}
}
// If no skills found via patterns, use first sentence as description
return [];
}
/**
* Categorize mentor by their skills
*/
private function categorizeMentorBySkills(array $skills): array
{
// Define category keywords (German and English)
$categoryKeywords = [
'tech-development' => [
'entwicklung', 'development', 'programming', 'software', 'code', 'coding',
'python', 'javascript', 'java', 'php', 'backend', 'frontend', 'fullstack',
'devops', 'cloud', 'aws', 'docker', 'kubernetes', 'api', 'database', 'sql',
'machine learning', 'ai', 'ki', 'artificial intelligence', 'deep learning',
'neural', 'tensorflow', 'pytorch', 'data science', 'algorithm', 'tech', 'it',
'mobile', 'app', 'web', 'security', 'blockchain', 'crypto', 'iot', 'embedded',
'github', 'gitlab', 'git', 'bitbucket', 'technologie', 'technology',
'react', 'angular', 'vue', 'node', 'typescript', 'swift', 'kotlin',
'android', 'ios', 'flutter', 'xamarin', 'unity', 'unreal',
'azure', 'gcp', 'serverless', 'microservices', 'ci/cd', 'jenkins',
'mysql', 'postgresql', 'mongodb', 'redis', 'elasticsearch', 'ingenieurwesen'
],
'business-strategy' => [
'strategy', 'strategie', 'business', 'geschäft', 'model', 'skalierung',
'scaling', 'growth', 'wachstum', 'management', 'führung', 'leadership',
'operations', 'prozesse', 'transformation', 'innovation', 'planung',
'roadmap', 'vision', 'mission', 'okr', 'kpi', 'agile', 'scrum', 'lean',
'startup', 'gründung', 'entrepreneurship', 'ceo', 'coo', 'executive',
'consulting', 'beratung', 'change', 'digital transformation'
],
'sales-marketing' => [
'sales', 'vertrieb', 'marketing', 'growth', 'hacking', 'b2b', 'b2c',
'seo', 'sea', 'sem', 'social media', 'content', 'brand', 'marke', 'pr',
'communication', 'kommunikation', 'crm', 'lead', 'conversion', 'funnel',
'customer', 'acquisition', 'retention', 'email', 'advertising', 'werbung',
'campaign', 'analytics', 'performance', 'influencer', 'storytelling',
'copywriting', 'demand', 'generation', 'outbound', 'inbound'
],
'finance-legal' => [
'finance', 'finanzen', 'funding', 'investment', 'investor', 'vc',
'venture', 'capital', 'förderung', 'grant', 'pitch', 'valuation',
'legal', 'recht', 'anwalt', 'lawyer', 'compliance', 'dsgvo', 'gdpr',
'vertrag', 'contract', 'ip', 'patent', 'steuer', 'tax', 'accounting',
'buchführung', 'controlling', 'budget', 'cashflow', 'revenue', 'profit',
'seed', 'series', 'exit', 'ipo', 'due diligence', 'term sheet'
],
'product-design' => [
'product', 'produkt', 'design', 'ux', 'ui', 'user experience', 'usability',
'customer', 'development', 'mvp', 'prototype', 'iteration', 'lean startup',
'feedback', 'research', 'testing', 'interface', 'interaction',
'creative', 'kreativ', 'brand', 'visual', 'figma', 'sketch', 'adobe',
'wireframe', 'mockup', 'persona', 'journey', 'map', 'sprint',
'discovery', 'validation', 'feature', 'roadmap', 'backlog'
]
];
if (empty($skills)) {
return ['business-strategy']; // Default category
}
// Convert skills to lowercase for matching
$skillsLower = array_map('strtolower', $skills);
$categoryScores = [];
foreach ($categoryKeywords as $category => $keywords) {
$score = 0;
$matches = [];
foreach ($skillsLower as $skill) {
// Remove hashtag if present
$skill = ltrim(trim($skill), '#');
// Split hyphenated words for better matching
$skillParts = preg_split('/[-\s]+/', $skill);
foreach ($keywords as $keyword) {
// Exact match: 3 points
if ($skill === $keyword) {
$score += 3;
$matches[] = $skill;
break; // Don't count same skill multiple times
}
// Check parts of hyphenated words
foreach ($skillParts as $part) {
if ($part === $keyword) {
$score += 2; // Give 2 points for exact part match
$matches[] = $skill;
break 2; // Break both loops
}
}
// Partial match: 1 point - but only for whole word matches
// Use word boundaries to avoid false positives like "it" in "exit"
if (preg_match('/\b' . preg_quote($keyword, '/') . '\b/i', $skill)) {
$score += 1;
$matches[] = $skill;
break;
}
}
}
// Only consider categories with score >= 2
if ($score >= 2) {
$categoryScores[$category] = [
'score' => $score,
'matches' => array_unique($matches)
];
}
}
// If no categories matched, return default
if (empty($categoryScores)) {
return ['business-strategy'];
}
// Sort by score and return top 3 categories
arsort($categoryScores);
return array_slice(array_keys($categoryScores), 0, 3);
}
/**
* AI Accelerator Mentors page
*/
#[Route('/accelerator/mentors', name: 'accelerator_mentors')]
#[Route('/en/accelerator/mentors', name: 'accelerator_mentors_en')]
public function acceleratorMentorsAction(Request $request): Response
{
$isEnglish = str_contains($request->getPathInfo(), '/en/');
// Get filters
$filters = [
'search' => $request->get('search'),
'expertise' => $request->get('expertise'),
'techStack' => $request->get('tech')
];
// Get mentors
$allMentors = $this->mentorService->getMentorsByCategory(
MentorService::CATEGORY_ACCELERATOR,
array_filter($filters)
);
// Prepare mentor data arrays (similar to STARTPLATZ mentors)
$mentorsData = [];
foreach ($allMentors as $mentor) {
$metadata = $mentor->getMentorMetadata() ?? [];
// Prepare skills array - use the actual skills field from member
$skills = [];
$skillsString = $mentor->getSkills();
if (!empty($skillsString)) {
// Split by spaces or commas, filter empty values
$skills = preg_split('/[\s,]+/', $skillsString);
$skills = array_filter($skills, function($s) { return !empty(trim($s)); });
}
// Categorize mentor based on skills
$categories = $this->categorizeMentorBySkills($skills);
// Prepare mentor data - use existing member fields with metadata fallback
$mentorData = [
'id' => $mentor->getId(),
'name' => trim($mentor->getFirstName() . ' ' . $mentor->getLastName()),
'firstName' => $mentor->getFirstName(),
'lastName' => $mentor->getLastName(),
'email' => $mentor->getEmail(),
'title' => $mentor->getJobTitle() ?? $metadata['title'] ?? '',
'academicTitle' => $mentor->getTitle() ?? '',
'company' => $mentor->getCompany() ?? $metadata['company'] ?? '',
'mentorFirma' => $mentor->getMentorFirma(), // Added for institution fallback
'description' => $mentor->getDescription() ?? $metadata['description'] ?? '',
'profileImage' => $mentor->getImageLink() ?? $metadata['profileImage'] ?? null,
'image' => $mentor->getImageLink() ?? $metadata['profileImage'] ?? null,
'skills' => $skills,
'categories' => $categories, // Add categories for filtering
'expertise' => $metadata['expertise'] ?? null,
'linkedin' => $metadata['linkedin'] ?? null,
'mentorProfile' => $mentor->getMentorProfile(),
'jobTitle' => $mentor->getJobTitle(),
'mentorMetadata' => $metadata, // Include full metadata for new fields
'github' => $metadata['github'] ?? null,
'twitter' => $metadata['twitter'] ?? null
];
$mentorsData[] = $mentorData;
}
// Get menu from WordPress
$menu = $this->menuService->getAcceleratorMenu($isEnglish, $request->getPathInfo());
return $this->render('@StartPlatzMentors/Public/accelerator-mentors.html.twig', [
'mentors' => $mentorsData,
'searchQuery' => $filters['search'] ?? '',
'currentExpertise' => $filters['expertise'] ?? null,
'currentTechStack' => $filters['techStack'] ?? null,
'isEnglish' => $isEnglish,
'pageTitle' => $isEnglish ? 'AI Accelerator Mentors' : 'KI Accelerator Mentoren',
'pageDescription' => $isEnglish
? 'Expert mentors for AI startups'
: 'Experten-Mentoren für KI-Startups',
'menu' => $menu
]);
}
/**
* Experts in Residence page
*/
#[Route('/accelerator/experts-in-residence', name: 'experts_residence')]
#[Route('/en/accelerator/experts-in-residence', name: 'experts_residence_en')]
public function expertsResidenceAction(Request $request): Response
{
$isEnglish = str_contains($request->getPathInfo(), '/en/');
// Get filters
$filters = [
'search' => $request->get('search'),
'specialization' => $request->get('specialization'),
'limit' => 20,
'offset' => ($request->get('page', 1) - 1) * 20
];
// Get experts
$allExperts = $this->mentorService->getMentorsByCategory(
MentorService::CATEGORY_EXPERTS,
array_filter($filters)
);
// Prepare expert data arrays (similar to STARTPLATZ mentors)
$expertsData = [];
foreach ($allExperts as $expert) {
// Get full metadata and expert-specific metadata
$fullMetadata = $expert->getMentorMetadata() ?? [];
$expertMetadata = $expert->getExpertMetadata(); // Uses the new expert namespace
// Prepare expert data - use Member entity directly with enriched metadata
$expertData = [
'id' => $expert->getId(),
'name' => trim($expert->getFirstName() . ' ' . $expert->getLastName()),
'firstName' => $expert->getFirstName(),
'lastName' => $expert->getLastName(),
'email' => $expert->getEmail(),
'profileImage' => $expert->getImageLink(),
'whoAmI' => $expert->getDescription() ?: $expert->getMentorProfile(), // Bio/description field with fallback
'skills' => $expert->getSkills(),
'linkedin' => $expert->getLinkedin(),
'twitter' => $expert->getTwitter(),
// Include the expert metadata for the template
'expertMetadata' => $expertMetadata,
// Legacy fields for backward compatibility
'title' => $expert->getJobTitle() ?? $expertMetadata['title'] ?? '',
'academicTitle' => $expert->getTitle() ?? '',
'company' => $expert->getCompany() ?? $expertMetadata['companyAffiliation'] ?? '',
'description' => $expert->getDescription(),
'image' => $expert->getImageLink(),
'specializations' => $expertMetadata['expertiseAreas'] ?? [],
'currentProject' => $fullMetadata['currentProject'] ?? null,
'website' => $fullMetadata['website'] ?? null
];
$expertsData[] = $expertData;
}
// Determine language for template
$language = $isEnglish ? 'en' : 'de';
// Get menu from WordPress
$menu = $this->menuService->getAcceleratorMenu($isEnglish, $request->getPathInfo());
return $this->render('@StartPlatzMentors/Public/experts-residence.html.twig', [
'experts' => $expertsData,
'searchQuery' => $filters['search'] ?? '',
'currentSpecialization' => $filters['specialization'] ?? null,
'isEnglish' => $isEnglish,
'language' => $language,
'pageTitle' => 'Expert-in-Residence Program',
'pageDescription' => $isEnglish
? 'Making local excellence in Artificial Intelligence visible and impactful'
: 'Lokale Exzellenz in der Künstlichen Intelligenz sichtbar und wirksam machen',
'menu' => $menu
]);
}
}