diff --git a/cron-fetch-forcedbanner.php b/cron-fetch-forcedbanner.php deleted file mode 100644 index 6100667..0000000 --- a/cron-fetch-forcedbanner.php +++ /dev/null @@ -1,165 +0,0 @@ -getRepository(User::class)->findByUsername('adcity-eu'); - - if (!$werbenetzwerkUser) { - echo "werbenetzwerkUser-Daten nicht gefunden.\n"; - exit(1); - } - - $werbenetzwerk = $entityManager->getRepository(Werbenetzwerk::class)->findByUser($werbenetzwerkUser); - - if (!$werbenetzwerk) { - echo "Werbenetzwerk-Daten nicht gefunden.\n"; - exit(1); - } - - - - if (!$werbenetzwerkUser) { - echo "Kein Werbenetzwerk-User gefunden.\n"; - exit(1); - } - - // API-Parameter vorbereiten - $apiUrl = 'https://www.adcity.eu/interface/'; - $params = [ - 'typ' => 'forcedtextlink', - 'id' => $werbenetzwerk->getUserId(), - 'pw' => $werbenetzwerk->getInterfacepasswort(), - 'uebrig' => 20, - 'reload' => 99999, - 'verguetung' => 0.0001, - 'ma' => 99999 - ]; - - $fullUrl = $apiUrl . '?' . http_build_query($params); - - // API-Anfrage durchführen - $response = file_get_contents($fullUrl); - - if ($response === false) { - echo "Fehler beim Abrufen der API-Daten.\n"; - exit(1); - } - - echo "Response received:\n"; - echo $response . "\n\n"; - - // Flatfile-Response parsen (alle Kampagnen pipe-getrennt in einer Zeile) - $parts = explode('|', trim($response)); - - $kampagnenCount = 0; - $updatedCount = 0; - $createdCount = 0; - - // Durch alle Kampagnen iterieren (je 5, 6 oder 7 Felder pro Kampagne) - $i = 0; - while ($i < count($parts)) { - // Mindestens 5 Felder erforderlich - if ($i + 4 >= count($parts)) { - break; - } - - // Erste 5 Felder auslesen - $kid = $parts[$i]; - $linktext = $parts[$i + 1]; - $reload = (int) $parts[$i + 2]; - $uebrig = (int) $parts[$i + 3]; - $verdienst = (float) $parts[$i + 4]; - - // Prüfen ob MA-Feld vorhanden ist (6. Feld) - $ma = 0; - $mailtext = null; - $fieldsConsumed = 5; - - // Heuristik: Wenn das nächste Feld numerisch ist und klein, ist es wahrscheinlich MA - if ($i + 5 < count($parts) && is_numeric($parts[$i + 5]) && $parts[$i + 5] < 1000) { - $ma = (int) $parts[$i + 5]; - $fieldsConsumed = 6; - - // Prüfen ob Nachrichtentext vorhanden ist (7. Feld) - if ($i + 6 < count($parts) && !is_numeric($parts[$i + 6])) { - $mailtext = $parts[$i + 6]; - $fieldsConsumed = 7; - } - } - - $i += $fieldsConsumed; - $kampagnenCount++; - - // Link-URL und Bild-URL generieren (USERID und SEITENID werden später beim Einbau ersetzt) - $destUrl = "https://www.adcity.eu/codes/forcedtextlinkklick.php?id={USERID}&bid=$kid&aid={SEITENID}"; - - echo "Processing: $linktext (KID: $kid, MA: $ma, Reload: $reload, Uebrig: $uebrig)\n"; - - // Prüfen ob Kampagne bereits existiert (anhand user_id, KID und is_tr=1) - $existingKampagne = $entityManager->getRepository(Kampagne::class) - ->findOneBy([ - 'user' => $werbenetzwerkUser, - 'kid' => $kid, - 'is_tr' => true - ]); - - if ($existingKampagne) { - // Kampagne aktualisieren (verguetung, reload, uebrig) - $existingKampagne->setVerguetung($verdienst); - $existingKampagne->setReload($reload); - $existingKampagne->setUebrig($uebrig); - $existingKampagne->setLinktext($linktext); - $existingKampagne->setMa($ma); - if ($mailtext) { - $existingKampagne->setMailtext($mailtext); - $existingKampagne->setIsMail(true); - } - - $entityManager->persist($existingKampagne); - $updatedCount++; - echo " → Updated existing campaign (verguetung: $verdienst, reload: $reload, uebrig: $uebrig)\n"; - } else { - // Neue Kampagne erstellen - $kampagne = new Kampagne(); - $kampagne->setDestUrl($destUrl); - $kampagne->setKid($kid); - $kampagne->setLinktext($linktext); - $kampagne->setMa($ma); - $kampagne->setReload($reload); - $kampagne->setVerguetung($verdienst); - $kampagne->setUebrig($uebrig); - $kampagne->setIsFb(false); - $kampagne->setIsTr(true); - $kampagne->setIsMail(false); - if ($mailtext) { - $kampagne->setMailtext($mailtext); - $kampagne->setIsMail(true); - } - $kampagne->setUser($werbenetzwerkUser); - - $entityManager->persist($kampagne); - $createdCount++; - echo " → Created new campaign\n"; - } - } - - $entityManager->flush(); - - echo "\n=== Summary ===\n"; - echo "Total campaigns processed: $kampagnenCount\n"; - echo "Created: $createdCount\n"; - echo "Updated: $updatedCount\n"; - echo "\nCron job completed successfully.\n"; - -} catch (\Exception $e) { - echo "Fehler: " . $e->getMessage() . "\n"; - echo $e->getTraceAsString() . "\n"; - exit(1); -} diff --git a/cron/adcity-forcedtextlink.php b/cron/adcity-forcedtextlink.php new file mode 100644 index 0000000..0105b1d --- /dev/null +++ b/cron/adcity-forcedtextlink.php @@ -0,0 +1,14 @@ +run(); +} catch (\Exception $e) { + echo "Fehler: " . $e->getMessage() . "\n"; + echo $e->getTraceAsString() . "\n"; + exit(1); +} diff --git a/doctrine-config.php b/doctrine-config.php index 3d77d07..dfc7bc9 100644 --- a/doctrine-config.php +++ b/doctrine-config.php @@ -12,6 +12,11 @@ require_once "config.php"; $paths = [__DIR__ . "/src/Entity"]; $isDevMode = true; +// Composer Autoloader für src/Cron und src/Repository +$loader = require __DIR__ . '/vendor/autoload.php'; +$loader->addPsr4('Cron\\', __DIR__ . '/src/Cron'); +$loader->addPsr4('Repository\\', __DIR__ . '/src/Repository'); + // Doctrine ORM Konfiguration $config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode); diff --git a/migrations/Version20251006214042.php b/migrations/Version20251006214042.php index 746b5c9..e094283 100644 --- a/migrations/Version20251006214042.php +++ b/migrations/Version20251006214042.php @@ -49,6 +49,7 @@ final class Version20251006214042 extends AbstractMigration id INT AUTO_INCREMENT NOT NULL, is_fb TINYINT(1) NOT NULL, is_tr TINYINT(1) NOT NULL, + is_ft TINYINT(1) NOT NULL, is_mail TINYINT(1) NOT NULL, dest_url VARCHAR(255) NOT NULL, kid VARCHAR(100) DEFAULT NULL, diff --git a/seed-kampagnen.php b/seed-kampagnen.php index 1242ddf..b5813c3 100644 --- a/seed-kampagnen.php +++ b/seed-kampagnen.php @@ -15,52 +15,10 @@ try { exit(0); } - // User für Zuweisung holen - $users = $entityManager->getRepository(User::class)->findAll(); - if (count($users) === 0) { - echo "Keine User gefunden. Bitte erst User anlegen.\n"; - exit(1); - } + // Keine Beispielkampagnen mehr einfügen + // AdCity-Kampagnen werden automatisch über den Cron eingefügt - // 10 Beispiel-Kampagnen erstellen - $kampagnenData = [ - ['is_fb' => true, 'is_tr' => false, 'is_mail' => false, 'dest_url' => 'https://example.com/promo1', 'linktext' => 'Super Angebot 1', 'ma' => 5, 'reload' => 24], - ['is_fb' => false, 'is_tr' => true, 'is_mail' => false, 'dest_url' => 'https://example.com/promo2', 'linktext' => 'Mega Deal 2', 'ma' => 3, 'reload' => 12], - ['is_fb' => true, 'is_tr' => true, 'is_mail' => false, 'dest_url' => 'https://example.com/promo3', 'linktext' => 'Exklusiv Angebot 3', 'ma' => 10, 'reload' => 48], - ['is_fb' => false, 'is_tr' => false, 'is_mail' => true, 'dest_url' => 'https://example.com/promo4', 'linktext' => 'Newsletter Special 4', 'ma' => 2, 'reload' => 6, 'mailtext' => 'Hallo, schau dir unser tolles Angebot an!'], - ['is_fb' => true, 'is_tr' => false, 'is_mail' => true, 'dest_url' => 'https://example.com/promo5', 'linktext' => 'Combo Deal 5', 'ma' => 7, 'reload' => 36, 'mailtext' => 'Exklusive Kombination nur für dich!'], - ['is_fb' => false, 'is_tr' => true, 'is_mail' => false, 'dest_url' => 'https://example.com/promo6', 'linktext' => 'Traffic Boost 6', 'ma' => 4, 'reload' => 18], - ['is_fb' => true, 'is_tr' => true, 'is_mail' => true, 'dest_url' => 'https://example.com/promo7', 'linktext' => 'All-in-One 7', 'ma' => 15, 'reload' => 72, 'mailtext' => 'Maximale Reichweite mit allen Kanälen!'], - ['is_fb' => false, 'is_tr' => false, 'is_mail' => false, 'dest_url' => 'https://example.com/promo8', 'linktext' => 'Basic Campaign 8', 'ma' => 1, 'reload' => 3], - ['is_fb' => true, 'is_tr' => false, 'is_mail' => false, 'dest_url' => 'https://example.com/promo9', 'linktext' => 'Facebook Power 9', 'ma' => 8, 'reload' => 24], - ['is_fb' => false, 'is_tr' => true, 'is_mail' => true, 'dest_url' => 'https://example.com/promo10', 'linktext' => 'Traffic + Mail 10', 'ma' => 6, 'reload' => 30, 'mailtext' => 'Doppelte Power für deine Kampagne!'], - ]; - - $userIndex = 0; - foreach ($kampagnenData as $data) { - $kampagne = new Kampagne(); - $kampagne->setIsFb($data['is_fb']); - $kampagne->setIsTr($data['is_tr']); - $kampagne->setIsMail($data['is_mail']); - $kampagne->setDestUrl($data['dest_url']); - $kampagne->setLinktext($data['linktext']); - $kampagne->setMa($data['ma']); - $kampagne->setReload($data['reload']); - - if (isset($data['mailtext'])) { - $kampagne->setMailtext($data['mailtext']); - } - - // User rotierend zuweisen - $kampagne->setUser($users[$userIndex % count($users)]); - $userIndex++; - - $entityManager->persist($kampagne); - } - - $entityManager->flush(); - - echo "10 Beispiel-Kampagnen wurden erfolgreich eingefügt.\n"; + echo "Keine Beispiel-Kampagnen eingefügt. Kampagnen werden über Cron-Jobs verwaltet.\n"; } catch (\Exception $e) { echo "Fehler beim Einfügen der Daten: " . $e->getMessage() . "\n"; } diff --git a/src/Cron/AbstractCron.php b/src/Cron/AbstractCron.php new file mode 100644 index 0000000..70946b2 --- /dev/null +++ b/src/Cron/AbstractCron.php @@ -0,0 +1,129 @@ +entityManager = $entityManager; + } + + /** + * Hauptmethode zum Ausführen des Cron-Jobs + */ + abstract public function run(): void; + + /** + * Gibt den Username des Werbenetzwerk-Users zurück + */ + abstract protected function getWerbenetzwerkUsername(): string; + + /** + * Lädt User und Werbenetzwerk-Daten aus der Datenbank + */ + protected function loadWerbenetzwerkData(): void + { + $username = $this->getWerbenetzwerkUsername(); + + // Werbenetzwerk-User holen + $this->werbenetzwerkUser = $this->entityManager + ->getRepository(User::class) + ->findByUsername($username); + + if (!$this->werbenetzwerkUser) { + throw new \RuntimeException("Werbenetzwerk-User '$username' nicht gefunden."); + } + + // Werbenetzwerk-Daten holen + $this->werbenetzwerk = $this->entityManager + ->getRepository(Werbenetzwerk::class) + ->findByUser($this->werbenetzwerkUser); + + if (!$this->werbenetzwerk) { + throw new \RuntimeException("Werbenetzwerk-Daten für User '$username' nicht gefunden."); + } + } + + /** + * Führt einen API-Call durch + */ + protected function fetchFromApi(string $url, array $params = []): string + { + $fullUrl = $url; + if (!empty($params)) { + $fullUrl .= '?' . http_build_query($params); + } + + $this->log("Fetching from API: $fullUrl"); + + $response = file_get_contents($fullUrl); + + if ($response === false) { + throw new \RuntimeException("Fehler beim Abrufen der API-Daten von: $fullUrl"); + } + + return $response; + } + + /** + * Prüft ob eine Kampagne bereits existiert + */ + protected function findExistingKampagne(string $kid, bool $isFb = false, bool $isTr = false, bool $isMail = false): ?Kampagne + { + $criteria = [ + 'user' => $this->werbenetzwerkUser, + 'kid' => $kid + ]; + + if ($isFb) { + $criteria['is_fb'] = true; + } + if ($isTr) { + $criteria['is_tr'] = true; + } + if ($isMail) { + $criteria['is_mail'] = true; + } + + return $this->entityManager + ->getRepository(Kampagne::class) + ->findOneBy($criteria); + } + + /** + * Speichert alle Änderungen in der Datenbank + */ + protected function flush(): void + { + $this->entityManager->flush(); + } + + /** + * Gibt eine Log-Nachricht aus + */ + protected function log(string $message): void + { + echo "[" . date('Y-m-d H:i:s') . "] $message\n"; + } + + /** + * Gibt eine Zusammenfassung aus + */ + protected function printSummary(int $total, int $created, int $updated): void + { + $this->log("=== Summary ==="); + $this->log("Total campaigns processed: $total"); + $this->log("Created: $created"); + $this->log("Updated: $updated"); + } +} diff --git a/src/Cron/AdCityForcedTextlinkCron.php b/src/Cron/AdCityForcedTextlinkCron.php new file mode 100644 index 0000000..1031f7f --- /dev/null +++ b/src/Cron/AdCityForcedTextlinkCron.php @@ -0,0 +1,336 @@ +cookieFile = sys_get_temp_dir() . '/adcity_cookies.txt'; + } + + /** + * Führt den Cron-Job aus + */ + public function run(): void + { + $this->log("Starting AdCity ForcedTextlink Cron..."); + + // Werbenetzwerk-Daten laden + $this->loadWerbenetzwerkData(); + + // Umrechnungskurs aktualisieren + $this->updateUmrechnungskurs(); + + // API-Anfrage durchführen + $response = $this->fetchFromApi(self::API_URL, [ + 'typ' => self::KAMPAGNE_TYPE, + 'id' => $this->werbenetzwerk->getUserId(), + 'pw' => $this->werbenetzwerk->getInterfacepasswort(), + 'uebrig' => 20, + 'reload' => 99999, + 'verguetung' => 0.0001, + 'ma' => 99999 + ]); + + $this->log("Response received:"); + $this->log($response); + + // Response parsen und verarbeiten + $stats = $this->processResponse($response); + + // Änderungen speichern + $this->flush(); + + // Zusammenfassung ausgeben + $this->printSummary($stats['total'], $stats['created'], $stats['updated']); + $this->log("Cron job completed successfully."); + } + + /** + * Verarbeitet die API-Response + */ + private function processResponse(string $response): array + { + $parts = explode('|', trim($response)); + + $kampagnenCount = 0; + $updatedCount = 0; + $createdCount = 0; + + // Durch alle Kampagnen iterieren (je 5, 6 oder 7 Felder pro Kampagne) + $i = 0; + while ($i < count($parts)) { + // Mindestens 5 Felder erforderlich + if ($i + 4 >= count($parts)) { + break; + } + + // Erste 5 Felder auslesen + $kid = $parts[$i]; + $linktext = $parts[$i + 1]; + $reload = (int) $parts[$i + 2] * 60; // API gibt Stunden zurück, DB speichert Minuten + $uebrig = (int) $parts[$i + 3]; + $verdienst = (float) $parts[$i + 4]; + + // Prüfen ob MA-Feld vorhanden ist (6. Feld) + $ma = 0; + $mailtext = null; + $fieldsConsumed = 5; + + // Heuristik: Wenn das nächste Feld numerisch ist und klein, ist es wahrscheinlich MA + if ($i + 5 < count($parts) && is_numeric($parts[$i + 5]) && $parts[$i + 5] < 1000) { + $ma = (int) $parts[$i + 5]; + $fieldsConsumed = 6; + + // Prüfen ob Nachrichtentext vorhanden ist (7. Feld) + if ($i + 6 < count($parts) && !is_numeric($parts[$i + 6])) { + $mailtext = $parts[$i + 6]; + $fieldsConsumed = 7; + } + } + + $i += $fieldsConsumed; + $kampagnenCount++; + + // Vergütung in Euro umrechnen + $verguetungEuro = $verdienst * $this->werbenetzwerk->getUmrechnungskurs(); + + // Kampagne verarbeiten + if ($this->processKampagne($kid, $linktext, $ma, $reload, $uebrig, $verdienst, $verguetungEuro, $mailtext)) { + $updatedCount++; + } else { + $createdCount++; + } + } + + return [ + 'total' => $kampagnenCount, + 'created' => $createdCount, + 'updated' => $updatedCount + ]; + } + + /** + * Verarbeitet eine einzelne Kampagne + * + * @return bool true wenn updated, false wenn created + */ + private function processKampagne( + string $kid, + string $linktext, + int $ma, + int $reload, + int $uebrig, + float $verdienst, + float $verguetungEuro, + ?string $mailtext + ): bool { + // Vergütung höher als 0.1 Euro filtern + if ($verguetungEuro > 0.1) { + $this->log("Skipping: $linktext (KID: $kid, Vergütung zu hoch: $verguetungEuro €)"); + + // Wenn Kampagne existiert, löschen + $existingKampagne = $this->entityManager + ->getRepository(Kampagne::class) + ->findOneBy([ + 'user' => $this->werbenetzwerkUser, + 'kid' => $kid, + 'is_ft' => true + ]); + if ($existingKampagne) { + $this->entityManager->remove($existingKampagne); + $this->log(" → Deleted existing campaign with high vergütung"); + } + + return false; + } + + // Link-URL generieren + $destUrl = "https://www.adcity.eu/codes/forcedtextlinkklick.php?id={USERID}&bid=$kid&aid={SEITENID}"; + + $this->log("Processing: $linktext (KID: $kid, MA: $ma, Reload: $reload, Uebrig: $uebrig)"); + + // Prüfen ob Kampagne bereits existiert (is_ft = true) + $existingKampagne = $this->entityManager + ->getRepository(Kampagne::class) + ->findOneBy([ + 'user' => $this->werbenetzwerkUser, + 'kid' => $kid, + 'is_ft' => true + ]); + + if ($existingKampagne) { + // Kampagne aktualisieren + $existingKampagne->setVerguetung($verguetungEuro); + $existingKampagne->setReload($reload); + $existingKampagne->setUebrig($uebrig); + $existingKampagne->setLinktext($linktext); + $existingKampagne->setMa($ma); + if ($mailtext) { + $existingKampagne->setMailtext($mailtext); + $existingKampagne->setIsMail(true); + } + + $this->entityManager->persist($existingKampagne); + $this->log(" → Updated (verguetung: $verguetungEuro €, reload: $reload, uebrig: $uebrig)"); + return true; + } else { + // Neue Kampagne erstellen + $kampagne = new Kampagne(); + $kampagne->setDestUrl($destUrl); + $kampagne->setKid($kid); + $kampagne->setLinktext($linktext); + $kampagne->setMa($ma); + $kampagne->setReload($reload); + $kampagne->setVerguetung($verguetungEuro); + $kampagne->setUebrig($uebrig); + $kampagne->setIsFb(false); + $kampagne->setIsTr(false); + $kampagne->setIsFt(true); + $kampagne->setIsMail(false); + if ($mailtext) { + $kampagne->setMailtext($mailtext); + $kampagne->setIsMail(true); + } + $kampagne->setUser($this->werbenetzwerkUser); + + $this->entityManager->persist($kampagne); + $this->log(" → Created new campaign"); + return false; + } + } + + /** + * Aktualisiert den Umrechnungskurs in der Datenbank + */ + private function updateUmrechnungskurs(): void + { + $this->log("Updating Umrechnungskurs..."); + + // Auf AdCity einloggen + $this->loginToAdCity(); + + // Umrechnungskurs von der Interface-Seite abrufen + $umrechnungskurs = $this->fetchUmrechnungskurs(); + + if ($umrechnungskurs !== null) { + $this->werbenetzwerk->setUmrechnungskurs($umrechnungskurs); + $this->entityManager->persist($this->werbenetzwerk); + $this->entityManager->flush(); + $this->log("Umrechnungskurs updated: $umrechnungskurs"); + } else { + $this->log("Warning: Could not fetch Umrechnungskurs"); + } + + // Cookie-Datei aufräumen + if (file_exists($this->cookieFile)) { + unlink($this->cookieFile); + } + } + + /** + * Loggt sich auf AdCity.eu ein + */ + private function loginToAdCity(): void + { + $this->log("Logging in to AdCity..."); + + $ch = curl_init(); + + curl_setopt_array($ch, [ + CURLOPT_URL => self::LOGIN_URL, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query([ + 'username' => self::USERNAME, + 'password' => self::PASSWORD, + 'login' => 'Login' + ]), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_COOKIEJAR => $this->cookieFile, + CURLOPT_COOKIEFILE => $this->cookieFile, + CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + throw new \RuntimeException("Login failed with HTTP code: $httpCode"); + } + + $this->log("Login successful"); + } + + /** + * Ruft den Umrechnungskurs von der Interface-Seite ab + */ + private function fetchUmrechnungskurs(): ?float + { + $this->log("Fetching Umrechnungskurs from interface page..."); + + $ch = curl_init(); + + curl_setopt_array($ch, [ + CURLOPT_URL => self::INTERFACE_URL, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_COOKIEFILE => $this->cookieFile, + CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200 || !$response) { + $this->log("Failed to fetch interface page (HTTP $httpCode)"); + return null; + } + + // Umrechnungskurs aus der HTML-Response extrahieren + // Suche nach Patterns wie "1 Pt = 0.0000000022 €" oder ähnlich + $patterns = [ + '/1\s*Pt\s*=\s*([\d.]+)\s*€/i', + '/1\s*pt\s*=\s*([\d.]+)\s*euro/i', + '/Umrechnungskurs[:\s]*([\d.]+)/i', + '/([\d.]+)\s*€\s*pro\s*Pt/i' + ]; + + foreach ($patterns as $pattern) { + if (preg_match($pattern, $response, $matches)) { + $umrechnungskurs = (float) $matches[1]; + $this->log("Found Umrechnungskurs: $umrechnungskurs"); + return $umrechnungskurs; + } + } + + $this->log("Could not find Umrechnungskurs in response"); + return null; + } +} diff --git a/src/Entity/Kampagne.php b/src/Entity/Kampagne.php index 8ef8880..9f74f2f 100644 --- a/src/Entity/Kampagne.php +++ b/src/Entity/Kampagne.php @@ -20,6 +20,9 @@ class Kampagne #[ORM\Column(type: 'boolean')] private bool $is_tr = false; + #[ORM\Column(type: 'boolean')] + private bool $is_ft = false; + #[ORM\Column(type: 'boolean')] private bool $is_mail = false; @@ -101,6 +104,17 @@ class Kampagne return $this; } + public function getIsFt(): bool + { + return $this->is_ft; + } + + public function setIsFt(bool $is_ft): self + { + $this->is_ft = $is_ft; + return $this; + } + public function getIsMail(): bool { return $this->is_mail;