Compare commits

...

6 commits
start ... main

Author SHA1 Message Date
a95adfcc3d build(deps): add phpstan-strict-rules 2023-08-08 13:41:11 +01:00
7e3324b77f feat(blog): use the article repository
Use the `ArticleRepository` within `BlogPageController` to load and
retrieve article nodes.
2023-08-08 13:41:11 +01:00
f09464f9f7 feat(blog): add article repository 2023-08-08 13:41:11 +01:00
09d4c662be feat(blog): add the initial blog page 2023-08-08 13:41:11 +01:00
c78f3d0435 test: add FrontPageTest 2023-08-08 13:41:11 +01:00
4e0615281c test: add AdminPageTest
Test that anonymous users cannot access the administration area and
users with the 'access administration pages' can.
2023-08-08 13:41:11 +01:00
12 changed files with 374 additions and 33 deletions

View file

@ -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"
}
}

51
composer.lock generated
View file

@ -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",

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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.');
}
}

View file

@ -0,0 +1,3 @@
name: My Module
type: module
core_version_requirement: ^10

View file

@ -0,0 +1,7 @@
blog.page:
path: /blog
defaults:
_controller: Drupal\my_module\Controller\BlogPageController
_title: Blog
requirements:
_permission: access content

View file

@ -0,0 +1,6 @@
services:
Drupal\my_module\Controller\BlogPageController:
autowire: true
Drupal\my_module\Repository\ArticleRepository:
autowire: true

View file

@ -0,0 +1,51 @@
<?php
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->renderer->render($build),
];
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,50 @@
<?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!');
}
/** @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');
}
}

View file

@ -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);
}
}