Sfoglia il codice sorgente

Add support for route enum binding

shalvah 2 anni fa
parent
commit
9f39acb9be

+ 1 - 1
src/Extracting/Strategies/Headers/GetFromHeaderTag.php

@@ -30,7 +30,7 @@ class GetFromHeaderTag extends Strategy
 
             try {
                 $parameterClass = new ReflectionClass($parameterClassName);
-            } catch (ReflectionException $e) {
+            } catch (ReflectionException) {
                 continue;
             }
 

+ 44 - 9
src/Extracting/Strategies/UrlParameters/GetFromLaravelAPI.php

@@ -37,6 +37,8 @@ class GetFromLaravelAPI extends Strategy
 
         $parameters = $this->inferBetterTypesAndExamplesForEloquentUrlParameters($parameters, $endpointData);
 
+        $parameters = $this->inferBetterTypesAndExamplesForEnumUrlParameters($parameters, $endpointData);
+
         $parameters = $this->setTypesAndExamplesForOthers($parameters, $endpointData);
 
         return $parameters;
@@ -47,18 +49,35 @@ class GetFromLaravelAPI extends Strategy
         // If $url is sth like /users/{id}, return "The ID of the user."
         // If $url is sth like /anything/{user_id}, return "The ID of the user."
 
-        return collect(["id", "slug"])->flatMap(function ($name) use ($url, $paramName) {
+        $strategies = collect(["id", "slug"])->map(function ($name) {
             $friendlyName = $name === 'id' ? "ID" : $name;
 
-            if ($paramName == $name) {
-                $thing = $this->getNameOfUrlThing($url, $paramName);
-                return ["The $friendlyName of the $thing."];
-            } else if (Str::is("*_$name", $paramName)) {
-                $thing = str_replace(["_", "-"], " ", str_replace("_$name", '', $paramName));
-                return ["The $friendlyName of the $thing."];
+            return function ($url, $paramName) use ($name, $friendlyName) {
+                if ($paramName == $name) {
+                    $thing = $this->getNameOfUrlThing($url, $paramName);
+                    return "The $friendlyName of the $thing.";
+                } else if (Str::is("*_$name", $paramName)) {
+                    $thing = str_replace(["_", "-"], " ", str_replace("_$name", '', $paramName));
+                    return "The $friendlyName of the $thing.";
+                }
+            };
+        })->toArray();
+
+        // If $url is sth like /categories/{category}, return "The category."
+        $strategies[] = function ($url, $paramName) {
+            $thing = $this->getNameOfUrlThing($url, $paramName);
+            if ($thing === $paramName) {
+                return "The $thing.";
+            }
+        };
+
+        foreach ($strategies as $strategy) {
+            if ($inferred = $strategy($url, $paramName)) {
+                return $inferred;
             }
-            return [];
-        })->first() ?: '';
+        }
+
+        return '';
     }
 
     protected function inferBetterTypesAndExamplesForEloquentUrlParameters(array $parameters, ExtractedEndpointData $endpointData): array
@@ -117,6 +136,22 @@ class GetFromLaravelAPI extends Strategy
         return $parameters;
     }
 
