diff --git a/bridges/KinoboxBridge.php b/bridges/KinoboxBridge.php new file mode 100644 index 00000000000..1c84b26d14d --- /dev/null +++ b/bridges/KinoboxBridge.php @@ -0,0 +1,310 @@ + [ + ], + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + defaultLinkTo($html, static::URI); + + // Router + switch ($this->queriedContext) { + case 'Articles': + $this->collectNews($html); + break; + } + } + + /** + * Returns the icon for the bridge. + * + * @return string The icon URL. + */ + public function getURI() + { + $uri = static::URI; + + // URI Router + switch ($this->queriedContext) { + case 'Articles': + $uri .= '/clanky'; + break; + } + + return $uri; + } + + /** + * Returns the name for the bridge. + * + * @return string The Name. + */ + public function getName() + { + $name = static::NAME; + + $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; + + switch ($this->queriedContext) { + case 'Articles': + break; + } + + return $name; + } + + /** + * Parse most used date formats + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object, manually + * fixing dates and times (set to 00:00:00.000). + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date) + { + // Define the regular expression pattern. + // It matches any character that is NOT (^): + // - a digit (0-9) + // - a forward slash (/) + // - a dot (.) + // - a colon (:) + // - a hyphen (-) + // - a whitespace character (\s includes space, tab, newline etc.) + // The 'u' modifier ensures it works correctly with UTF-8 strings. + $pattern = '/[^0-9\/\.\:\-]/u'; + + // Use preg_replace to remove all characters matching the pattern (i.e., unwanted chars). + // Replace them with an empty string ''. + $date = preg_replace($pattern, '', $date); + + // parse date based on most used formats + $df = $this->parseDateTimeFromString($date); + + return date_format($df, 'U'); + } + + /** + * Extracts the images from the article. + * + * @param object $article The article object. + * @return array An array of image URLs. + */ + private function extractNewsImages($article) + { + // Notice: We can have zero or more images (though it should mostly be 1) + $srcset = $article->find('img', 0)->getAttribute('srcset'); + + $images = []; + + // split multi srcset into array of images + $img_parts = preg_split('/\s+/', $srcset); + // Only 3th array is interesting + $images[] = html_entity_decode($img_parts[2]); + + return $images; + } + + #region Articles + + /** + * Collects uri, timestamp, title, content and images in the news articles from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectNews($html) + { + // Check if page contains articles + $articles = $html->find('ol.ArticlesPagingListContainer_grid__OzyvS', 0)->find('li') + or returnServerException('No articles found! Layout might have changed!'); + + foreach ($articles as $article) { + $item = []; + + // get uri of product + $item['uri'] = $this->extractNewsUri($article); + // Add images + $item['enclosures'] = $this->extractNewsImages($article); + // Add title + $item['title'] = $this->extractNewsTitle($article); + // Add description + $item['timestamp'] = $this->extractNewsDate($article); + // Add content + $item['content'] = $this->extractNewsDescription($article); + + // collect sources into rss article + $this->items[] = $item; + } + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractNewsUri($article) + { + // Return URI of the article + $element = $article->find('a', 0) + or returnServerException('Anchor not found!'); + + return $element->href; + } + + /** + * Extracts the date of the news article. + * + * @param object $article The article object. + * @return string The date of the news article. + */ + private function extractNewsDate($article) + { + // Check if date is set + $element = $article->find('.Space_container__MY8CT', 0) + or returnServerException('Date not found!'); + + // Format date + return $this->fixDate($element->plaintext); + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription($article) + { + // Extract description + $element = $article->find('strong', 0) + or returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractNewsTitle($article) + { + // Extract title + $element = $article->find('strong', 0) + or returnServerException('Title not found!'); + + return $element->plaintext; + } + + /** + * It attempts to recognize the date/time format in a string and create a DateTime object. + * + * It goes through the list of defined formats and tries to apply them to the input string. + * Returns the first successfully parsed DateTime object that matches the entire string. + * + * @param string $dateString A string potentially containing a date and/or time. + * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null. + */ + private function parseDateTimeFromString(string $dateString): ?DateTime + { + // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs! + // Order may matter if the formats are ambiguous. + // It is recommended to give more specific formats (with time, full year) before more general ones. + $possibleFormats = [ + // Czech formats (day.month.year) + 'd.m.Y H:i:s', // 10.04.2025 10:57:47 + 'j.n.Y H:i:s', // 10.4.2025 10:57:47 + 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47 + 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47 + 'd.m.Y H:i', // 10.04.2025 10:57 + 'j.n.Y H:i', // 10.4.2025 10:57 + 'd. m. Y H:i', // 10. 04. 2025 10:57 + 'j. n. Y H:i', // 10. 4. 2025 10:57 + 'd.m.Y', // 10.04.2025 + 'j.n.Y', // 10.4.2025 + 'd. m. Y', // 10. 04. 2025 + 'j. n. Y', // 10. 4. 2025 + + // ISO 8601 and international formats (year-month-day) + 'Y-m-d H:i:s', // 2025-04-10 10:57:47 + 'Y-m-d H:i', // 2025-04-10 10:57 + 'Y-m-d', // 2025-04-10 + 'YmdHis', // 20250410105747 + 'Ymd', // 20250410 + + // American formats (month/day/year) - beware of ambiguity! + 'm/d/Y H:i:s', // 04/10/2025 10:57:47 + 'n/j/Y H:i:s', // 4/10/2025 10:57:47 + 'm/d/Y H:i', // 04/10/2025 10:57 + 'n/j/Y H:i', // 4/10/2025 10:57 + 'm/d/Y', // 04/10/2025 + 'n/j/Y', // 4/10/2025 + + // Standard formats (including time zone) + DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00 + DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200 + DateTime::ISO8601, // example. 2025-04-10T105747+0200 + 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem + 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami + + // You can add more formats as needed... + // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale + // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale + ]; + + // Set locale for parsing month/day names (if using F, M, l, D) + // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8'); + + foreach ($possibleFormats as $format) { + // We will try to create a DateTime object from the given format + $dateTime = DateTime::createFromFormat($format, $dateString); + + // We check that the parsing was successful AND ALSO + // that there were no errors or warnings during the parsing. + // This is important to ensure that the format matches the ENTIRE string. + if ($dateTime !== false) { + $errors = DateTime::getLastErrors(); + if (!($errors)) { + // Success! We found a valid format for the entire string. + return $dateTime; + } + } + } + + // If no format matches or parsing failed + return null; + } + + #endregion +} \ No newline at end of file diff --git a/bridges/MakroBridge.php b/bridges/MakroBridge.php new file mode 100644 index 00000000000..0930d859072 --- /dev/null +++ b/bridges/MakroBridge.php @@ -0,0 +1,339 @@ + [], + 'TOP nabídky' => [] + ]; + + /** + * Fetches and processes data based on the selected context. + * + * This function retrieves the HTML content for the specified context's URI, + * resolves relative links within the content, and then delegates the data + * extraction to the appropriate method (currently only `collectNews`). + */ + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + defaultLinkTo($html, static::URI); + + // Router + switch ($this->queriedContext) { + case 'Aktuální letáky': + $this->collectNews($html); + break; + case 'TOP nabídky': + $this->collectLetter($html); + break; + } + } + + /** + * Returns the icon for the bridge. + * + * @return string The icon URL. + */ + public function getURI() + { + $uri = static::URI; + + // URI Router + switch ($this->queriedContext) { + case 'Aktuální letáky': + $uri .= '/aktualni-nabidka'; + break; + case 'TOP nabídky': + $uri .= '/top-nabidky'; + break; + } + + return $uri; + } + + /** + * Returns the name for the bridge. + * + * @return string The Name. + */ + public function getName() + { + $name = static::NAME; + + $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; + + switch ($this->queriedContext) { + case 'Aktuální letáky': + break; + case 'TOP nabídky': + break; + } + + return $name; + } + + /** + * Parse most used date formats + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object, manually + * fixing dates and times (set to 00:00:00.000). + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date) + { + $df = $this->parseDateTimeFromString($date); + + return date_format($df, 'U'); + } + + /** + * Extracts the images from the article. + * + * @param object $article The article object. + * @return array An array of image URLs. + */ + private function extractImages($article) + { + // Notice: We can have zero or more images (though it should mostly be 1) + $elements = $article->find('img'); + + $images = []; + + foreach ($elements as $img) { + $images[] = $img->src; + } + + return $images; + } + + #region Articles + + /** + * Collects uri, timestamp, title, content and images in the news articles from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectNews($html) + { + // Check if page contains articles + $articles = $html->find('.catalog') + or returnServerException('No articles found! Layout might have changed!'); + + foreach ($articles as $article) { + $item = []; + + // Extract article URI + $item['uri'] = $this->extractNewsUri($article); + + // Extract article title + $item['title'] = $this->extractNewsTitle($article); + + // Extract article content + $item['content'] = $this->extractNewsDescription($article); + + // Extract article images + $item['enclosures'] = $this->extractImages($article); + + // collect sources into rss article + $this->items[] = $item; + } + } + + /** + * Collects uri, timestamp, title, content and images in the letter articles from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectLetter($html) + { + // Check if page contains articles + $articles = $html->find('.catalog') + or returnServerException('No articles found! Layout might have changed!'); + + foreach ($articles as $article) { + $item = []; + + // Extract article URI + $item['uri'] = $this->extractNewsUri($article); + + // Extract article title + $item['title'] = $this->extractNewsTitle($article); + + // Extract article content + $item['content'] = $this->extractNewsDescription($article); + + // Extract article images + $item['enclosures'] = $this->extractImages($article); + + // collect sources into rss article + $this->items[] = $item; + } + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractNewsUri($article) + { + // Return URI of the article + $element = $article->find('a', 0) + or returnServerException('Anchor not found!'); + + return $element->href; + } + + /** + * Extracts the date of the news article. + * + * @param object $article The article object. + * @return string The date of the news article. + */ + private function extractNewsDate($article) + { + // Check if date is set + $element = $article->find('div.post__info', 0)->find('span', 0) + or returnServerException('Date not found!'); + + $date = trim(explode('|', $element->plaintext)[0]); + + // Format date + return $this->fixDate($date); + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription($article) + { + // Extract description + $element = $article->find('div.catalog-content', 0)->find('span', 1) + or returnServerException('Title not found!'); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractNewsTitle($article) + { + // Extract title + $element = $article->find('span.catalog-title', 0)->find('strong', 0) + or returnServerException('Title not found!'); + + return $element->plaintext; + } + + /** + * It attempts to recognize the date/time format in a string and create a DateTime object. + * + * It goes through the list of defined formats and tries to apply them to the input string. + * Returns the first successfully parsed DateTime object that matches the entire string. + * + * @param string $dateString A string potentially containing a date and/or time. + * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null. + */ + private function parseDateTimeFromString(string $dateString): ?DateTime + { + // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs! + // Order may matter if the formats are ambiguous. + // It is recommended to give more specific formats (with time, full year) before more general ones. + $possibleFormats = [ + // Czech formats (day.month.year) + 'd.m.Y H:i:s', // 10.04.2025 10:57:47 + 'j.n.Y H:i:s', // 10.4.2025 10:57:47 + 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47 + 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47 + 'd.m.Y H:i', // 10.04.2025 10:57 + 'j.n.Y H:i', // 10.4.2025 10:57 + 'd. m. Y H:i', // 10. 04. 2025 10:57 + 'j. n. Y H:i', // 10. 4. 2025 10:57 + 'd.m.Y', // 10.04.2025 + 'j.n.Y', // 10.4.2025 + 'd. m. Y', // 10. 04. 2025 + 'j. n. Y', // 10. 4. 2025 + + // ISO 8601 and international formats (year-month-day) + 'Y-m-d H:i:s', // 2025-04-10 10:57:47 + 'Y-m-d H:i', // 2025-04-10 10:57 + 'Y-m-d', // 2025-04-10 + 'YmdHis', // 20250410105747 + 'Ymd', // 20250410 + + // American formats (month/day/year) - beware of ambiguity! + 'm/d/Y H:i:s', // 04/10/2025 10:57:47 + 'n/j/Y H:i:s', // 4/10/2025 10:57:47 + 'm/d/Y H:i', // 04/10/2025 10:57 + 'n/j/Y H:i', // 4/10/2025 10:57 + 'm/d/Y', // 04/10/2025 + 'n/j/Y', // 4/10/2025 + + // Standard formats (including time zone) + DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00 + DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200 + DateTime::ISO8601, // example. 2025-04-10T105747+0200 + 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem + 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami + + // You can add more formats as needed... + // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale + // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale + ]; + + // Set locale for parsing month/day names (if using F, M, l, D) + // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8'); + + foreach ($possibleFormats as $format) { + // We will try to create a DateTime object from the given format + $dateTime = DateTime::createFromFormat($format, $dateString); + + // We check that the parsing was successful AND ALSO + // that there were no errors or warnings during the parsing. + // This is important to ensure that the format matches the ENTIRE string. + if ($dateTime !== false) { + $errors = DateTime::getLastErrors(); + if (!($errors)) { + // Success! We found a valid format for the entire string. + return $dateTime; + } + } + } + + // If no format matches or parsing failed + return null; + } + + #endregion +} diff --git a/bridges/PennyBridge.php b/bridges/PennyBridge.php new file mode 100644 index 00000000000..9eeecc7ad8f --- /dev/null +++ b/bridges/PennyBridge.php @@ -0,0 +1,699 @@ + [], + 'Promotional letter' => [] + ]; + + // Due to the fact that the URI is not static on Penny Market website (generated by javascript), we need to set it here + // Define your associative array (map) of keywords and URLs. This is mapping between penny product title and image URL. + // Keys should ideally be in lowercase for reliable comparison. + // Keys only without diacritics + + /** + * Fetches and processes data based on the selected context. + * + * This function retrieves the HTML content for the specified context's URI, + * resolves relative links within the content, and then delegates the data + * extraction to the appropriate method (currently only `collectNews`). + */ + public function collectData() + { + $html = getSimpleHTMLDOMCached($this->getURI(), self::CACHE_TIMEOUT); + + defaultLinkTo($html, static::URI); + + // Router + switch ($this->queriedContext) { + case 'Weekly offer': + $this->collectNews($html); + break; + case 'Promotional letter': + $this->collectLetter($html); // aaaaa + break; + } + } + + /** + * Returns the icon for the bridge. + * + * @return string The icon URL. + */ + public function getURI() + { + $uri = static::URI; + + // URI Router + switch ($this->queriedContext) { + case 'Weekly offer': + $uri .= 'nabidky'; + break; + case 'Promotional letter': + $uri .= 'nabidky/letaky'; + break; + } + + return $uri; + } + + /** + * Returns the name for the bridge. + * + * @return string The Name. + */ + public function getName() + { + $name = static::NAME; + + $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; + + switch ($this->queriedContext) { + case 'Weekly offer': + break; + case 'Promotional letter': + break; + } + + return $name; + } + + /** + * Parse most used date formats + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object, manually + * fixing dates and times (set to 00:00:00.000). + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date) + { + $df = $this->parseDateTimeFromString($date); + + return date_format($df, 'U'); + } + + /** + * Extracts the images from the article. + * + * @param object $article The article object. + * @return array An array of image URLs. + */ + private function extractImages($article) + { + // Notice: We can have zero or more images (though it should mostly be 1) + $elements = $article->find('img'); + + $images = []; + + foreach ($elements as $img) { + $images[] = $img->src; + } + + return $images; + } + + // region Weekly offer + + /** + * Collects uri, timestamp, title, content and images in the product offers from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectNews($html) + { + $urlField = []; + + // Check if page contains articles and split by class + $javascript = $html->find('script') or + returnServerException('No articles found! Layout might have changed!'); + + // Loop through the javascript to find the JSON data. The JSON data is in the form of a string, so we need to decode it + foreach ($javascript as $script) { + if ($script->hasAttribute('data-nuxt-data')) { + $json = json_decode($script->innertext); + } + } + + // define the needle. This is the URL prefix we are looking for + $needle = 'https://images.cdn.europe'; + + // Loop through the JSON data to find the image URLs and extract the product codes + foreach ($json as $key) { // This will search in the 2 jsons + if (gettype($key) === 'string') { + if (strpos($key, $needle) === 0) { + $value = $this->extractProductCode($key); + $value = preg_replace('/-/', '', $value); + $urlField[$value] = $key; + } + } + } + + // Check if page contains articles and split by class + $articles = $html->find('.ws-product-item-base') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + // Add URI + $item['uri'] = $this->extractNewsUri($article); + // Add title + $item['title'] = $this->extractNewsTitle($article) . ' - '; + // Add price to title + $item['title'] .= $this->extractNewsDescription4($article); + // Add SUPER NABIDKA if it exists + if ($this->extractNewsDescription3($article) != '') { + $item['title'] .= ' - ' . $this->extractNewsDescription3($article); + } + + // Add metric + $item['content'] = 'Jednotka: ' . $this->extractNewsDescription($article) . '

