Repository: api-platform/core (php)

Issue:
Not all Laravel filters are autoconfigured
**API Platform version(s) affected**: 4.1.3
<!-- Warning: if your version is 2.7 we ask you to try the 3.0 version. If the bug is present there we will fix it. If the bug is due to old 2.6 behavior we can't fix it.
In that last case we recommend to either upgrade to 3.0 or to temporarily replace concerned service with your own. -->

**Description**  
In `ApiPlatformDeferredProvider` not all default `FilterInterface` classes are added to the autoconfigure array. It currently is:

```php
        $this->autoconfigure($classes, EloquentFilterInterface::class, [
            BooleanFilter::class,
            EqualsFilter::class,
            PartialSearchFilter::class,
            DateFilter::class,
            OrderFilter::class,
            RangeFilter::class,
            SortFilter::class,
            SparseFieldset::class,
        ]);
```

Note for example, `StartSearchFilter::class` is missing.

**How to reproduce**  
Attempt to utilize the `StartSearchFilter` in a Laravel project.

**Possible Solution**  
Obviously the quick fix is to manually add these filters to the array. Is there a reason we don't automatically discover these filters similar to what we are doing with [Tempest](https://github.com/tempestphp/tempest-framework) though?


Pull request description:
fix(laravel): missing filters
fixes #7055

Patch:
diff --git a/src/Laravel/ApiPlatformDeferredProvider.php b/src/Laravel/ApiPlatformDeferredProvider.php
index af3eef2c7..c7d2e622e 100644
--- a/src/Laravel/ApiPlatformDeferredProvider.php
+++ b/src/Laravel/ApiPlatformDeferredProvider.php
@@ -26,6 +26,7 @@ use ApiPlatform\Laravel\Eloquent\Extension\FilterQueryExtension;
 use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface;
 use ApiPlatform\Laravel\Eloquent\Filter\BooleanFilter;
 use ApiPlatform\Laravel\Eloquent\Filter\DateFilter;
+use ApiPlatform\Laravel\Eloquent\Filter\EndSearchFilter;
 use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter;
 use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
 use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter;
@@ -33,6 +34,7 @@ use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider;
 use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
 use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter;
 use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter;
+use ApiPlatform\Laravel\Eloquent\Filter\StartSearchFilter;
 use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource\EloquentResourceCollectionMetadataFactory;
 use ApiPlatform\Laravel\Eloquent\State\CollectionProvider;
 use ApiPlatform\Laravel\Eloquent\State\ItemProvider;
@@ -149,11 +151,13 @@ class ApiPlatformDeferredProvider extends ServiceProvider implements DeferrableP
 
         $this->autoconfigure($classes, EloquentFilterInterface::class, [
             BooleanFilter::class,
-            EqualsFilter::class,
-            PartialSearchFilter::class,
             DateFilter::class,
+            EndSearchFilter::class,
+            EqualsFilter::class,
             OrderFilter::class,
+            PartialSearchFilter::class,
             RangeFilter::class,
+            StartSearchFilter::class,
             SortFilter::class,
             SparseFieldset::class,
         ]);
diff --git a/src/Symfony/Bundle/DataCollector/RequestDataCollector.php b/src/Symfony/Bundle/DataCollector/RequestDataCollector.php
index f409b5cd3..7ac7b6fa9 100644
--- a/src/Symfony/Bundle/DataCollector/RequestDataCollector.php
+++ b/src/Symfony/Bundle/DataCollector/RequestDataCollector.php
@@ -15,9 +15,6 @@ namespace ApiPlatform\Symfony\Bundle\DataCollector;
 
 use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
