From 4e0615281c45aa6c3416c80af7839153cbda24e8 Mon Sep 17 00:00:00 2001 From: Oliver Davies <oliver@oliverdavies.uk> Date: Tue, 8 Aug 2023 12:00:00 +0100 Subject: [PATCH 1/6] test: add AdminPageTest Test that anonymous users cannot access the administration area and users with the 'access administration pages' can. --- .../tests/src/Functional/AdminPageTest.php | 40 +++++++++++++++++++ .../tests/src/Functional/ExamplePageTest.php | 31 -------------- 2 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 web/modules/custom/example/tests/src/Functional/AdminPageTest.php delete mode 100644 web/modules/custom/example/tests/src/Functional/ExamplePageTest.php diff --git a/web/modules/custom/example/tests/src/Functional/AdminPageTest.php b/web/modules/custom/example/tests/src/Functional/AdminPageTest.php new file mode 100644 index 0000000..e5253b6 --- /dev/null +++ b/web/modules/custom/example/tests/src/Functional/AdminPageTest.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\example\Functional; + +use Drupal\Tests\BrowserTestBase; +use Symfony\Component\HttpFoundation\Response; + +final class AdminPageTest extends BrowserTestBase { + + public $defaultTheme = 'stark'; + + /** @test */ + public function the_admin_page_is_not_accessible_to_anonymous_users(): void { + $this->drupalGet(path: '/admin'); + + $assert = $this->assertSession(); + + $assert->statusCodeEquals(Response::HTTP_FORBIDDEN); + } + + /** @test */ + public function the_admin_page_is_accessible_by_admin_users(): void { + $adminUser = $this->createUser( + permissions: [ + 'access administration pages', + ], + ); + + $this->drupalLogin(account: $adminUser); + + $this->drupalGet(path: '/admin'); + + $assert = $this->assertSession(); + + $assert->statusCodeEquals(Response::HTTP_OK); + } + +} diff --git a/web/modules/custom/example/tests/src/Functional/ExamplePageTest.php b/web/modules/custom/example/tests/src/Functional/ExamplePageTest.php deleted file mode 100644 index 5ecb1b8..0000000 --- a/web/modules/custom/example/tests/src/Functional/ExamplePageTest.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -namespace Drupal\Tests\example\Functional; - -use Drupal\Tests\BrowserTestBase; -use Symfony\Component\HttpFoundation\Response; - -final class ExamplePageTest extends BrowserTestBase { - - public $defaultTheme = 'stark'; - - protected static $modules = [ - // Core. - 'node', - - // Custom. - "example" - ]; - - /** @test */ - public function should_load_the_example_page_for_anonymous_users(): void { - // Arrange. - - // Act. - $this->drupalGet('/@opdavies/drupal-module-template'); - - // Assert. - $this->assertSession()->statusCodeEquals(Response::HTTP_OK); - } - -} From c78f3d0435729c29336f2d5c7c48902311e5a513 Mon Sep 17 00:00:00 2001 From: Oliver Davies <oliver@oliverdavies.uk> Date: Tue, 8 Aug 2023 12:00:00 +0100 Subject: [PATCH 2/6] test: add FrontPageTest --- .../tests/src/Functional/FrontPageTest.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 web/modules/custom/example/tests/src/Functional/FrontPageTest.php diff --git a/web/modules/custom/example/tests/src/Functional/FrontPageTest.php b/web/modules/custom/example/tests/src/Functional/FrontPageTest.php new file mode 100644 index 0000000..24ae108 --- /dev/null +++ b/web/modules/custom/example/tests/src/Functional/FrontPageTest.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\example\Functional; + +use Drupal\Tests\BrowserTestBase; +use Symfony\Component\HttpFoundation\Response; + +final class FrontPageTest extends BrowserTestBase { + + public $defaultTheme = 'stark'; + + protected static $modules = ['node', 'views']; + + /** @test */ + public function the_front_page_loads_for_anonymous_users(): void { + $this->config('system.site') + ->set('page.front', '/node') + ->save(TRUE); + + $this->drupalGet('<front>'); + + $assert = $this->assertSession(); + + $assert->statusCodeEquals(Response::HTTP_OK); + $assert->pageTextContains('No front page content has been created yet.'); + } + +} From 09d4c662bebe35f6e09bb7aa76d2948bb590fc2f Mon Sep 17 00:00:00 2001 From: Oliver Davies <oliver@oliverdavies.uk> Date: Tue, 8 Aug 2023 12:00:00 +0100 Subject: [PATCH 3/6] feat(blog): add the initial blog page --- .../custom/my_module/my_module.info.yml | 3 ++ .../custom/my_module/my_module.routing.yml | 7 ++++ .../src/Controller/BlogPageController.php | 22 +++++++++++++ .../tests/src/Functional/BlogPageTest.php | 33 +++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 web/modules/custom/my_module/my_module.info.yml create mode 100644 web/modules/custom/my_module/my_module.routing.yml create mode 100644 web/modules/custom/my_module/src/Controller/BlogPageController.php create mode 100644 web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php diff --git a/web/modules/custom/my_module/my_module.info.yml b/web/modules/custom/my_module/my_module.info.yml new file mode 100644 index 0000000..60e27aa --- /dev/null +++ b/web/modules/custom/my_module/my_module.info.yml @@ -0,0 +1,3 @@ +name: My Module +type: module +core_version_requirement: ^10 diff --git a/web/modules/custom/my_module/my_module.routing.yml b/web/modules/custom/my_module/my_module.routing.yml new file mode 100644 index 0000000..9683277 --- /dev/null +++ b/web/modules/custom/my_module/my_module.routing.yml @@ -0,0 +1,7 @@ +blog.page: + path: /blog + defaults: + _controller: Drupal\my_module\Controller\BlogPageController + _title: Blog + requirements: + _permission: access content diff --git a/web/modules/custom/my_module/src/Controller/BlogPageController.php b/web/modules/custom/my_module/src/Controller/BlogPageController.php new file mode 100644 index 0000000..ae4d46a --- /dev/null +++ b/web/modules/custom/my_module/src/Controller/BlogPageController.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\my_module\Controller; + +use Drupal\Core\StringTranslation\StringTranslationTrait; + +final class BlogPageController { + + use StringTranslationTrait; + + /** + * @return array<string, mixed> + */ + public function __invoke(): array { + return [ + '#markup' => $this->t('Welcome to my blog!'), + ]; + } + +} diff --git a/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php b/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php new file mode 100644 index 0000000..1405f0e --- /dev/null +++ b/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\my_module\Functional; + +use Drupal\Tests\BrowserTestBase; +use Symfony\Component\HttpFoundation\Response; + +final class BlogPageTest extends BrowserTestBase { + + public $defaultTheme = 'stark'; + + protected static $modules = [ + // Core. + 'node', + + // Custom. + 'my_module', + ]; + + /** @test */ + public function the_blog_page_loads_for_anonymous_users_and_contains_the_right_text(): void { + $this->drupalGet('/blog'); + + $assert = $this->assertSession(); + + $assert->statusCodeEquals(Response::HTTP_OK); + $assert->responseContains('<h1>Blog</h1>'); + $assert->pageTextContains('Welcome to my blog!'); + } + +} 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 4/6] 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); + } + +} From 7e3324b77fb1d5f56138bb6081685db766888a16 Mon Sep 17 00:00:00 2001 From: Oliver Davies <oliver@oliverdavies.uk> Date: Tue, 8 Aug 2023 12:00:00 +0100 Subject: [PATCH 5/6] feat(blog): use the article repository Use the `ArticleRepository` within `BlogPageController` to load and retrieve article nodes. --- .../custom/my_module/my_module.services.yml | 3 ++ .../src/Controller/BlogPageController.php | 31 ++++++++++++++++++- .../tests/src/Functional/BlogPageTest.php | 17 ++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/web/modules/custom/my_module/my_module.services.yml b/web/modules/custom/my_module/my_module.services.yml index 23aee82..a97d210 100644 --- a/web/modules/custom/my_module/my_module.services.yml +++ b/web/modules/custom/my_module/my_module.services.yml @@ -1,3 +1,6 @@ services: + Drupal\my_module\Controller\BlogPageController: + autowire: true + Drupal\my_module\Repository\ArticleRepository: autowire: true diff --git a/web/modules/custom/my_module/src/Controller/BlogPageController.php b/web/modules/custom/my_module/src/Controller/BlogPageController.php index ae4d46a..59b6599 100644 --- a/web/modules/custom/my_module/src/Controller/BlogPageController.php +++ b/web/modules/custom/my_module/src/Controller/BlogPageController.php @@ -4,18 +4,47 @@ declare(strict_types=1); namespace Drupal\my_module\Controller; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityViewBuilderInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\my_module\Repository\ArticleRepository; final class BlogPageController { use StringTranslationTrait; + private EntityViewBuilderInterface $nodeViewBuilder; + + public function __construct( + private RendererInterface $renderer, + EntityTypeManagerInterface $entityTypeManager, + private ArticleRepository $articleRepository, + ) { + $this->nodeViewBuilder = $entityTypeManager->getViewBuilder(entity_type_id: 'node'); + } + /** * @return array<string, mixed> */ public function __invoke(): array { + $articles = $this->articleRepository->getAll(); + + if ($articles === []) { + return ['#markup' => $this->t('Welcome to my blog!')]; + } + + $build = []; + + foreach ($articles as $article) { + $build[] = $this->nodeViewBuilder->view( + entity: $article, + view_mode: 'teaser', + ); + } + return [ - '#markup' => $this->t('Welcome to my blog!'), + '#markup' => $this->renderer->render($build), ]; } diff --git a/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php b/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php index 1405f0e..f1c9764 100644 --- a/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php +++ b/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php @@ -30,4 +30,21 @@ final class BlogPageTest extends BrowserTestBase { $assert->pageTextContains('Welcome to my blog!'); } + /** @test */ + public function it_shows_articles(): void { + $this->createContentType(['type' => 'article']); + + $this->createNode([ + 'title' => 'This is a test article', + 'type' => 'article', + ]); + + $this->drupalGet('/blog'); + + $assert = $this->assertSession(); + + $assert->statusCodeEquals(Response::HTTP_OK); + $assert->pageTextContains('This is a test article'); + } + } From a95adfcc3d63030f8e9cbdcf8da2a282f3d2cbf3 Mon Sep 17 00:00:00 2001 From: Oliver Davies <oliver@oliverdavies.uk> Date: Tue, 8 Aug 2023 12:00:00 +0100 Subject: [PATCH 6/6] build(deps): add phpstan-strict-rules --- composer.json | 3 ++- composer.lock | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 3e854fd..aba8b2e 100644 --- a/composer.json +++ b/composer.json @@ -109,6 +109,7 @@ "require-dev": { "drupal/core-dev": "^10", "fenetikm/autoload-drupal": "dev-autoload-tests", - "phpspec/prophecy-phpunit": "^2" + "phpspec/prophecy-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^1.5" } } diff --git a/composer.lock b/composer.lock index 66ff841..6abbdd8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b7efed8e16e9b99d9aba5afbdb499669", + "content-hash": "2d3f22a75cc98e2996a5d6b9f3934faf", "packages": [ { "name": "asm89/stack-cors", @@ -8218,6 +8218,55 @@ }, "time": "2023-05-26T11:05:59+00:00" }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "b21c03d4f6f3a446e4311155f4be9d65048218e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/b21c03d4f6f3a446e4311155f4be9d65048218e6", + "reference": "b21c03d4f6f3a446e4311155f4be9d65048218e6", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.1" + }, + "time": "2023-03-29T14:47:40+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.27",