Repository: dedoc/scramble (PHP)

Issue:
Response Body Returning String when using ApiResponser Trait
Hello All
I use scamble version 0.11.13 with Laravel 11.
When I build an API, I always create an ApiResponser Trait to keep my API response consistent.

Such as:
```
{
    success: boolean,
    data: array,
    message: string
}
```
I create **ApiResponser.php** trait

```
<?php

namespace App\Traits;

use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;

trait ApiResponser
{
    /**
     * Building success response
     *
     * @param  string|array  $data
     * @param  ?string  $message
     * @param  int  $code
     * @return JsonResponse
     */
    public function successResponse($data, $message = null, $code = Response::HTTP_OK)
    {
        return response()->json([
            'success' => true,
            'data' => $data,
            'message' => $message,
        ], $code);
    }
}
```
and then I used in **Controller.php**

```
<?php

namespace App\Http\Controllers;

use App\Traits\ApiResponser;

abstract class Controller
{
    use ApiResponser;
}
```

In another controller, example **AuthController.php**

```
<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    /**
     * Login
     *
     * @unauthenticated
     *
     * @throws ValidationException
     */
    public function login(LoginRequest $request): JsonResponse
    {
        // LOGIC

        return $this->successResponse([
            'token' => 'blablabla',
            'token_type' => 'Bearer',
        ], __('Login successful'));
    }
}
```

