├── .codecov.yml
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .php-cs-fixer.dist.php
├── LICENSE
├── README.md
├── composer.json
├── config
├── doctrine-mapping
│ ├── NotFound.orm.xml
│ └── Redirect.orm.xml
├── form.xml
├── not_found.xml
├── redirect.xml
├── remove_not_found_subscriber.xml
└── validation.xml
├── phpunit.xml.dist
├── src
├── DependencyInjection
│ ├── Configuration.php
│ └── ZenstruckRedirectExtension.php
├── EventListener
│ ├── CreateNotFoundListener.php
│ ├── Doctrine
│ │ └── RemoveNotFoundSubscriber.php
│ ├── NotFoundListener.php
│ └── RedirectOnNotFoundListener.php
├── Form
│ └── Type
│ │ └── RedirectType.php
├── Model
│ ├── NotFound.php
│ └── Redirect.php
├── Service
│ ├── NotFoundManager.php
│ └── RedirectManager.php
└── ZenstruckRedirectBundle.php
└── translations
├── ZenstruckRedirectBundle.de.yml
├── ZenstruckRedirectBundle.en.yml
├── validators.de.yml
└── validators.en.yml
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: auto
6 | threshold: 1%
7 | patch:
8 | default:
9 | target: auto
10 | threshold: 50%
11 |
12 | comment: false
13 | github_checks:
14 | annotations: false
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.yml]
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /.github export-ignore
2 | /tests export-ignore
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /composer.lock
2 | /phpunit.xml
3 | /vendor/
4 | /var/
5 | /.phpunit.result.cache
6 | /.php-cs-fixer.cache
7 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | ['all' => true],
32 | ];
33 | ```
34 |
35 | ## Configuration
36 |
37 | **NOTE:** A `NotFound` or `Redirect` or both must be configured.
38 |
39 | ### Redirect
40 |
41 | 1. Create your redirect class inheriting the MappedSuperClass this bundle provides:
42 |
43 | ```php
44 | namespace App\Entity;
45 |
46 | use Zenstruck\RedirectBundle\Model\Redirect as BaseRedirect;
47 | use Doctrine\ORM\Mapping as ORM;
48 |
49 | /**
50 | * @ORM\Entity
51 | * @ORM\Table(name="redirects")
52 | */
53 | class Redirect extends BaseRedirect
54 | {
55 | /**
56 | * @ORM\Id
57 | * @ORM\Column(type="integer")
58 | * @ORM\GeneratedValue(strategy="AUTO")
59 | */
60 | private $id;
61 | }
62 | ```
63 |
64 | 2. Set this class in your `zenstruck_redirect.yml`:
65 |
66 | ```yaml
67 | zenstruck_redirect:
68 | redirect_class: App\Entity\Redirect
69 | ```
70 |
71 | 3. Update your schema (or use a migration):
72 | ```bash
73 | bin/console doctrine:schema:update --force
74 | ```
75 |
76 | ### NotFound
77 |
78 | 1. Create your not found class inheriting the MappedSuperClass this bundle provides:
79 |
80 | ```php
81 | namespace App\Entity;
82 |
83 | use Zenstruck\RedirectBundle\Model\NotFound as BaseNotFound;
84 | use Doctrine\ORM\Mapping as ORM;
85 |
86 | /**
87 | * @ORM\Entity
88 | * @ORM\Table(name="not_founds")
89 | */
90 | class NotFound extends BaseNotFound
91 | {
92 | /**
93 | * @ORM\Id
94 | * @ORM\Column(type="integer")
95 | * @ORM\GeneratedValue(strategy="AUTO")
96 | */
97 | private $id;
98 | }
99 | ```
100 |
101 | 2. Set this class in your `zenstruck_redirect.yml`:
102 |
103 | ```yaml
104 | zenstruck_redirect:
105 | not_found_class: App\Entity\NotFound
106 | ```
107 |
108 | 3. Update your schema (or use a migration):
109 | ```bash
110 | bin/console doctrine:schema:update --force
111 | ```
112 |
113 | ## Form Type
114 |
115 | This bundle provides a form type (`zenstruck_redirect`) for creating/editing redirects.
116 |
117 | ```php
118 | $redirect = // ...
119 | $form = $this->createForm('zenstruck_redirect', $redirect);
120 | ```
121 |
122 | You may want to disable the `source` field for already created redirects:
123 |
124 | ```php
125 | // new action
126 | $redirect = new Redirect();
127 | $form = $this->createForm('zenstruck_redirect', $redirect);
128 |
129 | // edit action
130 | $redirect = // get from database
131 | $form = $this->createForm('zenstruck_redirect', $redirect, array('disable_source' => true));
132 | ```
133 |
134 | ## Full Default Configuration
135 |
136 | ```yaml
137 | zenstruck_redirect:
138 | redirect_class: ~ # Required if not_found_class is not set
139 | not_found_class: ~ # Required if redirect_class is not set
140 | model_manager_name: ~
141 |
142 | # When enabled, when a redirect is updated or created, the NotFound entities with a matching path are removed.
143 | remove_not_founds: true
144 | ```
145 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zenstruck/redirect-bundle",
3 | "description": "Store redirects for your site and keeps statistics on redirects and 404 errors",
4 | "keywords": ["redirect"],
5 | "homepage": "https://github.com/kbond/ZenstruckRedirectBundle",
6 | "type": "symfony-bundle",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Kevin Bond",
11 | "email": "kevinbond@gmail.com"
12 | }
13 | ],
14 | "require": {
15 | "php": ">=8.0",
16 | "doctrine/doctrine-bundle": "^2.6",
17 | "doctrine/orm": "^2.6|^3.0",
18 | "symfony/framework-bundle": "^5.4|^6.0|^7.0"
19 | },
20 | "require-dev": {
21 | "matthiasnoback/symfony-dependency-injection-test": "^4.3 || ^5.0",
22 | "symfony/browser-kit": "^5.4|^6.0|^7.0",
23 | "symfony/form": "^5.4|^6.0|^7.0",
24 | "symfony/phpunit-bridge": "^6.0|^7.0",
25 | "symfony/translation": "^5.4|^6.0|^7.0",
26 | "symfony/validator": "^5.4|^6.0|^7.0",
27 | "zenstruck/foundry": "^1.38.3",
28 | "phpunit/phpunit": "9.6.13",
29 | "zenstruck/browser": "^1.6"
30 | },
31 | "autoload": {
32 | "psr-4": { "Zenstruck\\RedirectBundle\\": "src/" }
33 | },
34 | "autoload-dev": {
35 | "psr-4": { "Zenstruck\\RedirectBundle\\Tests\\": "tests/" }
36 | },
37 | "minimum-stability": "dev",
38 | "prefer-stable": true
39 | }
40 |
--------------------------------------------------------------------------------
/config/doctrine-mapping/NotFound.orm.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/config/doctrine-mapping/Redirect.orm.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/config/form.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 | %zenstruck_redirect.redirect_class%
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/config/not_found.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Zenstruck\RedirectBundle\Service\NotFoundManager
9 | Zenstruck\RedirectBundle\EventListener\CreateNotFoundListener
10 |
11 |
12 |
13 |
14 | %zenstruck_redirect.not_found_class%
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/config/redirect.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Zenstruck\RedirectBundle\Service\RedirectManager
9 | Zenstruck\RedirectBundle\EventListener\RedirectOnNotFoundListener
10 |
11 |
12 |
13 |
14 | %zenstruck_redirect.redirect_class%
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/config/remove_not_found_subscriber.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Zenstruck\RedirectBundle\EventListener\Doctrine\RemoveNotFoundSubscriber
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/config/validation.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | tests
22 |
23 |
24 |
25 |
26 |
27 | src
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\DependencyInjection;
13 |
14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder;
15 | use Symfony\Component\Config\Definition\ConfigurationInterface;
16 |
17 | /**
18 | * @author Kevin Bond
19 | *
20 | * @internal
21 | */
22 | final class Configuration implements ConfigurationInterface
23 | {
24 | public function getConfigTreeBuilder(): TreeBuilder
25 | {
26 | $treeBuilder = new TreeBuilder('zenstruck_redirect');
27 |
28 | $rootNode = $treeBuilder->getRootNode();
29 |
30 | $rootNode
31 | ->children()
32 | ->scalarNode('redirect_class')
33 | ->defaultNull()
34 | ->validate()
35 | ->ifTrue(fn($value) => !\is_subclass_of($value, 'Zenstruck\RedirectBundle\Model\Redirect'))
36 | ->thenInvalid('"redirect_class" must be an instance of "Zenstruck\RedirectBundle\Model\Redirect"')
37 | ->end()
38 | ->end()
39 | ->scalarNode('not_found_class')
40 | ->defaultNull()
41 | ->validate()
42 | ->ifTrue(fn($value) => !\is_subclass_of($value, 'Zenstruck\RedirectBundle\Model\NotFound'))
43 | ->thenInvalid('"not_found_class" must be an instance of "Zenstruck\RedirectBundle\Model\NotFound"')
44 | ->end()
45 | ->end()
46 | ->booleanNode('remove_not_founds')
47 | ->info('When enabled, when a redirect is updated or created, the NotFound entites with a matching path are removed.')
48 | ->defaultTrue()
49 | ->end()
50 | ->scalarNode('model_manager_name')->defaultNull()->end()
51 | ->end()
52 | ;
53 |
54 | return $treeBuilder;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/DependencyInjection/ZenstruckRedirectExtension.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\DependencyInjection;
13 |
14 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
15 | use Symfony\Component\Config\FileLocator;
16 | use Symfony\Component\DependencyInjection\ContainerBuilder;
17 | use Symfony\Component\DependencyInjection\Loader;
18 | use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
19 |
20 | /**
21 | * @author Kevin Bond
22 | *
23 | * @internal
24 | */
25 | final class ZenstruckRedirectExtension extends ConfigurableExtension
26 | {
27 | protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void
28 | {
29 | if (null === $mergedConfig['redirect_class'] && null === $mergedConfig['not_found_class']) {
30 | throw new InvalidConfigurationException('A "redirect_class" or "not_found_class" must be set for "zenstruck_redirect".');
31 | }
32 |
33 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
34 | $modelManagerName = $mergedConfig['model_manager_name'] ?: 'default';
35 |
36 | $container->setAlias('zenstruck_redirect.entity_manager', \sprintf('doctrine.orm.%s_entity_manager', $modelManagerName));
37 |
38 | if (null !== $mergedConfig['redirect_class']) {
39 | $container->setParameter('zenstruck_redirect.redirect_class', $mergedConfig['redirect_class']);
40 |
41 | $loader->load('redirect.xml');
42 | $loader->load('form.xml');
43 | }
44 |
45 | if (null !== $mergedConfig['not_found_class']) {
46 | $container->setParameter('zenstruck_redirect.not_found_class', $mergedConfig['not_found_class']);
47 |
48 | $loader->load('not_found.xml');
49 | }
50 |
51 | if ($mergedConfig['remove_not_founds'] && null !== $mergedConfig['not_found_class'] && null !== $mergedConfig['redirect_class']) {
52 | $loader->load('remove_not_found_subscriber.xml');
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/EventListener/CreateNotFoundListener.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\EventListener;
13 |
14 | use Symfony\Component\HttpKernel\Event\ExceptionEvent;
15 | use Zenstruck\RedirectBundle\Service\NotFoundManager;
16 |
17 | /**
18 | * @author Kevin Bond
19 | *
20 | * @internal
21 | */
22 | final class CreateNotFoundListener extends NotFoundListener
23 | {
24 | public function __construct(private NotFoundManager $notFoundManager)
25 | {
26 | }
27 |
28 | public function onKernelException(ExceptionEvent $event): void
29 | {
30 | if (!$this->isNotFoundException($event)) {
31 | return;
32 | }
33 |
34 | $this->notFoundManager->createFromRequest($event->getRequest());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/EventListener/Doctrine/RemoveNotFoundSubscriber.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\EventListener\Doctrine;
13 |
14 | use Doctrine\Persistence\Event\LifecycleEventArgs;
15 | use Symfony\Component\DependencyInjection\ContainerInterface;
16 | use Zenstruck\RedirectBundle\Model\Redirect;
17 | use Zenstruck\RedirectBundle\Service\NotFoundManager;
18 |
19 | /**
20 | * @author Kevin Bond
21 | *
22 | * @internal
23 | */
24 | final class RemoveNotFoundSubscriber
25 | {
26 | public function __construct(private ContainerInterface $container)
27 | {
28 | }
29 |
30 | public function postUpdate(LifecycleEventArgs $args): void
31 | {
32 | $this->remoteNotFoundForRedirect($args);
33 | }
34 |
35 | public function postPersist(LifecycleEventArgs $args): void
36 | {
37 | $this->remoteNotFoundForRedirect($args);
38 | }
39 |
40 | private function remoteNotFoundForRedirect(LifecycleEventArgs $args): void
41 | {
42 | $entity = $args->getObject();
43 |
44 | if (!$entity instanceof Redirect) {
45 | return;
46 | }
47 |
48 | $this->getNotFoundManager()->removeForRedirect($entity);
49 | }
50 |
51 | private function getNotFoundManager(): NotFoundManager
52 | {
53 | return $this->container->get('zenstruck_redirect.not_found_manager');
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/EventListener/NotFoundListener.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\EventListener;
13 |
14 | use Symfony\Component\HttpKernel\Event\ExceptionEvent;
15 | use Symfony\Component\HttpKernel\Exception\HttpException;
16 | use Symfony\Component\HttpKernel\HttpKernelInterface;
17 |
18 | /**
19 | * @author Kevin Bond
20 | *
21 | * @internal
22 | */
23 | abstract class NotFoundListener
24 | {
25 | abstract public function onKernelException(ExceptionEvent $event);
26 |
27 | final protected function isNotFoundException(ExceptionEvent $event): bool
28 | {
29 | if (HttpKernelInterface::MAIN_REQUEST !== $event->getRequestType()) {
30 | return false;
31 | }
32 |
33 | $exception = $event->getThrowable();
34 |
35 | return $exception instanceof HttpException && 404 === $exception->getStatusCode();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/EventListener/RedirectOnNotFoundListener.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\EventListener;
13 |
14 | use Symfony\Component\HttpFoundation\RedirectResponse;
15 | use Symfony\Component\HttpKernel\Event\ExceptionEvent;
16 | use Zenstruck\RedirectBundle\Service\RedirectManager;
17 |
18 | /**
19 | * @author Kevin Bond
20 | *
21 | * @internal
22 | */
23 | final class RedirectOnNotFoundListener extends NotFoundListener
24 | {
25 | public function __construct(private RedirectManager $redirectManager)
26 | {
27 | }
28 |
29 | public function onKernelException(ExceptionEvent $event): void
30 | {
31 | if (!$this->isNotFoundException($event)) {
32 | return;
33 | }
34 |
35 | if (null === $redirect = $this->redirectManager->findAndUpdate($event->getRequest()->getPathInfo())) {
36 | return;
37 | }
38 |
39 | $event->setResponse(new RedirectResponse(
40 | $redirect->getDestination(),
41 | $redirect->isPermanent() ? 301 : 302,
42 | ));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Form/Type/RedirectType.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\Form\Type;
13 |
14 | use Symfony\Component\Form\AbstractType;
15 | use Symfony\Component\Form\FormBuilderInterface;
16 | use Symfony\Component\Form\FormInterface;
17 | use Symfony\Component\OptionsResolver\OptionsResolver;
18 |
19 | /**
20 | * @author Kevin Bond
21 | */
22 | final class RedirectType extends AbstractType
23 | {
24 | /**
25 | * @param string $class The Redirect class name
26 | */
27 | public function __construct(private string $class)
28 | {
29 | }
30 |
31 | public function buildForm(FormBuilderInterface $builder, array $options): void
32 | {
33 | $builder
34 | ->add('source', null, [
35 | 'label' => 'form.source',
36 | 'translation_domain' => 'ZenstruckRedirectBundle',
37 | 'disabled' => $options['disable_source'],
38 | ])
39 |
40 | ->add('destination', null, [
41 | 'label' => 'form.destination',
42 | 'translation_domain' => 'ZenstruckRedirectBundle',
43 | ])
44 | ;
45 | }
46 |
47 | public function getBlockPrefix(): string
48 | {
49 | return 'zenstruck_redirect';
50 | }
51 |
52 | public function configureOptions(OptionsResolver $resolver): void
53 | {
54 | $class = $this->class;
55 |
56 | $resolver->setDefaults([
57 | 'data_class' => $this->class,
58 | 'disable_source' => false,
59 | 'empty_data' => fn(FormInterface $form) => new $class(
60 | $form->get('source')->getData(),
61 | $form->get('destination')->getData(),
62 | ),
63 | ]);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Model/NotFound.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\Model;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | abstract class NotFound
18 | {
19 | protected ?string $path;
20 |
21 | protected ?\DateTime $timestamp;
22 |
23 | protected ?string $referer;
24 |
25 | public function __construct(?string $path, protected ?string $fullUrl, ?string $referer = null, ?\DateTime $timestamp = null)
26 | {
27 | if (null === $timestamp) {
28 | $timestamp = new \DateTime('now');
29 | }
30 |
31 | $path = \trim((string) $path) ?: null;
32 |
33 | if (null !== $path) {
34 | $parse_url = \parse_url($path, \PHP_URL_PATH);
35 |
36 | if (null != $parse_url) {
37 | $path = '/'.\ltrim($parse_url, '/');
38 | } else {
39 | $path = '/';
40 | }
41 | }
42 |
43 | $this->path = $path;
44 | $this->referer = $referer;
45 | $this->timestamp = $timestamp;
46 | }
47 |
48 | public function getPath(): ?string
49 | {
50 | return $this->path;
51 | }
52 |
53 | public function getFullUrl(): ?string
54 | {
55 | return $this->fullUrl;
56 | }
57 |
58 | public function getTimestamp(): \DateTime
59 | {
60 | return $this->timestamp;
61 | }
62 |
63 | public function getReferer(): ?string
64 | {
65 | return $this->referer;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Model/Redirect.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\Model;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | abstract class Redirect
18 | {
19 | protected ?string $source;
20 |
21 | protected ?string $destination;
22 |
23 | protected bool $permanent;
24 |
25 | protected int $count = 0;
26 |
27 | protected ?\DateTime $lastAccessed = null;
28 |
29 | public function __construct(?string $source, ?string $destination, bool $permanent = true)
30 | {
31 | $this->setSource($source);
32 | $this->setDestination($destination);
33 | $this->setPermanent($permanent);
34 | }
35 |
36 | public static function createFromNotFound(NotFound $notFound, string $destination, bool $permanent = true): static
37 | {
38 | return new static($notFound->getPath(), $destination, $permanent);
39 | }
40 |
41 | public function getSource(): ?string
42 | {
43 | return $this->source;
44 | }
45 |
46 | public function setSource(?string $source): void
47 | {
48 | $source = \trim((string) $source) ?: null;
49 |
50 | if (null !== $source) {
51 | $source = $this->createAbsoluteUri($source);
52 | }
53 |
54 | $this->source = $source;
55 | }
56 |
57 | public function getDestination(): ?string
58 | {
59 | return $this->destination;
60 | }
61 |
62 | public function setDestination(?string $destination): void
63 | {
64 | $destination = \trim((string) $destination) ?: null;
65 |
66 | if (null !== $destination && null === \parse_url($destination, \PHP_URL_SCHEME)) {
67 | $destination = $this->createAbsoluteUri($destination, true);
68 | }
69 |
70 | $this->destination = $destination;
71 | }
72 |
73 | public function isPermanent(): bool
74 | {
75 | return $this->permanent;
76 | }
77 |
78 | public function setPermanent(bool $permanent): void
79 | {
80 | $this->permanent = $permanent;
81 | }
82 |
83 | public function getCount(): int
84 | {
85 | return $this->count;
86 | }
87 |
88 | public function increaseCount(int $amount = 1): void
89 | {
90 | $this->count += $amount;
91 | }
92 |
93 | public function getLastAccessed(): ?\DateTime
94 | {
95 | return $this->lastAccessed;
96 | }
97 |
98 | public function updateLastAccessed(?\DateTime $time = null): void
99 | {
100 | if (null === $time) {
101 | $time = new \DateTime('now');
102 | }
103 |
104 | $this->lastAccessed = $time;
105 | }
106 |
107 | protected function createAbsoluteUri(string $path, bool $allowQueryString = false): string
108 | {
109 | $parse_url = \parse_url($path, \PHP_URL_PATH);
110 |
111 | if (null != $parse_url) {
112 | $value = '/'.\ltrim($parse_url, '/');
113 | } else {
114 | $value = '/';
115 | }
116 |
117 | if ($allowQueryString && $query = \parse_url($path, \PHP_URL_QUERY)) {
118 | $value .= '?'.$query;
119 | }
120 |
121 | return $value;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/Service/NotFoundManager.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\Service;
13 |
14 | use Doctrine\Persistence\ObjectManager;
15 | use Symfony\Component\HttpFoundation\Request;
16 | use Zenstruck\RedirectBundle\Model\NotFound;
17 | use Zenstruck\RedirectBundle\Model\Redirect;
18 |
19 | /**
20 | * @author Kevin Bond
21 | */
22 | class NotFoundManager
23 | {
24 | /**
25 | * @param string $class The NotFound class name
26 | */
27 | public function __construct(private string $class, private ObjectManager $om)
28 | {
29 | }
30 |
31 | public function createFromRequest(Request $request): NotFound
32 | {
33 | $notFound = new $this->class(
34 | $request->getPathInfo(),
35 | $request->getUri(),
36 | $request->server->get('HTTP_REFERER'),
37 | );
38 |
39 | $this->om->persist($notFound);
40 | $this->om->flush();
41 |
42 | return $notFound;
43 | }
44 |
45 | /**
46 | * Deletes NotFound entities for a Redirect's path.
47 | */
48 | public function removeForRedirect(Redirect $redirect): void
49 | {
50 | $notFounds = $this->om->getRepository($this->class)->findBy(['path' => $redirect->getSource()]);
51 |
52 | foreach ($notFounds as $notFound) {
53 | $this->om->remove($notFound);
54 | }
55 |
56 | $this->om->flush();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Service/RedirectManager.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle\Service;
13 |
14 | use Doctrine\Persistence\ObjectManager;
15 | use Zenstruck\RedirectBundle\Model\Redirect;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | class RedirectManager
21 | {
22 | /**
23 | * @param string $class The Redirect class name
24 | */
25 | public function __construct(private string $class, private ObjectManager $om)
26 | {
27 | }
28 |
29 | public function findAndUpdate(string $source): ?Redirect
30 | {
31 | /** @var Redirect|null $redirect */
32 | $redirect = $this->om->getRepository($this->class)->findOneBy(['source' => $source]);
33 |
34 | if (null === $redirect) {
35 | return null;
36 | }
37 |
38 | $redirect->increaseCount();
39 | $redirect->updateLastAccessed();
40 | $this->om->flush();
41 |
42 | return $redirect;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/ZenstruckRedirectBundle.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\RedirectBundle;
13 |
14 | use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass;
15 | use Symfony\Component\DependencyInjection\ContainerBuilder;
16 | use Symfony\Component\HttpKernel\Bundle\Bundle;
17 |
18 | /**
19 | * @author Kevin Bond
20 | */
21 | final class ZenstruckRedirectBundle extends Bundle
22 | {
23 | public function build(ContainerBuilder $container): void
24 | {
25 | parent::build($container);
26 |
27 | $container->addCompilerPass(
28 | DoctrineOrmMappingsPass::createXmlMappingDriver(
29 | [__DIR__.'/../config/doctrine-mapping' => 'Zenstruck\RedirectBundle\Model'],
30 | [],
31 | false,
32 | [],
33 | true,
34 | ),
35 | );
36 | }
37 |
38 | public function getPath(): string
39 | {
40 | return \dirname(__DIR__);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/translations/ZenstruckRedirectBundle.de.yml:
--------------------------------------------------------------------------------
1 | form:
2 | source: Quelle
3 | destination: Ziel
4 |
--------------------------------------------------------------------------------
/translations/ZenstruckRedirectBundle.en.yml:
--------------------------------------------------------------------------------
1 | form:
2 | source: Source
3 | destination: Destination
4 |
--------------------------------------------------------------------------------
/translations/validators.de.yml:
--------------------------------------------------------------------------------
1 | zenstruck_redirect:
2 | source:
3 | unique: Diese Quelle ist bereits in Verwendung.
4 | blank: Bitte eine Quelle angeben.
5 | destination:
6 | blank: Bitte ein Ziel angeben.
7 |
--------------------------------------------------------------------------------
/translations/validators.en.yml:
--------------------------------------------------------------------------------
1 | zenstruck_redirect:
2 | source:
3 | unique: The source is already used.
4 | blank: Please enter a source.
5 | destination:
6 | blank: Please enter a destination.
7 |
--------------------------------------------------------------------------------