laminas/laminas-db

Update HydratingResultSet docs to discourage use of ClassMethodsHydrator

kynx opened this issue · 7 comments

kynx commented

As per laminas/laminas-hydrator#12, you can't use type declarations in entities populated by HydratingResultSet if using the ClassMethodsHydrator.

The response on that PR indicate casting the results of a database query are the responsibility of the DAL layer, which laminas/laminas-db unfortunately does not provide.

The documentation should be updated to reflect this.

@kynx
Example:

CREATE TABLE `test` (
  `foo` int(11) NOT NULL,
  `bar` float NOT NULL
) 
class Entity
{
    private $foo;
    private $bar;

    public function setFoo(int $foo)
    {
        $this->foo = $foo;
    }

    public function setBar(float $bar)
    {
        $this->bar = $bar;
    }
}
$adapter = new Laminas\Db\Adapter\Adapter([
    'driver'         => 'Pdo',
    'dsn'            => '',
    'driver_options' => [
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_EMULATE_PREPARES => false,
    ],
]);

$sql       = new Laminas\Db\Sql\Sql($adapter);
$select    = $sql->select('test');
$statement = $sql->prepareStatementForSqlObject($select);
$result    = $statement->execute();

$resultSet = new Laminas\Db\ResultSet\HydratingResultSet(
    new Laminas\Hydrator\ClassMethodsHydrator(),
    new Entity()
);
$resultSet->initialize($result);

/** @var Entity $entity */
foreach ($resultSet as $entity) {
    var_dump($entity);
}

Output:

class Entity#57 (2) {
  private $foo =>
  int(100)
  private $bar =>
  double(200.123)
}

(This does not work for SQLite.)

https://www.php.net/manual/pdo.setattribute.php#refsect1-pdo.setattribute-description

kynx commented

@froschdesign Thanks for the suggestion, but I've tried that.

Have a go with DECIMAL(14,3) instead of FLOAT. That comes out as a string in MySQL - and apparently in DB2 as well (https://discourse.laminas.dev/t/db2-datatypes-and-laminas-laminas-hydrator-decimal-numeric-and-bigint-are-treated-as-strings/1468) - so the hydrator will barf.

Also an unfortunate side effect of switching off ATTR_EMULATE_PREPARES is that you can no longer use named placeholders. A bit of a bummer when you've got a large codebase that worked fine until hydrator 3.

@kynx

Also an unfortunate side effect of switching off ATTR_EMULATE_PREPARES is that you can no longer use named placeholders.

Unfortunately I do not know what you have tested, but you should check this again.


Btw. I understand your situation but I only wanted to give you an example, unfortunately it does not represent a final solution.

kynx commented

@froschdesign

| Unfortunately I do not know what you have tested, but you should check this again.

Heh, yes, I'm losing track of the bumps I've hit. You cannot use the same named parameter twice - ie WHERE a = :name OR b = :name.

kynx commented

@froschdesign What do you think about this as an approach?

We add an optional third argument to the HydratingResultSet constructor that accepts a TypeCasterInterface. If present, this is responsible for casting scalar values in the current() row to the correct type. Something like:

interface TypeCasterInterface
{
    public function castRow(HydratorInterface $hydrator, array $row, $objectPrototype);
}

A ClassMethodsTypeCaster implementation could use a combination of reflection on the $objectPrototype and the naming strategy from the $hydrator to figure out which scalars in the row to cast to the correct type. It would need some caching to be performant.

This has the advantage of closely mirroring what PHP's implicit type casting was doing before hydrator 3. The problem would be if other filters / strategies were changing types between the raw row and the hydration. Maybe they could be detected somehow and those columns ignored? In any case, casting wouldn't be there by default, so it wouldn't break anything more than at present.

The only other solution I can see is altering Pdo\Result to make it understand the column metadata exposed in the PDOStatement. That would be "better", and along the lines of other DBALs, but I suspect full of gotchas: differences between drivers, the fact that driver:decl_type doesn't exist for the result of functions.... it's way more than I could take on right now.

dlanz commented

I'm having the same issue and an interim solution I’ve come up with is this (in my model class):

/**
 * Get getField1
 *
 * - For Numeric and BigInt datatypes
 *
 * @return int
 */
public function getField1(): int
{
    return (int)$this->Field1;
}

/**
 * Set setField1
 *
 * - For Numeric and BigInt datatypes
 *
 * @param int $Field1
 *
 * @return void
 */
public function setField1($Field1): void
{
    $this->Field1 = (int)$Field1;
}

/**
 * Get getField2
 *
 * - For Decimal datatypes
 *
 * @return float
 */
public function getField2(): float
{
    return (float)$this->Field2;
}

/**
 * Set setField2
 *
 * - For Decimal datatypes
 *
 * @param float $Field2
 *
 * @return void
 */
public function setField2($Field2): void
{
    $this->Field2 = (float)$Field2;
}

Removing typehinting from the parameter in the “set” methods allows for the field to come in as anything (IE: String), but casts it to the correct datatype. If it’s already the correct datatype, then no harm done. Then the return value in the “get” methods also ensure it returns the correct datatype (a little redundant though, I know).

This is just an alternative to using 2.4.2. Perhaps someone’s dependencies require 3.x?

This package is considered feature-complete, and is now in security-only maintenance mode, following a decision by the Technical Steering Committee.
If you have a security issue, please follow our security reporting guidelines.
If you wish to take on the role of maintainer, please nominate yourself

If you are looking for an actively maintained package alternative, we recommend: