Search code examples
phpphp-8.1

How to set a deep value in an object property or array column with link in PHP?


I need a function that can set its deepest value on an instance, whether it is an object or an array.

For example, I have a function like this that I wrote and played with, but the setValue function does not work, my silly placing of links here and there did not help me((

functions:


function setValue($name, &$initial, $value) {
   // Set function not work
    $names = explode('.', $name);
    $lastIndex = (count($names) - 1);

    $getter = (function ($name) {
        if (property_exists($this, $name) === false) {
            return null;
        }

        $value = &$this->{$name};

        return $value;
    })(...);

    $setter = (function ($name, $value) {
        if (property_exists($this, $name) === true) {
            $this->{$name} = $value;
        }
    })(...);

    $reference = &$initial;
    foreach ($names as $index => $keyOrProperty) {
        if (is_array($reference) === true) {
            if ($lastIndex === $index) {
                $reference[$keyOrProperty] = $value;

                break;
            }

            $nextReference = &$reference[$keyOrProperty];
            if ($nextReference === null) {
                break;
            }

            $reference = &$nextReference;

            continue;
        }

        if (is_object($reference) === true) {
            if ($lastIndex === $index) {
                $setter->call($reference, $keyOrProperty, $value);

                break;
            }

            $nextReference = $getter->call($reference, $keyOrProperty);
            if ($nextReference === null) {
                break;
            }

            $reference = &$nextReference;

            continue;
        }

        break;
    }
}

function getValue($name, $initial) {
    $names = explode('.', $name);

    $getter = (function ($name) {
        if (property_exists($this, $name) === false) {
            return $this;
        }

        return $this->{$name};
    })(...);

    $accumulator = static function (mixed $curr, string $name) use ($getter): mixed {
        if ($curr === null) {
            return null;
        }

        if (is_array($curr) === true) {
            return (array_key_exists($name, $curr) === true) ? $curr[$name] : null;
        }

        if (is_object($curr) === true) {
            return $getter->call($curr, $name);
        }

        return $curr;
    };

    return array_reduce($names, $accumulator, $initial);
}

test:

class MyFoo
{
    private MyBar $bar;

    private array $array;

    public function setBar(MyBar $bar): static
    {
        $this->bar = $bar;

        return $this;
    }

    public function setArray(array $array): static
    {
        $this->array = $array;

        return $this;
    }
}

class MyBar
{
    private MyFoo $foo;

    private array $array;

    public function setFoo(MyFoo $foo): static
    {
        $this->foo = $foo;

        return $this;
    }

    public function setArray(array $array): static
    {
        $this->array = $array;

        return $this;
    }
}


$instance = (new MyFoo())
    ->setBar(
        (new MyBar())
            ->setFoo(
                (new MyFoo())
                    ->setArray(
                        [
                            'myBar' => (new MyBar())
                                ->setArray(
                                    [
                                        'deep' => [
                                            'myFoo' => (new MyFoo())
                                                ->setBar(
                                                    (new MyBar())
                                                        ->setArray(['test' => 'is Test'])
                                                )
                                        ]
                                    ]
                                )
                        ]
                    )
            )
    );

echo getValue('bar.foo.array.myBar.array.deep.myFoo.bar.array.test', $instance);
setValue('bar.foo.array.myBar.array.deep.myFoo.bar.array.test', $instance, 'Set Test');
echo getValue('bar.foo.array.myBar.array.deep.myFoo.bar.array.test', $instance);

Is it even possible to write such a function, or does anyone have any ideas?


Solution

  • The main problem is that when you want to get an array from a property, the return must also be a reference. But you cannot use Closure::call, because its declaration return type is not a reference. You need to bind the closure first, then directly call the closure.

    # $nextReference = $getter->call($reference, $keyOrProperty);
    
    $cl = $getter->bindTo($reference, $reference);
    $nextReference =& $cl($keyOrProperty);
    

    Besides, anonymous functions are already closures, the first class callable syntax is not necessary here. Note the anonymous function has an ampersand to return a reference instead of a value.

    # $getter = (function ($name) {
    #     blabla
    # })(...);
    
    $getter = function &($name) {
        if (property_exists($this, $name) === false) {
            return null;
        }
        return $this->{$name};
    };