Skip to content

Commit b5dcd29

Browse files
author
olaru
committed
Added recommended contact form handling and template rendering
1 parent 7c5358a commit b5dcd29

14 files changed

Lines changed: 127 additions & 69 deletions

‎src/App/src/ConfigProvider.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Light\App\Factory\MarkdownRuntimeLoaderFactory;
2727
use Light\App\Factory\PackageGeneratorFactory;
2828
use Light\App\Factory\SitemapGeneratorFactory;
29+
use Light\App\Handler\GetContactCreateFormHandler;
2930
use Light\App\Handler\GetFeedViewHandler;
3031
use Light\App\Handler\GetIndexViewHandler;
3132
use Light\App\Handler\GetMarkdownArticleHandler;
@@ -145,6 +146,7 @@ public function getDependencies(): array
145146
GetMarkdownArticleHandler::class => GetMarkdownArticleHandlerFactory::class,
146147
GetSitemapViewHandler::class => GetSitemapViewHandlerFactory::class,
147148
GetPackagesViewHandler::class => GetPackagesViewHandlerFactory::class,
149+
GetContactCreateFormHandler::class => AttributedServiceFactory::class,
148150
PostContactCreateHandler::class => AttributedServiceFactory::class,
149151
ContactService::class => AttributedServiceFactory::class,
150152
FeedGenerator::class => FeedGeneratorFactory::class,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Light\App\Enum;
6+
7+
enum ContactTopicEnum: string
8+
{
9+
case Migration = 'migration';
10+
case Project = 'project';
11+
case Oss = 'oss';
12+
case Other = 'other';
13+
14+
public function label(): string
15+
{
16+
return match ($this) {
17+
self::Migration => 'Migration',
18+
self::Project => 'Project work',
19+
self::Oss => 'Open source',
20+
self::Other => 'Something else',
21+
};
22+
}
23+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Light\App\Handler;
6+
7+
use Dot\DependencyInjection\Attribute\Inject;
8+
use Laminas\Diactoros\Response\HtmlResponse;
9+
use Light\App\Enum\ContactTopicEnum;
10+
use Light\App\Service\ContactService;
11+
use Mezzio\Template\TemplateRendererInterface;
12+
use Psr\Http\Message\ResponseInterface;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Http\Server\RequestHandlerInterface;
15+
16+
class GetContactCreateFormHandler implements RequestHandlerInterface
17+
{
18+
public const string TEMPLATE = 'page::contact';
19+
20+
#[Inject(
21+
TemplateRendererInterface::class,
22+
)]
23+
public function __construct(
24+
protected TemplateRendererInterface $template,
25+
) {
26+
}
27+
28+
public function handle(ServerRequestInterface $request): ResponseInterface
29+
{
30+
$query = $request->getQueryParams();
31+
32+
return new HtmlResponse($this->template->render(self::TEMPLATE, [
33+
'topics' => ContactTopicEnum::cases(),
34+
'contact_success' => ($query['contact'] ?? null) === 'sent',
35+
'query' => ContactService::extractQuery($query),
36+
]));
37+
}
38+
}

‎src/App/src/Handler/PostContactCreateHandler.php‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
namespace Light\App\Handler;
66

77
use Dot\DependencyInjection\Attribute\Inject;
8+
use Fig\Http\Message\StatusCodeInterface;
89
use Laminas\Diactoros\Response\HtmlResponse;
910
use Laminas\Diactoros\Response\RedirectResponse;
11+
use Light\App\Enum\ContactTopicEnum;
1012
use Light\App\Service\ContactService;
13+
use Mezzio\Helper\UrlHelper;
1114
use Mezzio\Template\TemplateRendererInterface;
1215
use Psr\Http\Message\ResponseInterface;
1316
use Psr\Http\Message\ServerRequestInterface;
@@ -17,14 +20,14 @@
1720

