Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow filter strategies to set parameters (for invalidating the Doctrine cache) (Case 142739) #19

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion src/DependencyInjection/OnRequestDependencyInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Webfactory\VisibilityFilterBundle\Filter\SQLFilterAsParameterCollection;
use Webfactory\VisibilityFilterBundle\Filter\Strategy\FilterStrategy;
use Webfactory\VisibilityFilterBundle\Filter\VisibilityColumnConsideringSQLFilter;
use Webfactory\VisibilityFilterBundle\Filter\VisibilityColumnRetriever;
Expand Down Expand Up @@ -62,6 +63,6 @@ public function setUpFilter(GetResponseEvent $event): void
$visibilityFilter = $filterCollection->getFilter(VisibilityColumnConsideringSQLFilter::NAME);

$visibilityFilter->injectDependencies($this->filterStrategy, $this->visibilityColumnRetriever);
$visibilityFilter->setParameter('visibility', $this->filterStrategy->getFilterSql(''));
$this->filterStrategy->addParameters(new SQLFilterAsParameterCollection($visibilityFilter));
}
}
1 change: 1 addition & 0 deletions src/DependencyInjection/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ services:
alias: 'Webfactory\VisibilityFilterBundle\Filter\Strategy\ValueInField'

Webfactory\VisibilityFilterBundle\Filter\Strategy\ValueInField:
public: true
arguments:
- 'y'

Expand Down
41 changes: 41 additions & 0 deletions src/Filter/ParameterCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Webfactory\VisibilityFilterBundle\Filter;

use Doctrine\DBAL\Types\Types;
use InvalidArgumentException;

interface ParameterCollection
{
/**
* Sets a parameter that can be used by the filter.
*
* @param string $name Name of the parameter.
* @param string $value Value of the parameter.
* @param Types::*|null $type The parameter type. If specified, the given value will be run through
* the type conversion of this type. This is usually not needed for
* strings and numeric types.
*/
public function setParameter(string $name, $value, $type = null): void;

/**
* Gets a parameter to use in a query.
*
* The function is responsible for the right output escaping to use the
* value in a query.
*
* @param string $name Name of the parameter.
*
* @return string The SQL escaped parameter to use in a query.
*
* @throws InvalidArgumentException
*/
public function getParameter(string $name);

/**
* Checks if a parameter was set for the filter.
*
* @param string $name Name of the parameter.
*/
public function hasParameter(string $name): bool;
}
33 changes: 33 additions & 0 deletions src/Filter/SQLFilterAsParameterCollection.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class feels a bit weird to me: it cannot be used without an SQLFilter and only delegates to its methods of the name. While it's nice to have the ParameterCollection as an interface to state what exactly is needed, I'm not sure if this is worthwhile?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the reasons I waited for a review – I hoped you had a strong opinion on this.

Removing the class and the interface is always easier than re-introducing it, and I think the interface makes the bundle easier to use and avoids mistakes.

On the other hand, the interface should probably be adjusted when the Doctrine SQLFilter interface changes, making this just duplicated code.

Do we need a third opinion on this?

Copy link
Member Author

@janopae janopae Jun 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realised that I forgot to add support for setParameterList to the interface/the SQLFilterAsParamterCollection class. Removing the interface and using the SQLFilter directly would avoid mistakes like this in the first place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still no strong oppinion here, but I've given it some more thought:

You want to declare the type of the parameter for FilterStrategy->addParameters($parameters). If you would declare it as SQLFilter, the FilterStrategy interface becomes tightly coupled to the SQLFilter. Which you probably want to avoid, as exchanging the FilterStrategy is your bundle's ultimate extension point.

To preserve the freedom you currently have for FilterStrategy implementations, you should keep the ParameterCollection interface (and therefore the SQLFilterAsParameterCollection implementation).

I'm not entirely convinced that the FilterStrategy is a good extension point - maybe I lack the imagination for a useful non trivial implementation without an SQLFilter. But as I consider the FilterStrategy central for the bundle, my bottom line suggestion is: keep the ParameterCollection and SQLFilterAsParameterCollection 👍

Copy link
Member Author

@janopae janopae Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to declare the type of the parameter for FilterStrategy->addParameters($parameters). If you would declare it as SQLFilter, the FilterStrategy interface becomes tightly coupled to the SQLFilter. Which you probably want to avoid, as exchanging the FilterStrategy is your bundle's ultimate extension point.

To preserve the freedom you currently have for FilterStrategy implementations, you should keep the ParameterCollection interface (and therefore the SQLFilterAsParameterCollection implementation).

I'm not entirely convinced that the FilterStrategy is a good extension point - maybe I lack the imagination for a useful non trivial implementation without an SQLFilter.

