Skip to content

Commit

Permalink
Feature/referrer without query vars (#17)
Browse files Browse the repository at this point in the history
* Cleaning vars from context request in AdminUrlGenerator
* Remove query variables from referrer in association field value
* Readme update
  • Loading branch information
marcin-jozwikowski authored Aug 17, 2023
1 parent 880dde1 commit b1c9cc2
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 19 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
```shell
composer require marcin-jozwikowski/easy-admin-pretty-urls
```

1. Enable the bundle by adding it to your `config/bundles.php`
```php
[
...
MarcinJozwikowski\EasyAdminPrettyUrls\EasyAdminPrettyUrlsBundle::class => ['all' => true],
]
```

1. Add a routes set pointing to a directory containing your Controllers
```yaml
pretty_routes_name:
Expand All @@ -25,6 +27,16 @@
```
The `resource` is a directory path relative to your projects root directory. Type must always equal to `pretty_routes`. See _Fine-tuning_ / _Define routes manually_ section to learn how this step can be ommitted.

1. Make your main DashboardController extend `\MarcinJozwikowski\EasyAdminPrettyUrls\Controller\PrettyDashboardController` or manually override the a default template like so:
```php
public function configureCrud(): Crud
{
return parent::configureCrud()
->overrideTemplate('crud/field/association', '@EasyAdminPrettyUrls/crud/field/association.html.twig');
}
```


## Configuration

The following parameters are in use:
Expand Down
7 changes: 6 additions & 1 deletion config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ services:
- '../src/DependencyInjection/'
- 'EasyAdminPrettyUrlsBundle.php'

PrettyUrlFakedContext:
class: \EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider
arguments:
- '@MarcinJozwikowski\EasyAdminPrettyUrls\Provider\PrettyAdminContext'

EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator:
arguments:
- '@EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider'
- '@PrettyUrlFakedContext'
- '@MarcinJozwikowski\EasyAdminPrettyUrls\Routing\PrettyUrlsGenerator'
- '@EasyCorp\Bundle\EasyAdminBundle\Registry\DashboardControllerRegistry'
- '@EasyCorp\Bundle\EasyAdminBundle\Registry\CrudControllerRegistry'
Expand Down
17 changes: 17 additions & 0 deletions src/Controller/PrettyDashboardController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace MarcinJozwikowski\EasyAdminPrettyUrls\Controller;

use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;

abstract class PrettyDashboardController extends AbstractDashboardController
{
public function configureCrud(): Crud
{
return parent::configureCrud()
->overrideTemplate('crud/field/association', '@EasyAdminPrettyUrls/crud/field/association.html.twig');
}
}
44 changes: 44 additions & 0 deletions src/Provider/PrettyAdminContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace MarcinJozwikowski\EasyAdminPrettyUrls\Provider;

use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use MarcinJozwikowski\EasyAdminPrettyUrls\Routing\PrettyUrlsGenerator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

class PrettyAdminContext extends RequestStack
{
public function __construct(
private RequestStack $requestStack,
private bool $prettyUrlsIncludeMenuIndex,
) {
}

public function getCurrentRequest(): ?Request
{
if ($this->requestStack->getCurrentRequest()->get(EA::CONTEXT_REQUEST_ATTRIBUTE)) {
$result = clone $this->requestStack->getCurrentRequest();
$result->attributes = clone $result->attributes;
/** @var AdminContext $adminContext */
$adminContext = $result->get(EA::CONTEXT_REQUEST_ATTRIBUTE);
$contextRequest = clone $adminContext->getRequest();

$query = clone $contextRequest->query;
$query->remove(PrettyUrlsGenerator::EA_FQCN);
$query->remove(PrettyUrlsGenerator::EA_ACTION);
if ($this->prettyUrlsIncludeMenuIndex) {
$query->remove(PrettyUrlsGenerator::EA_MENU_INDEX);
$query->remove(PrettyUrlsGenerator::EA_SUBMENU_INDEX);
}
$contextRequest->query = $query;

return $result;
}

return null;
}
}
12 changes: 12 additions & 0 deletions src/Resources/views/crud/field/association.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% if 'toMany' == field.customOptions.get('associationType') %}
<span class="badge badge-secondary">{{ field.formattedValue }}</span>
{% else %}
{% if field.customOptions.get('relatedUrl') is not null %}
<a href="{{ field.customOptions.get('relatedUrl')|pretty_urls_remove_actions }}">{{ field.formattedValue }}</a>
{% else %}
{{ field.formattedValue }}
{% endif %}
{% endif %}
56 changes: 45 additions & 11 deletions src/Routing/PrettyUrlsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
/*
* This class utilizes the routes created in Loader class instead of just creating URL with all data in query
*/