-use ApiPlatform\State\Util\RequestAttributesExtractor;
-use Composer\InstalledVersions;
-use PackageVersions\Versions;
 use Psr\Container\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -42,17 +39,22 @@ final class RequestDataCollector extends DataCollector
         if ($request->attributes->get('_graphql', false)) {
             $resourceClasses = array_keys($request->attributes->get('_graphql_args', []));
         } else {
-            $resourceClasses = array_filter([$request->attributes->get('_api_resource_class')]);
+            $cl = $request->attributes->get('_api_resource_class');
+            $resourceClasses = $cl ? [$cl] : [];
         }
 
-        $requestAttributes = RequestAttributesExtractor::extractAttributes($request);
-        if (isset($requestAttributes['previous_data'])) {
-            $requestAttributes['previous_data'] = $this->cloneVar($requestAttributes['previous_data']);
+        $this->data['operation_name'] = $request->attributes->get('_api_operation_name');
+        $this->data['acceptable_content_types'] = $request->getAcceptableContentTypes();
+        $this->data['resources'] = array_map(fn (string $resourceClass): DataCollected => $this->collectDataByResource($resourceClass), $resourceClasses);
+
+        $parameters = [];
+        if ($operation = $request->attributes->get('_api_operation')) {
+            foreach ($operation->getParameters() ?? [] as $key => $param) {
+                $parameters[$key] = $this->cloneVar($param);
+            }
         }
 
-        $this->data['request_attributes'] = $requestAttributes;
-        $this->data['acceptable_content_types'] = $request->getAcceptableContentTypes();
-        $this->data['resources'] = array_map(fn (string $resourceClass): DataCollected => $this->collectDataByResource($resourceClass, $request), $resourceClasses);
+        $this->data['parameters'] = $parameters;
     }
 
     private function setFilters(ApiResource $resourceMetadata, int $index, array &$filters, array &$counters): void
@@ -68,40 +70,6 @@ final class RequestDataCollector extends DataCollector
         }
     }
 
-    // TODO: 4.1 remove Versions as its deprecated
-    public function getVersion(): ?string
-    {
-        if (class_exists(InstalledVersions::class)) {
-            return InstalledVersions::getPrettyVersion('api-platform/symfony') ?? InstalledVersions::getPrettyVersion('api-platform/core');
-        }
-
-        if (!class_exists(Versions::class)) {
-            return null;
-        }
-
-        try {
-            $version = strtok(Versions::getVersion('api-platform/symfony'), '@');
-        } catch (\OutOfBoundsException) {
-            $version = false;
-        }
-
-        if (false === $version) {
-            try {
-                $version = strtok(Versions::getVersion('api-platform/core'), '@');
-            } catch (\OutOfBoundsException) {
-                $version = false;
-            }
-        }
-
-        if (false === $version) {
-            return null;
-        }
-
-        preg_match('/^v(.*?)$/', $version, $output);
-
-        return $output[1] ?? $version;
-    }
-
     /**
      * {@inheritdoc}
      */
@@ -120,9 +88,14 @@ final class RequestDataCollector extends DataCollector
         return $this->data['acceptable_content_types'] ?? [];
     }
 
-    public function getRequestAttributes(): array
+    public function getOperationName(): ?string
+    {
+        return $this->data['operation_name'] ?? null;
+    }
+
+    public function getParameters(): array
     {
-        return $this->data['request_attributes'] ?? [];
+        return $this->data['parameters'] ?? [];
     }
 
     public function getResources(): array
@@ -138,7 +111,7 @@ final class RequestDataCollector extends DataCollector
         $this->data = [];
     }
 
