laminas/laminas-hydrator

Communicate which element fails in ArraySerializableHydrator

meidlinga opened this issue · 2 comments

Feature Request

Q A
New Feature yes
RFC no
BC Break no

Summary

When using the ArraySerializableHydrator and one of the strategies fails, it is impossible to see without debugging, which element failed. To achive this, I would see two options:

  • The strategy could throw or log an error. This is currently impossible, since the strategy does not know the name of the value to extract during extraction.
  • The ArraySerializableHydrator could catch throwables during extraction and rethrow them with the field name as information included in the message.

I would prefer the second option, since it would not change an existing interface.

Example

            try {
                $data[$name] = $this->extractValue($name, $value, $object);
            } catch (\Throwable $t) {
                throw new \Exception("Could not extract field " . $name, 0, $t);
            }

instead of

 $data[$name] = $this->extractValue($name, $value, $object);

@meidlinga
Can you provide a code example which illustrates the problem?
Thanks in advance! 👍

Hi @froschdesign ,

Thanks for the fast response. If this is accepted, I would be happy to provide a PR.

Please consider the following executable example.
It contains two different cases. One throwing an exception and a second one generating a type error. Additionally to extract, I included the same showcase for hydrate

Exceptions

Without the patch, the exception will be:

Exception: value cant be null in src/hydratortest.php:29
Stack trace:
#0 vendor/laminas/laminas-hydrator/src/AbstractHydrator.php(124): ExceptionStrategy->extract()
#1 vendor/laminas/laminas-hydrator/src/ArraySerializableHydrator.php(55): Laminas\Hydrator\AbstractHydrator->extractValue()
#2 src/hydratortest.php(54): Laminas\Hydrator\ArraySerializableHydrator->extract()
#3 {main}

It is not visible, which field was responsible for the error,
With the suggested patch it looks like this:

Exception: value cant be null in src/hydratortest.php:29
Stack trace:
#0 vendor/laminas/laminas-hydrator/src/AbstractHydrator.php(124): ExceptionStrategy->extract()
#1 vendor/laminas/laminas-hydrator/src/ArraySerializableHydrator.php(55): Laminas\Hydrator\AbstractHydrator->extractValue()
#2 src/hydratortest.php(54): Laminas\Hydrator\ArraySerializableHydrator->extract()
#3 {main}

Next Exception: Could not extract field bla in vendor/laminas/laminas-hydrator/src/ArraySerializableHydrator.php:57
Stack trace:
#0 src/hydratortest.php(54): Laminas\Hydrator\ArraySerializableHydrator->extract()
#1 {main}

Example code

<?php

include "../vendor/autoload.php";

use Laminas\Stdlib\ArraySerializableInterface;

use Laminas\Hydrator\ArraySerializableHydrator;
use Laminas\Hydrator\Strategy\DefaultStrategy;


class TestEntity implements ArraySerializableInterface
{
    public function exchangeArray(array $array)
    {
        // Omitted
    }

    public function getArrayCopy()
    {
        return ['foo' => 'foo', 'bar' => 'bar', 'bla' => null];
    }
}
$testArray = ['foo' => 'foo', 'bar' => null, 'bla' => 'bla'];

class ExceptionStrategy extends DefaultStrategy
{
    public function extract($value, ?object $object = null)
    {
        if (is_null($value)) throw new \Exception("value cant be null");
        return $value;
    }

    public function hydrate($value, ?array $data = null)
    {
        if (is_null($value)) throw new \Exception("value cant be null");
        return $value;
    }
}

class ExceptionHydrator extends ArraySerializableHydrator
{
    public function __construct()
    {
        $this->addStrategy('foo', new ExceptionStrategy());
        $this->addStrategy('bar', new ExceptionStrategy());
        $this->addStrategy('bla', new ExceptionStrategy());
    }
}

$exceptionHydrator = new ExceptionHydrator();

echo "# Extract\n";
try {
    $exceptionHydrator->extract(new TestEntity());
} catch (\Throwable $t) {
    echo $t;
}

echo "\n\n\n# Hydrate\n";
try {
    $exceptionHydrator->hydrate($testArray, new TestEntity());
} catch (\Throwable $t) {
    echo $t;
}


class TypeErrorStrategy extends DefaultStrategy
{

    function typeConstraint(object $value)
    {
        return value;
    }

    public function extract($value, ?object $object = null)
    {
        return $this->typeConstraint($value);
    }

    public function hydrate($value, ?array $data = null)
    {
        return $this->typeConstraint($value);
    }
}

class TypeErrorHydrator extends ArraySerializableHydrator
{
    public function __construct()
    {
        $this->addStrategy('foo', new TypeErrorStrategy());
        $this->addStrategy('bar', new TypeErrorStrategy());
        $this->addStrategy('bla', new TypeErrorStrategy());
    }
}

$typeErrorHydrator = new TypeErrorHydrator();

echo "\n\n\n# Extract\n";
try {
    $res = $typeErrorHydrator->extract(new TestEntity());
    print_r($res);
} catch (\Exception $e) {
    echo $e->getTraceAsString();
}

echo "\n\n\n# Hydrate\n";
try {
    $typeErrorHydrator->hydrate($testArray, new TestEntity());
} catch (\Exception $e) {
    echo $e->getTraceAsString();
}