Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/engines/invokable/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ If `$mentors` is empty or not defined, the engine automatically matches request
'created_at' → calls createdAt()
```

::: warning Reserved Method Names
Filter methods must not resolve to a method already defined by the base
`Filterable` class, such as `apply`, `filter`, or `getBuilder`. The engine
throws a `FilterableMethodConflictException` when a conflict is detected. Use
`$mentors` to map the request key to a unique filter method name.
:::

---

## Attribute Pipeline
Expand Down
28 changes: 14 additions & 14 deletions src/Engines/Invokable.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

namespace Kettasoft\Filterable\Engines;

use Illuminate\Support\Str;
use Kettasoft\Filterable\Filterable;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Kettasoft\Filterable\Support\Payload;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ForwardsCalls;
use Kettasoft\Filterable\Engines\Foundation\Attributes\AttributeContext;
use Kettasoft\Filterable\Engines\Foundation\Attributes\AttributePipeline;
use Kettasoft\Filterable\Engines\Foundation\Engine;
use Kettasoft\Filterable\Engines\Foundation\PayloadFactory;
use Kettasoft\Filterable\Engines\Foundation\Parsers\Dissector;
use Kettasoft\Filterable\Engines\Foundation\Attributes\AttributeContext;
use Kettasoft\Filterable\Engines\Foundation\Attributes\AttributePipeline;
use Kettasoft\Filterable\Engines\Foundation\Attributes\AttributeRegistry;
use Kettasoft\Filterable\Exceptions\FilterableMethodConflictException;
use Kettasoft\Filterable\Filterable;
use Kettasoft\Filterable\Support\Payload;

class Invokable extends Engine
{
Expand Down Expand Up @@ -43,20 +43,20 @@ public function execute(Builder $builder): Builder
$this->context->setAllowedFields($this->context->getFilterAttributes());

foreach ($this->context->getFilterAttributes() as $filter) {
$this->attempt(function () use ($filter) {
$method = $this->getMethodName($filter);

// Conflicts are configuration errors and must not be swallowed in non-strict mode.
if (method_exists(Filterable::class, $method)) {
throw new FilterableMethodConflictException($method);
}

$this->attempt(function () use ($filter, $method) {
$dissector = Dissector::parse($this->context->getRequest()->get($filter), $this->defaultOperator());

$payload = new Payload($filter, $dissector->operator, $this->sanitizeValue($filter, $dissector->value), $dissector->value);

$payload = (new PayloadFactory($this))->make($payload);

$method = $this->getMethodName($filter);

// Check for method name conflicts with Filterable core methods.
if (method_exists(Filterable::class, $method)) {
throw new \RuntimeException(sprintf("Filter method [%s] conflicts with core Filterable method.", [$method]));
}

$this->applyFilterMethod($filter, $method, $payload);

$this->commit($method, $payload);
Expand Down
16 changes: 16 additions & 0 deletions src/Exceptions/FilterableMethodConflictException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Kettasoft\Filterable\Exceptions;

class FilterableMethodConflictException extends StrictnessException
{
/**
* Create a new exception instance.
*
* @param string $method The conflicting method name.
*/
public function __construct(string $method)
{
parent::__construct(sprintf("Filter method [%s] conflicts with core Filterable method.", $method));
}
}
102 changes: 102 additions & 0 deletions tests/Unit/Engines/InvokableEngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Kettasoft\Filterable\Tests\Unit\Engines;

use Kettasoft\Filterable\Filterable;
use Kettasoft\Filterable\Exceptions\FilterableMethodConflictException;
use Kettasoft\Filterable\Tests\TestCase;
use Kettasoft\Filterable\Support\Payload;
use Kettasoft\Filterable\Tests\Models\Post;
Expand Down Expand Up @@ -982,4 +983,105 @@ public function title(Payload $payload)
$this->assertEquals('active', $posts->first()->status);
$this->assertGreaterThanOrEqual(100, $posts->first()->views);
}

/**
* @test
*/
public function it_throws_exception_when_filter_method_conflicts_with_core_filterable_methods()
{
$this->expectException(FilterableMethodConflictException::class);

request()->merge([
'apply' => 'test'
]);

$filter = new class extends Filterable {
protected $filters = ['apply'];
};

Post::filter($filter)->get();
}

/**
* @test
*/
public function it_throws_exception_for_multiple_core_method_conflicts()
{
$coreMethodsThatShouldConflict = [
'apply',
'filter',
'getData',
'getModel',
'getBuilder',
'getEngine'
];

foreach ($coreMethodsThatShouldConflict as $coreMethod) {
request()->merge([
$coreMethod => 'test_value'
]);

try {
$filter = new class($coreMethod) extends Filterable {
protected $filters = [];

public function __construct($method)
{
$this->filters = [$method];
parent::__construct();
}
};

Post::filter($filter)->get();

$this->fail("Expected FilterableMethodConflictException for method: {$coreMethod}");
} catch (FilterableMethodConflictException $e) {
$this->assertStringContainsString($coreMethod, $e->getMessage());
$this->assertStringContainsString('conflicts with core Filterable method', $e->getMessage());
}
}
}

/**
* @test
*/
public function it_allows_filter_methods_that_do_not_conflict()
{
Post::query()->delete();

Post::factory()->create(['status' => 'active', 'title' => 'Active Post']);
Post::factory()->create(['status' => 'pending', 'title' => 'Pending Post']);

request()->merge([
'custom_status' => 'active'
]);

$filter = new class extends Filterable {
protected $filters = ['custom_status'];

public function customStatus(Payload $payload)
{
$this->builder->where('status', $payload->value);
}
};

$posts = Post::filter($filter)->get();

$this->assertCount(1, $posts);
$this->assertEquals('active', $posts->first()->status);
$this->assertEquals('Active Post', $posts->first()->title);
}

/**
* @test
*/
public function it_properly_formats_exception_message()
{
$exception = new FilterableMethodConflictException('testMethod');

$this->assertSame(
'Filter method [testMethod] conflicts with core Filterable method.',
$exception->getMessage()
);
}
}
Loading