-    private function collectDataByResource(string $resourceClass, Request $request): DataCollected
+    private function collectDataByResource(string $resourceClass): DataCollected
     {
         $resourceMetadataCollection = $resourceClass ? $this->metadataFactory->create($resourceClass) : [];
         $filters = [];
diff --git a/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig b/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig
index 72d11e547..db8d6b45a 100644
--- a/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig
+++ b/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig
@@ -1,25 +1,33 @@
 {% extends '@WebProfiler/Profiler/layout.html.twig' %}
 
-{% macro operationLine(key, operation, actualOperationName) %}
+{% macro operationLine(key, operation, actualOperationName, n) %}
+    {% set context_id = 'operation-' ~ n %}
     <tr>
-        <th scope="row"{% if key == actualOperationName %} class="status-success"{% endif %}>{{ key }}</th>
-        <td{% if key == actualOperationName %} class="status-success"{% endif %}>{{- profiler_dump(operation, 1) -}}</td>
+        <th style="width: 40%" scope="row"{% if key == actualOperationName %} class="status-success"{% endif %}>{{ key }}</th>
+        <td style="width: 60%" {% if key == actualOperationName %} class="status-success"{% endif %}>
+          <a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</a>
+          <div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
+              {{- profiler_dump(operation, 2) -}}
+          </div>
+        </td>
     </tr>
 {% endmacro %}
 
-{% macro operationTable(object, name, actualOperationName) %}
+{% macro operationTable(object, actualOperationName) %}
     {% import _self as apiPlatform %}
     <table>
         <thead>
             <tr>
-                <th scope="col" class="key">{% if name is defined %}{{ name|capitalize }}{% endif %}Operations</th>
-                <th scope="col">Attributes</th>
+                <th scope="col" class="key">{% if name is defined %}{{ name|capitalize }}{% endif %}Name</th>
+                <th scope="col">Operation</th>
             </tr>
         </thead>
 
         <tbody>
+        {% set n = 0 %}
         {% for key, itemOperation in object %}
-            {{ apiPlatform.operationLine(key, itemOperation, actualOperationName) }}
+          {% set n = n + 1 %}
+          {{ apiPlatform.operationLine(key, itemOperation, actualOperationName, n) }}
         {% else %}
             <tr>
                 <td colspan="2" class="text-muted">
@@ -83,12 +91,6 @@
     {% endset %}
 
     {% set text %}
-        {% if collector.version %}
-            <div class="sf-toolbar-info-piece">
-                <b>Version</b>
-                <span>{{ collector.version }}</span>
-            </div>
-        {% endif %}
         {% if collector.resources|length == 0 %}
             <div class="sf-toolbar-info-piece">
                 <b>Resource Class</b>
@@ -139,10 +141,11 @@
             </div>
         </div>
         {% if dataCollected.resourceMetadataCollection is not empty %}
-            <h3>Resources</h3>
+            <h2>Resources</h2>
             <div class="tab-content metadata-tab-content">
                 <div class="sf-tabs">
-                    {% for resourceMetadata in dataCollected.resourceMetadataCollection %}
+                    {% for index, resourceMetadata in dataCollected.resourceMetadataCollection %}
+                        {% set context_id = 'context-' ~ resourceMetadata.resource.shortName ~ index %}
                         <div class="tab">
                             <h3 class="tab-title">
                                 {{ resourceMetadata.resource.uriTemplate ?: resourceMetadata.resource.shortName }}
@@ -151,16 +154,69 @@
                                 <table>
                                     <thead>
                                         <tr>
-                                            <th scope="col" class="key">Resource</th>
+                                            <th scope="col" class="key">Name</th>
+                                            <th>ApiResource</th>
                                         </tr>
                                     </thead>
                                     <tbody>
                                         <tr>
-                                            <td>{{- profiler_dump(resourceMetadata.resource, 1) -}}</td>
+                                          <td>{{ resourceMetadata.resource.shortName }}</td>
+                                            <td>
+                                              <div>
+                                                  <a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</a>
+                                                  <div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
+                                                      {{- profiler_dump(resourceMetadata.resource, 2) -}}
+                                                  </div>
+                                              </div>
+                                            </td>
                                         </tr>
                                     </tbody>
                                 </table>
-                                {{ apiPlatform.operationTable(resourceMetadata.operations, '', dataCollected.requestAttributes.operation_name|default('')) }}
+                                <h2>Operations</h2>
+                                {{ apiPlatform.operationTable(resourceMetadata.operations, collector.operationName) }}
+                                <h2>Parameters</h2>
+                                <table>
+                                    <thead>
+                                        <tr>
+                                            <th scope="col" class="key">Key</th>
+                                            <th scope="col">Value</th>
+                                            <th scope="col">Parameter</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                        {% if collector.parameters %}
+                                            {% for key, parameter in collector.parameters %}
+                                                {% set context_id = 'parameter-' ~ key %}
+                                                <tr>
+                                                    <td>
+                                                        {{ key }}
+                                                    </td>
+                                                    <td>
+                                                        {% if parameter.extraProperties['_api_values'] is defined %}
+                                                            {{ dump(parameter.extraProperties['_api_values']) }}
+                                                        {% else %}
+                                                            ∅
+                                                        {% endif %}
+                                                    </td>
+                                                    <td>
+                                                        <a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</a>
+                                                        <div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
+                                                          {{- profiler_dump(parameter, 3) -}}
+                                                        </div>
+
+                                                    </td>
+                                                </tr>
+                                            {% endfor %}
+                                        {% else %}
+                                            <tr>
+                                                <td class="text-muted" colspan="2">
+                                                    No available parameters declared for this resource.
+                                                </td>
+                                            </tr>
+                                        {% endif %}
+                                    </tbody>
+                                </table>
+                                <h2>Filters</h2>
                                 <table>
                                     <thead>
                                         <tr>
@@ -178,7 +234,7 @@
                                                         {% if ignored_filter %}
                                                             <span class="newline text-muted">ignored filter</span>
                                                         {% else %}
-                                                            {{ dump(filter) }}
+                                                            {{ filter }}
                                                         {% endif %}
                                                     </td>
                                                 </tr>
@@ -200,4 +256,3 @@
         {% endif %}
     {% endfor %}
 {% endblock %}
-


Test patch:
diff --git a/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php b/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php
index 9a5a35a00..087544cf9 100644
--- a/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php
+++ b/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php
@@ -20,8 +20,6 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInter
 use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
 use ApiPlatform\Symfony\Bundle\DataCollector\RequestDataCollector;
 use ApiPlatform\Tests\Fixtures\DummyEntity;
-use Composer\InstalledVersions;
-use PackageVersions\Versions;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Prophecy\PhpUnit\ProphecyTrait;
@@ -73,7 +71,6 @@ class RequestDataCollectorTest extends TestCase
             $this->response
         );
 
