From f09464f9f7d75b0e47403a8c2766bc805555f2eb Mon Sep 17 00:00:00 2001
From: Oliver Davies <oliver@oliverdavies.uk>
Date: Tue, 8 Aug 2023 12:00:00 +0100
Subject: [PATCH] feat(blog): add article repository

---
 .../custom/my_module/my_module.services.yml   |  3 +
 .../src/Repository/ArticleRepository.php      | 37 +++++++
 .../Repository/ArticleRepositoryTest.php      | 98 +++++++++++++++++++
 3 files changed, 138 insertions(+)
 create mode 100644 web/modules/custom/my_module/my_module.services.yml
 create mode 100644 web/modules/custom/my_module/src/Repository/ArticleRepository.php
 create mode 100644 web/modules/custom/my_module/tests/src/Kernel/Repository/ArticleRepositoryTest.php

diff --git a/web/modules/custom/my_module/my_module.services.yml b/web/modules/custom/my_module/my_module.services.yml
new file mode 100644
index 0000000..23aee82
--- /dev/null
+++ b/web/modules/custom/my_module/my_module.services.yml
@@ -0,0 +1,3 @@
+services:
+  Drupal\my_module\Repository\ArticleRepository:
+    autowire: true
diff --git a/web/modules/custom/my_module/src/Repository/ArticleRepository.php b/web/modules/custom/my_module/src/Repository/ArticleRepository.php
new file mode 100644
index 0000000..4cacbb0
--- /dev/null
+++ b/web/modules/custom/my_module/src/Repository/ArticleRepository.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\my_module\Repository;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\node\NodeInterface;
+
+final class ArticleRepository {
+
+  private EntityStorageInterface $nodeStorage;
+
+  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
+    $this->nodeStorage = $entityTypeManager->getStorage(entity_type_id: 'node');
+  }
+
+  /**
+   * @return array<int, NodeInterface>
+   */
+  public function getAll(): array {
+    /** @var array<int, NodeInterface> */
+    $articles = $this->nodeStorage->loadByProperties([
+      'status' => NodeInterface::PUBLISHED,
+      'type' => 'article',
+    ]);
+
+    // Sort the articles by their created time.
+    uasort($articles, function (NodeInterface $a, NodeInterface $b): int {
+      return $a->getCreatedTime() < $b->getCreatedTime() ? 1 : -1;
+    });
+
+    return $articles;
+  }
+
+}
diff --git a/web/modules/custom/my_module/tests/src/Kernel/Repository/ArticleRepositoryTest.php b/web/modules/custom/my_module/tests/src/Kernel/Repository/ArticleRepositoryTest.php
new file mode 100644
index 0000000..91799e3
--- /dev/null
+++ b/web/modules/custom/my_module/tests/src/Kernel/Repository/ArticleRepositoryTest.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\my_module\Kernel\Repository;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\my_module\Repository\ArticleRepository;
+use Drupal\node\Entity\Node;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+final class ArticleRepositoryTest extends EntityKernelTestBase {
+
+  use NodeCreationTrait;
+
+  protected $strictConfigSchema = FALSE;
+
+  public static $modules = [
+    'node',
+    'my_module',
+  ];
+
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installSchema(module: 'node', tables: ['node_access']);
+
+    $this->installConfig([
+      'filter',
+    ]);
+  }
+
+  /** @test */
+  public function nodes_that_are_not_articles_are_not_returned() {
+    $this->createNode(['type' => 'article'])->save();
+    $this->createNode(['type' => 'page'])->save();
+    $this->createNode(['type' => 'article'])->save();
+    $this->createNode(['type' => 'page'])->save();
+    $this->createNode(['type' => 'article'])->save();
+
+    $this->assertCount(5, Node::loadMultiple());
+
+    $repository = $this->container->get(ArticleRepository::class);
+    $articles = $repository->getAll();
+
+    $this->assertCount(3, $articles);
+  }
+
+  /** @test */
+  public function only_published_articles_are_returned() {
+    $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+
+    $repository = $this->container->get(ArticleRepository::class);
+    $articles = $repository->getAll();
+
+    $this->assertCount(3, $articles);
+  }
+
+  /** @test */
+  public function nodes_are_ordered_by_date_and_returned_newest_first() {
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-2 days'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 week'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 hour'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 year'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 month'))->getTimestamp(),
+    ]);
+
+    $repository = $this->container->get(ArticleRepository::class);
+    $nodes = $repository->getAll();
+    $nodeIds = array_keys($nodes);
+
+    $this->assertSame([3, 1, 2, 5, 4], $nodeIds);
+  }
+
+}