diff --git a/composer.json b/composer.json index 0731cd5..b5ecfb2 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "webmozart/assert": "^1.9" }, "require-dev": { + "doctrine/doctrine-bundle": "^1.12.13", "phpspec/phpspec": "^6.1", "phpunit/phpunit": "^8.5", "roave/security-advisories": "dev-master", diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 6b6c211..19c6e9f 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,9 +5,12 @@ namespace Setono\SyliusRedirectPlugin\DependencyInjection; use Setono\SyliusRedirectPlugin\Form\Type\RedirectType; +use Setono\SyliusRedirectPlugin\Model\NotFound; use Setono\SyliusRedirectPlugin\Model\Redirect; +use Setono\SyliusRedirectPlugin\Repository\NotFoundRepository; use Setono\SyliusRedirectPlugin\Repository\RedirectRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; +use Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType; use Sylius\Bundle\ResourceBundle\SyliusResourceBundle; use Sylius\Component\Resource\Factory\Factory; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; @@ -26,7 +29,9 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->addDefaultsIfNotSet() ->children() - ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() + ->scalarNode('driver') + ->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM) + ->end() ->integerNode('remove_after') ->info('0 means disabled. If the value is > 0 then redirects that have not been accessed in the last x days will be removed') ->defaultValue(0) @@ -61,6 +66,22 @@ private function addResourcesSection(ArrayNodeDefinition $node): void ->end() ->end() ->end() + ->arrayNode('not_found') + ->addDefaultsIfNotSet() + ->children() + ->variableNode('options')->end() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(NotFound::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(NotFoundRepository::class)->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(Factory::class)->end() + ->scalarNode('form')->defaultValue(DefaultResourceType::class)->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/EventListener/NotFoundSubscriber.php b/src/EventListener/NotFoundSubscriber.php index 19d8766..eba9a04 100644 --- a/src/EventListener/NotFoundSubscriber.php +++ b/src/EventListener/NotFoundSubscriber.php @@ -5,17 +5,27 @@ namespace Setono\SyliusRedirectPlugin\EventListener; use Doctrine\Common\Persistence\ObjectManager; +use Setono\SyliusRedirectPlugin\Model\NotFoundInterface; +use Setono\SyliusRedirectPlugin\Model\RedirectionPath; +use Setono\SyliusRedirectPlugin\Repository\NotFoundRepositoryInterface; use Setono\SyliusRedirectPlugin\Resolver\RedirectionPathResolverInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Resource\Factory\FactoryInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\KernelEvents; use Webmozart\Assert\Assert; -class NotFoundSubscriber implements EventSubscriberInterface +/** + * This subscriber listens to 404s in the application. It has two outcomes, either it: + * - redirects the user to a destination specified in the redirect table, or + * - logs the 404, so the user can act upon it + */ +final class NotFoundSubscriber implements EventSubscriberInterface { /** @var ObjectManager */ private $objectManager; @@ -26,14 +36,24 @@ class NotFoundSubscriber implements EventSubscriberInterface /** @var RedirectionPathResolverInterface */ private $redirectionPathResolver; + /** @var FactoryInterface */ + private $notFoundFactory; + + /** @var NotFoundRepositoryInterface */ + private $notFoundRepository; + public function __construct( ObjectManager $objectManager, ChannelContextInterface $channelContext, - RedirectionPathResolverInterface $redirectionPathResolver + RedirectionPathResolverInterface $redirectionPathResolver, + FactoryInterface $notFoundFactory, + NotFoundRepositoryInterface $notFoundRepository ) { $this->objectManager = $objectManager; $this->channelContext = $channelContext; $this->redirectionPathResolver = $redirectionPathResolver; + $this->notFoundFactory = $notFoundFactory; + $this->notFoundRepository = $notFoundRepository; } public static function getSubscribedEvents(): array @@ -59,11 +79,30 @@ public function onKernelException(ExceptionEvent $event): void ); if ($redirectionPath->isEmpty()) { - return; + $this->log($event->getRequest()); + } else { + $this->redirect($event, $redirectionPath); } - $redirectionPath->markAsAccessed(); $this->objectManager->flush(); + } + + private function log(Request $request): void + { + $notFound = $this->notFoundRepository->findOneByUrl($request->getUri()); + if (null === $notFound) { + /** @var NotFoundInterface $notFound */ + $notFound = $this->notFoundFactory->createNew(); + + $this->objectManager->persist($notFound); + } + + $notFound->onRequest($request); + } + + private function redirect(ExceptionEvent $event, RedirectionPath $redirectionPath): void + { + $redirectionPath->markAsAccessed(); $lastRedirect = $redirectionPath->last(); Assert::notNull($lastRedirect); diff --git a/src/Model/NotFound.php b/src/Model/NotFound.php new file mode 100644 index 0000000..f4c7516 --- /dev/null +++ b/src/Model/NotFound.php @@ -0,0 +1,82 @@ +id; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } + + public function getCount(): int + { + return $this->count; + } + + public function setCount(int $count): void + { + $this->count = $count; + } + + public function isIgnored(): bool + { + return $this->ignored; + } + + public function setIgnored(bool $ignored): void + { + $this->ignored = $ignored; + } + + public function getLastRequestAt(): ?DateTimeInterface + { + return $this->lastRequestAt; + } + + public function setLastRequestAt(DateTimeInterface $lastRequestAt): void + { + $this->lastRequestAt = $lastRequestAt; + } + + public function onRequest(Request $request): void + { + ++$this->count; + $this->lastRequestAt = new DateTime(); + $this->url = $request->getUri(); + } +} diff --git a/src/Model/NotFoundInterface.php b/src/Model/NotFoundInterface.php new file mode 100644 index 0000000..6895484 --- /dev/null +++ b/src/Model/NotFoundInterface.php @@ -0,0 +1,32 @@ +createQueryBuilder('o') + ->andWhere('o.url = :url') + ->setParameter('url', $url) + ->getQuery() + ->getOneOrNullResult() + ; + } +} diff --git a/src/Repository/NotFoundRepositoryInterface.php b/src/Repository/NotFoundRepositoryInterface.php new file mode 100644 index 0000000..bb1161d --- /dev/null +++ b/src/Repository/NotFoundRepositoryInterface.php @@ -0,0 +1,13 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/event_listener.xml b/src/Resources/config/services/event_listener.xml index b1b69f4..e7c83c9 100644 --- a/src/Resources/config/services/event_listener.xml +++ b/src/Resources/config/services/event_listener.xml @@ -15,13 +15,15 @@ - - + + + + diff --git a/tests/Application/.env b/tests/Application/.env index 203a87a..5dc3167 100644 --- a/tests/Application/.env +++ b/tests/Application/.env @@ -12,7 +12,7 @@ APP_SECRET=EDITME # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For a sqlite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Set "serverVersion" to your server version to avoid edge-case exceptions and extra database calls -DATABASE_URL=mysql://root:root@127.0.0.1/setono_sylius_redirect_%kernel.environment%?serverVersion=5.5 +DATABASE_URL=mysql://root@127.0.0.1/setono_sylius_redirect_%kernel.environment%?serverVersion=5.5 ###< doctrine/doctrine-bundle ### ###> symfony/swiftmailer-bundle ###