Keeping the FilterStrategy Interface decoupled from the SQLFilter was indeed a reason why I introduced this interface, but it was not because I actually wanted to allow any other implementations to be used. Using other implementations than the SQLFilter-based SQLFilterAsParameterCollection is actually not even possible using the current implementation of this bundle, as the code that calls addParameters creates an instance of this class.

The ParameterCollection interface doesn't add any freedom to users of this bundle (except in unit tests, where the interface indeed eases mocking or the creation of test implementations). FilterStrategys that don't work together with an SQLFilter behind the scenes are not possible using this bundle.

The ParameterCollection interface was rather introduced for restraining the freedom of the implementor. I wanted to disallow calling any other methods on the SQLFilterthan the ones needed for managing parameters. Also, I thought that working with an SQLFilter instance in a FilterStrategy might be confusing if you don't understand the internals of this bundle. Even if you do understand the bundle, I think it would be odd (as the FilterStrategy is there to be called by an SQLFilter implementation, not to call it).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure whether these reasons suffice as an argument for the added complexity, or whether additional documentation is needed in order to make sure that people who try to understand the implementation don't stumple over this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Webfactory\VisibilityFilterBundle\Filter;

use Doctrine\ORM\Query\Filter\SQLFilter;

class SQLFilterAsParameterCollection implements ParameterCollection
{
/**
* @var SQLFilter
*/
private $sqlFilter;

public function __construct(SQLFilter $sqlFilter)
{
$this->sqlFilter = $sqlFilter;
}

final public function setParameter($name, $value, $type = null): void
{
$this->sqlFilter->setParameter($name, $value, $type);
}

final public function getParameter($name)
{
return $this->sqlFilter->getParameter($name);
}

final public function hasParameter($name): bool
{
return $this->sqlFilter->hasParameter($name);
}
}
13 changes: 12 additions & 1 deletion src/Filter/Strategy/FilterStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,26 @@

namespace Webfactory\VisibilityFilterBundle\Filter\Strategy;

use Webfactory\VisibilityFilterBundle\Filter\ParameterCollection;

/**
* Implement this to apply custom filtering logic to your project.
*/
interface FilterStrategy
{
/**
* Adds all parameters to the collection that affect the resulting filter SQL string. If the filter SQL depends on
* factors other than parameters added to the collection, the cache won't be invalidated, which will have undesired
* consequences!
*
* So please add all parameters to the collection.
Comment on lines +14 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"undesired consequences" and "please" seem too vague for my taste. Would you mind explaining it in more detail (or simply remove the unprecise wording)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which will have undesired consequences!

suggestion: which will have undesired consequences such as objects being visible for users who should not see them. (no acclamation mark)

Adds all parameters to the collection

seems to contradict

So please add all parameters to the collection.

Either the method adds all parameters or the user has to do it. Maybe "Adds the parameters" would be better, as it avoids using the identical wording.

So please add all parameters to the collection.

Suggestion: To avoid this, make sure that all parameters are added to the collection.

*/
public function addParameters(ParameterCollection $parameters): void;

/**
* @param string $visibilityFieldAlias SQL alias for the visibility field of the requested entity
*
* @return string A string of SQL that will be included in the WHERE clause to any query requesting an entity with a visibility field
*/
public function getFilterSql(string $visibilityFieldAlias): string;
public function getFilterSql(string $visibilityFieldAlias, ParameterCollection $parameters): string;
}
9 changes: 8 additions & 1 deletion src/Filter/Strategy/ValueInField.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Webfactory\VisibilityFilterBundle\Filter\Strategy;

use Webfactory\VisibilityFilterBundle\Filter\ParameterCollection;

/**
* Filters queries so that only entries with a certain value in their visibility field (@see VisibilityColumn) will
* be retrieved from the database.
Expand All @@ -21,7 +23,12 @@ public function __construct($visibleValue)
$this->visibleValue = $visibleValue;
}

public function getFilterSql(string $visibilityFieldAlias): string
public function addParameters(ParameterCollection $parameters): void
{
// not needed, as $this->visibleValue is immutable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not so sure about that: $this->visibleValue has no type checks, so it could be a mutable object with a __toString() method. But maybe that is considered a misuse and out of scope for this PR. (Could be solved in a separate PR, e.g. with a typed constructor signature, if you like)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a mutable object with a __toString() method would require ignoring the @param annotation and passing a wrong type to the constructor. So I would indeed consider it a misuse.

Would you consider passing a wrong type according to the docblock as something to be officially supported? That would mean that changing a DockBlock type declaration into a PHP type declaration was a breaking change.

The constructor uses a dockblock type declaration because it's a union type, and union types got introduced in PHP 8. This library however also supports PHP >=7.2. So we sadly can't turn it into a PHP type declaration until we drop support for PHP 7.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a misuse, the // not needed comment is fine with me.

I just would like to make accidental misuse harder. If you want to support PHP 7.2, what about something like this?

    /**
     * @param string|int|bool $visibleValue
     */
    public function __construct($visibleValue)
    {
        if (!is_string($visibleValue) && !is_int($visibleValue) && !is_bool($visibleValue)) {
          throw new InvalidArgumentException();
        }
        $this->visibleValue = $visibleValue;
    }

}

