Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
a95adfcc3d | |||
7e3324b77f | |||
f09464f9f7 | |||
09d4c662be | |||
c78f3d0435 | |||
4e0615281c |
12 changed files with 374 additions and 33 deletions
composer.jsoncomposer.lock
web/modules/custom
example/tests/src/Functional
my_module
|
@ -109,6 +109,7 @@
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"drupal/core-dev": "^10",
|
"drupal/core-dev": "^10",
|
||||||
"fenetikm/autoload-drupal": "dev-autoload-tests",
|
"fenetikm/autoload-drupal": "dev-autoload-tests",
|
||||||
"phpspec/prophecy-phpunit": "^2"
|
"phpspec/prophecy-phpunit": "^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
51
composer.lock
generated
51
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "b7efed8e16e9b99d9aba5afbdb499669",
|
"content-hash": "2d3f22a75cc98e2996a5d6b9f3934faf",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "asm89/stack-cors",
|
"name": "asm89/stack-cors",
|
||||||
|
@ -8218,6 +8218,55 @@
|
||||||
},
|
},
|
||||||
"time": "2023-05-26T11:05:59+00:00"
|
"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",
|
"name": "phpunit/php-code-coverage",
|
||||||
"version": "9.2.27",
|
"version": "9.2.27",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
3
web/modules/custom/my_module/my_module.info.yml
Normal file
3
web/modules/custom/my_module/my_module.info.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
name: My Module
|
||||||
|
type: module
|
||||||
|
core_version_requirement: ^10
|
7
web/modules/custom/my_module/my_module.routing.yml
Normal file
7
web/modules/custom/my_module/my_module.routing.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
blog.page:
|
||||||
|
path: /blog
|
||||||
|
defaults:
|
||||||
|
_controller: Drupal\my_module\Controller\BlogPageController
|
||||||
|
_title: Blog
|
||||||
|
requirements:
|
||||||
|
_permission: access content
|
6
web/modules/custom/my_module/my_module.services.yml
Normal file
6
web/modules/custom/my_module/my_module.services.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
services:
|
||||||
|
Drupal\my_module\Controller\BlogPageController:
|
||||||
|
autowire: true
|
||||||
|
|
||||||
|
Drupal\my_module\Repository\ArticleRepository:
|
||||||
|
autowire: true
|
|
@ -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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in a new issue