Rename custom module directories

- Rename `opdavies_blog` to `blog`.
- Rename `opdavies_blog_test` to `blog_test`.
- Rename `opdavies_talks` to `talks`.
- Rename `opdavies_talks_test` to `talks_test`.

The files within the directories haven't changed, so there is no
breaking change caused by renaming the directories.

 Please enter the commit message for your changes. Lines starting
This commit is contained in:
Oliver Davies 2020-09-04 21:19:17 +01:00
parent d7d5a6c8a3
commit cbe60209e6
59 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,6 @@
opdavies_blog.settings:
type: config_object
label: 'Blog module configuration'
mapping:
zapier_post_tweet_url:
type: string

View file

@ -0,0 +1,11 @@
services:
Drupal\opdavies_blog\Command\ExportBodyValuesForThemePurgingCommand:
arguments: ['@database']
autowire: true
tags:
- { name: drush.command }
Drupal\opdavies_blog\Command\FormatTagNamesCommand:
autowire: true
tags:
- { name: drush.command }

View file

@ -0,0 +1,20 @@
<?php
/**
* @file
* Entity type build hooks.
*/
declare(strict_types=1);
use Drupal\discoverable_entity_bundle_classes\Storage\Node\NodeStorage;
/**
* Implements hook_entity_type_build().
*/
function opdavies_blog_entity_type_build(array &$entityTypes): void {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entityTypes */
if (isset($entityTypes['node'])) {
$entityTypes['node']->setStorageClass(NodeStorage::class);
}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Node links alter hooks.
*/
declare(strict_types=1);
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
/**
* Implements hook_node_links_alter().
*/
function opdavies_blog_node_links_alter(array &$links, NodeInterface $node): void {
if (!method_exists($node, 'getExternalLink')) {
return;
}
if ($link = $node->getExternalLink()) {
$links['node']['#links']['node-readmore']['url'] = Url::fromUri($link['uri']);
$links['node']['#links']['node-readmore']['title'] = t('Read more<span class="visually-hidden"> about @title</span> (<span class="visually-hidden">on </span>@domain)', [
'@domain' => $link['title'],
'@title' => $node->label(),
]);
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Block preprocess hooks.
*/
declare(strict_types=1);
/**
* Implements hook_preprocess_HOOK().
*/
function opdavies_blog_preprocess_block(array &$variables): void {
// Add the 'markup' class to blocks.
if (in_array($variables['plugin_id'], ['views_block:featured_blog_posts-block_1'])) {
$variables['attributes']['class'][] = 'markup';
}
}

View file

@ -0,0 +1,21 @@
<?php
/**
* @file
* Node preprocess functions.
*/
declare(strict_types=1);
/**
* Implements hook_preprocess_HOOK().
*/
function opdavies_blog_preprocess_node(array &$variables): void {
if (!method_exists($variables['node'], 'getExternalLink')) {
return;
}
if ($link = $variables['node']->getExternalLink()) {
$variables['url'] = $link['uri'];
}
}

View file

@ -0,0 +1,9 @@
name: Oliver Davies blog
type: module
core_version_requirement: ^8 || ^9
package: Custom
dependencies:
- drupal:node
- discoverable_entity_bundle_classes:discoverable_entity_bundle_classes
- hook_event_dispatcher:hook_event_dispatcher
- paragraphs:paragraphs

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
use Drupal\opdavies_blog\Repository\PostRepository;
/**
* Mark existing blog posts as sent to social media.
*/
function opdavies_blog_update_8001(): void {
$posts = \Drupal::service(PostRepository::class)->getAll();
foreach ($posts as $post) {
$post->set('field_sent_to_social_media', TRUE);
$post->save();
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Custom module.
*/
declare(strict_types=1);
use Symfony\Component\Finder\Finder;
$finder = Finder::create()
->in(__DIR__ . DIRECTORY_SEPARATOR . 'hooks')
->name('/.[php|inc]$/');
foreach ($finder as $file) {
include $file->getPathname();
}

View file

@ -0,0 +1,12 @@
services:
Drupal\opdavies_blog\EventSubscriber\PushBlogPostToSocialMedia:
autowire: true
tags:
- { name: event_subscriber }
Drupal\opdavies_blog\EventSubscriber\ReorderBlogTags:
tags:
- { name: event_subscriber }
Drupal\opdavies_blog\Repository\PostRepository:
autowire: true

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_blog\Command;
use Drupal\Core\Database\Connection;
use Illuminate\Support\Collection;
final class ExportBodyValuesForThemePurgingCommand {
private static array $tableNames = [
'block_content__body',
'node__body',
];
private string $filename = 'body-field-values.txt';
private Connection $database;
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* Drush command to export body field values into a file.
*
* @command opdavies:export-body-values-for-theme-purging
*/
public function handle(): void {
$values = Collection::make(self::$tableNames)
->flatMap(fn(string $tableName) => $this->getValuesFromTable($tableName))
->implode(PHP_EOL);
file_put_contents($this->getFilePath(), $values);
}
private function getFilePath(): string {
return drupal_get_path('theme', 'opdavies') . DIRECTORY_SEPARATOR . $this->filename;
}
private function getValuesFromTable(string $tableName): array {
return $this->database->select($tableName)
->fields($tableName, ['body_value'])
->execute()
->fetchCol();
}
}

View file

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_blog\Command;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drush\Commands\DrushCommands;
final class FormatTagNamesCommand extends DrushCommands {
/**
* A lookup table for new name overrides.
*
* @var array
* An associative array, keyed by the original tag name. The value is either
* an overridden tag name or FALSE if the tag name is not to be changed.
*/
private static $tagNames = [
'accessible-bristol' => 'Accessible Bristol',
'admin:hover' => FALSE,
'aria' => 'ARIA',
'cck' => 'CCK',
'centos' => 'CentOS',
'css' => 'CSS',
'dcbristol' => FALSE,
'ddev' => 'DDEV',
'drupal-association' => 'Drupal Association',
'drupal-bristol' => 'Drupal Bristol',
'drupal-commerce' => 'Drupal Commerce',
'drupal-planet' => 'Drupal Planet',
'drupal-vm' => 'Drupal VM',
'drupal-vm-generator' => 'Drupal VM Generator',
'drupalcamp' => 'DrupalCamp',
'drupalcamp-bristol' => 'DrupalCamp Bristol',
'drupalcamp-london' => 'DrupalCamp London',
'drupalcamp-north' => 'DrupalCamp North',
'drupalcon' => 'DrupalCon',
'entity-api' => 'Entity API',
'fancy-slide' => 'Fancy Slide',
'field-collection' => 'Field Collection',
'filefield' => 'FileField',
'form-api' => 'Form API',
'git-flow' => 'Git Flow',
'github' => 'GitHub',
'github-actions' => 'GitHub Actions',
'illuminate-collections' => 'Illuminate Collections',
'image-caption' => 'Image Caption',
'imagecache' => 'ImageCache',
'imagefield' => 'ImageField',
'imagefield-import' => 'ImageField Import',
'javascript' => 'JavaScript',
'laravel-collections' => 'Laravel Collections',
'laravel-mix' => 'Laravel Mix',
'linux-journal' => 'Linux Journal',
'mac-os-x' => 'macOS',
'mamp' => 'MAMP',
'mod_rewrite' => FALSE,
'npm' => FALSE,
'oliverdavies.co.uk' => FALSE,
'php' => 'PHP',
'php-south-wales' => 'PHP South Wales',
'phpstorm' => 'PhpStorm',
'phpsw' => 'PHPSW',
'phpunit' => 'PHPUnit',
'postcss' => 'PostCSS',
'psr' => 'PSR',
'regular-expression' => 'Regular expressions',
'sequel-pro' => 'Sequel Pro',
'settings.php' => FALSE,
'sql' => 'SQL',
'ssh' => 'SSH',
'sublime-text' => 'Sublime Text',
'svn' => 'SVN',
'swdug' => 'SWDUG',
'symfonylive' => 'SymfonyLive',
'tailwind-css' => 'Tailwind CSS',
'tdd' => 'TDD',
'test-driven-drupal' => 'Test Driven Drupal',
'views-attach' => 'Views Attach',
'virtualbox' => 'VirtualBox',
'vuejs' => 'VueJS',
'virtualhostx' => 'VirtualHostX',
];
/**
* The taxonomy term storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
private $termStorage;
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
parent::__construct();
$this->termStorage = $entityTypeManager->getStorage('taxonomy_term');
}
/**
* Drush command for updating legacy tag names.
*
* @command opdavies:update-tag-names
*/
public function updateTagNames(): void {
foreach ($this->getTags() as $tag) {
$name = $tag->label();
$newName = $this->getNewTagName($name);
if ($newName === NULL) {
$this->writeln(sprintf('Skipping %s.', $name));
continue;
}
$this->writeln(sprintf('Updating %s to %s.', $name, $newName));
$tag->name = $newName;
$tag->save();
}
}
private function getTags(): array {
return $this->termStorage->loadByProperties([
'vid' => 'tags',
]);
}
private function getNewTagName(string $tagName): ?string {
if (!array_key_exists($tagName, static::$tagNames)) {
return str_replace('-', ' ', ucfirst($tagName));
}
if (static::$tagNames[$tagName] === FALSE) {
return NULL;
}
return static::$tagNames[$tagName];
}
}

View file

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_blog\Entity\Node;
use Drupal\discoverable_entity_bundle_classes\ContentEntityBundleInterface;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Illuminate\Support\Collection;
/**
* Defines an blog post node class.
*
* @ContentEntityBundleClass(
* label = @Translation("Blog post"),
* entity_type = "node",
* bundle = "post"
* );
*/
class Post extends Node implements ContentEntityBundleInterface {
public function getExternalLink(): ?array {
return ($link = $this->get('field_external_link')->get(0))
? $link->getValue()
: NULL;
}
/**
* @return Collection|Term[]
*/
public function getTags(): Collection {
return new Collection($this->get('field_tags')->referencedEntities());
}
public function hasBeenSentToSocialMedia(): bool {
return (bool) $this->get('field_sent_to_social_media')->getString();
}
public function hasTweet(): bool {
return (bool) $this->get('field_has_tweet')->getString();
}
public function isExternalPost(): bool {
return (bool) $this->getExternalLink();
}
public function setTags(array $tags): void {
$this->set('field_tags', $tags);
}
public function toTweet(): string {
$parts = [
$this->label(),
$this->url('canonical', ['absolute' => TRUE]),
$this->convertTermsToHashtags(),
];
return implode(PHP_EOL . PHP_EOL, $parts);
}
private function convertTermsToHashtags(): string {
return $this->getTags()
->filter(fn(Term $term) => !$this->tagsToRemove()
->contains($term->label()))
->map(fn(Term $term) => $this->convertTermToHashtag($term))
->implode(' ');
}
private function tagsToRemove(): Collection {
// TODO: Move these values into configuration/settings.php.
return new Collection([
'Drupal Planet',
]);
}
private function convertTermToHashtag(Term $tag): string {
return '#' . (new Collection(explode(' ', $tag->label())))
->map(fn(string $word): string => ucfirst($word))
->implode('');
}
}

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_blog\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\hook_event_dispatcher\Event\Entity\BaseEntityEvent;
use Drupal\hook_event_dispatcher\HookEventDispatcherInterface;
use Drupal\opdavies_blog\Entity\Node\Post;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class PushBlogPostToSocialMedia implements EventSubscriberInterface {
private ClientInterface $client;
private ImmutableConfig $config;
public function __construct(
ConfigFactoryInterface $configFactory,
Client $client
) {
$this->client = $client;
$this->config = $configFactory->get('opdavies_blog.settings');
}
/**
* @inheritDoc
*/
public static function getSubscribedEvents() {
return [
HookEventDispatcherInterface::ENTITY_INSERT => 'onEntityUpdate',
HookEventDispatcherInterface::ENTITY_UPDATE => 'onEntityUpdate',
];
}
public function onEntityUpdate(BaseEntityEvent $event): void {
$entity = $event->getEntity();
if ($entity->getEntityTypeId() != 'node') {
return;
}
/** @var Post $entity */
if ($entity->bundle() != 'post') {
return;
}
if (!$entity->isPublished()) {
return;
}
// If this post has already been sent to social media, do not send it again.
if ($entity->hasBeenSentToSocialMedia()) {
return;
}
if ($entity->isExternalPost()) {
return;
}
if (!$url = $this->config->get('zapier_post_tweet_url')) {
return;
}
$this->client->post($url, [
'form_params' => [
'message' => $entity->toTweet(),
],
]);
$entity->set('field_sent_to_social_media', TRUE);
$entity->save();
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_blog\EventSubscriber;
use Drupal\hook_event_dispatcher\Event\Entity\BaseEntityEvent;
use Drupal\hook_event_dispatcher\HookEventDispatcherInterface;
use Drupal\opdavies_blog\Entity\Node\Post;
use Drupal\taxonomy\TermInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class ReorderBlogTags implements EventSubscriberInterface {
/**
* @inheritDoc
*/
public static function getSubscribedEvents() {
return [
HookEventDispatcherInterface::ENTITY_PRE_SAVE => 'onEntityPreSave',
];
}
public function onEntityPresave(BaseEntityEvent $event): void {
$entity = $event->getEntity();
if ($entity->getEntityTypeId() != 'node') {
return;
}
/** @var Post $entity */
if ($entity->bundle() != 'post') {
return;
}
$sortedTags = $entity->getTags()
->sortBy(fn(TermInterface $tag) => $tag->label());
$entity->setTags($sortedTags->toArray());
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_blog\Repository;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Illuminate\Support\Collection;
final class PostRepository {
private EntityStorageInterface $nodeStorage;
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->nodeStorage = $entityTypeManager->getStorage('node');
}
public function getAll(): Collection {
return new Collection(
$this->nodeStorage->loadByProperties([
'type' => 'post',
])
);
}
}

View file

@ -0,0 +1,22 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_external_link
- node.type.post
module:
- link
id: node.post.field_external_link
field_name: field_external_link
entity_type: node
bundle: post
label: 'External link'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
link_type: 16
title: 2
field_type: link

View file

@ -0,0 +1,22 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_has_tweet
- node.type.post
id: node.post.field_has_tweet
field_name: field_has_tweet
entity_type: node
bundle: post
label: 'Has tweet'
description: 'Check to include Twitter''s widget.js script for this page.'
required: false
translatable: false
default_value:
-
value: 0
default_value_callback: ''
settings:
on_label: 'Yes'
off_label: 'No'
field_type: boolean

View file

@ -0,0 +1,22 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_sent_to_social_media
- node.type.post
id: node.post.field_sent_to_social_media
field_name: field_sent_to_social_media
entity_type: node
bundle: post
label: 'Sent to social media'
description: ''
required: false
translatable: false
default_value:
-
value: 0
default_value_callback: ''
settings:
on_label: 'On'
off_label: 'Off'
field_type: boolean

View file

@ -0,0 +1,28 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_tags
- node.type.post
- taxonomy.vocabulary.tags
id: node.post.field_tags
field_name: field_tags
entity_type: node
bundle: post
label: Tags
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:taxonomy_term'
handler_settings:
target_bundles:
tags: tags
sort:
field: name
direction: asc
auto_create: true
auto_create_bundle: ''
field_type: entity_reference

View file

@ -0,0 +1,18 @@
langcode: en
status: true
dependencies:
module:
- link
- node
id: node.field_external_link
field_name: field_external_link
entity_type: node
type: link
settings: { }
module: link
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View file

@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
module:
- node
id: node.field_has_tweet
field_name: field_has_tweet
entity_type: node
type: boolean
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View file

@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
module:
- node
id: node.field_sent_to_social_media
field_name: field_sent_to_social_media
entity_type: node
type: boolean
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View file

@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- node
- taxonomy
id: node.field_tags
field_name: field_tags
entity_type: node
type: entity_reference
settings:
target_type: taxonomy_term
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View file

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies: { }
third_party_settings: { }
name: 'Blog post'
type: post
description: 'A single blog post.'
help: ''
new_revision: true
preview_mode: 1
display_submitted: true

View file

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
name: Tags
vid: tags
description: 'Tags for categorising blog posts.'
weight: 0

View file

@ -0,0 +1,4 @@
name: Oliver Davies Posts Test
type: module
core_version_requirement: ^8 || ^9
hidden: true

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_blog_test\Factory;
use Assert\Assert;
use Drupal\node\Entity\Node;
use Drupal\opdavies_blog\Entity\Node\Post;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\TermInterface;
use Illuminate\Support\Collection;
final class PostFactory {
private Collection $tags;
private string $title = 'This is a test blog post';
public function __construct() {
$this->tags = new Collection();
}
public function create(array $overrides = []): Post {
$this->tags->each(function (TermInterface $tag): void {
Assert::that($tag->bundle())->same('tags');
});
$values = [
'field_tags' => $this->tags->toArray(),
'title' => $this->title,
'type' => 'post',
];
/** @var Post $post */
$post = Node::create($values + $overrides);
return $post;
}
public function setTitle(string $title): self {
Assert::that($title)->notEmpty();
$this->title = $title;
return $this;
}
public function withTags(array $tags): self {
foreach ($tags as $tag) {
Assert::that($tag)->notEmpty()->string();
$this->tags->push(
Term::create(['vid' => 'tags', 'name' => $tag])
);
}
return $this;
}
}

View file

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\custom\Kernel\Entity\Node;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\opdavies_blog_test\Factory\PostFactory;
final class PostTest extends EntityKernelTestBase {
public static $modules = [
// Core.
'node',
'link',
'taxonomy',
// Contrib.
'discoverable_entity_bundle_classes',
// Custom.
'opdavies_blog',
'opdavies_blog_test',
];
/** @test */
public function it_can_determine_if_a_post_contains_a_tweet(): void {
$post = (new PostFactory())->create();
$post->save();
$this->assertFalse($post->hasTweet());
$post = (new PostFactory())->create(['field_has_tweet' => TRUE]);
$post->save();
$this->assertTrue($post->hasTweet());
}
/** @test */
public function it_converts_a_post_to_a_tweet(): void {
$post = (new PostFactory())
->setTitle('Creating a custom PHPUnit command for DDEV')
->withTags(['Automated testing', 'DDEV', 'Drupal', 'Drupal 8', 'PHP'])
->create();
$post->save();
$expected = <<<EOF
Creating a custom PHPUnit command for DDEV
http://localhost/node/1
#AutomatedTesting #DDEV #Drupal #Drupal8 #PHP
EOF;
$this->assertSame($expected, $post->toTweet());
}
/** @test */
public function certain_terms_are_not_added_as_hashtags(): void {
$post = (new PostFactory())
->setTitle('Drupal Planet should not be added as a hashtag')
->withTags(['Drupal', 'Drupal Planet', 'PHP'])
->create();
$post->save();
$expected = <<<EOF
Drupal Planet should not be added as a hashtag
http://localhost/node/1
#Drupal #PHP
EOF;
$this->assertSame($expected, $post->toTweet());
}
protected function setUp() {
parent::setUp();
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['opdavies_blog_test']);
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\Tests\opdavies_blog\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\opdavies_blog\Entity\Node\Post;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\VocabularyInterface;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
final class ReorderBlogTagsTest extends EntityKernelTestBase {
use NodeCreationTrait;
use TaxonomyTestTrait;
public static $modules = [
// Core.
'node',
'taxonomy',
'link',
// Contrib.
'discoverable_entity_bundle_classes',
'hook_event_dispatcher',
// Custom.
'opdavies_blog_test',
'opdavies_blog',
];
/** @test */
public function it_reorders_tags_on_blog_posts_to_be_arranged_alphabetically(): void {
/** @var VocabularyInterface $vocabulary */
$vocabulary = Vocabulary::load('tags');
$this->createTerm($vocabulary, ['name' => 'Drupal']); // 1
$this->createTerm($vocabulary, ['name' => 'PHP']); // 2
$this->createTerm($vocabulary, ['name' => 'Symfony']); // 3
$post = $this->createNode([
'field_tags' => [3, 1, 2],
'type' => 'post',
]);
/** @var Post $post */
$post = Node::load($post->id());
$this->assertSame(
['Drupal', 'PHP', 'Symfony'],
$post->getTags()
->map(fn(TermInterface $tag) => $tag->label())
->toArray()
);
}
protected function setUp() {
parent::setUp();
$this->installConfig([
'filter',
'opdavies_blog_test',
]);
$this->installEntitySchema('taxonomy_vocabulary');
$this->installEntitySchema('taxonomy_term');
}
}