1821
class PostContactCreateHandler implements RequestHandlerInterface
1922
{
20-
public const string TEMPLATE = 'page::contact';
21-
2223
#[Inject(
2324
TemplateRendererInterface::class,
25+
UrlHelper::class,
2426
ContactService::class,
2527
)]
2628
public function __construct(
2729
protected TemplateRendererInterface $template,
30+
protected UrlHelper $urlHelper,
2831
protected ContactService $contactService,
2932
) {
3033
}
@@ -42,14 +45,23 @@ public function handle(ServerRequestInterface $request): ResponseInterface
4245
if ($errors !== [] || $mailFailed) {
4346
$rawBody = $request->getParsedBody();
4447

45-
return new HtmlResponse($this->template->render(self::TEMPLATE, [
48+
return new HtmlResponse($this->template->render(GetContactCreateFormHandler::TEMPLATE, [
49+
'topics' => ContactTopicEnum::cases(),
4650
'contact_errors' => $errors,
4751
'contact_values' => $data,
4852
'contact_mail_failed' => $mailFailed,
4953
'query' => ContactService::extractQuery(is_array($rawBody) ? $rawBody : []),
5054
]));
5155
}
5256

53-
return new RedirectResponse('/contact/?contact=sent#contact-form', 303);
57+
return new RedirectResponse(
58+
$this->urlHelper->generate(
59+
GetContactCreateFormHandler::TEMPLATE,
60+
[],
61+
['contact' => 'sent'],
62+
'contact-form'
63+
),
64+
StatusCodeInterface::STATUS_SEE_OTHER
65+
);
5466
}
5567
}

‎src/App/src/RoutesDelegator.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Light\App;
66

7+
use Light\App\Handler\GetContactCreateFormHandler;
78
use Light\App\Handler\GetFeedViewHandler;
89
use Light\App\Handler\GetIndexViewHandler;
910
use Light\App\Handler\GetMarkdownArticleHandler;
@@ -26,6 +27,7 @@ public function __invoke(ContainerInterface $container, string $serviceName, cal
2627
$app->get('/sitemap/', [GetSitemapViewHandler::class], 'app::sitemap');
2728
$app->get('/{categorySlug}/{slug}.md', [GetMarkdownArticleHandler::class], 'app::markdown-article');
2829

30+
$app->get('/contact/', [GetContactCreateFormHandler::class], GetContactCreateFormHandler::TEMPLATE);
2931
$app->post('/contact/', [PostContactCreateHandler::class], 'app::create-contact');
3032

3133
// Route name kept as `page::…` because `@layout/default.html.twig` links it by name.

‎src/App/src/Service/ContactService.php‎

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Dot\Mail\Service\MailServiceInterface;
99
use Laminas\Validator\EmailAddress;
1010
use Laminas\Validator\NotEmpty;
11+
use Light\App\Enum\ContactTopicEnum;
1112
use Throwable;
1213

1314
use function htmlspecialchars;
@@ -35,13 +36,6 @@
3536
*/
3637
final class ContactService
3738
{
38-
public const array TOPICS = [
39-
'migration' => 'Migration',
40-
'project' => 'Project work',
41-
'oss' => 'Open source',
42-
'other' => 'Something else',
43-
];
44-
4539
private const array REQUIRED_FIELDS = [
4640
'name' => 'Name',
4741
'email' => 'Work email',
@@ -76,11 +70,10 @@ public static function extractQuery(array $source): array
7670
*/
7771
public function normalize(mixed $parsedBody): array
7872
{
79-
$raw = is_array($parsedBody) ? $parsedBody : [];
80-
$topic = $this->stringValue($raw, 'topic');
73+
$raw = is_array($parsedBody) ? $parsedBody : [];
8174

8275
return [
83-
'topic' => isset(self::TOPICS[$topic]) ? $topic : '',
76+
'topic' => ContactTopicEnum::tryFrom($this->stringValue($raw, 'topic'))->value ?? '',
8477
'name' => $this->stringValue($raw, 'name'),
8578
'email' => $this->stringValue($raw, 'email'),
8679
'company' => $this->stringValue($raw, 'company'),
@@ -144,7 +137,7 @@ private function stringValue(array $raw, string $key): string
144137
private function buildBody(array $data): string
145138
{
146139
$rows = [
147-
'Topic' => self::TOPICS[$data['topic']] ?? '',
140+
'Topic' => ContactTopicEnum::tryFrom($data['topic'])?->label() ?? '',
148141
'Name' => $data['name'],
149142
'Work email' => $data['email'],
150143
'Company' => $data['company'],

‎src/App/src/Service/SitemapGenerator.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ public function write(): int
7171
$this->appendUrl($dom, $urlset, $this->baseUrl . '/dotkernel-packages-oss-lifecycle/');
7272
$count++;
7373

74+
$this->appendUrl($dom, $urlset, $this->baseUrl . '/contact/');
75+
$count++;
76+
7477
foreach ($this->pageRoutes as $routeUri) {
7578
$this->appendUrl($dom, $urlset, sprintf('%s/%s/', $this->baseUrl, $routeUri));
7679
$count++;

‎src/Blog/templates/page/contact.html.twig‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55

66
{% block stylesheets %}<link href="{{ asset('css/contact.css') }}" rel="stylesheet" />{% endblock %}
77

8-
{% set topics = [
9-
{id: 'migration', label: 'Migration'},
10-
{id: 'project', label: 'Project work'},
11-
{id: 'oss', label: 'Open source'},
12-
{id: 'other', label: 'Something else'},
13-
] %}
14-
158
{% set support_links = [
169
{label: 'Support documentation', href: 'https://github.com/dotkernel/.github/blob/main/SUPPORT.md'},
1710
{label: 'Security policy', href: 'https://github.com/dotkernel/.github/blob/main/SECURITY.md'},
@@ -78,7 +71,7 @@
7871
<div class="contact-form-card" id="contact-form">
7972
{% set values = contact_values|default({}) %}
8073
{% set errors = contact_errors|default({}) %}
81-
{% set selected_topic = values.topic|default('') ?: topics[0].id %}
74+
{% set selected_topic = values.topic|default('') ?: (topics|first).value %}
8275
<form class="contact-form" method="post" action="{{ path('app::create-contact') }}" data-contact-form>
8376
<div class="contact-form-head">
8477
<h2>Send a message</h2>
@@ -97,8 +90,8 @@
9790
<input type="hidden" name="topic" value="{{ selected_topic }}" data-contact-topic>
9891
<div class="contact-pills">
9992
{% for topic in topics %}
100-
<button type="button" class="contact-pill" data-topic="{{ topic.id }}"
101-
aria-pressed="{{ topic.id == selected_topic ? 'true' : 'false' }}">{{ topic.label }}</button>
93+
<button type="button" class="contact-pill" data-topic="{{ topic.value }}"
94+
aria-pressed="{{ topic.value == selected_topic ? 'true' : 'false' }}">{{ topic.label() }}</button>
10295
{% endfor %}
10396
</div>
10497
</fieldset>

‎src/Page/src/Handler/GetPageViewHandler.php‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Fig\Http\Message\StatusCodeInterface;
88
use Laminas\Diactoros\Response\HtmlResponse;
99
use Laminas\Diactoros\Response\TextResponse;
10-
use Light\App\Service\ContactService;
1110
use Light\Blog\Repository\CategoryRepository;
1211
use Light\Blog\Repository\PostRepository;
1312
use Light\Page\Service\PageServiceInterface;
@@ -47,13 +46,10 @@ public function handle(ServerRequestInterface $request): ResponseInterface
4746

4847
$posts = $this->postRepository->getRecentPosts(3);
4948
$categories = $this->categoryRepository->getCategories();
50-
$query = $request->getQueryParams();
5149
return new HtmlResponse(
5250
$this->template->render($template, [
53-
'posts' => $posts,
54-
'categories' => $categories,
55-
'contact_success' => ($query['contact'] ?? null) === 'sent',
56-
'query' => ContactService::extractQuery($query),
51+
'posts' => $posts,
52+
'categories' => $categories,
5753
])
5854
);
5955
}

‎test/Unit/App/Handler/PostContactCreateHandlerTest.php‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
use Dot\Mail\Email;
88
use Dot\Mail\Result\MailResult;
99
use Dot\Mail\Service\MailServiceInterface;
10+
use Fig\Http\Message\StatusCodeInterface;
1011
use Laminas\Diactoros\Response\HtmlResponse;
1112
use Laminas\Diactoros\Response\RedirectResponse;
13+
use Light\App\Enum\ContactTopicEnum;
1214
use Light\App\Handler\PostContactCreateHandler;
1315
use Light\App\Service\ContactService;
16+
use Mezzio\Helper\UrlHelper;
1417
use Mezzio\Template\TemplateRendererInterface;
1518
use PHPUnit\Framework\MockObject\Exception;
1619
use PHPUnit\Framework\TestCase;
@@ -63,11 +66,18 @@ public function testValidSubmissionRedirectsOnSuccess(): void
6366
$template = $this->createMock(TemplateRendererInterface::class);
6467
$template->expects($this->never())->method('render');
6568

66-
$handler = new PostContactCreateHandler($template, $this->service(new MailResult(true)));
69+
$urlHelper = $this->createMock(UrlHelper::class);
70+
$urlHelper
71+
->expects($this->once())
72+
->method('generate')
73+
->with('page::contact', [], ['contact' => 'sent'], 'contact-form')
74+
->willReturn('/contact/?contact=sent#contact-form');
75+
76+
$handler = new PostContactCreateHandler($template, $urlHelper, $this->service(new MailResult(true)));
6777
$response = $handler->handle($request);
6878

6979
$this->assertInstanceOf(RedirectResponse::class, $response);
70-
$this->assertSame(303, $response->getStatusCode());
80+
$this->assertSame(StatusCodeInterface::STATUS_SEE_OTHER, $response->getStatusCode());
7181
$this->assertSame('/contact/?contact=sent#contact-form', $response->getHeaderLine('Location'));
7282
}
7383

@@ -90,15 +100,20 @@ public function testMissingRequiredFieldsRerendersWithErrors(): void
90100
->with(
91101
'page::contact',
92102
$this->callback(function (array $params): bool {
93-
return $params['contact_mail_failed'] === false
103+
return $params['topics'] === ContactTopicEnum::cases()
104+
&& $params['contact_mail_failed'] === false
94105
&& array_key_exists('name', $params['contact_errors'])
95106
&& array_key_exists('message', $params['contact_errors'])
96107
&& $params['contact_values']['email'] === 'jane@example.com';
97108
})
98109
)
99110
->willReturn('<html></html>');
100111

101-
$handler = new PostContactCreateHandler($template, $this->service(new MailResult(true)));
112+
$handler = new PostContactCreateHandler(
113+
$template,
114+
$this->createStub(UrlHelper::class),
115+
$this->service(new MailResult(true))
116+
);
102117
$response = $handler->handle($request);
103118

104119
$this->assertInstanceOf(HtmlResponse::class, $response);
@@ -131,7 +146,11 @@ public function testRerenderKeepsAnyExtraQueryParamsAsDynamicHiddenFields(): voi
131146
)
132147
->willReturn('<html></html>');
133148

134-
$handler = new PostContactCreateHandler($template, $this->service(new MailResult(true)));
149+
$handler = new PostContactCreateHandler(
150+
$template,
151+
$this->createStub(UrlHelper::class),
152+
$this->service(new MailResult(true))
153+
);
135154
$handler->handle($request);
136155
}
137156

@@ -158,6 +177,7 @@ public function testMailTransportFailureRerendersWithMailFailedFlag(): void
158177

159178
$handler = new PostContactCreateHandler(
160179
$template,
180+
$this->createStub(UrlHelper::class),
161181
$this->service(new RuntimeException('SMTP connection refused'))
162182
);
163183
$response = $handler->handle($request);

0 commit comments

Comments
 (0)