-1

I'm trying to make some very rudimental database mapping conversion where I'm fetching data from the database and then trying to convert that array to an instance of an arbitrary class. This should be dynamic so that I can use the same function no matter the output object class/properties.

Let's say I have CASE1:

$student = [
            'name'    => 'Jhon',
            'surname' => 'Wick',
            'age'     => 40
        ];
        
        class Student{
            private string $name;
            private string $surname;
            private int $age;

            ... getter and setters
        }

CASE2:

        $car = [
          'manufacturer' => 'Toyota',
          'model' => 'one of their model',
          'commercialName' => 'the name'
        ];
        class Car{
            private $manufacturer;
            private $model;
            private $commercialName;
            
            // Getter and Setter
        }

And I want something that transforms the $student array var to a Student instance or the $car to Car. How can I do that? I know I can do that using something like:

$funcName = 'get' . ucfirst($camelCaseName);
        if (method_exists($this, $funcName)) {
            $funcValue = $this->$funcName();
    }

But I'm searching for something a little more modern like using Reflection. What is the best approach to this? What could be an efficient solution?

To give further info, this is needed for an extension of the WordPress $wpdb object. If possible I wouldn't like to use public class properties because I may need to actually call the class setter in some case to change some other class value. Let's say something like giving birth date should calculate age

Dharman
  • 30,962
  • 25
  • 85
  • 135
Diego
  • 1,610
  • 1
  • 14
  • 26
  • Some of the answers here would probably help you: https://stackoverflow.com/questions/2715465/converting-a-php-array-to-class-variables – ADyson Jan 26 '23 at 12:07
  • Are you asking for https://www.php.net/manual/en/mysqli-result.fetch-object.php ? Is this the function you are looking for? – Dharman Jan 26 '23 at 12:11
  • What is "modern" in using Reflection? How using Reflection makes your code better? – Your Common Sense Jan 26 '23 at 12:26
  • @YourCommonSense "modern" is the term i chose to say "better then cycling all the columns and manually building the method name with a dynamic var like $this->$funcName()" – Diego Jan 26 '23 at 13:35
  • Use a hydrator for that purpose: https://www.github.com/laminas/laminas-hydrator and don 't reinvent the wheel. Or at least look how the ClassMethodsHydrator of that package works. ;) – Marcel Jan 26 '23 at 14:29
  • Don't extend the $wpdb object, seriously. If you do you become dependent on that object, which may change in future. For example: [SQLite database integration](https://wordpress.org/plugins/sqlite-database-integration/). Instead create a new object type that uses it. – O. Jones Jan 26 '23 at 14:49
  • @O.Jonesi'm not literally extending wpdb class. I've build another class that internally uses wpdb to make the queries, that's all – Diego Jan 26 '23 at 16:41

2 Answers2

1

As I stated out in the comments already, all that you need is the process of hydration. The boys and girls from Laminas got a good maintained package called laminas/laminas-hydrator which can do the job for you.

An easy example:

<?php

declare(strict_types=1);

namespace Marcel;

use Laminas\Hydrator\ClassMethodsHydrator;
use Marcel\Model\MyModel;

$hydrator = new ClassMethodsHydrator();

// hydrate arry data into an instance of your entity / data object
// using setter methods
$object = $hydrator->hydrate([ ... ], new MyModel());

// vice versa using the getter methods
$array = $hydrator->extract($object);

Your approach is not so wrong. It is at least going in the right direction. In my eyes you should not use private properties. What kind of advantage do you expect from using private properties? They only bring disadvantages. Think of extensions of your model. Protected properties do the same job for just accessing the properties via getters and setters. Protected properties are much more easy to handle.

<?php

declare(strict_types=1);

namespace Marcel;

class MyDataObject
{
    public ?string $name = null;
    public ?int $age = null;

    public function getName(): ?string
    {
        return $name;
    }
    
    public function setName(?string $name): void
    {
        $this->name = $name;
    }

    public function getAge(): ?int
    {
        return $this->age;
    }

    public function setAge(?int $age): void
    {
        $this->age = $age;
    }
}

class MyOwnHydrator
{
    public function hydrate(array $data, object $object): object
    {
        foreach ($data as $property => $value) {
            // beware of naming your properties in the data array the right way
            $setterName = 'set' . ucfirst($property);
            if (is_callable([$object, $setterName])) {
                $object->{$setterName}($value);
            }
        }

        return $object;
    }
}

// example
$data = [
    'age' => 43,
    'name' => 'Marcel',
    'some_other_key' => 'blah!', // won 't by hydrated
];

$hydrator = new MyOwnHydrator();
$object = new MyDataObject();

$object = $hydrator->hydrate($data, $object);

This is the simplest hydrator you can get. It iterates through the data array, creates setter method names and checks if they are callable. If so the value will be hydrated into the object by calling the setter method. At the end you 'll get a hydrated object.

But beware. There are some stumbling blocks with this solution that need to be considered. The keys of your data array must be named exactly like the properties of your data object or entity. You have to use some naming strategies, when you want to use underscore separated keys in your array but your object properties are camel cased, e.g. Another problem could be type hints. What if the example had a birthday and only accepts DateTime instances and your data array only contains the birhtday as string "1979-12-19"? What if your data array contains sub arrays, that should be hydrated in n:m relations?

All that is done already in the menetiond laminas package. If you don 't need all that fancy stuff, follow your first thought and build your own hydrator. ;)

Marcel
  • 4,854
  • 1
  • 14
  • 24
  • Ok, i will accept this answer and just deal with the fact that there's no easy simple solution that actually works better then manually building method name and calling them. A real hydrator package is needed – Diego Jan 26 '23 at 16:45
0

I would say drop setters/getters and use readonly properties:

class Car{
    public function __construct(
        public readonly string $manufacturer,
        public readonly string $model,
        public readonly string $commercialName,
    );
}

...

new Car(...$car);
Justinas
  • 41,402
  • 5
  • 66
  • 96
  • I fully support readonly properties, but I don't think it answers OP's question. You are still creating an instance of the object yourself instead of using a generic function. – Dharman Jan 26 '23 at 12:16
  • @Dharman _And I want something that transforms the $student array var to a Student instance or the $car to Car_ - seems like it answers OP question, just move it to some helper – Justinas Jan 26 '23 at 12:18
  • That's *exactly* what DTOs are for. – Your Common Sense Jan 26 '23 at 12:27
  • This approach makes it harder to use the Car class to populate just a few of the fields, which sometimes happen in my case. I'll edit the question to provide further details. Setting the fields to nullable may solve this secondary issue. What can't be solved this was is for example if setting a property of the class should add some secondary data (like re-calculating the age starting from birth date, just as an example) – Diego Jan 26 '23 at 13:29