public function getFilterSql(string $visibilityFieldAlias, ParameterCollection $parameters): string
{
return $visibilityFieldAlias.' = '.$this->getSqlLiteral($this->visibleValue);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Filter/VisibilityColumnConsideringSQLFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAli
return ''; // Entity doesn't have visibility information
}

return $this->filterStrategy->getFilterSql($targetTableAlias.'.'.$visibilityColumn);
return $this->filterStrategy->getFilterSql($targetTableAlias.'.'.$visibilityColumn, new SQLFilterAsParameterCollection($this));
}

private function assertSetUpCorrectly(): void
Expand Down
16 changes: 16 additions & 0 deletions tests/DependencyInjection/OnRequestDependencyInjectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Webfactory\VisibilityFilterBundle\Filter\Strategy\FilterStrategy;
use Webfactory\VisibilityFilterBundle\Filter\Strategy\ValueInField;
use Webfactory\VisibilityFilterBundle\Filter\VisibilityColumnConsideringSQLFilter;
use Webfactory\VisibilityFilterBundle\Tests\Fixtures\Kernel\TestKernel;
use Webfactory\VisibilityFilterBundle\Tests\Fixtures\VisibilityColumnConsideringSQLFilterMock;
Expand Down Expand Up @@ -67,4 +69,18 @@ public function injects_dependencies_into_filter(): void
$filterMock = $this->entityManager->getFilters()->getFilter(VisibilityColumnConsideringSQLFilter::NAME);
static::assertTrue($filterMock->haveDependenciesBeenInjected());
}

/**
* @test
*/
public function calls_addParameters_on_strategy(): void
{
$strategyMock = $this->createMock(FilterStrategy::class);
static::$container->set(ValueInField::class, $strategyMock); // alias can't be overwritten at runtime – so we override the default filter strategy

$strategyMock->expects($this->once())->method('addParameters');

$masterRequestEvent = new GetResponseEvent(static::$kernel, new Request(), HttpKernelInterface::MASTER_REQUEST);
$this->eventDispatcher->dispatch($masterRequestEvent, KernelEvents::REQUEST);
}
}
9 changes: 5 additions & 4 deletions tests/Filter/Strategy/ValueInFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Webfactory\VisibilityFilterBundle\Tests\Filter\Strategy;

use PHPUnit\Framework\TestCase;
use Webfactory\VisibilityFilterBundle\Filter\ParameterCollection;
use Webfactory\VisibilityFilterBundle\Filter\Strategy\ValueInField;

class ValueInFieldTest extends TestCase
Expand All @@ -14,7 +15,7 @@ public function gives_correct_SQL_clause_for_string(): void
{
$valueInField = new ValueInField('y');

$sqlClause = $valueInField->getFilterSql('testAlias');
$sqlClause = $valueInField->getFilterSql('testAlias', $this->createMock(ParameterCollection::class));

static::assertEquals('testAlias = "y"', $sqlClause);
}
Expand All @@ -26,7 +27,7 @@ public function gives_correct_SQL_clause_for_int(): void
{
$valueInField = new ValueInField(2);

$sqlClause = $valueInField->getFilterSql('testAlias');
$sqlClause = $valueInField->getFilterSql('testAlias', $this->createMock(ParameterCollection::class));

static::assertEquals('testAlias = 2', $sqlClause);
}
Expand All @@ -38,7 +39,7 @@ public function gives_correct_SQL_clause_for_false(): void
{
$valueInField = new ValueInField(false);

$sqlClause = $valueInField->getFilterSql('testAlias');
$sqlClause = $valueInField->getFilterSql('testAlias', $this->createMock(ParameterCollection::class));

static::assertEquals('testAlias = 0', $sqlClause);
}
Expand All @@ -50,7 +51,7 @@ public function gives_correct_SQL_clause_for_true(): void
{
$valueInField = new ValueInField(true);

$sqlClause = $valueInField->getFilterSql('testAlias');
$sqlClause = $valueInField->getFilterSql('testAlias', $this->createMock(ParameterCollection::class));

static::assertEquals('testAlias = 1', $sqlClause);
}
Expand Down