gabordemooij / redbean

ORM layer that creates models, config and database on the fly
https://www.redbeanphp.com
2.3k stars 279 forks source link

Models seem to behave strangely with var types #896

Closed benmajor closed 2 years ago

benmajor commented 2 years ago

We have noticed some odd behaviour in RedBean, which I suspect may be related to fetchAs and models. For instance, in our Organization model, we have the following getter defined:

/**
 * Get the owner of the organization
 * 
 * @return User
 */
public function getOwner()
{
    return $this->bean->fetchAs('user')->owner;
}

This correctly returns the user bean, and we're then able to use the getId() method on that bean, but it returns a string, rather than integer, so a strict equality check fails. The below is from a method inside of our user Model:

var_dump($organization->getOwner()->getId());
var_dump($this->getId());

Which produces:

string(1) "1"
int(1)

Obviously this causes the following check to fail:

if ($organization->getOwner()->getId() === $this->getId()) {
    return true;
}

Of course, we can solve this by changing to a non-strict equality check, but it would be good to try and narrow down where the behaviour is coming from, as I don't think this should be the default behaviour inside of RedBean?

In case it helps, here's our __call() magic method that handles the getters and setters of Models via an AbstractEntity class, in case it may be useful in identifying the cause of this:


/**
 * Magic get() and set() definition
 *
 * @param string $method The method name called
 * @param mixed $params The parameters of the method
 */
public function __call($method, $params)
{
    $start = substr($method, 0, 3);

    if ($start == 'get' || $start == 'set') {
        $property = lcfirst(substr($method, 3));

        // It's a getter:
        if ($start == 'get') {
            $value = $this->bean->$property;

            // Is it a valid datetime?
            if (is_string($value) && \DateTime::createFromFormat('Y-m-d H:i:s', $value) !== false) {
                return \DateTime::createFromFormat('Y-m-d H:i:s', $value);
            }

            // Is it a valid date?
            if (is_string($value) && \DateTime::createFromFormat('Y-m-d', $value) !== false) {
                return \DateTime::createFromFormat('Y-m-d', $value);
            }

            // Attempt to decode:
            if (is_string($value)) {
                $json = json_decode($value);

                // Is it a JSON string?
                if (json_last_error() === JSON_ERROR_NONE) {
                    return $json;
                }
            }

            // Return value of the object:
            return $value;
        }

        // It's a setter:
        else {
            // We need at least one param for a setter:
            if ( ! count($params)) {
                throw new EntityExcpetion('Setter for '.$property.' expects at least 1 parameter.');
            }

            $param = $params[0];

            // It's a date:
            if ($param instanceof \DateTime) {
                $param = $param->format('Y-m-d H:i:s');
            }

            // Now set the value:
            $this->bean->$property = $param;

            // Return the entity to preserve method chaining:
            return $this;
        }
    }
}
gabordemooij commented 2 years ago

Thank you for reporting this, I will look into this issue.

gabordemooij commented 2 years ago

I cannot seem to reproduce this issue:

(careful, contains nuke())

require 'rb.php';
R::setup('mysql:host=localhost;dbname=redbean','root','root' );
R::getDatabaseAdapter()->getDatabase()->stringifyFetches(false);
R::getDatabaseAdapter()->getDatabase()->getPDO()->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
R::nuke();
$organisation = R::dispense('organisation');
$organisation->name = 'Big Ben';
$owner = R::dispense('user');
$owner->name = 'Ben';
$organisation->owner = $owner;
$id = R::store($organisation);
$organisation = R::load('organisation',$id);
$owner = $organisation->fetchAs('user')->owner;
var_dump($owner->id);

gives:

int(1)
benmajor commented 2 years ago

Okay, thanks for looking into this. I will do some more investigation and see if I can figure out what's going on.

I wonder if it's related to me not calling stringifyFetches(false) anywhere?

gabordemooij commented 2 years ago

This might cause an issue yes. For MySQL you also need to set ATTR_EMULATE_PREPARES. Please note that RedBeanPHP and PDO cannot really enforce types, it's always up to the implementation of the database driver to honour these requests.

benmajor commented 2 years ago

Sorry for the delay in coming back regarding this. Calling stringifyFetches(false) seems to solve the problem. Can I ask why the default value for this is true?

gabordemooij commented 2 years ago

I think this is mainly for backward compatibility. Personally I never bother to change it since I regard PHP as a dynamically typed programming language and I am fine with the associated risks and limitations of that.

marios88 commented 2 years ago

According to this all $bean->getId() should return string|null

https://github.com/gabordemooij/redbean/blob/a9fd27ffc5486fee93e16c7eb206427a35ac67b8/RedBeanPHP/OODBBean.php#L738-L751