'; + // Add date from and to + $item['content'] .= 'Akce: ' . $this->extractNewsDescription1($article) . ' ' . $this->extractNewsDescription2($article) . '

'; + $item['content'] .= '---

'; + // Add price + $item['content'] .= 'Cena: ' . $this->extractNewsDescription4($article) . '

'; + // Add price per metric + $item['content'] .= 'Cena za jednotku: ' . $this->extractNewsDescription5($article) . '

'; + + // Add promotional price if it exists + if ($this->extractNewsDescription6($article) != null) { + $item['content'] .= '---

'; + // Add price + $item['content'] .= 'Cena s PENNY kartou: ' . $this->extractNewsDescription6($article) . '

'; + // Add price per metric + $item['content'] .= 'Cena za jednotku s PENNY kartou: ' . $this->extractNewsDescription7($article) . '

'; + } + + // Initialize images array + $images = []; + + // Extract images by product code. The product code is in the format XX-XXXXXX + $productCode = $this->extractProductCodeFromEnd($item['uri']); + if ($productCode !== false && array_key_exists($productCode, $urlField)) { + $images[] = $urlField[$this->extractProductCodeFromEnd($item['uri'])]; + + $item['enclosures'] = $images; + } + + // Add to rss query + $this->items[] = $item; + } + } + + /** + * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectLetter($html) + { + // Check if page contains articles and split by class + $articles = $html->find('div.ws-column.col-12.flex-column.col-sm-6.col-md-3') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + // Check if article contains letter + // If it doesn't, continue to next article + if ($this->extractLetterUri($article) == null) { + continue; + } + + // Add URI + $item['uri'] = $this->extractLetterUri($article); + // Add title + $item['title'] = 'Promo leták: ' . $this->extractLetterTitle($article); + // Add content + $item['content'] = $this->extractLetterDescription($article); + // Add to rss query + $this->items[] = $item; + } + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractNewsUri($article) + { + // Return URI of the article + $element = $article->find('.ws-product-tile__link', 0) or + returnServerException('Anchor not found!'); + + return $element->href; + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractLetterUri($article) + { + // Return URI of the article + $element = $article->find('a.ws-btn', 0); + + // Element empty check + if ($element == null) { + return ''; + } + + return $element->href; + } + + /** + * Extracts the date of the news article. + * + * @param object $article The article object. + * @return string The date of the news article. + */ + private function extractNewsDate($article) + { + // Check if date is set + $element = $article->find('div.post__info', 0)->find('span', 0) or + returnServerException('Date not found!'); + + $date = trim(explode('|', $element->plaintext)[0]); + + // Format date + return $this->fixDate($date); + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription($article) + { + // Extract description + $element = $article->find('ul.ws-product-information__piece-description', 0)->find('li', 0) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription1($article) + { + // Extract description + $element = $article->find('div.ws-product-price-validity', 0)->find('div', 0) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription2($article) + { + // Extract description + $element = $article->find('div.ws-product-price-validity', 0)->find('div', 1) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription3($article) + { + // Extract description + $element = $article->find('div.ws-product-badge-text', 0); + + // Check if element is not null + // If it is null, return empty string + // If it is not null, return the inner text + // This is to avoid errors when the element is not found + // and to ensure that the function always returns a string + if ($element != null) { + return $element->innertext; + } else { + return ''; + } + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription4($article) + { + // Extract description + $element = $article->find('div.ws-product-price-type__value', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription5($article) + { + // Extract description + $element = $article->find('div.ws-product-price-type__label', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription6($article) + { + // Extract description + $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1); + + // Element empty check + if ($element == null) { + return ''; + } + + // Not null, so we can safely access the element + $element = $element->find('div.ws-product-price-type__value', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription7($article) + { + // Extract description + $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1); + + // Element empty check + if ($element == null) { + return ''; + } + + // Not null, so we can safely access the element + $element = $element->find('div.ws-product-price-type__label', 0); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractNewsTitle($article) + { + // Extract title + $element = $article->find('span.show-sr-and-print', 0) or + returnServerException('Title not found!'); + + return $element->plaintext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractLetterTitle($article) + { + // Extract title + $element = $article->find('div.ws-text', 0) or + returnServerException('Title not found!'); + + // Element empty check + if ($element == null) { + return ''; + } + + return $element->plaintext; + } + + /** + * Extracts the description of the letter article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractLetterDescription($article) + { + // Extract description + $element = $article->find('a', 0); + + return $element; + } + + /** + * It attempts to recognize the date/time format in a string and create a DateTime object. + * + * It goes through the list of defined formats and tries to apply them to the input string. + * Returns the first successfully parsed DateTime object that matches the entire string. + * + * @param string $dateString A string potentially containing a date and/or time. + * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null. + */ + private function parseDateTimeFromString(string $dateString): ?DateTime + { + // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs! + // Order may matter if the formats are ambiguous. + // It is recommended to give more specific formats (with time, full year) before more general ones. + $possibleFormats = [ + // Czech formats (day.month.year) + 'd.m.Y H:i:s', // 10.04.2025 10:57:47 + 'j.n.Y H:i:s', // 10.4.2025 10:57:47 + 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47 + 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47 + 'd.m.Y H:i', // 10.04.2025 10:57 + 'j.n.Y H:i', // 10.4.2025 10:57 + 'd. m. Y H:i', // 10. 04. 2025 10:57 + 'j. n. Y H:i', // 10. 4. 2025 10:57 + 'd.m.Y', // 10.04.2025 + 'j.n.Y', // 10.4.2025 + 'd. m. Y', // 10. 04. 2025 + 'j. n. Y', // 10. 4. 2025 + // ISO 8601 and international formats (year-month-day) + 'Y-m-d H:i:s', // 2025-04-10 10:57:47 + 'Y-m-d H:i', // 2025-04-10 10:57 + 'Y-m-d', // 2025-04-10 + 'YmdHis', // 20250410105747 + 'Ymd', // 20250410 + // American formats (month/day/year) - beware of ambiguity! + 'm/d/Y H:i:s', // 04/10/2025 10:57:47 + 'n/j/Y H:i:s', // 4/10/2025 10:57:47 + 'm/d/Y H:i', // 04/10/2025 10:57 + 'n/j/Y H:i', // 4/10/2025 10:57 + 'm/d/Y', // 04/10/2025 + 'n/j/Y', // 4/10/2025 + // Standard formats (including time zone) + DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00 + DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200 + DateTime::ISO8601, // example. 2025-04-10T105747+0200 + 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem + 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami + // You can add more formats as needed... + // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale + // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale + ]; + + // Set locale for parsing month/day names (if using F, M, l, D) + // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8'); + + foreach ($possibleFormats as $format) { + // We will try to create a DateTime object from the given format + $dateTime = DateTime::createFromFormat($format, $dateString); + + // We check that the parsing was successful AND ALSO + // that there were no errors or warnings during the parsing. + // This is important to ensure that the format matches the ENTIRE string. + if ($dateTime !== false) { + $errors = DateTime::getLastErrors(); + if (!($errors)) { + // Success! We found a valid format for the entire string. + return $dateTime; + } + } + } + + // If no format matches or parsing failed + return null; + } + + /** + * Finds values from an associative array whose keys are substrings of a given text. + * + * The function iterates through the `$map` associative array. For each key, + * it checks if that key exists as a substring within the input `$text`. + * If found, the corresponding value from the map is added to the result array. + * The search is case-sensitive and treats special characters literally. + * + * @param string $text The input text string to search within. + * @param array $map An associative array (key => value). Keys from this array will be searched for in `$text`. + * @return array An array of values whose corresponding keys were found as substrings in `$text`. Returns an empty array if no keys are found. + */ + private function findValuesByKeySubstring(string $text, array $map): array + { + $foundValues = []; // Initialize array for found values + + // Iterate through each key => value pair in the map + foreach ($map as $key => $value) { + // Use strpos(), which finds the position of the first occurrence of a substring. + // Returns the position (including 0) or `false` if the substring is not found. + // We use `!== false` to correctly handle the case where the key starts at position 0. + // Cast key to string for robustness (though array keys are usually strings or ints). + // `strpos` treats special characters in the key and text literally. + + // echo "Key: $key, Text: $text
\n"; + if (strpos($text, $key) !== false) { + // If the key was found in the text, add its corresponding value to the result array + $foundValues[] = $value; + } + } + + // Return the array of found values + return $foundValues; + } + + /** + * Removes Czech diacritics from a given string. + * + * This function replaces Czech characters with their ASCII equivalents. + * For example, 'á' becomes 'a', 'č' becomes 'c', etc. + * + * @param string $text The input string with Czech diacritics. + * @return string The string with Czech diacritics removed. + */ + private function removeCzechDiacritics(string $text): string + { + $czech = [ + 'á', 'č', 'ď', 'é', 'ě', 'í', 'ň', 'ó', 'ř', 'š', 'ť', 'ú', 'ů', 'ý', 'ž', + 'Á', 'Č', 'Ď', 'É', 'Ě', 'Í', 'Ň', 'Ó', 'Ř', 'Š', 'Ť', 'Ú', 'Ů', 'Ý', 'Ž' + ]; + $ascii = [ + 'a', 'c', 'd', 'e', 'e', 'i', 'n', 'o', 'r', 's', 't', 'u', 'u', 'y', 'z', + 'A', 'C', 'D', 'E', 'E', 'I', 'N', 'O', 'R', 'S', 'T', 'U', 'U', 'Y', 'Z' + ]; + + return str_replace($czech, $ascii, $text); + } + + /** + * Extracts a product code from a given URL. + * + * This method attempts to retrieve a product code by: + * 1. Using a regular expression to match the pattern XX-XXXXXX in the URL. + * 2. Alternatively, splitting the URL by dashes and checking if the first two parts are numeric. + * + * @param string $url The URL from which to extract the product code. + * @return string|false The extracted product code if found, or false if no valid code is present. + */ + private function extractProductCode($url) + { + // Use the regular expression to find the pattern XX-XXXXXX in the last part of the URL + if (preg_match('/\/(\d{2}-\d{6})-/', $url, $matches)) { + return $matches[1]; // Returns the first captured group + } + + // Alternative approach with expansion and parsing + $parts = explode('/', $url); + $lastPart = end($parts); + + // Divide last part by dash + $elements = explode('-', $lastPart); + + // If we have the right format, the first two parts should be "88" and "200897" + if (count($elements) >= 3 && is_numeric($elements[0]) && is_numeric($elements[1])) { + return $elements[0] . '-' . $elements[1]; + } + + return false; // Returns false if the code has not been found + } + + + /** + * Extracts a numerical product code from the end of a given URL. + * + * This method attempts to retrieve a product code by: + * 1. Using a regular expression to match a numeric sequence at the end of the URL. + * 2. Alternatively, splitting the URL by dashes and checking if the last segment is numeric. + * + * @param string $url The URL from which to extract the product code. + * @return string|false The extracted product code if found, or false if no valid code is present. + */ + private function extractProductCodeFromEnd($url) + { + // Using preg _ match to find the number at the end of the URL + if (preg_match('/-(\d+)$/', $url, $matches)) { + return $matches[1]; + } + + // Alternative approach - divide the URL by dash and take the last part + $parts = explode('-', $url); + $lastPart = end($parts); + + // If the last part is numerical, return it + if (is_numeric($lastPart)) { + return $lastPart; + } + + return false; // Return false if code not found + } + + + // endregion +} + diff --git a/bridges/StavebninyDEKBridge.php b/bridges/StavebninyDEKBridge.php new file mode 100644 index 00000000000..1461f65f74c --- /dev/null +++ b/bridges/StavebninyDEKBridge.php @@ -0,0 +1,640 @@ + [], + 'Events' => [], + 'Topic' => [] + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + defaultLinkTo($html, static::URI); + + // Router + switch ($this->queriedContext) { + case 'News': + $this->collectNews($html); + break; + case 'Events': + $this->collectEvents($html); + break; + case 'Topic': + $this->collectTopic($html); + break; + } + } + + /** + * Returns the icon for the bridge. + * + * @return string The icon URL. + */ + public function getURI() + { + $uri = static::URI; + + // URI Router + switch ($this->queriedContext) { + case 'News': + $uri .= 'akce/nabidka/'; + break; + case 'Events': + $uri .= 'pobocka-praha-hostivar/akce/udalosti/'; + break; + case 'Topic': + $uri .= 'pobocka-praha-hostivar/akce/temata/'; + break; + } + + return $uri; + } + + /** + * Returns the keyword URL map for the bridge. + * + * @return string The Name. + */ + public function getKeywordUrlMap() + { + // Get the keyword URL map from the class constant + $keywordUrlMap = static::KEYWORDURLMAP; + + // returns the keyword URL map + return $keywordUrlMap; + } + + /** + * Returns the name for the bridge. + * + * @return string The Name. + */ + public function getName() + { + $name = static::NAME; + + $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; + + switch ($this->queriedContext) { + case 'News': + break; + case 'Events': + break; + case 'Topic': + break; + } + + return $name; + } + + /** + * Parse most used date formats + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object, manually + * fixing dates and times (set to 00:00:00.000). + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date) + { + $df = $this->parseDateTimeFromString($date); + + return date_format($df, 'U'); + } + + /** + * Extracts the images from the article. + * + * @param object $article The article object. + * @return array An array of image URLs. + */ + private function extractImages($article) + { + // Notice: We can have zero or more images (though it should mostly be 1) + $elements = $article->find('img'); + + $images = []; + + foreach ($elements as $img) { + $images[] = $img->src; + } + + return $images; + } + + // region Weekly offer + + /** + * Collects uri, timestamp, title, content and images in the product offers from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectNews($html) + { + // Check if page contains articles and split by class + $articles = $html->find('.com-news-feature-prerex') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + // Add URI + $item['uri'] = $this->extractNewsUri($article); +// echo $item['uri'] . '
'; + // Add title + $item['title'] = $this->extractNewsTitle($article); +// echo $item['title'] . '
'; + $item['enclosures'] = $this->extractImages($article); + + // Add to rss query + $this->items[] = $item; + } + } + + /** + * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectEvents($html) + { + // Check if page contains articles and split by class + $articles = $html->find('.com-news-common-prerex') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + // Add URI + $item['uri'] = $this->extractEventUri($article); + // Add title + $item['title'] = $this->extractEventTitle($article); + // Add content + $item['content'] = $this->extractEventDescription($article); + // Parse time + $newsDate = $this->extractDate($article); + // Remove prefix + $newsDate = str_replace('zveřejněno: ', '', $newsDate); + // Fix date + $item['timestamp'] = $this->fixDate($newsDate); + // Add images + $item['enclosures'] = $this->extractImages($article); + + // Add to rss query + $this->items[] = $item; + } + } + + /** + * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectTopic($html) + { + // Check if page contains articles and split by class + $articles = $html->find('.com-news-common-prerex') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + // Add URI + $item['uri'] = $this->extractEventUri($article); + // Add title + $item['title'] = $this->extractEventTitle($article); + // Add content + $item['content'] = $this->extractEventDescription($article); + // Parse time + $newsDate = $this->extractDate($article); + // Remove prefix + $newsDate = str_replace('zveřejněno: ', '', $newsDate); + // Fix date + $item['timestamp'] = $this->fixDate($newsDate); + // Add images + $item['enclosures'] = $this->extractImages($article); + + // Add to rss query + $this->items[] = $item; + } + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractEventUri($article) + { + return $article->href; + } + + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractNewsUri($article) + { + // Return URI of the article + $element = $article->find('a', 0) or + returnServerException('Anchor not found!'); + + return $element->href; + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractLetterUri($article) + { + // Return URI of the article + $element = $article->find('a.ws-btn', 0); + + // Element empty check + if ($element == null) { + return ''; + } + + return $element->href; + } + + /** + * Extracts the date of the news article. + * + * @param object $article The article object. + * @return string The date of the news article. + */ + private function extractDate($article) + { + // Check if date is set + $element = $article->find('div.com-news-common-prerex__date', 0) or + returnServerException('Date not found!'); + + return $element->plaintext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription($article) + { + // Extract description + $element = $article->find('ul.ws-product-information__piece-description', 0)->find('li', 0) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription1($article) + { + // Extract description + $element = $article->find('div.ws-product-price-validity', 0)->find('div', 0) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription2($article) + { + // Extract description + $element = $article->find('div.ws-product-price-validity', 0)->find('div', 1) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription3($article) + { + // Extract description + $element = $article->find('div.ws-product-badge-text', 0); + + // Check if element is not null + // If it is null, return empty string + // If it is not null, return the inner text + // This is to avoid errors when the element is not found + // and to ensure that the function always returns a string + if ($element != null) { + return $element->innertext; + } else { + return ''; + } + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription4($article) + { + // Extract description + $element = $article->find('div.ws-product-price-type__value', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription5($article) + { + // Extract description + $element = $article->find('div.ws-product-price-type__label', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription6($article) + { + // Extract description + $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1); + + // Element empty check + if ($element == null) { + return ''; + } + + // Not null, so we can safely access the element + $element = $element->find('div.ws-product-price-type__value', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractEventDescription($article) + { + // Extract description + $element = $article->find('.com-news-common-prerex__text', 0); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractNewsTitle($article) + { + // Extract title + $element = $article->find('img', 0) or + returnServerException('Title not found!'); + + return $element->alt; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractEventTitle($article) + { + // Extract title + $element = $article->find('div.com-news-common-prerex__right-box', 0)->find('h3', 0) + or returnServerException('Title not found!'); + + return $element->plaintext; + } + + /** + * Extracts the description of the letter article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractLetterDescription($article) + { + // Extract description + $element = $article->find('a', 0); + + return $element; + } + + /** + * It attempts to recognize the date/time format in a string and create a DateTime object. + * + * It goes through the list of defined formats and tries to apply them to the input string. + * Returns the first successfully parsed DateTime object that matches the entire string. + * + * @param string $dateString A string potentially containing a date and/or time. + * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null. + */ + private function parseDateTimeFromString(string $dateString): ?DateTime + { + // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs! + // Order may matter if the formats are ambiguous. + // It is recommended to give more specific formats (with time, full year) before more general ones. + $possibleFormats = [ + // Czech formats (day.month.year) + 'd.m.Y H:i:s', // 10.04.2025 10:57:47 + 'j.n.Y H:i:s', // 10.4.2025 10:57:47 + 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47 + 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47 + 'd.m.Y H:i', // 10.04.2025 10:57 + 'j.n.Y H:i', // 10.4.2025 10:57 + 'd. m. Y H:i', // 10. 04. 2025 10:57 + 'j. n. Y H:i', // 10. 4. 2025 10:57 + 'd.m.Y', // 10.04.2025 + 'j.n.Y', // 10.4.2025 + 'd. m. Y', // 10. 04. 2025 + 'j. n. Y', // 10. 4. 2025 + // ISO 8601 and international formats (year-month-day) + 'Y-m-d H:i:s', // 2025-04-10 10:57:47 + 'Y-m-d H:i', // 2025-04-10 10:57 + 'Y-m-d', // 2025-04-10 + 'YmdHis', // 20250410105747 + 'Ymd', // 20250410 + // American formats (month/day/year) - beware of ambiguity! + 'm/d/Y H:i:s', // 04/10/2025 10:57:47 + 'n/j/Y H:i:s', // 4/10/2025 10:57:47 + 'm/d/Y H:i', // 04/10/2025 10:57 + 'n/j/Y H:i', // 4/10/2025 10:57 + 'm/d/Y', // 04/10/2025 + 'n/j/Y', // 4/10/2025 + // Standard formats (including time zone) + DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00 + DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200 + DateTime::ISO8601, // example. 2025-04-10T105747+0200 + 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem + 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami + // You can add more formats as needed... + // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale + // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale + ]; + + // Set locale for parsing month/day names (if using F, M, l, D) + // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8'); + + foreach ($possibleFormats as $format) { + // We will try to create a DateTime object from the given format + $dateTime = DateTime::createFromFormat($format, $dateString); + + // We check that the parsing was successful AND ALSO + // that there were no errors or warnings during the parsing. + // This is important to ensure that the format matches the ENTIRE string. + if ($dateTime !== false) { + $errors = DateTime::getLastErrors(); + if (!($errors)) { + // Success! We found a valid format for the entire string. + return $dateTime; + } + } + } + + // If no format matches or parsing failed + return null; + } + + /** + * Finds values from an associative array whose keys are substrings of a given text. + * + * The function iterates through the `$map` associative array. For each key, + * it checks if that key exists as a substring within the input `$text`. + * If found, the corresponding value from the map is added to the result array. + * The search is case-sensitive and treats special characters literally. + * + * @param string $text The input text string to search within. + * @param array $map An associative array (key => value). Keys from this array will be searched for in `$text`. + * @return array An array of values whose corresponding keys were found as substrings in `$text`. Returns an empty array if no keys are found. + */ + private function findValuesByKeySubstring(string $text, array $map): array + { + $foundValues = []; // Initialize array for found values + + // Iterate through each key => value pair in the map + foreach ($map as $key => $value) { + // Use strpos(), which finds the position of the first occurrence of a substring. + // Returns the position (including 0) or `false` if the substring is not found. + // We use `!== false` to correctly handle the case where the key starts at position 0. + // Cast key to string for robustness (though array keys are usually strings or ints). + // `strpos` treats special characters in the key and text literally. + + // echo "Key: $key, Text: $text
\n"; + if (strpos($text, $key) !== false) { + // If the key was found in the text, add its corresponding value to the result array + $foundValues[] = $value; + } + } + + // Return the array of found values + return $foundValues; + } + + /** + * Removes Czech diacritics from a given string. + * + * This function replaces Czech characters with their ASCII equivalents. + * For example, 'á' becomes 'a', 'č' becomes 'c', etc. + * + * @param string $text The input string with Czech diacritics. + * @return string The string with Czech diacritics removed. + */ + private function removeCzechDiacritics(string $text): string + { + $czech = [ + 'á', 'č', 'ď', 'é', 'ě', 'í', 'ň', 'ó', 'ř', 'š', 'ť', 'ú', 'ů', 'ý', 'ž', + 'Á', 'Č', 'Ď', 'É', 'Ě', 'Í', 'Ň', 'Ó', 'Ř', 'Š', 'Ť', 'Ú', 'Ů', 'Ý', 'Ž' + ]; + $ascii = [ + 'a', 'c', 'd', 'e', 'e', 'i', 'n', 'o', 'r', 's', 't', 'u', 'u', 'y', 'z', + 'A', 'C', 'D', 'E', 'E', 'I', 'N', 'O', 'R', 'S', 'T', 'U', 'U', 'Y', 'Z' + ]; + + return str_replace($czech, $ascii, $text); + } + + // endregion + + /** + * formatTitleFromURI + */ + private function formatTitleFromURI(string $uri): string + { + // get last part of the URI + $title = basename($uri); + + // Pattern: /[^\p{L}\p{N}]+/u + // [^...] - Match any character NOT in the set + // \p{L} - Any Unicode letter (including 'é', 'ü', 'ñ', etc.) + // \p{N} - Any Unicode number (0-9 and other numeric characters) + // + - Match one or more occurrences of the preceding pattern consecutively + // /u - Unicode modifier, essential for \p{} constructs + $pattern = '/[^\p{L}\p{N}]+/u'; + $replacement = ' '; // Replace with a single space + + // lets replace + $title = preg_replace($pattern, $replacement, $title); + + // first letter to uppercase + $title = ucfirst($title); + + return trim((string)$title); + } +} \ No newline at end of file diff --git a/bridges/StavebninyIZOMATBridge.php b/bridges/StavebninyIZOMATBridge.php new file mode 100644 index 00000000000..ac9567f1268 --- /dev/null +++ b/bridges/StavebninyIZOMATBridge.php @@ -0,0 +1,729 @@ + [], + 'Blog' => [] +// 'Promo' => [] + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + defaultLinkTo($html, static::URI); + + // Router + switch ($this->queriedContext) { + case 'News': + $this->collectNews($html); + break; + case 'Blog': + $this->collectBlog($html); + break; + case 'Promo': + $this->collectPromo($html); + break; + } + } + + /** + * Returns the icon for the bridge. + * + * @return string The icon URL. + */ + public function getURI() + { + $uri = static::URI; + + // URI Router + switch ($this->queriedContext) { + case 'News': + $uri .= '/aktuality/'; + break; + case 'Blog': + $uri .= '/blog/'; + break; + case 'Promo': + $uri .= '/'; + break; + } + + return $uri; + } + + /** + * Returns the keyword URL map for the bridge. + * + * @return string The Name. + */ + public function getKeywordUrlMap() + { + // Get the keyword URL map from the class constant + $keywordUrlMap = static::KEYWORDURLMAP; + + // returns the keyword URL map + return $keywordUrlMap; + } + + /** + * Returns the name for the bridge. + * + * @return string The Name. + */ + public function getName() + { + $name = static::NAME; + + $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; + + switch ($this->queriedContext) { + case 'News': + break; + case 'Blog': + break; + case 'Promo': + break; + } + + return $name; + } + + /** + * Parse most used date formats + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object, manually + * fixing dates and times (set to 00:00:00.000). + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date) + { + echo $date; + $df = $this->parseDateTimeFromString($date); + echo $df; + exit; + + return date_format($df, 'U'); + } + + /** + * Extracts the images from the article. + * + * @param object $article The article object. + * @return array An array of image URLs. + */ + private function extractImages($article) + { + // Notice: We can have zero or more images (though it should mostly be 1) + $elements = $article->find('img'); + + $images = []; + + foreach ($elements as $img) { + $images[] = $img->src; + } + + return $images; + } + + // region Weekly offer + + /** + * Collects uri, timestamp, title, content and images in the product offers from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectNews($html) + { + // Check if page contains articles and split by class + $articles = $html->find('.col-12.col-md-6') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + if (!($this->isValidUrl($this->extractNewsUri($article)))) { + continue; + } + + // Add URI + $item['uri'] = $this->extractNewsUri($article); + + // Add title + $item['title'] = $this->extractNewsTitle($article); + + // Add content + $item['content'] = $this->extractNewsDescription($article); + + // Add images + $imgset = []; + $imgset[] = $this->extractNewsIMG($article); + $item['enclosures'] = $imgset; + $this->items[] = $item; + } + } + + /** + * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectBlog($html) + { + // Check if page contains articles and split by class + $articles = $html->find('div.col-12.col-md-6') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + // Check if page contains articles and split by class + if (!($this->isValidUrl($this->extractNewsUri($article)))) { + continue; + } + + // Add URI + $item['uri'] = $this->extractNewsUri($article); + + // Add title + $item['title'] = $this->extractBlogTitle($article); + + // Add content + $item['content'] = $this->extractBlogDescription($article); + + // Add images + $imgset = []; + $imgset[] = $this->extractNewsIMG($article); + $item['enclosures'] = $imgset; + + // Add to rss query + $this->items[] = $item; + } + } + + /** + * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectPromo($html) + { + // Check if page contains articles and split by class + $articles = $html->find('div.product-wrap') or + returnServerException('No articles found! Layout might have changed!'); + + // Articles loop + foreach ($articles as $article) { + $item = []; + + // Add URI + $item['uri'] = $this->extractNewsUri($article); + echo $item['uri'] . '
'; +/* + // Add title + $item['title'] = $this->extractEventTitle($article); + // Add content + $item['content'] = $this->extractEventDescription($article); + // Parse time + $newsDate = $this->extractDate($article); + // Remove prefix + $newsDate = str_replace("zveřejněno: ", "", $newsDate); + // Fix date + $item['timestamp'] = $this->fixDate($newsDate); + // Add images + $item['enclosures'] = $this->extractImages($article); + + // Add to rss query + $this->items[] = $item; +*/ + } + } + + /** + * Extracts the IMG of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractNewsIMG($article) + { + // Return URI of the article + $element = $article->find('.img-fluid', 0); + + // Element empty check + if ($element == null) { + return ''; + } + + return $element->getAttribute('data-src'); + } + + /** + * Extracts the IMG of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractBlogIMG($article) + { + // Return URI of the article + $element = $article->find('.img-fluid.loaded', 0); + + // Element empty check + if ($element == null) { + return ''; + } + + return $element->getAttribute('data-src'); + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractNewsUri($article) + { + // Return URI of the article + $element = $article->find('a', 0); + + // Element empty check + if ($element == null) { + return ''; + } + + return $element->href; + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractLetterUri($article) + { + // Return URI of the article + $element = $article->find('a.ws-btn', 0); + + // Element empty check + if ($element == null) { + return ''; + } + + return $element->href; + } + + /** + * Extracts the date of the news article. + * + * @param object $article The article object. + * @return string The date of the news article. + */ + private function extractDate($article) + { + // Check if date is set + $element = $article->find('div.blog-article__date', 0) or + returnServerException('Date not found!'); + + return $element->plaintext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription1($article) + { + // Extract description + $element = $article->find('div.ws-product-price-validity', 0)->find('div', 0) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractBlogDescription($article) + { + // Extract description + $element = $article->find('p.blog-article__text', 0) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription2($article) + { + // Extract description + $element = $article->find('div.ws-product-price-validity', 0)->find('div', 1) or + returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription3($article) + { + // Extract description + $element = $article->find('div.ws-product-badge-text', 0); + + // Check if element is not null + // If it is null, return empty string + // If it is not null, return the inner text + // This is to avoid errors when the element is not found + // and to ensure that the function always returns a string + if ($element != null) { + return $element->innertext; + } else { + return ''; + } + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription4($article) + { + // Extract description + $element = $article->find('div.ws-product-price-type__value', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription5($article) + { + // Extract description + $element = $article->find('div.ws-product-price-type__label', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription6($article) + { + // Extract description + $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1); + + // Element empty check + if ($element == null) { + return ''; + } + + // Not null, so we can safely access the element + $element = $element->find('div.ws-product-price-type__value', 0); + + return $element->innertext; + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription($article) + { + // Extract description + $element = $article->find('.blog-article__text.content-text.card-text.articles-desc', 0); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractBlogTitle($article) + { + // Extract title + $element = $article->find('a.blog-article__link', 0) or + returnServerException('Title not found!'); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractNewsTitle($article) + { + // Extract title + $element = $article->find('a.blog-article__link', 0) or + returnServerException('Title not found!'); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractEventTitle($article) + { + // Extract title + $element = $article->find('div.com-news-common-prerex__right-box', 0)->find('h3', 0) + or returnServerException('Title not found!'); + + return $element->plaintext; + } + + /** + * Extracts the description of the letter article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractLetterDescription($article) + { + // Extract description + $element = $article->find('a', 0); + + return $element; + } + + /** + * It attempts to recognize the date/time format in a string and create a DateTime object. + * + * It goes through the list of defined formats and tries to apply them to the input string. + * Returns the first successfully parsed DateTime object that matches the entire string. + * + * @param string $dateString A string potentially containing a date and/or time. + * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null. + */ + private function parseDateTimeFromString(string $dateString): ?DateTime + { + // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs! + // Order may matter if the formats are ambiguous. + // It is recommended to give more specific formats (with time, full year) before more general ones. + $possibleFormats = [ + // Czech formats (day.month.year) + 'd.m.Y H:i:s', // 10.04.2025 10:57:47 + 'j.n.Y H:i:s', // 10.4.2025 10:57:47 + 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47 + 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47 + 'd.m.Y H:i', // 10.04.2025 10:57 + 'j.n.Y H:i', // 10.4.2025 10:57 + 'd. m. Y H:i', // 10. 04. 2025 10:57 + 'j. n. Y H:i', // 10. 4. 2025 10:57 + 'd.m.Y', // 10.04.2025 + 'j.n.Y', // 10.4.2025 + 'd. m. Y', // 10. 04. 2025 + 'j. n. Y', // 10. 4. 2025 + // ISO 8601 and international formats (year-month-day) + 'Y-m-d H:i:s', // 2025-04-10 10:57:47 + 'Y-m-d H:i', // 2025-04-10 10:57 + 'Y-m-d', // 2025-04-10 + 'YmdHis', // 20250410105747 + 'Ymd', // 20250410 + // American formats (month/day/year) - beware of ambiguity! + 'm/d/Y H:i:s', // 04/10/2025 10:57:47 + 'n/j/Y H:i:s', // 4/10/2025 10:57:47 + 'm/d/Y H:i', // 04/10/2025 10:57 + 'n/j/Y H:i', // 4/10/2025 10:57 + 'm/d/Y', // 04/10/2025 + 'n/j/Y', // 4/10/2025 + // Standard formats (including time zone) + DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00 + DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200 + DateTime::ISO8601, // example. 2025-04-10T105747+0200 + 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem + 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami + // You can add more formats as needed... + // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale + // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale + ]; + + // Set locale for parsing month/day names (if using F, M, l, D) + // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8'); + + foreach ($possibleFormats as $format) { + // We will try to create a DateTime object from the given format + $dateTime = DateTime::createFromFormat($format, $dateString); + + // We check that the parsing was successful AND ALSO + // that there were no errors or warnings during the parsing. + // This is important to ensure that the format matches the ENTIRE string. + if ($dateTime !== false) { + $errors = DateTime::getLastErrors(); + if (!($errors)) { + // Success! We found a valid format for the entire string. + return $dateTime; + } + } + } + + // If no format matches or parsing failed + return null; + } + + /** + * Finds values from an associative array whose keys are substrings of a given text. + * + * The function iterates through the `$map` associative array. For each key, + * it checks if that key exists as a substring within the input `$text`. + * If found, the corresponding value from the map is added to the result array. + * The search is case-sensitive and treats special characters literally. + * + * @param string $text The input text string to search within. + * @param array $map An associative array (key => value). Keys from this array will be searched for in `$text`. + * @return array An array of values whose corresponding keys were found as substrings in `$text`. Returns an empty array if no keys are found. + */ + private function findValuesByKeySubstring(string $text, array $map): array + { + $foundValues = []; // Initialize array for found values + + // Iterate through each key => value pair in the map + foreach ($map as $key => $value) { + // Use strpos(), which finds the position of the first occurrence of a substring. + // Returns the position (including 0) or `false` if the substring is not found. + // We use `!== false` to correctly handle the case where the key starts at position 0. + // Cast key to string for robustness (though array keys are usually strings or ints). + // `strpos` treats special characters in the key and text literally. + + // echo "Key: $key, Text: $text
\n"; + if (strpos($text, $key) !== false) { + // If the key was found in the text, add its corresponding value to the result array + $foundValues[] = $value; + } + } + + // Return the array of found values + return $foundValues; + } + + /** + * Removes Czech diacritics from a given string. + * + * This function replaces Czech characters with their ASCII equivalents. + * For example, 'á' becomes 'a', 'č' becomes 'c', etc. + * + * @param string $text The input string with Czech diacritics. + * @return string The string with Czech diacritics removed. + */ + private function removeCzechDiacritics(string $text): string + { + $czech = [ + 'á', 'č', 'ď', 'é', 'ě', 'í', 'ň', 'ó', 'ř', 'š', 'ť', 'ú', 'ů', 'ý', 'ž', + 'Á', 'Č', 'Ď', 'É', 'Ě', 'Í', 'Ň', 'Ó', 'Ř', 'Š', 'Ť', 'Ú', 'Ů', 'Ý', 'Ž' + ]; + $ascii = [ + 'a', 'c', 'd', 'e', 'e', 'i', 'n', 'o', 'r', 's', 't', 'u', 'u', 'y', 'z', + 'A', 'C', 'D', 'E', 'E', 'I', 'N', 'O', 'R', 'S', 'T', 'U', 'U', 'Y', 'Z' + ]; + + return str_replace($czech, $ascii, $text); + } + + // endregion + + /** + * formatTitleFromURI + */ + private function formatTitleFromURI(string $uri): string + { + // get last part of the URI + $title = basename($uri); + + // Pattern: /[^\p{L}\p{N}]+/u + // [^...] - Match any character NOT in the set + // \p{L} - Any Unicode letter (including 'é', 'ü', 'ñ', etc.) + // \p{N} - Any Unicode number (0-9 and other numeric characters) + // + - Match one or more occurrences of the preceding pattern consecutively + // /u - Unicode modifier, essential for \p{} constructs + $pattern = '/[^\p{L}\p{N}]+/u'; + $replacement = ' '; // Replace with a single space + + // lets replace + $title = preg_replace($pattern, $replacement, $title); + + // first letter to uppercase + $title = ucfirst($title); + + return trim((string)$title); + } + + /** + * Checks if the given string is a valid URL. + * + * This function uses the PHP filter FILTER_VALIDATE_URL. + * It requires the presence of a scheme (e.g., "http://", "https://") for successful validation. + * + * @param string $url The string to be checked. + * @return bool Returns true if the string is a valid URL, false otherwise. + */ + private function isValidUrl(string $url): bool + { + // Use filter_var with the FILTER_VALIDATE_URL filter. + // This function returns the filtered URL on success (which is not false), + // or false on validation failure. + // Comparing with !== false ensures returning an actual boolean true/false value. + return filter_var($url, FILTER_VALIDATE_URL) !== false; + } +} \ No newline at end of file diff --git a/bridges/StavimbydlimBridge.php b/bridges/StavimbydlimBridge.php new file mode 100644 index 00000000000..9182b9b3ebd --- /dev/null +++ b/bridges/StavimbydlimBridge.php @@ -0,0 +1,288 @@ + [ + ], + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + defaultLinkTo($html, static::URI); + + // Router + switch ($this->queriedContext) { + case 'Articles': + $this->collectNews($html); + break; + } + } + + /** + * Returns the icon for the bridge. + * + * @return string The icon URL. + */ + public function getURI() + { + $uri = static::URI; + + // URI Router + switch ($this->queriedContext) { + case 'Articles': + $uri .= '/'; + break; + } + + return $uri; + } + + /** + * Returns the name for the bridge. + * + * @return string The Name. + */ + public function getName() + { + $name = static::NAME; + + $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; + + switch ($this->queriedContext) { + case 'Articles': + break; + } + + return $name; + } + + /** + * Parse most used date formats + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object, manually + * fixing dates and times (set to 00:00:00.000). + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date) + { + $df = $this->parseDateTimeFromString($date); + + return date_format($df, 'U'); + } + + /** + * Extracts the images from the article. + * + * @param object $article The article object. + * @return array An array of image URLs. + */ + private function extractImages($article) + { + // Notice: We can have zero or more images (though it should mostly be 1) + $elements = $article->find('img'); + + $images = []; + + foreach ($elements as $img) { + $images[] = $img->src; + } + + return $images; + } + + #region Articles + + /** + * Collects uri, timestamp, title, content and images in the news articles from the HTML and transforms to rss. + * + * @param object $html The HTML object. + * @return void + */ + private function collectNews($html) + { + // Check if page contains articles + $articles = $html->find('.post.type-post.status-publish.format-standard') + or returnServerException('No articles found! Layout might have changed!'); + + foreach ($articles as $article) { + $item = []; + + $item['uri'] = $this->extractNewsUri($article); + // Add title + $item['title'] = $this->extractNewsTitle($article); + // Add description + $item['content'] = $this->extractNewsDescription($article); + // Add images + $item['enclosures'] = $this->extractImages($article); + // Add timestamp + $item['timestamp'] = $this->extractNewsDate($article); + + // collect sources into rss article + $this->items[] = $item; + } + } + + /** + * Extracts the URI of the news article. + * + * @param object $article The article object. + * @return string The URI of the news article. + */ + private function extractNewsUri($article) + { + // Return URI of the article + $element = $article->find('a', 0) + or returnServerException('Anchor not found!'); + + return $element->href; + } + + /** + * Extracts the date of the news article. + * + * @param object $article The article object. + * @return string The date of the news article. + */ + private function extractNewsDate($article) + { + // Check if date is set + $element = $article->find('time.entry-date.published', 0) + or returnServerException('Date not found!'); + + // Format date + return $this->fixDate($element->plaintext); + } + + /** + * Extracts the description of the news article. + * + * @param object $article The article object. + * @return string The description of the news article. + */ + private function extractNewsDescription($article) + { + // Extract description + $element = $article->find('div.entry-summary', 0)->find('p', 0) + or returnServerException('Description not found!'); + + return $element->innertext; + } + + /** + * Extracts the title of the news article. + * + * @param object $article The article object. + * @return string The title of the news article. + */ + private function extractNewsTitle($article) + { + // Extract title + $element = $article->find('a', 0) + or returnServerException('Title not found!'); + + return $element->plaintext; + } + + /** + * It attempts to recognize the date/time format in a string and create a DateTime object. + * + * It goes through the list of defined formats and tries to apply them to the input string. + * Returns the first successfully parsed DateTime object that matches the entire string. + * + * @param string $dateString A string potentially containing a date and/or time. + * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null. + */ + private function parseDateTimeFromString(string $dateString): ?DateTime + { + // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs! + // Order may matter if the formats are ambiguous. + // It is recommended to give more specific formats (with time, full year) before more general ones. + $possibleFormats = [ + // Czech formats (day.month.year) + 'd.m.Y H:i:s', // 10.04.2025 10:57:47 + 'j.n.Y H:i:s', // 10.4.2025 10:57:47 + 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47 + 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47 + 'd.m.Y H:i', // 10.04.2025 10:57 + 'j.n.Y H:i', // 10.4.2025 10:57 + 'd. m. Y H:i', // 10. 04. 2025 10:57 + 'j. n. Y H:i', // 10. 4. 2025 10:57 + 'd.m.Y', // 10.04.2025 + 'j.n.Y', // 10.4.2025 + 'd. m. Y', // 10. 04. 2025 + 'j. n. Y', // 10. 4. 2025 + + // ISO 8601 and international formats (year-month-day) + 'Y-m-d H:i:s', // 2025-04-10 10:57:47 + 'Y-m-d H:i', // 2025-04-10 10:57 + 'Y-m-d', // 2025-04-10 + 'YmdHis', // 20250410105747 + 'Ymd', // 20250410 + + // American formats (month/day/year) - beware of ambiguity! + 'm/d/Y H:i:s', // 04/10/2025 10:57:47 + 'n/j/Y H:i:s', // 4/10/2025 10:57:47 + 'm/d/Y H:i', // 04/10/2025 10:57 + 'n/j/Y H:i', // 4/10/2025 10:57 + 'm/d/Y', // 04/10/2025 + 'n/j/Y', // 4/10/2025 + + // Standard formats (including time zone) + DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00 + DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00 + DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200 + DateTime::ISO8601, // example. 2025-04-10T105747+0200 + 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem + 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami + + // You can add more formats as needed... + // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale + // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale + ]; + + // Set locale for parsing month/day names (if using F, M, l, D) + // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8'); + + foreach ($possibleFormats as $format) { + // We will try to create a DateTime object from the given format + $dateTime = DateTime::createFromFormat($format, $dateString); + + // We check that the parsing was successful AND ALSO + // that there were no errors or warnings during the parsing. + // This is important to ensure that the format matches the ENTIRE string. + if ($dateTime !== false) { + $errors = DateTime::getLastErrors(); + if (!($errors)) { + // Success! We found a valid format for the entire string. + return $dateTime; + } + } + } + + // If no format matches or parsing failed + return null; + } + + #endregion +} \ No newline at end of file