+    protected function inferBetterTypesAndExamplesForEnumUrlParameters(array $parameters, ExtractedEndpointData $endpointData): array
+    {
+        $typeHintedEnums = UrlParamsNormalizer::getTypeHintedEnums($endpointData->method);
+        foreach ($typeHintedEnums as $argumentName => $enum) {
+            $parameters[$argumentName]['type'] = $this->normalizeTypeName($enum->getBackingType());
+
+            try {
+                $parameters[$argumentName]['example'] = $enum->getCases()[0]->getBackingValue();
+            } catch (Throwable) {
+                $parameters[$argumentName]['example'] = null;
+            }
+        }
+
+        return $parameters;
+    }
+
     protected function setTypesAndExamplesForOthers(array $parameters, ExtractedEndpointData $endpointData): array
     {
         foreach ($parameters as $name => $parameter) {

+ 24 - 3
src/Extracting/UrlParamsNormalizer.php

@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
 use Illuminate\Routing\Route;
 use Illuminate\Support\Str;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
+use ReflectionEnum;
+use ReflectionException;
 use ReflectionFunctionAbstract;
 
 /*
@@ -13,9 +15,6 @@ use ReflectionFunctionAbstract;
  */
 class UrlParamsNormalizer
 {
-    // TODO enum binding https://laravel.com/docs/9.x/routing#implicit-enum-binding
-
-
     /**
      * Normalize a URL from Laravel-style to something that's clearer for a non-Laravel user.
      * For instance, `/posts/{post}` would be clearer as `/posts/{id}`,
@@ -103,6 +102,28 @@ class UrlParamsNormalizer
         return $arguments;
     }
 
+
+    /**
+     * Return the type-hinted method arguments in the action that are enums,
+     * The arguments will be returned as an array of the form: [<variable_name> => $instance]
+     */
+    public static function getTypeHintedEnums(ReflectionFunctionAbstract $method): array
+    {
+        $arguments = [];
+        foreach ($method->getParameters() as $argument) {
+            $argumentType = $argument->getType();
+            if (!($argumentType instanceof \ReflectionNamedType)) continue;
+            try {
+                $reflectionEnum = new ReflectionEnum($argumentType->getName());
+                $arguments[$argument->getName()] = $reflectionEnum;
+            } catch (ReflectionException) {
+                continue;
+            }
+        }
+
+        return $arguments;
+    }
+
     /**
      * Given a URL that uses Eloquent model binding (for instance `/posts/{post}` -> `public function show(Post
      * $post)`), we need to figure out the field that Eloquent uses to retrieve the Post object. By default, this would

+ 16 - 0
tests/Fixtures/TestController.php

@@ -581,4 +581,20 @@ class TestController extends Controller
     {
         return null;
     }
+
+    /**
+     * Can only run on PHP 8.1
+    public function withInjectedEnumAndModel(Category $category, TestUser $user)
+    {
+        return null;
+    }
+     */
+}
+
+/**
+enum Category: string
+{
+    case Fruits = 'fruits';
+    case People = 'people';
 }
+*/

+ 10 - 2
tests/Strategies/UrlParameters/GetFromLaravelAPITest.php

@@ -20,9 +20,10 @@ class GetFromLaravelAPITest extends BaseLaravelTest
     use ArraySubsetAsserts;
 
     /** @test */
-    public function can_fetch_from_url()
+    public function can_infer_type_from_model_binding()
     {
         $endpoint = $this->endpointForRoute("users/{id}", TestController::class, 'withInjectedModel');
+        // $endpoint = $this->endpointForRoute("categories/{category}/users/{id}/", TestController::class, 'withInjectedEnumAndModel');
         $results = $this->fetch($endpoint);
 
         $this->assertArraySubset([
@@ -30,7 +31,14 @@ class GetFromLaravelAPITest extends BaseLaravelTest
             "description" => "The ID of the user.",
             "required" => true,
             "type" => "integer",
-        ], $results['id']);
+        ], $results['id']);/*
+        $this->assertArraySubset([
+            "name" => "category",
+            "description" => "The category.",
+            "required" => true,
+            "type" => "string",
+            "example" => \Knuckles\Scribe\Tests\Fixtures\Category::cases()[0]->value,
+        ], $results['category']);*/
         $this->assertIsInt($results['id']['example']);
     }
 

+ 0 - 5
tests/Unit/ExtractedEndpointDataTest.php

@@ -41,11 +41,6 @@ class ExtractedEndpointDataTest extends BaseLaravelTest
     /** @test */
     public function normalizes_nonresource_url_params_with_inline_bindings()
     {
-        if (version_compare($this->app->version(), '7.0.0', '<')) {
-            $this->markTestSkipped("Field binding syntax was introduced in Laravel 7.");
-            return;
-        }
-
         Route::get('things/{thing:slug}', [TestController::class, 'show']);
         $route = $this->getRoute(['prefixes' => '*']);