nikic / PHP-Parser

A PHP parser written in PHP

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`Enum` as a `Attribute` parameter

piotrooo opened this issue · comments

Hello!

I think I found bug. Lets consider following snippet:

<?php

use PhpParser\BuilderFactory;
use PhpParser\Node\Name;
use PhpParser\PrettyPrinter\Standard;

enum HttpMethod: string
{
    case GET = 'GET';
}

#[Attribute(Attribute::TARGET_METHOD)]
readonly class HTTP
{
    public HttpMethod $httpMethod;

    public function __construct(
        HttpMethod|string $httpMethod,
        public string $path
    )
    {
        if (is_string($httpMethod)) {
            $httpMethod = HttpMethod::from($httpMethod);
        }
        $this->httpMethod = $httpMethod;
    }
}

interface Api
{
    #[HTTP(httpMethod: HttpMethod::GET, path: '/users')]
    public function getUsers();
}

$reflectionClass = new ReflectionClass(Api::class);

$builderFactory = new BuilderFactory();

$class = $builderFactory->class('ApiImpl')
    ->implement('\\' . Api::class);

$reflectionMethod = $reflectionClass->getMethod('getUsers');

$method = $builderFactory
    ->method($reflectionMethod->getName())
    ->makePublic();

$reflectionAttributes = $reflectionMethod->getAttributes();
foreach ($reflectionAttributes as $reflectionAttribute) {
    $name = new Name($reflectionAttribute->getName());
    $attribute = $builderFactory->attribute($name, $reflectionAttribute->getArguments());

    $method->addAttribute($attribute);
}

$class->addStmt($method->getNode());

$proxyNamespaceBuilder = $builderFactory
    ->namespace('Namespace')
    ->addStmt($class);

$standard = new Standard();
echo $standard->prettyPrint([$proxyNamespaceBuilder->getNode()]);

When I try to create a proxy class with method which takes enum as a parameters I receive an exception:

Fatal error: Uncaught LogicException: Invalid value in /opt/project/vendor/nikic/php-parser/lib/PhpParser/BuilderHelpers.php on line 280

Call Stack:
    0.0004     401096   1. {main}() /opt/scratches/scratch_37.php:0
    0.0052    1342792   2. PhpParser\BuilderFactory->attribute($name = class PhpParser\Node\Name { protected $attributes = []; public $parts = [0 => 'HTTP'] }, $args = ['httpMethod' => enum HttpMethod::GET('GET'), 'path' => '/users']) /opt/scratches/scratch_37.php:54
    0.0052    1346616   3. PhpParser\BuilderFactory->args($args = ['httpMethod' => enum HttpMethod::GET('GET'), 'path' => '/users']) /opt/project/vendor/nikic/php-parser/lib/PhpParser/BuilderFactory.php:26
    0.0053    1351336   4. PhpParser\BuilderHelpers::normalizeValue($value = enum HttpMethod::GET('GET')) /opt/project/vendor/nikic/php-parser/lib/PhpParser/BuilderFactory.php:252

When I added a following handling in BuilderHelpers::normalizeValue() code works OK.

public static function normalizeValue($value) : Expr {
    if ($value instanceof Node\Expr) {
        return $value;
    }

    if (is_null($value)) {
        return new Expr\ConstFetch(
            new Name('null')
        );
    }

    if (is_bool($value)) {
        return new Expr\ConstFetch(
            new Name($value ? 'true' : 'false')
        );
    }

    if (is_int($value)) {
        return new Scalar\LNumber($value);
    }

    if (is_float($value)) {
        return new Scalar\DNumber($value);
    }

    if (is_string($value)) {
        return new Scalar\String_($value);
    }

    if (is_array($value)) {
        $items = [];
        $lastKey = -1;
        foreach ($value as $itemKey => $itemValue) {
            // for consecutive, numeric keys don't generate keys
            if (null !== $lastKey && ++$lastKey === $itemKey) {
                $items[] = new Expr\ArrayItem(
                    self::normalizeValue($itemValue)
                );
            } else {
                $lastKey = null;
                $items[] = new Expr\ArrayItem(
                    self::normalizeValue($itemValue),
                    self::normalizeValue($itemKey)
                );
            }
        }

        return new Expr\Array_($items);
    }

    // !!! HERE !!!
    if (is_object($value)) {
        return new Expr\ClassConstFetch(new Name($value::class), $value->name);
    }

    throw new \LogicException('Invalid value');
}

What do you think about this fix? It's ok? Or it should be handled in the another way?

Hi @piotrooo ,
I am not sure if it is related or not (in case of "not related", please let me know and I'll report a new issue). When I use enum as default value in overridden method I end up with the same error.

enum PriorityEnum: int
{
    case HIGH = 5;
    case NORMAL = 1;
}

class BaseProduct
{
    public function create(ProductData $productData): Product
    {}
}

class Product extend BaseProduct: int
{
    public function create(
        ProductData $productData,
        PriorityEnum = PriorityEnum::HIGH
    ): Product
    {}
}

Your fix works for me

@piotrooo Great that you found a fix. But why didn't you create the PR with it? Then it could have been polished and merged.

Now it's still not solved.

Here is the PR: #940

@piotrooo Great that you found a fix. But why didn't you create the PR with it? Then it could have been polished and merged.

Now it's still not solved.

Here is the PR: #940

That was only a idea how to solve it. I wasn't 100% sure that solution is correct. Sorry about that.

Your solution is more elegant.

Hi @piotrooo , I am not sure if it is related or not (in case of "not related", please let me know and I'll report a new issue). When I use enum as default value in overridden method I end up with the same error.

I'm not quite sure what you want to do so I cannot confirm your error is related. But if you receiving a similar exception - that could be the same.