Repository: pimcore/pimcore (language: php)

Issue:
[Bug]: DataObject controller arguments resolver is throwing errors
### Pimcore version

10.6.1

### Steps to reproduce

When using the DataObject resolver for controller arguments, you get errors when:
- The argument is optional nullable
- The value isn't passed as request attribute (but for example via the `#[CurrentUser]` attribute)

I noticed this error when updating from `10.5.21` to `10.6.1`. Tested on skeleton versions `10.6.1` (throwing errors) and `11.0.0` (working correctly). It looks like the changes from 11.0 aren't correctly backported to Symfony 5.

Create DataObject "FrontendUser" with an input field "username". It should implement the `\Symfony\Component\Security\Core\User\UserInterface` for the second part of the PoC, which can be done by using the provided `AbstractFrontendUser` (somewhere below) as parent class.

## Proof-of-concept:
`src/Controller/PocController.php`
```php
<?php

declare(strict_types=1);

namespace App\Controller;

use Pimcore\Controller\FrontendController;
use Pimcore\Model\DataObject\FrontendUser;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Attribute\CurrentUser;

#[Route('/poc')]
class PocController extends FrontendController
{
    #[Route('/show-by-id/{user}')]
    public function showById(?FrontendUser $user = null): Response
    {
        # http://localhost/poc/show-by-id/2
        #   Expected: shows correct full path of user
        #   Actual:   shows correct full path of user
        # http://localhost/poc/show-by-id
        #   Expected: shows NULL
        #   Actual:   Error "Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver::resolve()" must yield at least one value.

        return new Response($user === null ? 'NULL' : $user->getFullPath());
    }

    #[Route('/show-current', name: 'poc_show_current')]
    public function showCurrentUser(#[CurrentUser] ?FrontendUser $user = null): Response
    {
        # http://localhost/poc/login
        #   Expected: shows correct full path of user
        #   Actual:   Error "Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver::resolve()" must yield at least one value.
        # http://localhost/poc/logout
        #   Expected: shows NULL
        #   Actual:   Error "Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver::resolve()" must yield at least one value.

        return new Response($user === null ? 'NULL' : $user->getFullPath());
    }

    #[Route('/login')]
    public function login(TokenStorageInterface $tokenStorage): Response
    {
        $user = FrontendUser::getById(2);
        $tokenStorage->setToken(
            new UsernamePasswordToken($user, 'frontend', $user->getRoles())
        );
        return $this->redirectToRoute('poc_show_current');
    }

    #[Route('/logout')]
    public function logout(TokenStorageInterface $tokenStorage): Response
    {
        $tokenStorage->setToken(null);
        return $this->redirectToRoute('poc_show_current');
    }
}
```

## Other relevant code

`src/Model/AbstractFrontendUser.php`
```php
<?php

declare(strict_types=1);

namespace App\Model;

use Pimcore\Model\DataObject\Concrete;
use Symfony\Component\Security\Core\User\UserInterface;

abstract class AbstractFrontendUser extends Concrete implements UserInterface
{
    abstract public function getUsername(): ?string;
    
    public function getRoles(): array {  return ['ROLE_POC']; }
    public function getPassword(): string { return '$2y$10$IlGZ6PbfsFLwJDtAE7p5Ye0SXclueWXrinE6LYYELDx6qigY7.YKm'; }
    public function getSalt(): ?string { return null; }
    public function eraseCredentials(): void {}
    public function getUserIdentifier(): string { return $this->getUsername(); }
}
```

`config/services.yaml`
```yaml
services:
    # Add user provider
    poc.userprovider:
        class: Pimcore\Security\User\ObjectUserProvider
        arguments:
            $className: 'Pimcore\Model\DataObject\FrontendUser'
```

`config/packages/security.yaml`
```yaml
security:
    providers:
        # Register user provider
        poc_frontend:
            id: poc.userprovider

    firewalls:
        # Add PoC firewall
        frontend:
            pattern: ^/poc/
            provider: poc_frontend
```

### Expected / actual behavior

http://localhost/poc/show-by-id/2
        #   Expected: shows correct full path of user
        #   Actual:   shows correct full path of user
http://localhost/poc/show-by-id
        #   Expected: shows NULL
        #   Actual:   Error "Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver::resolve()" must yield at least one value.

http://localhost/poc/login
        #   Expected: shows correct full path of user
        #   Actual:   Error "Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver::resolve()" must yield at least one value.
http://localhost/poc/logout
        #   Expected: shows NULL
        #   Actual:   Error "Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver::resolve()" must yield at least one value.


Discussion / hints:
PS. I'm, currently working on a fix for this, PR coming up soon

Fix patch:
diff --git a/bundles/CoreBundle/Request/ParamConverter/DataObjectParamResolver.php b/bundles/CoreBundle/Request/ParamConverter/DataObjectParamResolver.php
index 602e259cc93..adf1a83c026 100644
--- a/bundles/CoreBundle/Request/ParamConverter/DataObjectParamResolver.php
+++ b/bundles/CoreBundle/Request/ParamConverter/DataObjectParamResolver.php
@@ -32,11 +32,9 @@
 class DataObjectParamResolver implements ArgumentValueResolverInterface
 {
     /**
-     * {@inheritdoc}
-     *
-     * @throws NotFoundHttpException When invalid data object ID given
+     * @return DataObjectParam[]
      */
-    public function resolve(Request $request, ArgumentMetadata $argument): iterable
+    private function getDataObjectOptions(Request $request, ArgumentMetadata $argument): array
     {
         $options = $argument->getAttributes(DataObjectParam::class, ArgumentMetadata::IS_INSTANCEOF);
 
@@ -53,22 +51,25 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
             }
         }
 
-        $class = $options[0]->class ?? $argument->getType();
-        if (null === $class || !is_subclass_of($class, AbstractObject::class)) {
-            return [];
-        }
+        return $options;
+    }
 
-        $param = $argument->getName();
-        if (!$request->attributes->has($param)) {
-            return [];
-        }
+    /**
+     * {@inheritdoc}
+     *
+     * @throws NotFoundHttpException When invalid data object ID given
+     */
+    public function resolve(Request $request, ArgumentMetadata $argument): iterable
+    {
+        $options = $this->getDataObjectOptions($request, $argument);
 
+        $class = $options[0]->class ?? $argument->getType();
+        $param = $argument->getName();
         $value = $request->attributes->get($param);
 
         if (!$value && $argument->isNullable()) {
             $request->attributes->set($param, null);
-
-            return [];
+            return [null];
         }
 
         /** @var Concrete|null $object */
@@ -90,10 +91,14 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
 
     public function supports(Request $request, ArgumentMetadata $argument)
     {
-        if (null === $argument->getType()) {
+        $param = $argument->getName();
+        if (!$request->attributes->has($param)) {
             return false;
         }
 
-        return is_subclass_of($argument->getType(), AbstractObject::class);
+        $options = $this->getDataObjectOptions($request, $argument);
+        $class = $options[0]->class ?? $argument->getType();
+
+        return $class !== null && is_subclass_of($class, AbstractObject::class);
     }
 }


Test patch: