Skip to content

Commit 02da302

Browse files
committed
feat: added title slugify Twig extension
1 parent 5a4e685 commit 02da302

File tree

8 files changed

+142
-12
lines changed

8 files changed

+142
-12
lines changed

docs/development.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,14 @@ Example:
428428

429429
{{ 'string' | translate }}
430430

431+
### Title slugify Twig Extensions
432+
433+
The title slugify extension is used to create a slug from a string.
434+
435+
Example:
436+
437+
{{ "Hello World"|slugify }} {# outputs: hello-world #}
438+
431439
### User name Twig Extensions
432440

433441
The username extension is used to get the name of a user by its ID.

phpmyfaq/assets/templates/admin/content/comments.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<div class="d-flex align-items-center me-3">
5454
<i aria-hidden="true" class="bi bi-chat-dots me-1"></i>
5555
<a
56-
href="../?action=faq&cat={{ comment.categoryId }}&id={{ comment.recordId }}&artlang={{ currentLocale }}">
56+
href="../content/{{ comment.categoryId }}/{{ comment.recordId }}/{{ currentLocale }}/{{ comment.recordId | faqQuestion | slugify }}.html">
5757
{{ comment.recordId | faqQuestion | raw }}
5858
</a>
5959
</div>
@@ -102,7 +102,7 @@
102102
{{ comment.date | format_datetime(locale=currentLocale) }}
103103
</div>
104104
<div class="d-flex align-items-center me-3">
105-
<a href="../?action=news&newsid={{ comment.recordId }}&artlang={{ currentLocale }}">
105+
<a href="../news/{{ comment.recordId }}/{{ currentLocale }}/news.html">
106106
<i class="bi bi-newspaper"></i>
107107
</a>
108108
</div>

phpmyfaq/src/phpMyFAQ/Comments.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public function getAllComments(string $type = CommentType::FAQ): array
154154
}
155155

