XMF Slug - URL-Friendly Identifiers¶
Overview¶
Xmf\Slug provides utilities for generating URL-friendly slugs from titles and text. Slugs are essential for SEO-friendly URLs and human-readable identifiers.
Basic Usage¶
Generating Slugs¶
use Xmf\Slug;
// Basic slug generation
$slug = Slug::generate('Hello World!');
// Result: "hello-world"
$slug = Slug::generate('XOOPS 2026: The Future of CMS');
// Result: "xoops-2026-the-future-of-cms"
$slug = Slug::generate('Café & Restaurant Guide');
// Result: "cafe-restaurant-guide"
With Options¶
// Limit length
$slug = Slug::generate('A Very Long Title That Should Be Truncated', [
'maxLength' => 30
]);
// Result: "a-very-long-title-that-should"
// Custom separator
$slug = Slug::generate('Hello World', [
'separator' => '_'
]);
// Result: "hello_world"
// Preserve case
$slug = Slug::generate('iPhone Review', [
'lowercase' => false
]);
// Result: "iPhone-Review"
In Entities¶
Slug Value Object¶
<?php
declare(strict_types=1);
namespace XoopsModules\MyModule\ValueObject;
use Xmf\Slug as SlugGenerator;
final class Slug
{
private function __construct(
private readonly string $value
) {
if (empty($value)) {
throw new \InvalidArgumentException('Slug cannot be empty');
}
if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $value)) {
throw new \InvalidArgumentException('Invalid slug format');
}
}
public static function fromTitle(string $title): self
{
return new self(SlugGenerator::generate($title, [
'maxLength' => 100
]));
}
public static function fromString(string $value): self
{
return new self($value);
}
public function toString(): string
{
return $this->value;
}
public function __toString(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}
In Article Entity¶
final class Article
{
public function __construct(
private ArticleId $id,
private string $title,
private Slug $slug,
private string $content
) {}
public static function create(string $title, string $content): self
{
return new self(
id: ArticleId::generate(),
title: $title,
slug: Slug::fromTitle($title),
content: $content
);
}
public function getSlug(): Slug
{
return $this->slug;
}
public function getUrl(): string
{
return "/articles/{$this->slug}";
}
}
Unique Slug Generation¶
Repository Integration¶
interface ArticleRepositoryInterface
{
public function findBySlug(Slug $slug): ?Article;
public function slugExists(Slug $slug): bool;
}
final class ArticleService
{
public function __construct(
private readonly ArticleRepositoryInterface $repository
) {}
public function createWithUniqueSlug(string $title, string $content): Article
{
$baseSlug = Slug::fromTitle($title);
$slug = $this->makeUnique($baseSlug);
$article = new Article(
ArticleId::generate(),
$title,
$slug,
$content
);
$this->repository->save($article);
return $article;
}
private function makeUnique(Slug $slug): Slug
{
if (!$this->repository->slugExists($slug)) {
return $slug;
}
$counter = 1;
do {
$newSlug = Slug::fromString($slug->toString() . '-' . $counter);
$counter++;
} while ($this->repository->slugExists($newSlug));
return $newSlug;
}
}
Database Schema¶
CREATE TABLE `{PREFIX}_mymodule_articles` (
`id` VARCHAR(26) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`slug` VARCHAR(100) NOT NULL,
`content` MEDIUMTEXT,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
URL Routing¶
Route Definition¶
// config/routes.php
return [
'article.show' => [
'path' => '/articles/{slug}',
'controller' => ArticleController::class,
'action' => 'show',
'requirements' => [
'slug' => '[a-z0-9]+(?:-[a-z0-9]+)*'
]
],
];
Controller¶
final class ArticleController
{
public function show(string $slug): Response
{
$article = $this->repository->findBySlug(
Slug::fromString($slug)
);
if (!$article) {
throw new NotFoundException('Article not found');
}
return $this->render('article/show', [
'article' => $article
]);
}
}
Transliteration¶
Handling Non-ASCII Characters¶
// Xmf\Slug handles transliteration automatically
$slug = Slug::generate('Привет мир');
// Result: "privet-mir" (Cyrillic transliterated)
$slug = Slug::generate('日本語タイトル');
// Result: depends on transliteration library
$slug = Slug::generate('Ελληνικά');
// Result: "ellinika" (Greek transliterated)
Best Practices¶
- Unique Slugs - Enforce uniqueness at database level
- Reasonable Length - Limit to 100 characters
- Lowercase Only - Use lowercase for consistency
- Hyphens - Use hyphens, not underscores
- No Special Chars - Only alphanumeric and hyphens
- Preserve Words - Don't break words when truncating
Related Documentation¶
- EntityId - ULID identifiers
- Domain-Model - Entity design
- Database-Schema - Schema design