Skip to content

Commit

Permalink
Merge pull request #55 from delyriand/feature/same-slug-multiple-pages
Browse files Browse the repository at this point in the history
Allows 2 pages to have the same slug depending on channel and locale
  • Loading branch information
maximehuran authored Sep 8, 2023
2 parents 98d14bb + 18a83fc commit 9821223
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 23 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"dealerdirect/phpcodesniffer-composer-installer": true,
"symfony/thanks": true,
"ergebnis/composer-normalize": true,
"symfony/flex": true
"symfony/flex": true,
"php-http/discovery": true
}
}
}
54 changes: 39 additions & 15 deletions src/Repository/PageRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace MonsieurBiz\SyliusCmsPagePlugin\Repository;

use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\QueryBuilder;
use MonsieurBiz\SyliusCmsPagePlugin\Entity\PageInterface;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
Expand All @@ -31,22 +30,32 @@ public function createListQueryBuilder(string $localeCode): QueryBuilder
;
}

/**
* @throws NoResultException
* @throws NonUniqueResultException
*/
public function existsOneByChannelAndSlug(ChannelInterface $channel, ?string $locale, string $slug): bool
public function existsOneByChannelAndSlug(ChannelInterface $channel, ?string $locale, string $slug, array $excludedPages = []): bool
{
$count = (int) $this
->createQueryBuilder('p')
->select('COUNT(p.id)')
->innerJoin('p.translations', 'translation', 'WITH', 'translation.locale = :locale')
->andWhere('translation.slug = :slug')
->andWhere(':channel MEMBER OF p.channels')
$queryBuilder = $this->createQueryBuilderExistOne($channel, $locale, $slug);
if (!empty($excludedPages)) {
$queryBuilder
->andWhere('p.id NOT IN (:excludedPages)')
->setParameter('excludedPages', $excludedPages)
;
}

$count = (int) $queryBuilder
->getQuery()
->getSingleScalarResult()
;

return $count > 0;
}