156156
/**
157-
* Checks, if comments are disabled for the FAQ record.
157+
* Checks if comments are disabled for the FAQ record.
158158
*
159159
* @param int $recordId ID of FAQ or news entry
160160
* @param string $recordLang Language

phpmyfaq/src/phpMyFAQ/Controller/Administration/CommentsController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use phpMyFAQ\Pagination;
2727
use phpMyFAQ\Session\Token;
2828
use phpMyFAQ\Twig\Extensions\FaqTwigExtension;
29+
use phpMyFAQ\Twig\Extensions\TitleSlugifierTwigExtension;
2930
use Symfony\Component\HttpFoundation\Request;
3031
use Symfony\Component\HttpFoundation\Response;
3132
use Symfony\Component\Routing\Attribute\Route;
@@ -73,6 +74,7 @@ public function index(Request $request): Response
7374

7475
$this->addExtension(new IntlExtension());
7576
$this->addExtension(new AttributeExtension(FaqTwigExtension::class));
77+
$this->addExtension(new AttributeExtension(TitleSlugifierTwigExtension::class));
7678
return $this->render(file: '@admin/content/comments.twig', context: [
7779
...$this->getHeader($request),
7880
...$this->getFooter(),

phpmyfaq/src/phpMyFAQ/Twig/Extensions/CreateLinkTwigExtension.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use phpMyFAQ\Configuration;
2424
use phpMyFAQ\Faq;
2525
use phpMyFAQ\Link;
26+
use phpMyFAQ\Link\Util\TitleSlugifier;
2627
use Twig\Attribute\AsTwigFilter;
2728
use Twig\Attribute\AsTwigFunction;
2829
use Twig\Extension\AbstractExtension;
@@ -34,12 +35,18 @@ class CreateLinkTwigExtension extends AbstractExtension
3435
public static function categoryLink(int $categoryId): string
3536
{
3637
$configuration = Configuration::getConfigurationInstance();
37-
$urlString = '%sindex.php?action=show&cat=%d';
38-
$url = sprintf($urlString, $configuration->getDefaultUrl(), $categoryId);
3938

4039
$category = new Category($configuration);
4140
$categoryEntity = $category->getCategoryData($categoryId);
4241

42+
$urlString = '%scategory/%d/%s.html';
43+
$url = sprintf(
44+
$urlString,
45+
$configuration->getDefaultUrl(),
46+
$categoryId,
47+
TitleSlugifier::slug($categoryEntity->getName()),
48+
);
49+
4350
$link = new Link($url, $configuration);
4451
$link->setTitle($categoryEntity->getName());
4552

@@ -51,12 +58,22 @@ public static function categoryLink(int $categoryId): string
5158
public static function faqLink(int $categoryId, int $faqId, string $faqLanguage): string
5259
{
5360
$configuration = Configuration::getConfigurationInstance();
54-
$urlString = '%sindex.php?action=faq&cat=%d&id=%d&artlang=%s';
55-
$url = sprintf($urlString, $configuration->getDefaultUrl(), $categoryId, $faqId, $faqLanguage);
5661

5762
$faq = new Faq($configuration);
63+
$question = $faq->getQuestion($faqId);
64+
65+
$urlString = '%scontent/%d/%d/%s/%s.html';
66+
$url = sprintf(
67+
$urlString,
68+
$configuration->getDefaultUrl(),
69+
$categoryId,
70+
$faqId,
71+
$faqLanguage,
72+
TitleSlugifier::slug($question),
73+
);
74+
5875
$link = new Link($url, $configuration);
59-
$link->setTitle($faq->getQuestion($faqId));
76+
$link->setTitle($question);
6077

6178
return $link->toString();
6279
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/**
4+
* Twig extension to slugify titles
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ\Template
11+
* @author Thorsten Rinne <thorsten@phpmyfaq.de>
12+
* @copyright 2026 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2026-01-05
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ\Twig\Extensions;
21+
22+
use phpMyFAQ\Link\Util\TitleSlugifier;
23+
use Twig\Attribute\AsTwigFilter;
24+
use Twig\Extension\AbstractExtension;
25+
26+
class TitleSlugifierTwigExtension extends AbstractExtension
27+
{
28+
#[AsTwigFilter(name: 'slugify')]
29+
public static function slugify(string $title): string
30+
{
31+
return TitleSlugifier::slug($title);
32+
}
33+
}

tests/phpMyFAQ/Twig/Extensions/CreateLinkTwigExtensionTest.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ public function testClassHasCorrectImports(): void
152152
'use phpMyFAQ\Configuration;',
153153
'use phpMyFAQ\Faq;',
154154
'use phpMyFAQ\Link;',
155+
'use phpMyFAQ\Link\Util\TitleSlugifier;',
155156
'use Twig\Attribute\AsTwigFilter;',
156157
'use Twig\Attribute\AsTwigFunction;',
157158
'use Twig\Extension\AbstractExtension;',
@@ -177,10 +178,11 @@ public function testMethodCreatesCorrectUrlFormat(): void
177178
$filename = new ReflectionClass(CreateLinkTwigExtension::class)->getFileName();
178179
$source = file_get_contents($filename);
179180

180-
// Should create URL with proper format
181-
$this->assertStringContainsString('index.php?action=show&cat=', $source);
181+
// Should create URL with proper format using TitleSlugifier
182+
$this->assertStringContainsString('category/%d/%s.html', $source);
182183
$this->assertStringContainsString('sprintf', $source);
183184
$this->assertStringContainsString('$categoryId', $source);
185+
$this->assertStringContainsString('TitleSlugifier::slug', $source);
184186
}
185187

186188
public function testMethodIsStaticForTwigCompatibility(): void
@@ -293,8 +295,8 @@ public function testUrlGenerationPattern(): void
293295
// Should use Configuration to get default URL
294296
$this->assertStringContainsString('$configuration->getDefaultUrl()', $source);
295297

296-
// Should format URL with category ID
297-
$this->assertStringContainsString('%sindex.php?action=show&cat=%d', $source);
298+
// Should format URL with category ID and slug
299+
$this->assertStringContainsString('%scategory/%d/%s.html', $source);
298300
}
299301

300302
public function testMethodHandlesConfigurationProperly(): void
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace phpMyFAQ\Twig\Extensions;
4+
5+
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
6+
use PHPUnit\Framework\Attributes\DataProvider;
7+
use PHPUnit\Framework\TestCase;
8+
use Twig\Environment;
9+
use Twig\Error\LoaderError;
10+
use Twig\Error\RuntimeError;
11+
use Twig\Error\SyntaxError;
12+
use Twig\Extension\AttributeExtension;
13+
use Twig\Loader\ArrayLoader;
14+
15+
#[AllowMockObjectsWithoutExpectations]
16+
class TitleSlugifierTwigExtensionTest extends TestCase
17+
{
18+
/**
19+
* @throws SyntaxError
20+
* @throws RuntimeError
21+
* @throws LoaderError
22+
*/
23+
#[DataProvider('slugifyProvider')]
24+
public function testSlugifyFilter(string $input, string $expected): void
25+
{
26+
$loader = new ArrayLoader([
27+
'index' => '{{ title|slugify }}',
28+
]);
29+
$twig = new Environment($loader);
30+
$twig->addExtension(new AttributeExtension(TitleSlugifierTwigExtension::class));
31+
32+
$output = $twig->render('index', ['title' => $input]);
33+
$this->assertSame($expected, $output);
34+
}
35+
36+
/**
37+
* @return array<string, array<string, string>>
38+
*/
39+
public static function slugifyProvider(): array
40+
{
41+
return [
42+
'simple title' => [
43+
'input' => 'Hello World',
44+
'expected' => 'hello-world',
45+
],
46+
'title with umlauts' => [
47+
'input' => 'Über uns',
48+
'expected' => 'ueber-uns',
49+
],
50+
'title with punctuation' => [
51+
'input' => 'What is PHP?',
52+
'expected' => 'what-is-php',
53+
],
54+
'title with apostrophe' => [
55+
'input' => "It's a test",
56+
'expected' => 'it_s-a-test',
57+
],
58+
'title with multiple spaces' => [
59+
'input' => 'Multiple spaces here',
60+
'expected' => 'multiple-spaces-here',
61+
],
62+
'title with dashes' => [
63+
'input' => 'HD-Ready TV',
64+
'expected' => 'hd_ready-tv',
65+
],
66+
];
67+
}
68+
}

0 commit comments

Comments
 (0)