-        $this->assertEquals([], $dataCollector->getRequestAttributes());
         $this->assertEquals(['foo', 'bar'], $dataCollector->getAcceptableContentTypes());
         $this->assertEquals([], $dataCollector->getResources());
     }
@@ -88,14 +85,13 @@ class RequestDataCollectorTest extends TestCase
             $this->filterLocator->reveal()
         );
 
-        $this->assertEquals([], $dataCollector->getRequestAttributes());
         $this->assertEquals([], $dataCollector->getAcceptableContentTypes());
         $this->assertEquals([], $dataCollector->getResources());
     }
 
     public function testWithResource(): void
     {
-        $this->apiResourceClassWillReturn(DummyEntity::class, ['_api_operation_name' => 'get', '_api_receive' => true]);
+        $this->apiResourceClassWillReturn(DummyEntity::class, ['_api_operation_name' => 'get']);
         $this->request->attributes = $this->attributes->reveal();
 
         $this->filterLocator->has('foo')->willReturn(false)->shouldBeCalled();
@@ -112,14 +108,6 @@ class RequestDataCollectorTest extends TestCase
             $this->response
         );
 
-        $this->assertEquals([
-            'resource_class' => DummyEntity::class,
-            'has_composite_identifier' => false,
-            'operation_name' => 'get',
-            'receive' => true,
-            'respond' => true,
-            'persist' => true,
-        ], $dataCollector->getRequestAttributes());
         $this->assertEquals(['foo', 'bar'], $dataCollector->getAcceptableContentTypes());
 
         $resource = $dataCollector->getResources()[0];