public function existsOneEnabledByChannelAndSlug(ChannelInterface $channel, ?string $locale, string $slug): bool
{
$queryBuilder = $this->createQueryBuilderExistOne($channel, $locale, $slug);
$queryBuilder
->andWhere('p.enabled = true')
->setParameter('channel', $channel)
->setParameter('locale', $locale)
->setParameter('slug', $slug)
;

$count = (int) $queryBuilder
->getQuery()
->getSingleScalarResult()
;
Expand All @@ -73,4 +82,19 @@ public function findOneEnabledBySlugAndChannelCode(string $slug, string $localeC
->getOneOrNullResult()
;
}

private function createQueryBuilderExistOne(ChannelInterface $channel, ?string $locale, string $slug): QueryBuilder
{
return $this
->createQueryBuilder('p')
->select('COUNT(p.id)')
->innerJoin('p.translations', 'translation', 'WITH', 'translation.locale = :locale')
->andWhere('translation.slug = :slug')
->andWhere(':channel MEMBER OF p.channels')
->andWhere('p.enabled = true')
->setParameter('channel', $channel)
->setParameter('locale', $locale)
->setParameter('slug', $slug)
;
}
}
4 changes: 3 additions & 1 deletion src/Repository/PageRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ interface PageRepositoryInterface extends RepositoryInterface
{
public function createListQueryBuilder(string $localeCode): QueryBuilder;

public function existsOneByChannelAndSlug(ChannelInterface $channel, ?string $locale, string $slug): bool;
public function existsOneByChannelAndSlug(ChannelInterface $channel, ?string $locale, string $slug, array $excludedPages = []): bool;

public function existsOneEnabledByChannelAndSlug(ChannelInterface $channel, ?string $locale, string $slug): bool;

public function findOneEnabledBySlugAndChannelCode(string $slug, string $localeCode, string $channelCode): ?PageInterface;
}
12 changes: 12 additions & 0 deletions src/Resources/config/sylius/grid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ sylius_grid:
type: string
label: monsieurbiz_cms_page.ui.form.title
sortable: translation.title
channels:
type: twig
label: sylius.ui.channels
options:
template: '@SyliusAdmin/Grid/Field/_channels.html.twig'
enabled:
type: twig
label: monsieurbiz_cms_page.ui.form.enabled
Expand Down Expand Up @@ -56,3 +61,10 @@ sylius_grid:
label: monsieurbiz_cms_page.ui.form.content
options:
fields: [translation.content]
channel:
type: entity
label: sylius.ui.channel
options:
fields: [channels.id]
form_options:
class: "%sylius.model.channel.class%"
2 changes: 2 additions & 0 deletions src/Resources/config/validation/Page.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ MonsieurBiz\SyliusCmsPagePlugin\Entity\Page:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
fields: [code]
groups: [monsieurbiz]
- MonsieurBiz\SyliusCmsPagePlugin\Validator\Constraints\UniqueSlugByChannel:
groups: [monsieurbiz]
properties:
code:
- NotBlank:
Expand Down
5 changes: 0 additions & 5 deletions src/Resources/config/validation/PageTranslation.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
MonsieurBiz\SyliusCmsPagePlugin\Entity\PageTranslation:
constraints:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
fields: [slug, locale]
errorPath: slug
groups: [monsieurbiz]
properties:
title:
- NotBlank:
Expand Down
4 changes: 4 additions & 0 deletions src/Resources/translations/validators.en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
monsieurbiz_cms_page:
ui:
slug:
unique: 'This slug is already used for another page with channel %channel% and locale %locale%.'
4 changes: 4 additions & 0 deletions src/Resources/translations/validators.fr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
monsieurbiz_cms_page:
ui:
slug:
unique: 'Ce slug est déjà utilisé pour une autre page avec le canal %channel% et la locale %locale%.'
2 changes: 1 addition & 1 deletion src/Routing/PageSlugConditionChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function __construct(
public function isPageSlug(string $slug): bool
{
try {
return $this->pageRepository->existsOneByChannelAndSlug(
return $this->pageRepository->existsOneEnabledByChannelAndSlug(
$this->channelContext->getChannel(),
$this->localeContext->getLocaleCode(),
$slug
Expand Down
31 changes: 31 additions & 0 deletions src/Validator/Constraints/UniqueSlugByChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of Monsieur Biz' Cms Page plugin for Sylius.
*
* (c) Monsieur Biz <[email protected]>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace MonsieurBiz\SyliusCmsPagePlugin\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

final class UniqueSlugByChannel extends Constraint
{
public string $message = 'monsieurbiz_cms_page.ui.slug.unique';

public function validatedBy(): string
{
return self::class . 'Validator';
}

public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
63 changes: 63 additions & 0 deletions src/Validator/Constraints/UniqueSlugByChannelValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of Monsieur Biz' Cms Page plugin for Sylius.
*
* (c) Monsieur Biz <[email protected]>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace MonsieurBiz\SyliusCmsPagePlugin\Validator\Constraints;

use MonsieurBiz\SyliusCmsPagePlugin\Entity\PageInterface;
use MonsieurBiz\SyliusCmsPagePlugin\Repository\PageRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;

final class UniqueSlugByChannelValidator extends ConstraintValidator
{
private PageRepositoryInterface $pageRepository;

public function __construct(PageRepositoryInterface $pageRepository)
{
$this->pageRepository = $pageRepository;
}

/**
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*
* @param mixed $value
*/
public function validate($value, Constraint $constraint): void
{
/** @var PageInterface $value */
Assert::isInstanceOf($value, PageInterface::class);
/** @var UniqueSlugByChannel $constraint */
Assert::isInstanceOf($constraint, UniqueSlugByChannel::class);

// Check if the slug is unique for each channel and locale
foreach ($value->getTranslations() as $translation) {
foreach ($value->getChannels() as $channel) {
if ($this->pageRepository->existsOneByChannelAndSlug(
$channel,
$translation->getLocale(),
$translation->getSlug(),
$value->getId() ? [$value] : []
)) {
$this->context->buildViolation($constraint->message, [
'%channel%' => $channel->getCode(),
'%locale%' => $translation->getLocale(),
])
->atPath(sprintf('translations[%s].slug', $translation->getLocale()))
->addViolation()
;
}
}
}
}
}

0 comments on commit 9821223

Please sign in to comment.