XMF Slug - URL-Friendly Identifiers
Overview
Section titled “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
Section titled “Basic Usage”Generating Slugs
Section titled “Generating Slugs”use Xmf\Slug;
// Basic slug generation$slug = Slug::generate('Hello World!');// Result: "hello-world"
$slug = Slug::generate('XOOPS 4.0: The Future of CMS');// Result: "xoops-4.0-the-future-of-cms"
$slug = Slug::generate('Café & Restaurant Guide');// Result: "cafe-restaurant-guide"With Options
Section titled “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
Section titled “In Entities”Slug Value Object
Section titled “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
Section titled “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
Section titled “Unique Slug Generation”Repository Integration
Section titled “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
Section titled “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
Section titled “URL Routing”Route Definition
Section titled “Route Definition”return [ 'article.show' => [ 'path' => '/articles/{slug}', 'controller' => ArticleController::class, 'action' => 'show', 'requirements' => [ 'slug' => '[a-z0-9]+(?:-[a-z0-9]+)*' ] ],];Controller
Section titled “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
Section titled “Transliteration”Handling Non-ASCII Characters
Section titled “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
Section titled “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
Section titled “Related Documentation”- EntityId - ULID identifiers
- ../../../03-Module-Development/Patterns/Domain-Model - Entity design
- ../../../03-Module-Development/Database/Database-Schema - Schema design