diff --git a/migrations/Version20231119134901.php b/migrations/Version20231119134901.php
new file mode 100644
index 0000000..f079778
--- /dev/null
+++ b/migrations/Version20231119134901.php
@@ -0,0 +1,29 @@
+addSql("INSERT INTO sources (id, name) VALUES ('eefdac30-454b-4455-99e8-cfe3a2bb6111','symfony.com')");
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql("DELETE FROM sources WHERE id='eefdac30-454b-4455-99e8-cfe3a2bb6111'");
+ }
+}
diff --git a/src/Feed/Application/Service/FeedProvider/SymfonyFeedProvider.php b/src/Feed/Application/Service/FeedProvider/SymfonyFeedProvider.php
new file mode 100644
index 0000000..d8611fe
--- /dev/null
+++ b/src/Feed/Application/Service/FeedProvider/SymfonyFeedProvider.php
@@ -0,0 +1,63 @@
+client->request('GET', 'https://feeds.feedburner.com/symfony/blog');
+
+ $feed = new DOMDocument();
+
+ $feed->loadXML($response->getContent());
+
+ $newsItems = [];
+
+ foreach ($feed->getElementsByTagName('item') as $entry) {
+ try {
+ $title = DOM::getString($entry, 'title');
+ $summary = trim(str_replace(["\r", "\n"], ' ', DOM::getString($entry, 'description')));
+ $updated = DOM::getDateTime($entry, 'pubDate');
+ $link = DOM::getString($entry, 'link');
+ } catch (OutOfBoundsException | LogicException $exception) {
+ $this->logger->warning('[{source}] Failed to parse entry: {reason}.', [
+ 'source' => self::getSource(),
+ 'reason' => $exception->getMessage(),
+ ]);
+
+ continue;
+ }
+
+ $feedItems = new FeedItem(
+ $title,
+ $summary,
+ $link,
+ $updated,
+ self::getSource()
+ );
+
+ $newsItems[] = $feedItems;
+ }
+
+ return $newsItems;
+ }
+}
diff --git a/tests/Unit/Feed/Application/Service/FeedProvider/SymfonyFeedProviderTest.php b/tests/Unit/Feed/Application/Service/FeedProvider/SymfonyFeedProviderTest.php
new file mode 100644
index 0000000..393fc1c
--- /dev/null
+++ b/tests/Unit/Feed/Application/Service/FeedProvider/SymfonyFeedProviderTest.php
@@ -0,0 +1,208 @@
+
+
+
+ Symfony Blog
+
+ https://symfony.com/blog/
+ Most recent posts published on the Symfony project blog
+ Sun, 19 Nov 2023 21:44:09 +0100
+ Sun, 19 Nov 2023 08:58:00 +0100
+ en
+ -
+
+ https://symfony.com/blog/symfonycon-brussels-2023-a-memorable-game-ux-with-livecomponents?utm_source=Symfony%20Blog%20Feed&utm_medium=feed
+
+SymfonyCon Brussels 2023 is just around the corner and will start on:
+
+
+December 5-6: Workshop days. It is possible to attend 1 two-day training or 2 one-day trainings!
+December 7-8: Conference days with 3 parallels tracks and 1 unconference track…
+
+
+
+
+SymfonyCon Brussels 2023 is just around the corner and will start on:
+
+
+- December 5-6: Workshop days. It is possible to attend 1 two-day training or 2 one-day trainings!
+- December 7-8: Conference days with 3 parallels tracks and 1 unconference track in English.
+
+
+🎤 New speaker announcement!
+
+We are thrilled to announce you the next speaker: Simon André, Future developer, SensioLabs will present the topic "A Memorable Game UX with LiveComponents":
+
+"Symfony UX empowers developers to create rich, dynamic web interfaces that bridge the gap between backend logic and a memorable user experience.
+
+
+ ]]>
+ https://symfony.com/blog/symfonycon-brussels-2023-a-memorable-game-ux-with-livecomponents?utm_source=Symfony%20Blog%20Feed&utm_medium=feed
+
+ Sat, 18 Nov 2023 15:30:00 +0100
+ https://symfony.com/blog/symfonycon-brussels-2023-a-memorable-game-ux-with-livecomponents?utm_source=Symfony%20Blog%20Feed&utm_medium=feed#comments-list
+
+ -
+
+ https://symfony.com/blog/new-in-symfony-6-4-mailer-translation-notifier-webhook-and-remoteevent-integrations?utm_source=Symfony%20Blog%20Feed&utm_medium=feed
+ Symfony provides out-of-the-box compatibility with tens of third-party services
+for Mailer, Translation, Notifier, Webhook and RemoteEvent
+components. In Symfony 6.4 we're introducing even more integrations:
+
+Mailer Integrations
+
+ Scaleway transaction…
+ Symfony provides out-of-the-box compatibility with tens of third-party services
+for Mailer, Translation, Notifier, Webhook and RemoteEvent
+components. In Symfony 6.4 we're introducing even more integrations:
+]]]>
+ https://symfony.com/blog/new-in-symfony-6-4-autowirelocator-and-autowireiterator-attributes?utm_source=Symfony%20Blog%20Feed&utm_medium=feed
+
+ Thu, 16 Nov 2023 13:10:00 +0100
+ https://symfony.com/blog/new-in-symfony-6-4-autowirelocator-and-autowireiterator-attributes?utm_source=Symfony%20Blog%20Feed&utm_medium=feed#comments-list
+
+
+
+XML;
+
+ private const MALFORMED_XML = <<
+
+
+ Symfony Blog
+
+ https://symfony.com/blog/
+ Most recent posts published on the Symfony project blog
+ Sun, 19 Nov 2023 21:44:09 +0100
+ Sun, 19 Nov 2023 08:58:00 +0100
+ en
+ -
+
+ https://symfony.com/blog/symfonycon-brussels-2023-a-memorable-game-ux-with-livecomponents?utm_source=Symfony%20Blog%20Feed&utm_medium=feed
+ https://symfony.com/blog/symfonycon-brussels-2023-a-memorable-game-ux-with-livecomponents?utm_source=Symfony%20Blog%20Feed&utm_medium=feed
+
+ Sat, 18 Nov 2023 15:30:00 +0100
+ https://symfony.com/blog/symfonycon-brussels-2023-a-memorable-game-ux-with-livecomponents?utm_source=Symfony%20Blog%20Feed&utm_medium=feed#comments-list
+
+
+
+XML;
+
+ protected function setUp(): void
+ {
+ $this->logger = new InMemoryLogger();
+
+ parent::setUp();
+ }
+
+ /**
+ * @test
+ */
+ public function it_should_fetch_the_external_feed(): void
+ {
+ // Arrange
+ $client = new MockHttpClient([
+ new MockResponse(self::EXTERNAL_FEED)
+ ]);
+
+ $feedProvider = new SymfonyFeedProvider($client, $this->logger);
+
+ // Act
+ $feedItems = $feedProvider->fetchFeedItems();
+
+ // Assert
+ self::assertCount(2, $feedItems);
+
+ self::assertSame("SymfonyCon Brussels 2023: A Memorable Game UX with LiveComponents", $feedItems[0]->title);
+ self::assertEquals(
+ "SymfonyCon Brussels 2023 is just around the corner and will start on: December 5-6: Workshop days. It is possible to attend 1 two-day training or 2 one-day trainings! December 7-8: Conference days with 3 parallels tracks and 1 unconference track…",
+ $feedItems[0]->summary
+ );
+ self::assertSame(
+ "https://symfony.com/blog/symfonycon-brussels-2023-a-memorable-game-ux-with-livecomponents?utm_source=Symfony%20Blog%20Feed&utm_medium=feed",
+ $feedItems[0]->url
+ );
+ self::assertEquals(\DateTime::createFromFormat('Y-m-d H:i', '2023-11-18 14:30'), $feedItems[0]->updated);
+ self::assertSame("symfony.com", $feedItems[0]->source);
+
+ self::assertSame("New in Symfony 6.4: Mailer, Translation, Notifier, Webhook and RemoteEvent Integrations", $feedItems[1]->title);
+ self::assertEquals(
+ "Symfony provides out-of-the-box compatibility with tens of third-party services for Mailer, Translation, Notifier, Webhook and RemoteEvent components. In Symfony 6.4 we're introducing even more integrations: Mailer Integrations Scaleway transaction…",
+ $feedItems[1]->summary
+ );
+ self::assertSame(
+ "https://symfony.com/blog/new-in-symfony-6-4-mailer-translation-notifier-webhook-and-remoteevent-integrations?utm_source=Symfony%20Blog%20Feed&utm_medium=feed",
+ $feedItems[1]->url
+ );
+ self::assertEquals(\DateTime::createFromFormat('Y-m-d H:i', '2023-11-16 12:10'), $feedItems[1]->updated);
+ self::assertSame("symfony.com", $feedItems[1]->source);
+ }
+
+ /**
+ * @test
+ */
+ public function it_should_get_the_source_name(): void
+ {
+ // Assert
+ self::assertSame('symfony.com', SymfonyFeedProvider::getSource());
+ }
+
+ /**
+ * @test
+ */
+ public function it_should_log_if_entry_cant_be_parsed(): void
+ {
+ // Arrange
+ $client = new MockHttpClient([
+ new MockResponse(self::MALFORMED_XML)
+ ]);
+
+ $feedProvider = new SymfonyFeedProvider($client, $this->logger);
+
+ // Act
+ $feedItems = $feedProvider->fetchFeedItems();
+
+ // Assert
+ self::assertCount(1, $this->logger->recordedLogs);
+
+ $log = $this->logger->recordedLogs[0];
+
+ self::assertSame(LogLevel::WARNING, $log->level);
+ self::assertSame('[{source}] Failed to parse entry: {reason}.', $log->message);
+ self::assertSame([
+ 'source' => SymfonyFeedProvider::getSource(),
+ 'reason' => "`description` does not exist in DOMElement",
+ ], $log->context);
+ }
+}