class PrettyUrlsGenerator implements UrlGeneratorInterface
{
public const EA_FQCN = 'crudControllerFqcn';
public const EA_ACTION = 'crudAction';
public const EA_MENU_INDEX = 'menuIndex';
public const EA_SUBMENU_INDEX = 'submenuIndex';
public const MENU_PATH = 'menuPath';
public const EA_REFERRER = 'referrer';

public function __construct(
private RouterInterface $router,
Expand All @@ -40,21 +42,32 @@ public function getContext(): RequestContext
return $this->router->getContext();
}

public function cleanUpParametersArray(array $parameters): array
{
$prettyParams = $parameters;
unset($prettyParams[static::EA_FQCN]);
unset($prettyParams[static::EA_ACTION]);

if ($this->prettyUrlsIncludeMenuIndex && $menuIndex = $this->generateMenuIndexPart($parameters)) {
// when the route can contain menu information - remove that from the parameters
unset($prettyParams[static::EA_MENU_INDEX]);
unset($prettyParams[static::EA_SUBMENU_INDEX]);
$prettyParams[self::MENU_PATH] = $menuIndex;
}

if (isset($prettyParams[static::EA_REFERRER])) {
$prettyParams[static::EA_REFERRER] = $this->sanitizeUrl($prettyParams[static::EA_REFERRER]);
}

return $prettyParams;
}

public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
// at first check if all necessary params are provided
if (isset($parameters[static::EA_FQCN]) && isset($parameters[static::EA_ACTION])) {
if ($this->areParametersValid($parameters)) {
$prettyName = $this->generateNameFromParameters($parameters); // get name of the route to use
$prettyParams = $parameters; // copy all parameters and remove those that are defined in the route
unset($prettyParams[static::EA_FQCN]);
unset($prettyParams[static::EA_ACTION]);

if ($this->prettyUrlsIncludeMenuIndex && $menuIndex = $this->generateMenuIndexPart($parameters)) {
// when the route can contain menu information - remove that from the parameters
unset($prettyParams[static::EA_MENU_INDEX]);
unset($prettyParams[static::EA_SUBMENU_INDEX]);
$prettyParams[self::MENU_PATH] = $menuIndex;
}
$prettyParams = $this->cleanUpParametersArray($parameters); // copy all parameters and remove those that are defined in the route

try {
// generate the url using the route and any remaining parameters
Expand Down Expand Up @@ -89,4 +102,25 @@ private function generateMenuIndexPart(array $parameters): ?string
$parameters[self::EA_SUBMENU_INDEX] ?? -1,
);
}

public function sanitizeUrl(string $value): string
{
$mainQueryReferrerUrlQuery = parse_url($value, PHP_URL_QUERY); // we're just interested in the query part of the URL
if (empty($mainQueryReferrerUrlQuery)) {
return $value;
}

$referrerParams = [];
parse_str(urldecode($mainQueryReferrerUrlQuery), $referrerParams); // decode the parameters to be usable
if ($this->areParametersValid($referrerParams)) {
return $this->generate('easyadmin', $referrerParams); // generate the pretty URL for given parameters. Name is just for fallback
}

return $value;
}

private function areParametersValid(array $parameters): bool
{
return isset($parameters[static::EA_FQCN]) && isset($parameters[static::EA_ACTION]);
}
}
47 changes: 47 additions & 0 deletions src/Twig/PrettyUrlsExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace MarcinJozwikowski\EasyAdminPrettyUrls\Twig;

use MarcinJozwikowski\EasyAdminPrettyUrls\Routing\PrettyUrlsGenerator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class PrettyUrlsExtension extends AbstractExtension
{
public function __construct(
protected PrettyUrlsGenerator $prettyUrlsGenerator,
) {
}

public function getFilters(): array
{
return [
new TwigFilter('pretty_urls_remove_actions', [$this, 'prettyUrlsRemoveActions'], ['is_safe' => ['html']]),
];
}

public function prettyUrlsRemoveActions(string $value): string
{
$mainUrlQuery = parse_url($value, PHP_URL_QUERY); // split the main url into part
if (empty($mainUrlQuery)) {
return $value;
}
$mainQueryParams = [];
parse_str($mainUrlQuery, $mainQueryParams); // parse the query part into key-value array
if (empty($mainQueryParams[PrettyUrlsGenerator::EA_REFERRER])) {
return $value;
}

$matches = [];
preg_match('#referrer=([\d\w/,-?%]+)[&]?#', $value, $matches); // match the original referrer
if ($matches[1]) {
$finalReferrer = $this->prettyUrlsGenerator->sanitizeUrl($mainQueryParams[PrettyUrlsGenerator::EA_REFERRER]);

return str_replace($matches[1], $finalReferrer, $value); // replace the old referrer with the new one
}

return $value;
}
}
26 changes: 26 additions & 0 deletions tests/PrettyDashboardControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace MarcinJozwikowski\EasyAdminPrettyUrls\Tests;

use MarcinJozwikowski\EasyAdminPrettyUrls\Tests\data\ExampleClassImplementingDashboard;
use PHPUnit\Framework\TestCase;

/**
* @covers \MarcinJozwikowski\EasyAdminPrettyUrls\Controller\PrettyDashboardController
*/
class PrettyDashboardControllerTest extends TestCase
{
public function testConfigureCrud(): void
{
$controller = new ExampleClassImplementingDashboard();

self::assertEquals(
[
'crud/field/association' => '@EasyAdminPrettyUrls/crud/field/association.html.twig',
],
$controller->configureCrud()->getAsDto()->getOverriddenTemplates(),
);
}
}
72 changes: 72 additions & 0 deletions tests/PrettyUrlsExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace MarcinJozwikowski\EasyAdminPrettyUrls\Tests;

use MarcinJozwikowski\EasyAdminPrettyUrls\Routing\PrettyUrlsGenerator;
use MarcinJozwikowski\EasyAdminPrettyUrls\Twig\PrettyUrlsExtension;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Twig\TwigFilter;

use function PHPUnit\Framework\assertEquals;

/**
* @covers \MarcinJozwikowski\EasyAdminPrettyUrls\Twig\PrettyUrlsExtension
*/
class PrettyUrlsExtensionTest extends TestCase
{
private PrettyUrlsGenerator|MockObject $generator;
private PrettyUrlsExtension $tested;

public function setUp(): void
{
$this->generator = $this->createMock(PrettyUrlsGenerator::class);
$this->tested = new PrettyUrlsExtension($this->generator);
}

public function testGetFilters(): void
{
$result = $this->tested->getFilters();
self::assertIsArray($result);
self::assertCount(1, $result);
self::assertInstanceOf(TwigFilter::class, $result[0]);
self::assertEquals('pretty_urls_remove_actions', $result[0]->getName());
self::assertIsArray($result[0]->getCallable());
self::assertEquals($this->tested, $result[0]->getCallable()[0]);
self::assertEquals('prettyUrlsRemoveActions', $result[0]->getCallable()[1]);
}

/**
* @dataProvider removeActionData
*/
public function testRemoveAction(string $url, string $expected, ?array $sanitizeArguments, string $sanitizeResult): void
{
if ($sanitizeArguments) {
$this->generator->method('sanitizeUrl')
->with(...$sanitizeArguments)
->willReturn($sanitizeResult);
}

$result = $this->tested->prettyUrlsRemoveActions($url);

assertEquals($expected, $result);
}

public function removeActionData(): array
{
$randomPath = substr(sha1(random_bytes(8)), 1, 4).'/'.substr(sha1(random_bytes(8)), 1, 6);

return [
['', '', null, ''],
['https://some.url', 'https://some.url', null, ''],
['/some/path', '/some/path', null, ''],
['/some/path?page=12', '/some/path?page=12', null, ''],
['/some/path?page=12&referrer=', '/some/path?page=12&referrer=', null, ''],
['/some/path?page=12&referrer=/ref', '/some/path?page=12&referrer='.$randomPath, ['/ref'], $randomPath],
['/some/path?page=12&referrer=/ref?action=index', '/some/path?page=12&referrer='.$randomPath, ['/ref?action=index'], $randomPath],
['/some/path?page=12&referrer=/'.$randomPath, '/some/path?page=12&referrer='.$randomPath, ['/'.$randomPath], $randomPath],
];
}
}
Loading

0 comments on commit b1c9cc2

Please sign in to comment.