The expected Response Body in scramble is
```
{
  "success": boolean,
  "data": {
    "token": string,
    "token_type": string
  },
  "message": string
}
```
But response body in scramble is string
![000210-wAhUYmRP@2x](https://github.com/user-attachments/assets/22d3f781-18eb-4d89-9d1a-fb3bb07ef124)

Is there something wrong with me? Or how should I return API responses with a consistent format?
Thank you



Discussion: @arislanhaikal This is Scramble's issue. I will fix!
```php
public function index(Requests $request): JsonResource
{
    // more code including `$user`
    return $this->returnData($user);
}

public function returnData($user): JsonResource
{
    // some more code
    return new UsersResource($user);
}
```
Above code is also returning the similar format (OP) in scramble. Just drop here just in case

![Screenshot from 2024-09-13 11-11-15](https://github.com/user-attachments/assets/4bd2148f-d7bb-438d-ac91-2a6fea1ed6a5)

I am also facing the same issue for my response:

`{
    "error": false,
    "data": [
        {
            "id": 1,
            "name": "USA",
            "slug": "usa"
        }
    ]
}`
@aashirhaq, I have no idea why. Please share some code examples. Otherwise I won't be able to help
@valpuia604 that code that you've excluded matters. First of all make sure you use the latest version. If it doesn't work, include enough code so I can reproduce.
@romalytvynenko I am using `dedoc/scramble` v0.11.13

Here is my full code:

Controller file:

    public function index(Request $request)
    {
        $data = array();
        $error = false;
        $message = null;
        $params = $request->all();

        try {
            $records = $this->service->list($params);

            $data = PaymentMethod::collection($records);
        } catch (\Exception $e) {
            throw $e;
            Log::error($e);
        }
        return $this->responder->respond($error, $data, $message);
    }
      
And, the **respond** method is below:
    
       public function respond($error = 500, $data = array(), $message = null)
    {
        $responseData = array();
        if(!$message && $error)
            $message = $this->getMessage($error);
        $status = $error ? true : false;
        $responseData = [
            'error' => $status,
            'data' => array()
        ];
        if($error && $error === 422){
            $responseData['data']['errors'] = $data;
        }
        if($error){
            $responseData['data']['code'] = $error;
        }
        if($error && $message){
            $responseData['data']['message'] = $message;
        }
        if(!$status && !$error){
            $error = 200;
            $responseData['data'] = $data;
        }
        return response()->json($responseData, $error, [], JSON_UNESCAPED_SLASHES);
    }
    
Here is **PaymentMethod** resource file code:

```
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PaymentMethod extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'iD' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'type' => $this->type,
            'gatewayType' => $this->gateway_type,
            'logoUrl' => $this->logo_uri,
        ];
    }
}

```

Response is Generating from above code is here:

```
{
    "error": false,
    "data": [
        {
            "iD": 49,
            "name": "Credit/Debit Card",
            "slug": "credit-debit-card",
            "type": "payment",
            "gatewayType": null,
            "logoUrl": "credit-debit-card-94257.png"
        }
    ]
}
```

> @valpuia604 that code that you've excluded matters. First of all make sure you use the latest version. If it doesn't work, include enough code so I can reproduce.

Hi I've updated to `0.11.13` and got the same output, below are how to reproduced

```php
/**
* @unauthenticated
*/
public function index(LoginRequest $request): JsonResponse|JsonResource
{
    $user = User::where('username', $request->validated('username'))
        ->first();

    if (! $user || ! Hash::check($request->validated('password'), $user->password)) {
        return $this->returnError(__('auth.failed'));
    }

    return $this->loadUser($user);
}

/**
* Why not include in `index` instead of calling below method?
*
* Well I have another login method which check a little bit different
* So try to follow DRY
*/
public function loadUser($user): JsonResponse|JsonResource
{
    if (! $user->is_active) {
        return $this->returnError(__('messages.acc_disabled'));
    }

    $user['token'] = $user->createToken('auth_token')->plainTextToken;

    return new UsersResource($user);
}

public function returnError($messageDescription): JsonResponse
{
    return response()->json([
        'message' => [
            'title' => __('messages.error'),
            'description' => $messageDescription,
        ],
        'status' => false,
    ], 422);
}

// Login Request

public function rules(): array
{
    return [
        'username' => 'required',
        'password' => 'required',
    ];
}
```
@aashirhaq your example is not supported yet, as it is too complex: type is being built conditionally. For now, Scramble doesn't support that.
> > @valpuia604 that code that you've excluded matters. First of all make sure you use the latest version. If it doesn't work, include enough code so I can reproduce.
> 
> Hi I've updated to `0.11.13` and got the same output, below are how to reproduced
> 
> ```
> /**
> * @unauthenticated
> */
> public function index(LoginRequest $request): JsonResponse|JsonResource
> {
>     $user = User::where('username', $request->validated('username'))
>         ->first();
> 
>     if (! $user || ! Hash::check($request->validated('password'), $user->password)) {
>         return $this->returnError(__('auth.failed'));
>     }
> 
>     return $this->loadUser($user);
> }
> 
> /**
> * Why not include in `index` instead of calling below method?
> *
> * Well I have another login method which check a little bit different
> * So try to follow DRY
> */
> public function loadUser($user): JsonResponse|JsonResource
> {
>     if (! $user->is_active) {
>         return $this->returnError(__('messages.acc_disabled'));
>     }
> 
>     $user['token'] = $user->createToken('auth_token')->plainTextToken;
> 
>     return new UsersResource($user);
> }
> 
> public function returnError($messageDescription): JsonResponse
> {
>     return response()->json([
>         'message' => [
>             'title' => __('messages.error'),
>             'description' => $messageDescription,
>         ],
>         'status' => false,
>     ], 422);
> }
> 
> // Login Request
> 
> public function rules(): array
> {
>     return [
>         'username' => 'required',
>         'password' => 'required',
>     ];
> }
> ```

Digging deeper and it's not about another method calls, it's only return type `JsonResponse|JsonResource`
If I didn't specify the return type it's working fine
> Digging deeper and it's not about another method calls, it's only return type `JsonResponse|JsonResource`
> If I didn't specify the return type it's working fine

Still not working inside trait method calls
@aashirhaq Please create reproduction repository on GitHub. Otherwise, I won't be able to help.
> @aashirhaq Please create reproduction repository on GitHub. Otherwise, I won't be able to help.

Hi @romalytvynenko, i make simple example for this issue on github [laravel-scramble-example](https://github.com/arislanhaikal/laravel-scramble-example) . Can you check please!
@aashirhaq Thanks. I'm on it, but it may take some time
Hello, Roman,

I encountered a similar problem with rendering the response body as a string. However, there seems to be something mystical going on.
I use: v0.11.27

Code to Reproduce:

```php
final class ApiResponse
{
    public static function json(
        mixed $data = [], 
        string $message = null, 
        $status = Response::HTTP_OK
    ): JsonResponse {
        $success = match (true) {
            $status >= 200 && $status < 300 => true,
            default => false,
        };

        return new JsonResponse([
            'success' => $success,
            'message' => $message,
            'data' => $data,
        ], $status);
    }
}
```

**Observed Behavior**
 - Without a type declaration for the $status argument, the code works as expected and produces the correct json response body.
![image](https://github.com/user-attachments/assets/c3715783-2d36-4cdd-85c2-66d45925f4b0)

 - Issue: When I add a type declaration (int) to the $status argument, the response body is rendered as a string.

![image](https://github.com/user-attachments/assets/d112d2db-0f69-4064-b434-6391af47c6d1)



Hey, any updates on this one?
I have the same issue. We have a "tryExecute" function inside our base Controller class that is used in every API controller (in order to consolidate all exceptions and map them to their respective status code) and the generated docs also state string as result.

Simple code example:
```PHP
abstract class BaseController
{
    protected function tryExecute(\Closure $callback){
        try {
            $data = $callback();
            return response($data, 200);
        } catch (\Exception $ex){
            return response($ex->getMessage(), 500);
        }
    }
}
```

```PHP
class TestController extends BaseController
{
    public function index(Request $request)
    {
        return $this->tryExecute(function () use ($request) {
            
            $request->validate([
                'firstname' => 'required|string',
                'lastname' => "required|string",
            ]);
            
            return [
                'hello' => $request->firstname . $request->lastname
            ];
        });
    }
}
```
@arislanhaikal thanks for the reproduction repository. Indeed there was a problem in Scramble that I've fixed. But this only partially fixes the issue in the repro repository.

The issue lies on this line:
```php
$this->validErrorCode($code) ? $code : Response::HTTP_OK
```

Here is the full method:
```php
public function successResponse($data, $message = null, $code = Response::HTTP_OK)
{
    return response()->json([
        'success' => true,
        'data' => $data,
        'message' => $message,
    ], $this->validErrorCode($code) ? $code : Response::HTTP_OK);
}
```

Here is how Scramble infers the types from this method (I will use PHPStan syntax):
```php
/**
 * @template TData
 * @template TMessage = null
 * @template TCode = 200
 * @param TData $data
 * @param TMessage $message
 * @param TCode $code 
 * @return Illuminate\Http\JsonResponse<array{success: boolean(true), data: TData, message: TMessage}, TCode|200, array<mixed>>
 */
public function successResponse($data, $message = null, $code = Response::HTTP_OK)
{
    return response()->json([
        'success' => true,
        'data' => $data,
        'message' => $message,
    ], $this->validErrorCode($code) ? $code : Response::HTTP_OK);
}
```

Scramble automatically introduces template types to preserve the type through the codebase. As you can see, for HTTP code Scramble inferred type `TCode|200` which comes from the line:
```php
$this->validErrorCode($code) ? $code : Response::HTTP_OK
```

So when you write in your controller something like this:
```php
return $this->successResponse(new UserResource(User::create($data)), code: 201);
```

Scramble infers the type:
```
Illuminate\Http\JsonResponse<array{
  success: boolean(true), 
  data: App\Http\Resources\UserResource<unknown>, 
  message: null
}, 201|200, array<mixed>>
```

So the inferred HTTP code type is `201|200`. In this case Scramble doesn't document it due to it is not clear which HTTP code the response actually is. If Scramble documented both codes, that would also be incorrect.

So this method will only work if you don't pass `code` (as the inferred HTTP code will be `200`):
```php
return $this->successResponse(new UserResource(User::create($data)));
```

One possible way to fix it is to throw an exception in case the code is incorrect:
```php
public function successResponse($data, $message = null, $code = Response::HTTP_OK)
{
    $this->ensureValidErrorCode($code);

    return response()->json([
        'success' => true,
        'data' => $data,
        'message' => $message,
    ], $code);
}

private function ensureValidErrorCode($code) 
{
   if (! $this->validErrorCode($code)) {
        throw Exception(/*...*/);
   }
}
```

I will release the fix for trait methods issues soon.
@flow96 for now the example you've provided is not inferred automatically. I'm working on bringing PHPStan-like type annotations support and then it will work with some manual types.



Fix (patch):
diff --git a/src/Infer/Reflector/MethodReflector.php b/src/Infer/Reflector/MethodReflector.php
index b469094d3..dc0994de6 100644
--- a/src/Infer/Reflector/MethodReflector.php
+++ b/src/Infer/Reflector/MethodReflector.php
@@ -70,16 +70,16 @@ public function getAstNode(): ?ClassMethod
 
             $partialClass = "<?php$lines class $className {\n".$methodDoc."\n".$this->getMethodCode()."\n}";
 
-            $statements = $this->parser->parseContent($partialClass)->getStatements();
             $node = (new NodeFinder)
                 ->findFirst(
-                    $statements,
+                    $this->parser->parseContent($partialClass)->getStatements(),
                     fn (Node $node) => $node instanceof Node\Stmt\ClassMethod && $node->name->name === $this->name,
                 );
+            $fileNameContext = FileNameResolver::createForFile($this->getReflection()->getFileName());
 
             $traverser = new NodeTraverser;
 
-            $traverser->addVisitor(new class($this->getClassReflector()->getNameContext()) extends NameResolver
+            $traverser->addVisitor(new class($fileNameContext->nameContext) extends NameResolver
             {
                 public function __construct($nameContext)
                 {
@@ -92,9 +92,7 @@ public function beforeTraverse(array $nodes): ?array
                     return null;
                 }
             });
-            $traverser->addVisitor(new PhpDocResolver(
-                new FileNameResolver($this->getClassReflector()->getNameContext()),
-            ));
+            $traverser->addVisitor(new PhpDocResolver($fileNameContext));
 
             $traverser->traverse([$node]);