Skip to content

Commit

Permalink
BC BREAK! Use FQCN by default for controller and action.
Browse files Browse the repository at this point in the history
Instead of converting pretty names to controller names automatically,
  this needs to be done explicitly by passing a callback to the Invoker contructor.

Closes #6
  • Loading branch information
jasny committed Sep 9, 2019
1 parent bcb5177 commit 9643965
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 136 deletions.
79 changes: 58 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ In all examples we'll use the following function to get the routes;
function getRoutes(): array
{
return [
'GET /' => ['controller' => 'info'],
'GET /' => ['controller' => 'InfoController'],

'GET /users' => ['controller' => 'user', 'action' => 'list'],
'POST /users' => ['controller' => 'user', 'action' => 'add'],
'GET /users/{id}' => ['controller' => 'user', 'action' => 'get'],
'POST|PUT /users/{id}' => ['controller' => 'user', 'action' => 'update'],
'DELETE /users/{id}' => ['controller' => 'user', 'action' => 'delete'],
'GET /users' => ['controller' => 'UserController', 'action' => 'listAction'],
'POST /users' => ['controller' => 'UserController', 'action' => 'addAction'],
'GET /users/{id}' => ['controller' => 'UserController', 'action' => 'getAction'],
'POST|PUT /users/{id}' => ['controller' => 'UserController', 'action' => 'updateAction'],
'DELETE /users/{id}' => ['controller' => 'UserController', 'action' => 'deleteAction'],

'GET /users/{id}/photos' => ['action' => 'list-photos'],
'POST /users/{id}/photos' => ['action' => 'add-photos'],
'GET /users/{id}/photos' => ['action' => 'ListPhotosAction'],
'POST /users/{id}/photos' => ['action' => 'AddPhotosAction'],

'POST /export' => ['include' => 'scripts/export.php'],
];
Expand All @@ -85,6 +85,44 @@ class UserController

_Note that the path variables are always strings._

#### Pretty controller and action names

By default the `controller` and `action` should be configured with a fully qualified class name (includes namespace).
However, it's possible to use a pretty name instead and have the `Invoker` convert it to an fqcn.

```php
function getRoutes(): array
{
return [
'GET /' => ['controller' => 'info'],

'GET /users' => ['controller' => 'user', 'action' => 'list'],
'POST /users' => ['controller' => 'user', 'action' => 'add'],
'GET /users/{id}' => ['controller' => 'user', 'action' => 'get'],
'POST|PUT /users/{id}' => ['controller' => 'user', 'action' => 'update'],
'DELETE /users/{id}' => ['controller' => 'user', 'action' => 'delete'],

'GET /users/{id}/photos' => ['action' => 'list-photos'],
'POST /users/{id}/photos' => ['action' => 'add-photos'],

'POST /export' => ['include' => 'scripts/export.php'],
];
}
```

Pass a callable to the `Invoker` that converts the pretty controller and action names

```php
$stud = fn($str) => strtr(ucwords($str, '-'), ['-' => '']);
$camel = fn($str) => strtr(lcfirst(ucwords($str, '-')), ['-' => '']);

$invoker = new Invoker(function (?string $controller, ?string $action) use ($stud, $camel) {
return $controller !== null
? [$stud($controller) . 'Controller', $camel($action ?? 'default') . 'Action']
: [$stud($action) . 'Action', '__invoke'];
});
```

### Basic

By default the generator generates a function to route requests.
Expand Down Expand Up @@ -331,7 +369,7 @@ both the route properties and path variables as PSR-7 `ServerRequest` attributes
When calling `generate`, you pass the name of the middleware class. This may include a namespace.

```php
use Jasny\SwitchRoute\Generator as RouteGenerator;
use Jasny\SwitchRoute\Generator;

$generate = new Generator\GenerateRouteMiddleware();

Expand All @@ -354,8 +392,8 @@ arguments. To specify a fixed value for an argument of the action, the route arg

```php
[
'GET /users/{id}/photos' => ['action' => 'list-photos', '{page}' => 1],
'GET /users/{id}/photos/{page}' => ['action' => 'list-photos'],
'GET /users/{id}/photos' => ['action' => 'ListPhotosAction', '{page}' => 1],
'GET /users/{id}/photos/{page}' => ['action' => 'ListPhotosAction'],
];
```

Expand All @@ -373,7 +411,7 @@ during construction (optional DI).
When calling `generate`, you pass the name of the middleware class. This may include a namespace.

```php
use Jasny\SwitchRoute\Generator as RouteGenerator;
use Jasny\SwitchRoute\Generator;

$generate = new Generator\GenerateInvokeMiddleware();

Expand Down Expand Up @@ -414,24 +452,23 @@ the middleware will also set the `Allow` header.
The `Invoker` is generates snippets for invoking the action or including the file as stated in the selected route. This
includes converting the `controller` and/or `action` attribute to a class name and possibly method name.

By default, the if the route has a `controller` property, the invoker will add 'Controller' and apply stud casing to
create the controller class name. The `action` property is taken as method, applying camel casing. It defaults to
`defaultAction`.
By default, the if the route has a `controller` property, use it as the class name. The `action` property is taken as
method. It defaults to `defaultAction`.

If only an `action` property is present, the invoker will add `Action` and apply stud casing to create the action class
name. This must be an [invokable object](https://www.php.net/manual/en/language.oop5.magic.php#object.invoke).
If only an `action` property is present, the invoker will use that as class name. The class must define an
[invokable object](https://www.php.net/manual/en/language.oop5.magic.php#object.invoke).

You can change how the invokable class and method names are generated by passing a callback to the constructor. The can
be used for instance to add a namespace to the controller and action class.
be used for instance convert pretty names to fully qualified class names (FQCN) for the the controller and action class.

```php
$stud = fn($str) => strtr(ucwords($str, '-'), ['-' => '']);
$camel = fn($str) => strtr(lcfirst(ucwords($str, '-')), ['-' => '']);

$invoker = new RouteInvoker(function (?string $controller, ?string $action) {
$invoker = new Invoker(function (?string $controller, ?string $action) use ($stud, $camel) {
return $controller !== null
? ['App\\Generated\\' . $stud($controller) . 'Controller', $camel($action ?? 'default') . 'Action']
: ['App\\Generated\\' . $stud($action) . 'Action', '__invoke'];
? ['App\\' . $stud($controller) . 'Controller', $camel($action ?? 'default') . 'Action']
: ['App\\' . $stud($action) . 'Action', '__invoke'];
});
```

Expand Down
6 changes: 4 additions & 2 deletions src/Generator/GenerateFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,18 @@ public function __invoke(string $name, array $routes, array $structure): string
$switchCode = self::indent($this->generateSwitch($structure));
$defaultCode = self::indent($this->generateDefault($default));

[$namespace, $function] = $this->generateNs($name);

return <<<CODE
<?php
declare(strict_types=1);
{$namespace}
/**
* This function is generated by SwitchRoute.
* Do not modify it manually. Any changes will be overwritten.
*/
function {$name}(string \$method, string \$path)
function {$function}(string \$method, string \$path)
{
\$segments = \$path === "/" ? [] : explode("/", trim(\$path, "/"));
\$allowedMethods = [];
Expand Down
19 changes: 7 additions & 12 deletions src/Invoker.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function __construct(?callable $createInvokable = null, ?ReflectionFactor
* @return string
* @throws ReflectionException
*/
public function generateInvocation(array $route, callable $genArg, string $new = '(new %s)'): string
public function generateInvocation(array $route, callable $genArg, string $new = '(new \\%s)'): string
{
['controller' => $controller, 'action' => $action] = $route + ['controller' => null, 'action' => null];

Expand All @@ -62,7 +62,7 @@ public function generateInvocation(array $route, callable $genArg, string $new =

$reflection = $this->getReflection($invokable);
$call = $reflection instanceof ReflectionFunction
? $invokable
? '\\' . $invokable
: $this->generateInvocationMethod($invokable, $reflection, $new);

return $call . '(' . $this->generateInvocationArgs($reflection, $genArg) . ')';
Expand All @@ -87,7 +87,7 @@ protected function generateInvocationMethod(

return $invokable[1] === '__invoke' || $invokable[1] === ''
? sprintf($new, $invokable[0])
: ($reflection->isStatic() ? "{$invokable[0]}::" : sprintf($new, $invokable[0]) . "->") . $invokable[1];
: ($reflection->isStatic() ? "\\{$invokable[0]}::" : sprintf($new, $invokable[0]) . "->") . $invokable[1];
}

/**
Expand Down Expand Up @@ -182,7 +182,7 @@ public function generateDefault(): string
}

/**
* Default method to create invokable from controller and action name
* Default method to create invokable from controller and action FQCN.
*
* @param string|null $controller
* @param string|null $action
Expand All @@ -194,13 +194,8 @@ final public static function createInvokable(?string $controller, ?string $actio
throw new \BadMethodCallException("Neither controller or action is set");
}

[$class, $method] = $controller !== null
? [$controller . 'Controller', ($action ?? 'default') . 'Action']
: [$action . 'Action', '__invoke'];

return [
strtr(ucwords($class, '-'), ['-' => '']),
strtr(lcfirst(ucwords($method, '-')), ['-' => ''])
];
return $controller !== null
? [$controller, ($action ?? 'defaultAction')]
: [$action, '__invoke'];
}
}
19 changes: 13 additions & 6 deletions tests/functional/FunctionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,21 @@ class FunctionTest extends TestCase
public static function setUpBeforeClass(): void
{
self::$root = vfsStream::setup('tmp');
$file = vfsStream::url('tmp/generated/routes.php');
$file = vfsStream::url('tmp/generated/route.php');

$invoker = new Invoker(function ($class, $action) {
[$class, $method] = Invoker::createInvokable($class, $action);
return [__NAMESPACE__ . '\\Support\\Basic\\' . $class, $method];
$invoker = new Invoker(function ($controller, $action) {
[$class, $method] = $controller !== null
? [$controller . 'Controller', ($action ?? 'default') . 'Action']
: [$action . 'Action', '__invoke'];

return [
__NAMESPACE__ . '\\Support\\Basic\\' . strtr(ucwords($class, '-'), ['-' => '']),
strtr(lcfirst(ucwords($method, '-')), ['-' => ''])
];
});

$generator = new Generator(new Generator\GenerateFunction($invoker));
$generator->generate('route', $file, [__CLASS__, 'getRoutes'], true);
$generator->generate(__NAMESPACE__ . '\\route', $file, [__CLASS__, 'getRoutes'], true);

require $file;
}
Expand All @@ -45,7 +51,8 @@ public static function tearDownAfterClass(): void
*/
public function test(string $method, string $path, $expected)
{
$result = route($method, $path);
$fn = \Closure::fromCallable(__NAMESPACE__ . '\\route');
$result = $fn($method, $path);

$this->assertEquals($expected, $result);
}
Expand Down
12 changes: 9 additions & 3 deletions tests/functional/MiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,15 @@ protected static function createInvokeMiddleware(): void
{
$file = '/tmp/generated/InvokeMiddleware.php';//vfsStream::url('tmp/generated/InvokeMiddleware.php');

$invoker = new Invoker(function ($class, $action) {
[$class, $method] = Invoker::createInvokable($class, $action);
return [__NAMESPACE__ . '\\Support\\Psr\\' . $class, $method];
$invoker = new Invoker(function ($controller, $action) {
[$class, $method] = $controller !== null
? [$controller . 'Controller', ($action ?? 'default') . 'Action']
: [$action . 'Action', '__invoke'];

return [
__NAMESPACE__ . '\\Support\\Psr\\' . strtr(ucwords($class, '-'), ['-' => '']),
strtr(lcfirst(ucwords($method, '-')), ['-' => ''])
];
});

$generator = new Generator(new Generator\GenerateInvokeMiddleware($invoker));
Expand Down
25 changes: 23 additions & 2 deletions tests/unit/Generator/GenerateFunctionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected function getRouteArgs()

// Not found last
usort($routeArgs, function ($a, $b) {
return isset($a[0]['action']) && $a[0]['action'] === 'not-found' ? 1 : -1;
return isset($a[0]['action']) && $a[0]['action'] === 'NotFoundAction' ? 1 : -1;
});

return $routeArgs;
Expand All @@ -56,7 +56,7 @@ public function testGenerate()
->withConsecutive(...$routeArgs)
->willReturnCallback(function ($route, callable $genArg) {
['controller' => $controller, 'action' => $action] = $route + ['controller' => null, 'action' => null];
$arg = $action !== 'not-found' ? $genArg('id', '', null) : $genArg('allowedMethods', '', []);
$arg = $action !== 'NotFoundAction' ? $genArg('id', '', null) : $genArg('allowedMethods', '', []);
return sprintf("call('%s', '%s', %s)", $controller, $action, $arg);
});

Expand All @@ -68,6 +68,26 @@ public function testGenerate()
$this->assertEquals($expected, $code);
}

public function testGenerateFunctionNamespace()
{
$routes = ['GET /' => ['controller' => 'info']];
$structure = ["\0" => (new Endpoint('/'))->withRoute('GET', ['controller' => 'info'], [])];

$invoker = $this->createMock(Invoker::class);
$invoker->expects($this->once())->method('generateInvocation')
->with(['controller' => 'info'], $this->isInstanceOf(Closure::class))
->willReturn('info()');
$invoker->expects($this->once())->method('generateDefault')
->willReturn("return false;");

$generate = new GenerateFunction($invoker);

$code = $generate('Generated\route_generate_function_test_function_ns', $routes, $structure);

$expected = file_get_contents(__DIR__ . '/expected/generate-function-test-function-ns.phps');
$this->assertEquals($expected, $code);
}

public function testGenerateDefaultRoute()
{
$routes = ['GET /' => ['controller' => 'info']];
Expand All @@ -79,6 +99,7 @@ public function testGenerateDefaultRoute()
->willReturn('info()');
$invoker->expects($this->once())->method('generateDefault')
->willReturn("http_response_code(404);\necho \"Not Found\";");

$generate = new GenerateFunction($invoker);

$code = $generate('route_generate_function_test_default', $routes, $structure);
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/Generator/GenerateRouteMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public function testGenerate()

public function testGenerateDefaultRoute()
{
$routes = ['GET /' => ['controller' => 'info']];
$structure = ["\0" => (new Endpoint('/'))->withRoute('GET', ['controller' => 'info'], [])];
$routes = ['GET /' => ['controller' => 'InfoController']];
$structure = ["\0" => (new Endpoint('/'))->withRoute('GET', ['controller' => 'InfoController'], [])];

$generate = new GenerateRouteMiddleware();

Expand All @@ -45,8 +45,8 @@ public function testGenerateDefaultRoute()

public function testGenerateWithNamespace()
{
$routes = ['GET /' => ['controller' => 'info']];
$structure = ["\0" => (new Endpoint('/'))->withRoute('GET', ['controller' => 'info'], [])];
$routes = ['GET /' => ['controller' => 'InfoController']];
$structure = ["\0" => (new Endpoint('/'))->withRoute('GET', ['controller' => 'InfoController'], [])];

$generate = new GenerateRouteMiddleware();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Generated;

/**
* This function is generated by SwitchRoute.
* Do not modify it manually. Any changes will be overwritten.
*/
function route_generate_function_test_function_ns(string $method, string $path)
{
$segments = $path === "/" ? [] : explode("/", trim($path, "/"));
$allowedMethods = [];

switch ($segments[0] ?? "\0") {
case "\0":
$allowedMethods = ['GET'];
switch ($method) {
case 'GET':
return info();
}
break 1;
}

return false;
}
Loading

0 comments on commit 9643965

Please sign in to comment.