-
-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Undefined property error when a property exists in proxied class but not initialised in proxy #511
Comments
This seems like the correct behavior to mr, but let's inspect further. Can you maybe show your entity, and maybe the generated proxy file? |
Err... It now breaks due to the // check protected property access via compatible class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$object = isset($caller['object']) ? $caller['object'] : '';
$expectedType = self::$protectedProperties1a2d1[$name];
if ($object instanceof $expectedType) {
return $this->$name; // HERE
} Was I being crazy...?! Anyway... Here are all the files I've got which are possibly related (btw I'm using Laravel): <?php
namespace App\Doctrine;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* @ODM\Document(collection="labs")
* @ODM\HasLifecycleCallbacks
*/
class Lab extends BaseModel
{
use Traits\CreatedAt, Traits\UpdatedAt;
/**
* @ODM\Id
* @var string
*/
public $_id;
/**
* @ODM\EmbedOne(targetDocument=Address::class)
* @var Address
*/
public $address;
} <?php
namespace App\Doctrine;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/** @ODM\EmbeddedDocument */
class Address extends BaseModel
{
/**
* @ODM\Field(type="string")
* @var string
*/
public $street;
/**
* @ODM\Field(type="string")
* @var string
*/
public $city;
/**
* @ODM\Field(type="string")
* @var string
*/
public $state;
/**
* @ODM\Field(type="string")
* @var string
*/
public $country;
/**
* @ODM\Field(type="string")
* @var string
*/
public $zip;
} <?php
namespace App\Doctrine;
use Carbon\Carbon;
use MyCLabs\Enum\Enum;
class BaseModel
{
protected $excludedFromArray = ['_id', 'lazyPropertiesDefaults'];
private $preserveOriginalName = false;
public function __construct(array $data = null)
{
if (!empty($data)) {
foreach ($data as $key => $value) {
$this->$key = $value;
}
}
}
public function __get($name)
{
$accessor = 'get_' . $this->snakeCase($name);
if (method_exists($this, $accessor)) {
return $this->$accessor();
}
return $this->$name ?? null;
}
public function __set($name, $value)
{
$mutator = 'set_' . $this->snakeCase($name);
if (method_exists($this, $mutator)) {
// an additional bool parameter ($toRemove = false) is provided
$this->$mutator($value, false);
} else {
$this->$name = $value;
}
}
public function __unset($name)
{
$this->__set($name, null);
}
public function __isset($name)
{
return $this->__get($name) !== null;
}
public function __clone()
{
foreach (get_object_vars($this) as $key => $value) {
$this->$key = $this->cloneRecursively($value);
}
}
/**
* @return array
*/
public function getExcludedFromArray(): array
{
return $this->excludedFromArray;
}
/**
* @param array $excludedFromArray
*/
public function setExcludedFromArray(array $excludedFromArray): void
{
$this->excludedFromArray = $excludedFromArray;
}
/**
* @param mixed $item
* @return mixed
*/
private function cloneRecursively($item)
{
if (is_object($item)) {
return clone $item;
}
if (is_array($item)) {
return array_map([self::class, 'cloneRecursively'], $item);
}
return $item;
}
public function toArray(): array
{
$reflect = new \ReflectionClass($this);
$result = [];
foreach ($reflect->getProperties(\ReflectionProperty::IS_PUBLIC) as $key) {
$key = $key->getName();
if (strpos($key, '__') === 0
|| in_array($key, $this->getExcludedFromArray(), true)
|| method_exists($this, 'get_' . $this->snakeCase($key))) {
continue;
}
$result[$key] = $this->toArrayRecursively($this->$key);
}
foreach ($reflect->getMethods() as $method) {
$method = $method->getName();
if (preg_match('/^get_(.+)$/', $method, $matches)) {
$key = $matches[1];
if (strpos($key, '__') === 0 || in_array($key, $this->getExcludedFromArray(), true)) {
continue;
}
$result[$key] = $this->toArrayRecursively($this->{$matches[0]}());
}
}
return $result;
}
/**
* @param mixed $item
* @return mixed
*/
private function toArrayRecursively($item)
{
if ($item instanceof Carbon) {
return $item->format('c');
}
if ($item instanceof \DateTimeInterface) {
return Carbon::instance($item)->format('c');
}
if ($item instanceof Enum) {
return $item->getValue();
}
if ($item instanceof \IteratorAggregate) {
$item = $item->getIterator();
}
if ($item instanceof \Iterator) {
$item = iterator_to_array($item);
}
if (is_object($item) && method_exists($item, 'toArray')) {
return $item->toArray();
}
if (is_array($item)) {
return array_map([self::class, 'toArrayRecursively'], $item);
}
return $item;
}
protected function snakeCase($name): string
{
if ($this->preserveOriginalName) {
return $name;
}
return strtolower(preg_replace('/[^A-Z0-9]+/i', '_', $name));
}
} <?php
namespace App\Doctrine\Traits;
use Carbon\Carbon;
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
trait CreatedAt
{
/**
* @ODM\Field(type="date")
* @var \DateTime|Carbon
*/
public $created_at;
/**
* @ODM\PrePersist
* @param LifecycleEventArgs $eventArgs
*/
public function doPrePersistCreatedAt(LifecycleEventArgs $eventArgs): void
{
if (empty($this->created_at)) {
$this->created_at = Carbon::now();
}
}
} <?php
namespace App\Doctrine\Traits;
use Carbon\Carbon;
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
trait UpdatedAt
{
/**
* @ODM\Field(type="date")
* @var \DateTime|Carbon
*/
public $updated_at;
/**
* @ODM\PrePersist
* @param LifecycleEventArgs $eventArgs
*/
public function doPrePersistUpdatedAt(LifecycleEventArgs $eventArgs): void
{
$this->updated_at = Carbon::now();
}
/**
* @ODM\PreUpdate
* @param LifecycleEventArgs $eventArgs
*/
public function doPreUpdateUpdatedAt(LifecycleEventArgs $eventArgs): void
{
$this->updated_at = Carbon::now();
}
} <?php
namespace Proxies\__PM__\App\Doctrine\Lab;
class Generated6805517574cc3696bcee09ef22e16ded extends \App\Doctrine\Lab implements \ProxyManager\Proxy\GhostObjectInterface
{
/**
* @var \Closure|null initializer responsible for generating the wrapped object
*/
private $initializerc3072 = null;
/**
* @var bool tracks initialization status - true while the object is initializing
*/
private $initializationTrackerce44e = false;
/**
* @var bool[] map of public properties of the parent class
*/
private static $publicPropertiesda3cc = [
'address' => true,
'created_at' => true,
'updated_at' => true,
];
/**
* @var array[][] visibility and default value of defined properties, indexed by
* property name and class name
*/
private static $privatePropertiese171f = [
'preserveOriginalName' => [
'App\\Doctrine\\BaseModel' => true,
],
];
/**
* @var string[][] declaring class name of defined protected properties, indexed by
* property name
*/
private static $protectedProperties1a2d1 = [
'excludedFromArray' => 'App\\Doctrine\\BaseModel',
];
private static $signature6805517574cc3696bcee09ef22e16ded = 'YTo0OntzOjk6ImNsYXNzTmFtZSI7czoxNjoiQXBwXERvY3RyaW5lXExhYiI7czo3OiJmYWN0b3J5IjtzOjQ0OiJQcm94eU1hbmFnZXJcRmFjdG9yeVxMYXp5TG9hZGluZ0dob3N0RmFjdG9yeSI7czoxOToicHJveHlNYW5hZ2VyVmVyc2lvbiI7czo0NjoiMi4yLjNANGQxNTQ3NDJlMzFjMzUxMzdkNTM3NGM5OThlOGY4NmI1NGRiMmUyZiI7czoxMjoicHJveHlPcHRpb25zIjthOjE6e3M6MTc6InNraXBwZWRQcm9wZXJ0aWVzIjthOjE6e2k6MDtzOjM6Il9pZCI7fX19';
/**
* Triggers initialization logic for this ghost object
*
* @param string $methodName
* @param mixed[] $parameters
*
* @return mixed
*/
private function callInitializer728ea($methodName, array $parameters)
{
if ($this->initializationTrackerce44e || ! $this->initializerc3072) {
return;
}
$this->initializationTrackerce44e = true;
$this->address = NULL;
$this->created_at = NULL;
$this->updated_at = NULL;
$this->excludedFromArray = array (
0 => '_id',
1 => 'lazyPropertiesDefaults',
);
static $cacheApp_Doctrine_BaseModel;
$cacheApp_Doctrine_BaseModel ?: $cacheApp_Doctrine_BaseModel = \Closure::bind(function ($instance) {
$instance->preserveOriginalName = false;
}, null, 'App\\Doctrine\\BaseModel');
$cacheApp_Doctrine_BaseModel($this);
$properties = [
'address' => & $this->address,
'created_at' => & $this->created_at,
'updated_at' => & $this->updated_at,
'' . "\0" . '*' . "\0" . 'excludedFromArray' => & $this->excludedFromArray,
];
static $cacheFetchApp_Doctrine_BaseModel;
$cacheFetchApp_Doctrine_BaseModel ?: $cacheFetchApp_Doctrine_BaseModel = \Closure::bind(function ($instance, array & $properties) {
$properties['' . "\0" . 'App\\Doctrine\\BaseModel' . "\0" . 'preserveOriginalName'] = & $instance->preserveOriginalName;
}, $this, 'App\\Doctrine\\BaseModel');
$cacheFetchApp_Doctrine_BaseModel($this, $properties);
$result = $this->initializerc3072->__invoke($this, $methodName, $parameters, $this->initializerc3072, $properties);
$this->initializationTrackerce44e = false;
return $result;
}
/**
* Constructor for lazy initialization
*
* @param \Closure|null $initializer
*/
public static function staticProxyConstructor($initializer)
{
static $reflection;
$reflection = $reflection ?? $reflection = new \ReflectionClass(__CLASS__);
$instance = $reflection->newInstanceWithoutConstructor();
unset($instance->address, $instance->created_at, $instance->updated_at, $instance->excludedFromArray);
\Closure::bind(function (\App\Doctrine\BaseModel $instance) {
unset($instance->preserveOriginalName);
}, $instance, 'App\\Doctrine\\BaseModel')->__invoke($instance);
$instance->initializerc3072 = $initializer;
return $instance;
}
public function __get($name)
{
$this->initializerc3072 && ! $this->initializationTrackerce44e && $this->callInitializer728ea('__get', array('name' => $name));
if (isset(self::$publicPropertiesda3cc[$name])) {
return $this->$name;
}
if (isset(self::$protectedProperties1a2d1[$name])) {
if ($this->initializationTrackerce44e) {
return $this->$name;
}
// check protected property access via compatible class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$object = isset($caller['object']) ? $caller['object'] : '';
$expectedType = self::$protectedProperties1a2d1[$name];
if ($object instanceof $expectedType) {
return $this->$name;
}
$class = isset($caller['class']) ? $caller['class'] : '';
if ($class === $expectedType || is_subclass_of($class, $expectedType) || $class === 'ReflectionProperty') {
return $this->$name;
}
} elseif (isset(self::$privatePropertiese171f[$name])) {
// check private property access via same class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$class = isset($caller['class']) ? $caller['class'] : '';
static $accessorCache = [];
if (isset(self::$privatePropertiese171f[$name][$class])) {
$cacheKey = $class . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function & ($instance) use ($name) {
return $instance->$name;
}, null, $class);
return $accessor($this);
}
if ($this->initializationTrackerce44e || 'ReflectionProperty' === $class) {
$tmpClass = key(self::$privatePropertiese171f[$name]);
$cacheKey = $tmpClass . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function & ($instance) use ($name) {
return $instance->$name;
}, null, $tmpClass);
return $accessor($this);
}
}
return parent::__get($name);
}
public function __set($name, $value)
{
$this->initializerc3072 && $this->callInitializer728ea('__set', array('name' => $name, 'value' => $value));
if (isset(self::$publicPropertiesda3cc[$name])) {
return ($this->$name = $value);
}
if (isset(self::$protectedProperties1a2d1[$name])) {
// check protected property access via compatible class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$object = isset($caller['object']) ? $caller['object'] : '';
$expectedType = self::$protectedProperties1a2d1[$name];
if ($object instanceof $expectedType) {
return ($this->$name = $value);
}
$class = isset($caller['class']) ? $caller['class'] : '';
if ($class === $expectedType || is_subclass_of($class, $expectedType) || $class === 'ReflectionProperty') {
return ($this->$name = $value);
}
} elseif (isset(self::$privatePropertiese171f[$name])) {
// check private property access via same class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$class = isset($caller['class']) ? $caller['class'] : '';
static $accessorCache = [];
if (isset(self::$privatePropertiese171f[$name][$class])) {
$cacheKey = $class . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function ($instance, $value) use ($name) {
return ($instance->$name = $value);
}, null, $class);
return $accessor($this, $value);
}
if ('ReflectionProperty' === $class) {
$tmpClass = key(self::$privatePropertiese171f[$name]);
$cacheKey = $tmpClass . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function ($instance, $value) use ($name) {
return ($instance->$name = $value);
}, null, $tmpClass);
return $accessor($this, $value);
}
}
return parent::__set($name, $value);
}
public function __isset($name)
{
$this->initializerc3072 && $this->callInitializer728ea('__isset', array('name' => $name));
if (isset(self::$publicPropertiesda3cc[$name])) {
return isset($this->$name);
}
if (isset(self::$protectedProperties1a2d1[$name])) {
// check protected property access via compatible class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$object = isset($caller['object']) ? $caller['object'] : '';
$expectedType = self::$protectedProperties1a2d1[$name];
if ($object instanceof $expectedType) {
return isset($this->$name);
}
$class = isset($caller['class']) ? $caller['class'] : '';
if ($class === $expectedType || is_subclass_of($class, $expectedType)) {
return isset($this->$name);
}
} else {
// check private property access via same class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$class = isset($caller['class']) ? $caller['class'] : '';
static $accessorCache = [];
if (isset(self::$privatePropertiese171f[$name][$class])) {
$cacheKey = $class . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function ($instance) use ($name) {
return isset($instance->$name);
}, null, $class);
return $accessor($this);
}
if ('ReflectionProperty' === $class) {
$tmpClass = key(self::$privatePropertiese171f[$name]);
$cacheKey = $tmpClass . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function ($instance) use ($name) {
return isset($instance->$name);
}, null, $tmpClass);
return $accessor($this);
}
}
return parent::__isset($name);
}
public function __unset($name)
{
$this->initializerc3072 && $this->callInitializer728ea('__unset', array('name' => $name));
if (isset(self::$publicPropertiesda3cc[$name])) {
unset($this->$name);
return;
}
if (isset(self::$protectedProperties1a2d1[$name])) {
// check protected property access via compatible class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$object = isset($caller['object']) ? $caller['object'] : '';
$expectedType = self::$protectedProperties1a2d1[$name];
if ($object instanceof $expectedType) {
unset($this->$name);
return;
}
$class = isset($caller['class']) ? $caller['class'] : '';
if ($class === $expectedType || is_subclass_of($class, $expectedType) || $class === 'ReflectionProperty') {
unset($this->$name);
return;
}
} elseif (isset(self::$privatePropertiese171f[$name])) {
// check private property access via same class
$callers = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$caller = isset($callers[1]) ? $callers[1] : [];
$class = isset($caller['class']) ? $caller['class'] : '';
static $accessorCache = [];
if (isset(self::$privatePropertiese171f[$name][$class])) {
$cacheKey = $class . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function ($instance) use ($name) {
unset($instance->$name);
}, null, $class);
return $accessor($this);
}
if ('ReflectionProperty' === $class) {
$tmpClass = key(self::$privatePropertiese171f[$name]);
$cacheKey = $tmpClass . '#' . $name;
$accessor = isset($accessorCache[$cacheKey])
? $accessorCache[$cacheKey]
: $accessorCache[$cacheKey] = \Closure::bind(function ($instance) use ($name) {
unset($instance->$name);
}, null, $tmpClass);
return $accessor($this);
}
}
return parent::__unset($name);
}
public function __clone()
{
$this->initializerc3072 && $this->callInitializer728ea('__clone', []);
parent::__clone();
}
public function __sleep()
{
$this->initializerc3072 && $this->callInitializer728ea('__sleep', []);
return array_keys((array) $this);
}
public function setProxyInitializer(\Closure $initializer = null)
{
$this->initializerc3072 = $initializer;
}
public function getProxyInitializer()
{
return $this->initializerc3072;
}
public function initializeProxy() : bool
{
return $this->initializerc3072 && $this->callInitializer728ea('initializeProxy', []);
}
public function isProxyInitialized() : bool
{
return ! $this->initializerc3072;
}
} If you've got any trouble reproducing this issue I can also figure out some time to set up a minimal repo tomorrow. |
Are you running on 7.3 or 7.4? Which version of ProxyManager? Also, that lot of code is really only hit if a property stays unset, not really otherwise (unless you are accessing it from a scope where it isn't visible from) |
I'm using PHP 7.3.10 and ProxyManager 2.2.3.
if ($document instanceof GhostObjectInterface) {
$document->setProxyInitializer(null);
} |
Yeah, I think I wrote that code myself. Is your proxy completely uninitialised, or is there some partial initialisation going on? |
Partial. Properties set by the hydrator are there but callInitializer728ea() is never called (got a breakpoint on the first line of the function and was never hit). |
Hmm, and the set initializer was never called? Or was it called at aome point? Specifically, I'd expect unset properties to still be unset if the initializer was skipped. The initializer provided by this library, if called, will set all defined properties to their default values before calling userland logic. |
I noticed that this issue can only be reproduced when the object is loaded through a reference relation ship. I've put together a much simpler example at https://github.com/Frederick888/mongodb-odm-example It's built upon a Laravel empty project and the only 4 relevant files are:
You can $ git clone https://github.com/Frederick888/mongodb-odm-example.git
$ cd mongodb-odm-example
$ mkdir -p doctrine/proxies doctrine/hydrators
$ cp .env.example .env
$ printf 'DATABASE_MONGO=%s\n' "$MONGO_CONNECTION_STRING" | tee -a .env
$ php artisan mongo-seed
$ php artisan mongo-test0 # fail
$ php artisan mongo-test1 # success ...to see the error. (I actually wonder whether this is still a bug of ProxyManager or something missing from mongodb-odm when loading referenced documents.)
What did you mean by 'unset properties'? The ones that are unset in |
Eh, sorry, can't really do much with an example app. The ODM has a test suite though: see https://github.com/doctrine/mongodb-odm/tree/master/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket for examples |
Possibly the problem: the default behavior is to initialize all properties to the defaults when an initializer is called. If they stay unset, then it likely means that the ODM did something "manually" that wasn't expected. |
Ok, I'll figure out some time to write a test case using the suite. Btw are you able to reproduce this issue on your end right now? |
Not really - that's why I asked for an isolated test :D |
$ ./vendor/bin/phpunit --filter GH2080Test
PHPUnit 8.4.1 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 677 ms, Memory: 38.00 MB
There was 1 error:
1) Doctrine\ODM\MongoDB\Tests\Functional\Ticket\GH2080Test::testReferencedDocumentPropertyInitialisation
Undefined property: Proxies\__PM__\Doctrine\ODM\MongoDB\Tests\Functional\Ticket\GH2080Apple\Generateddf8b6d0ca953dfdced173aed374a86ac::$excludedFromArray
/home/frederick/Programming/PHP/mongodb-odm/tests/Proxies/Proxies__PM__DoctrineODMMongoDBTestsFunctionalTicketGH2080AppleGenerateddf8b6d0ca953dfdced173aed374a86ac.php:139
/home/frederick/Programming/PHP/mongodb-odm/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2080Test.php:124
/home/frederick/Programming/PHP/mongodb-odm/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2080Test.php:29
ERRORS!
Tests: 1, Assertions: 1, Errors: 1. |
@Frederick888 can you try your test against doctrine/mongodb-odm#2082 ? |
@Ocramius Yup it's working now! Thanks! |
Closing here then. If relevant, contribute your test to upstream ODM 👍 |
I originally reported this issue at doctrine/mongodb-odm#2080 and was asked to file another ticket here.
I encountered this problem after upgrading to mongodb-odm v2.0.0 and since some properties in my model are never set in the database, it results in
Undefined property
errors when the generated proxy tries to get their values:I just went through the slides linked from readme but I honestly have got only a rough gist of the idea. Please let me know if I should provide any other details.
The text was updated successfully, but these errors were encountered: