Skip to content

Instantly share code, notes, and snippets.

@Nek-
Last active July 11, 2024 09:58
Show Gist options
  • Save Nek-/918835e0a8d5f816c9647cff2822dddf to your computer and use it in GitHub Desktop.
Save Nek-/918835e0a8d5f816c9647cff2822dddf to your computer and use it in GitHub Desktop.
Slugify using Symfony 7+ and Doctrine 3+
<?php
class Example
{
#[ORM\Column(length: 255)]
private string $name;
#[ORM\Column(length: 255, unique: true)]
#[Slug(targetProperty: 'name', unique: true)]
private string $slug;
}
<?php
declare(strict_types=1);
namespace App\Tooling\Doctrine\Slug;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Slug
{
public function __construct(
public string $targetProperty,
public bool $unique = true,
public bool $updateOnChange = false
) {
if (true === $this->updateOnChange) {
throw new \Exception('This behavior is not supported yet, please change the SlugifyListener to add its support.');
}
}
}
<?php
declare(strict_types=1);
namespace App\Tooling\Doctrine\Slug;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\String\Slugger\AsciiSlugger;
#[AsDoctrineListener('prePersist')]
class SlugifyListener
{
public function __construct(private PropertyAccessorInterface $accessor)
{
}
public function prePersist(PrePersistEventArgs $args): void
{
$entity = $args->getObject();
$reflection = new \ReflectionObject($entity);
$entityManager = $args->getObjectManager();
foreach ($reflection->getProperties() as $property) {
/** @var \ReflectionAttribute<Slug>[] $attributes */
$attributes = $property->getAttributes(Slug::class);
if (empty($attributes)) {
continue;
}
/** @var Slug $slugAttribute */
$slugAttribute = $attributes[0]->newInstance();
$slug = $this->slugify(
$entity,
$property,
$slugAttribute
);
if (empty($slug)) {
return;
}
if ($slugAttribute->unique) {
$metadata = $entityManager->getClassMetadata($entity::class);
$table = $metadata->getTableName();
$sqlField = $metadata->getColumnName($property->getName());
$connection = $entityManager->getConnection();
$resultSet = $connection->executeQuery(<<<SQL
SELECT $sqlField FROM $table WHERE $sqlField LIKE '$slug%'
SQL)->fetchAllAssociative();
if (!empty($resultSet)) {
$slug = $this->incrementSlug(array_map(fn ($item) => $item['slug'], $resultSet), $slug);
$this->accessor->setValue($entity, $property->getName(), $slug);
}
}
}
}
/**
* @param array<string, string>|null $changeSet
*/
private function slugify(object $entity, \ReflectionProperty $property, Slug $slugAttribute, ?array $changeSet = null, bool $isUpdate = false): ?string
{
if (null !== $changeSet && !\array_key_exists($slugAttribute->targetProperty, $changeSet)) {
return null;
}
if ($isUpdate && $slugAttribute->updateOnChange) {
return $this->accessor->getValue($entity, $property->getName());
}
$originalValue = $this->accessor->getValue($entity, $slugAttribute->targetProperty);
$slug = (new AsciiSlugger())->slug($originalValue)->toString();
$this->accessor->setValue($entity, $property->getName(), $slug);
return $slug;
}
/**
* @param array<int, string> $slugsInDatabase
*/
private function incrementSlug(array $slugsInDatabase, string $slug): string
{
if (1 === \count($slugsInDatabase)) {
return $slug . '-1';
}
if (1 !== preg_match('/-(\d+)$/', $slugsInDatabase[0], $matches)) {
return $slug . '-1';
}
$i = ((int) $matches[1]) + 1;
return $slug . '-' . $i;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment