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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"psr/log": "^3.0",
"psr/cache": "^2.0|^3.0",
"psr/simple-cache": "^2.0|^3.0",
"pivotphp/core-routing": "^1.0"
"pivotphp/core-routing": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^9.0|^10.0",
Expand Down
15 changes: 15 additions & 0 deletions docs/technical/routing/SYNTAX_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ $app->get('/status', [HealthController::class, 'getStatus']);
$app->get('/info', [ApiController::class, 'getInfo']);
```

> **✅ Método de instância via nome de classe**: também é possível referenciar um
> método de instância pelo nome da classe (`[UserController::class, 'index']`).
> A instância é resolvida pelo container de DI quando o controller está registrado
> nele; caso contrário, a classe é instanciada de forma lazy (construtor sem
> argumentos obrigatórios).

```php
// Controller registrado no container (DI)
$app->singleton(UserController::class, fn($c) => new UserController($c->get(UserRepository::class)));
$app->get('/users', [UserController::class, 'index']); // DI via container

// Fallback: instanciação lazy (construtor sem argumentos)
$app->get('/health', [HealthController::class, 'healthCheck']);
```

#### Vantagens dos Array Callables

- **Organização**: Código organizado em classes e métodos
Expand Down
44 changes: 39 additions & 5 deletions src/Core/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,40 @@ public function getMiddleware(string $name): mixed
return null;
}

/**
* Resolve um handler de rota em formato array para a forma executável,
* resolvendo métodos de instância via container (DI) quando disponível.
*
* @param callable|array $handler Handler original
* @return callable|array
*/
private function resolveHandler(callable|array $handler): callable|array
{
if (!is_array($handler) || count($handler) !== 2) {
return $handler;
}

$class = $handler[0] ?? null;
$method = $handler[1] ?? null;

if (!is_string($class) || !is_string($method) || !class_exists($class) || !method_exists($class, $method)) {
return $handler;
}

if ((new \ReflectionMethod($class, $method))->isStatic()) {
return $handler;
}

if ($this->container->has($class)) {
/** @var object $instance */
$instance = $this->container->get($class);

return [$instance, $method];
}

return $handler;
}

/**
* Registra uma rota GET.
*
Expand All @@ -524,7 +558,7 @@ public function getMiddleware(string $name): mixed
*/
public function get(string $path, callable|array $handler): self
{
$this->router->get($path, $handler);
$this->router->get($path, $this->resolveHandler($handler));
return $this;
}

Expand All @@ -537,7 +571,7 @@ public function get(string $path, callable|array $handler): self
*/
public function post(string $path, callable|array $handler): self
{
$this->router->post($path, $handler);
$this->router->post($path, $this->resolveHandler($handler));
return $this;
}

Expand All @@ -550,7 +584,7 @@ public function post(string $path, callable|array $handler): self
*/
public function put(string $path, callable|array $handler): self
{
$this->router->put($path, $handler);
$this->router->put($path, $this->resolveHandler($handler));
return $this;
}

Expand All @@ -563,7 +597,7 @@ public function put(string $path, callable|array $handler): self
*/
public function delete(string $path, callable|array $handler): self
{
$this->router->delete($path, $handler);
$this->router->delete($path, $this->resolveHandler($handler));
return $this;
}

Expand All @@ -576,7 +610,7 @@ public function delete(string $path, callable|array $handler): self
*/
public function patch(string $path, callable|array $handler): self
{
$this->router->patch($path, $handler);
$this->router->patch($path, $this->resolveHandler($handler));
return $this;
}

Expand Down
24 changes: 17 additions & 7 deletions src/Utils/CallableResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,26 @@ private static function resolveArrayCallable(array $handler): callable
);
}

// Verificar se o método é realmente estático
// Verificar se o método é estático ou de instância
$reflection = new \ReflectionMethod($objectOrClass, $method);
if (!$reflection->isStatic()) {
throw new InvalidArgumentException(
"Method '{$objectOrClass}::{$method}' is not static. Use an instance instead."
);

if ($reflection->isStatic()) {
/** @var callable */
return [$objectOrClass, $method];
}

/** @var callable */
return [$objectOrClass, $method];
// Método de instância referenciado pelo nome da classe: resolve de
// forma lazy (instancia na chamada). Quando a aplicação usa o
// container de DI, Application::resolveHandler() já converte para
// [$instance, 'method'] antes de chegar aqui.
return static function (...$args) use ($objectOrClass, $method) {
$instance = new $objectOrClass();

/** @var callable $handler */
$handler = [$instance, $method];

return $handler(...$args);
};
}

// Caso 2: Método de instância [$instance, 'method']
Expand Down
60 changes: 60 additions & 0 deletions tests/Integration/Routing/ArrayCallableContainerResolutionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace PivotPHP\Core\Tests\Integration\Routing;

use PHPUnit\Framework\TestCase;
use PivotPHP\Core\Core\Application;
use PivotPHP\Core\Http\Request;
use PivotPHP\Core\Tests\Integration\Routing\DiGreetingController;
use PivotPHP\Core\Tests\Integration\Routing\HealthController;
use PivotPHP\Routing\Router\Router;

/**
* Cobre a resolução de [Classe::class, 'métodoDeInstância'] pela aplicação:
* via container quando o controller está registrado (DI), e via instanciação
* lazy quando não está.
*/
class ArrayCallableContainerResolutionTest extends TestCase
{
protected function setUp(): void
{
Router::clear();
}

protected function tearDown(): void
{
Router::clear();
}

public function testInstanceMethodResolvedViaContainer(): void
{
$app = new Application(__DIR__ . '/../../..');
$app->instance(DiGreetingController::class, new DiGreetingController('di:'));

$app->get('/di-greet', [DiGreetingController::class, 'greet']);

$response = $app->handle(new Request('GET', '/di-greet', '/di-greet'));

$this->assertSame(200, $response->getStatusCode());

$body = json_decode($response->getBodyAsString(), true);
$this->assertSame('di: hello', $body['greeting']);
}

public function testNonBoundControllerFallsBackToLazyInstantiation(): void
{
$app = new Application(__DIR__ . '/../../..');

// HealthController tem construtor sem argumentos → instanciação lazy funciona.
$app->get('/lazy-health', [HealthController::class, 'healthCheck']);

$response = $app->handle(new Request('GET', '/lazy-health', '/lazy-health'));

$this->assertSame(200, $response->getStatusCode());

$body = json_decode($response->getBodyAsString(), true);
$this->assertSame('ok', $body['status']);
}
}
25 changes: 25 additions & 0 deletions tests/Integration/Routing/DiGreetingController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace PivotPHP\Core\Tests\Integration\Routing;

use PivotPHP\Core\Http\Request;
use PivotPHP\Core\Http\Response;

/**
* Controller com dependência de construtor (não instanciável via `new` sem
* argumentos), usado para provar a resolução via container em
* [Classe::class, 'métodoDeInstância'].
*/
final class DiGreetingController
{
public function __construct(private string $prefix)
{
}

public function greet(Request $req, Response $res): Response
{
return $res->json(['greeting' => $this->prefix . ' hello']);
}
}
7 changes: 4 additions & 3 deletions tests/Utils/CallableResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ public function testArrayCallableWithNonExistentInstanceMethod(): void

public function testArrayCallableWithNonStaticMethod(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("is not static. Use an instance instead");
// [Classe::class, 'métodoDeInstância'] resolve de forma lazy (closure).
$resolved = CallableResolver::resolve([TestCallableClass::class, 'instanceMethod']);

CallableResolver::resolve([TestCallableClass::class, 'instanceMethod']);
$this->assertInstanceOf(\Closure::class, $resolved);
$this->assertEquals('instance method called', $resolved());
}

public function testArrayCallableWithPrivateMethod(): void
Expand Down
Loading