@@ -147,59 +135,11 @@ class RequestDataCollectorTest extends TestCase
         );
     }
 
-    public function testVersionCollection(): void
-    {
-        $this->apiResourceClassWillReturn(DummyEntity::class);
-
-        $this->filterLocator->has('a_filter')->willReturn(false);
-        $this->filterLocator->has('foo')->willReturn(false);
-
-        $dataCollector = new RequestDataCollector(
-            $this->metadataFactory->reveal(),
-            $this->filterLocator->reveal(),
-        );
-
-        $dataCollector->collect(
-            $this->request->reveal(),
-            $this->response
-        );
-
-        if (class_exists(InstalledVersions::class)) {
-            $this->assertTrue(null !== $dataCollector->getVersion());
-        } else {
-            $this->assertSame(null !== $dataCollector->getVersion(), class_exists(Versions::class));
-        }
-    }
-
-    public function testWithPreviousData(): void
-    {
-        $data = new \stdClass();
-        $data->a = $data;
-
-        $this->apiResourceClassWillReturn(DummyEntity::class, ['_api_operation_name' => 'get', '_api_receive' => true, 'previous_data' => $data]);
-        $this->request->attributes = $this->attributes->reveal();
-
-        $this->filterLocator->has('foo')->willReturn(false);
-        $this->filterLocator->has('a_filter')->willReturn(true);
-        $this->filterLocator->get('a_filter')->willReturn(new \stdClass());
-
-        $dataCollector = new RequestDataCollector(
-            $this->metadataFactory->reveal(),
-            $this->filterLocator->reveal()
-        );
-
-        $dataCollector->collect(
-            $this->request->reveal(),
-            $this->response
-        );
-
-        $this->assertArrayHasKey('previous_data', $requestAttributes = $dataCollector->getRequestAttributes());
-        $this->assertNotSame($requestAttributes['previous_data']->data, $requestAttributes['previous_data']);
-    }
-
     private function apiResourceClassWillReturn(?string $data, array $context = []): void
     {
         $this->attributes->get('_api_resource_class')->shouldBeCalled()->willReturn($data);
+        $this->attributes->get('_api_operation_name')->shouldBeCalled()->willReturn($context['_api_operation_name'] ?? null);
+        $this->attributes->get('_api_operation')->shouldBeCalled()->willReturn($context['_api_operation'] ?? new Get());
         $this->attributes->get('_graphql', false)->shouldBeCalled()->willReturn(false);
         $this->attributes->all()->willReturn([
             '_api_resource_class' => $data,
diff --git a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php
index e32ef32e9..fccb1b7c2 100644
--- a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php
+++ b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php
@@ -124,9 +124,9 @@ final class ApiPlatformProfilerPanelTest extends WebTestCase
         $tabContent = $crawler->filter('.tab:nth-of-type(1)');
         $this->assertStringEndsWith('Dummy', trim($tabContent->filter('h3')->html()), 'the resource shortname should be displayed.');
 
-        $this->assertCount(3, $tabContent->filter('table'));
-        $this->assertSame('Resource', $tabContent->filter('table:first-of-type thead th:first-of-type')->html());
-        $this->assertSame('Operations', $tabContent->filter('table:nth-of-type(2) thead th:first-of-type')->html());
-        $this->assertSame('Filters', $tabContent->filter('table:nth-of-type(3) thead th:first-of-type')->html());
+        $this->assertCount(4, $tabContent->filter('table'));
+        $this->assertSame('Name', $tabContent->filter('table:first-of-type thead th:first-of-type')->html());
+        $this->assertSame('Name', $tabContent->filter('table:nth-of-type(2) thead th:first-of-type')->html());
+        $this->assertSame('Key', $tabContent->filter('table:nth-of-type(3) thead th:first-of-type')->html());
     }
 }