fbeen/uniqueslugbundle
fbeen/uniqueslugbundle is a Symfony bundle that generates unique, URL-friendly slugs for Doctrine entities. It helps prevent duplicate slugs, supports SEO-friendly URLs, and integrates with entity lifecycle events for automatic slug creation and updates.
This is a Symfony bundle, not a Laravel package. If you're evaluating it from a Laravel codebase, composer require fbeen/uniqueslugbundle will install it but nothing will wire up — there's no service provider, no facade, no config publish. Your realistic options are (a) use it in a Symfony app, or (b) read its ~few hundred lines of source and lift the slug-uniqueness logic into a Laravel SlugService. Option (b) is often the right call given the package's age.
Assuming a Symfony app:
composer require fbeen/uniqueslugbundleconfig/bundles.php):
return [
// ...
Fbeen\UniqueSlugBundle\FbeenUniqueSlugBundle::class => ['all' => true],
];
On legacy Symfony, add it to AppKernel::registerBundles().Resources/config/services.yaml (or DependencyInjection/) to find the service ID and its class, then that class to read the actual method signature, then DependencyInjection/Configuration.php for any configurable separator/length/transliteration options. The README is thin; the service class is the documentation.Post (or Page) entity a unique, URL-safe slug derived from its title at creation time.// src/Controller/PostController.php
public function create(Request $request, EntityManagerInterface $em, /* slugger service */ $slugger)
{
$post = new Post();
$post->setTitle($request->request->get('title'));
// Shape is illustrative — confirm the exact method name/args against the
// bundle's service class. It's essentially (entity, sourceString, property).
$post->setSlug($slugger->uniqueSlug($post, $post->getTitle(), 'slug'));
$em->persist($post);
$em->flush();
}
Add a unique index on the column in the same migration so the DB enforces what the bundle only checks:
/** @ORM\Column(type="string", length=191, unique=true) */
private $slug;
Explicit generation in the controller/service layer. Best starting point: easy to debug, easy to override for imports or admin edits. Generate only when the slug is empty or the source field actually changed — don't regenerate on every save or you silently break every inbound link.
Doctrine event subscriber for automatic slugs. This is the pattern most teams settle on. Inject the slugger into a subscriber rather than trying to put service calls in entity lifecycle callbacks (entities can't cleanly hold services).
// src/EventListener/SlugSubscriber.php
class SlugSubscriber implements EventSubscriber
{
public function __construct(private $slugger) {}
public function getSubscribedEvents(): array
{
return [Events::prePersist, Events::preUpdate];
}
public function prePersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof SluggableInterface || $entity->getSlug() !== null) {
return;
}
$entity->setSlug($this->slugger->uniqueSlug($entity, $entity->getSlugSource(), 'slug'));
}
}
# config/services.yaml
App\EventListener\SlugSubscriber:
arguments: ['@fbeen_unique_slug.slugger'] # verify the service ID
tags: ['doctrine.event_subscriber']
Define a tiny SluggableInterface (getSlugSource(), getSlug(), setSlug()) in your app so the subscriber stays generic and you aren't coupled to the bundle's own trait.
Form types / manual override. Generate a suggestion for the slug field on the create form, let the editor override it, and on edit only regenerate if the title changed and the slug hasn't been manually pinned. A slugLocked boolean is cheaper than explaining to users why their URL changed.
Route slugs (the "routes" half of the description). Use the same uniqueness guarantee for CMS-style paths — pages, categories, landing pages — then look up by slug instead of ID:
/**
* @Route("/p/{slug}", name="page_show")
*/
public function show(string $slug, PageRepository $pages, EntityManagerInterface $em)
{
$page = $pages->findOneBy(['slug' => $slug]) ?? throw $this->createNotFoundException();
return $this->render('page/show.html.twig', ['page' => $page]);
}
Bulk imports and fixtures. For a CSV import of 5,000 rows, the per-entity uniqueness check costs a query each. Either pre-load existing slugs into an in-memory array_flip set and generate suffixes locally, or flush in chunks and accept the query cost. Don't call the slugger 5,000 times inside a single un-flushed unit of work — you'll get duplicate suffixes because the earlier inserts aren't visible to the check.
Testing. Point the slugger at an in-memory SQLite database in a unit test and assert the collision behaviour (foo, foo-1, foo-2) rather than testing it through the UI. The suffixed-slug rules are the only non-obvious logic in the package.
composer.json constraints before adopting. Expect PHP 7-era signatures, YAML/annotation DI, and possible breakage on Symfony 6/7 (service names, EventSubscriber interfaces, autowiring changes). Budget an hour to read the source and fork it if needed; it's small.UniqueConstraintViolationException, then retry with the next suffix. The bundle cannot solve this for you.preUpdate and Doctrine change sets. Doctrine computes the change set before preUpdate, and changes made inside the listener don't propagate reliably to the UPDATE. Prefer generating the slug before flush() (setter, controller, or a preFlush listener) rather than mutating the entity in preUpdate.strlen(base) + 2 can exceed the column. Set length: 191 (or 255) and truncate the base string before appending the suffix, or you'll get a truncation error on a 250-character German title.id-based slug) rather than letting an empty string hit the unique index — the first one succeeds and every subsequent one explodes.slug_history table and 301-redirect old slugs to the current one, or lock the slug after publish. This is the single most common production regret with slug generators.decorates: fbeen_unique_slug.slugger) to swap in your own transliteration or suffix strategy instead of editing vendor code. If there's a Configuration.php, that's where separator/prefix/length keys live. from a rich-text editor) rather than in the uniqueness logic.How can I help you explore Laravel packages today?