Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:123517 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by qa.php.net (Postfix) with ESMTPS id EB1FA1A009C for ; Wed, 5 Jun 2024 14:50:38 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1717599103; bh=C40GNr+CG4nFURjsTNxA3VpNhmGymOoCSyzdYMx4gM8=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=gqQKUCvCE739K8wNZdPrJVsG8P1KDztXh2EwGFrhQ03JEmojlw+RP2tvpnTdFxase MTqdj2yOtpqtbDliWl0C0wWjY21ENsrT96VxNIHmagBYjXSPValB15PDUJ9mgsGhIf qfbe2+BZLXMkomJnnhikEOawC0FzbMWDiNpFSN3Q6gF0S4ahFU0z+AG8wWm26RzziV ncxoND179JXa/teGb4MxiYcwbYqNq5gpn0GkpKGFA6WNbvrV7cJ1BXOGZr5/Q1lewJ Ax82PR6bmoUqrGf7Ynr2cVrKG8qQQ9ZhLms+q/9F/m+bAp0QyPALiTKl7BxhfRDWL4 WyguhI6zyp1cQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id B5431180653 for ; Wed, 5 Jun 2024 14:51:40 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=0.6 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,FREEMAIL_FROM, RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE,SPF_PASS, T_SCC_BODY_TEXT_LINE autolearn=no autolearn_force=no version=4.0.0 X-Spam-Virus: Error (Cannot connect to unix socket '/var/run/clamav/clamd.ctl': connect: Connection refused) X-Envelope-From: Received: from mail-ed1-f43.google.com (mail-ed1-f43.google.com [209.85.208.43]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Wed, 5 Jun 2024 14:51:40 +0000 (UTC) Received: by mail-ed1-f43.google.com with SMTP id 4fb4d7f45d1cf-57a4ce82f30so5356949a12.0 for ; Wed, 05 Jun 2024 07:50:35 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1717599034; x=1718203834; darn=lists.php.net; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=7pPPxZ+EOlexShDOH2c61obuhLa+ZgviHJq8ODG1r1M=; b=V0h6aqyOpqIAICS9nJNBRCnfCWenPSDcjlncgFSZsy5KnhB9MTH19YmLtxQqCCsiSR UScVBdOW4RBLA7jbSizsDzjfcr1tdCZTnGYCbe13lW+SiYO2cAicJWbeazxhuhzfGGoj 6Re6AN/Xn9XHV7H81WvZNHxjm12zE5zVwzlUaz1xqknrkuDKwajoK6vfppSNP/Zi/oJu gkKK4M99MHBrEHxzgVb1B8CnCVJbYOpRgYTY5Uw19zde5kV1PqNyEVIbI5/0w3Yqanno OYINzaIHam84almeXmFw5yb8OHp1RR5wdnZUFxS1118QUruTjVkx/egLXmM2pt48dEqU t/8w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1717599034; x=1718203834; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=7pPPxZ+EOlexShDOH2c61obuhLa+ZgviHJq8ODG1r1M=; b=hCcPcXGfh4UnWgF2uMm3++T/U4mWKAo2YyT0cTRq7HyeN8N6T2FrtbY3AajBpM+7PS dleVr9XwgLC+xr52SYQtypR6z2uy7UTL6AguGvdYfJR+mbirb4T/sb4P7Jnp8r5AlLNa 316YnNNCt8a3EZkEAbYnCL9FYoWCCt2LrOztZFlLMoNGLhlDL/g+elj//pOBY8V/sMGI lDtaRiGjJmKfQb//JZ03olzHe/dSsroPp3+06GaW4X+q9iVhkQq1RjNXHE4WrPjFLjZF mLbygOxrB289BhBIj9Nw76sxdMU21RhH5uCZh5F0bPOascxzT2E+bnHaooDik+O6KXib s2tQ== X-Forwarded-Encrypted: i=1; AJvYcCXjoSbvWV3+fjgPSxTehhix+hoK3vUyErd9i6AFLBp0Kewh6PYIKXzYBfIB4en43x+yDUDG8cV4MXgxEZ+Uf32tbYRB5q8vGg== X-Gm-Message-State: AOJu0YwGUyqHm3Bdh5E98xB0JDaAiBgTg/TQcxUGOckAlmdCYbZWo7AO iKZ4lINdJ9jQJKeIFQNui5c6xoCdUuexJiwP7vpLzFLwBm8A/9GmsANDO2CUW15+fS/0qZJfU/S GMTWs0Nrc6+AMjdaJEGCeJ1JGq3s= X-Google-Smtp-Source: AGHT+IH5J/Txp0kAlPRkQghz32QYcPkCBgFt76iyT3iyEkIuIVHp1FmD3XfvAhbOPx9/aTf7Qm5pH/DeANMM0v1S6+4= X-Received: by 2002:a17:906:8472:b0:a63:3e99:6565 with SMTP id a640c23a62f3a-a699f66630fmr216763666b.23.1717599034119; Wed, 05 Jun 2024 07:50:34 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net MIME-Version: 1.0 References: In-Reply-To: Date: Wed, 5 Jun 2024 16:50:21 +0200 Message-ID: Subject: Re: [PHP-DEV] [RFC] Lazy Objects To: =?UTF-8?Q?Tim_D=C3=BCsterhus?= Cc: Nicolas Grekas , PHP Internals List Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable From: arnaud.lb@gmail.com (Arnaud Le Blanc) Hi Tim, That's a lot of interesting feedback. I will try to answer some of your points, and Nicolas will follow with other points. On Tue, Jun 4, 2024 at 9:16=E2=80=AFPM Tim D=C3=BCsterhus wrote: > - int $options =3D 0 > > Not a fan of flag parameters that take a bitset, those provide for a > terrible DX due to magic numbers. Perhaps make this a regular (named) > parameter, or an list of enum LazyObjectOptions { case > SkipInitOnSerialize; }? The primary reason for choosing to represent $options as a bitset is that it's consistent with the rest of the Reflection API (e.g. ReflectionClass::getProperties() uses a bitset for the $filter parameter). I don't get your point about magic numbers since we are using constants to abstract them. > - skipProperty() > > Not a fan of the method name, because it doesn't really say what it > does, without consulting the docs. Perhaps `skipInitializationFor()` or > similar? We have opted for skipInitializerForProperty() > - setProperty() > > Not a fan of the method name, because it is not a direct counterpart to > `getProperty()`. Unfortunately I don't have a better suggestion. We have renamed setRawProperty() to setRawPropertyValue() in the RFC. We are open to other suggestions. We have also removed setProperty(), as we believe that there is no use-case for it. > - The examples should be expanded and clarified, especially the one for > makeLazyProxy(): Agreed. We will add examples and clarify some behaviors. > My understanding is that the $object that is passed to the first > parameter of makeLazyProxy() is completely replaced. Is this > understanding correct? What does that mean for spl_object_hash(), > spl_object_id()? What does this mean for WeakMap and WeakReference? What > does this mean for objects that are only referenced from within $object? The object is updated in-place, and retains its identity. It is not replaced. What makeLazyGhost() and makeLazyProxy() do is equivalent to calling `unset()` on all properties, and setting a flag on the object internally. Apart from setting the internal flag, this is achievable in userland by iterating on all properties via the Reflection API, and using unset() in the right scope with a Closure. spl_object_id(), spl_object_hash(), SplObjectStorage, WeakMap, WeakReference, strict equality, etc are not affected by makeLazy*(). The intended use of makeLazyGhost() and makeLazyProxy() is to call them either on an object created with ReflectionClass::newInstanceWithoutConstructor(), or on $this in a constructor. The latter is the reason why these APIs take an existing object. The proposed patch integrates into the object handlers fallback code path used to manage accesses to undefined properties. We implement lazy initialization by hooking into undefined property accesses, without impacting the fast path. > Consider this example: > > class Foo { > public function __destruct() { echo __METHOD__; } > } > > class Bar { > public string $s; > public ?Foo $foo; > > public function __destruct() { echo __METHOD__; } > } > > $bar =3D new Bar(); > $bar->foo =3D new Foo(); > > ReflectionLazyObject::makeLazyProxy($bar, function (Bar $bar) { > $result =3D new Bar(); > $result->foo =3D null; > $result->s =3D 'init'; > return $result; > }); > > var_dump($bar->s); > > My understanding is that this will dump `string(4) "init"`. Will the > destructor of Foo be called? Will the destructor of Bar be called? This will print: Foo::__destruct (during makeLazyProxy()) string(4) "init" (during var_dump()) and eventually Bar::__destruct (when $bar is released) > - What happens if I make an object lazy that already has all properties > initialized? Will that be a noop? Will that throw? Will that create a > lazy object that will never automatically be initialized? All properties are unset as described earlier, and the object is flagged as lazy. The object will automatically initialize when trying to observe its properties. However, making a fully initialized object lazy is not the intended use-cas= e. > - Cloning, unless __clone() is implemented and accesses a property. > > The semantics of cloning a lazy object should be explicitly spelled out > in the RFC, ideally with an example of the various edge cases (should > any exist). Agreed. We are working on expanding the RFC about this > - Before calling the initializer, properties that were not initialized > with ReflectionLazyObject::skipProperty(), > ReflectionLazyObject::setProperty(), > ReflectionLazyObject::setRawProperty() are initialized to their default > value. > > Should skipProperty() also skip the initialization to the default value? > My understanding is that it allows skipping the initialization on > access, but when initialization actually happens it should probably be > set to a well-defined value, no? > > Am I also correct in my understanding that this should read "initialized > to their default value (if any)", meaning that properties without a > default value are left uninitialized? The primary effect of skipProperty() is to mark a property as non-lazy, so that accessing it does not trigger the initialization of the entire object. It also sets the property to its default value if any, otherwise it is left as undef. Accessing this property afterwards has exactly the same effect as doing so on an object created with ReflectionClass::newInstanceWithoutConstructor() (including triggering errors when reading an uninitialized property). > - If an exception is thrown while calling the initializer, the object is > reverted to its pre-initialization state and is still considered lazy. > > Does this mean that the initializer will be called once again when > accessing another property? Yes. The goal is to prevent transition from lazy to initialized when an unexpected error occurred in the initializer. > Will the "revert to its pre-initialization" > work properly when you have nested lazy objects? An example would > probably help. Only the effects on the object itself are reverted. External side effects are not reverted. > - The initializer is called with the object as first parameter. > > What is the behavior of accessing the object properties, while the > initializer is active? Based on the examples, I assume it will not be > recursively called, similarly to how the hooks work? For ghost objects, the initializer is supposed to initialize the object itself like a constructor would do. During initializer execution, the object has exactly the same state and behavior as it would have in its constructor during `new`: - The object is not lazy. - Properties have their default value (if any) and are accessed without triggering a nested initialization. - If setRawPropertyValue() was used, some properties may have a value different from their default. For virtual proxies, the initializer is supposed to return another object. Accessing the proxy object during initialization does not trigger recursive initializations. Some properties will have a value if setRawPropertyValue() or skipInitializationForProperty(). > - The object is marked as non-lazy and the initializer is released. > > What does it mean for the initializer to be released? Consider the > following example: > > ReflectionLazyObject::makeLazyGhost($o, $init =3D function ($o) use > (&$init) { > $o->init =3D $init; > }); "released" here means that the initializer is not referenced anymore by this object, and may be freed if it is not referenced anywhere else. > - The return value of the initializer has to be an instance of a parent > or a child class of the lazy-object and it must have the same properties. > > Would returning a parent class not violate the LSP? Consider the > following example: > > class A { public string $s; } > class B extends A { public function foo() { } } > > $o =3D new B(); > ReflectionLazyObject::makeLazyProxy($o, function (B $o) { > return new A(); > }); > > $o->foo(); // works > $o->s =3D 'init'; > $o->foo(); // breaks $o->foo() calls B::foo() in both cases here, as $o is always the proxy object. We need to double check, but we believe that this rule doesn't break LSP. > - The destructor of lazy non-initialized objects is not called. > > That sounds unsafe. Consider the following example: > > class Mutex { > public string $s; > public function __construct() { > // take lock > } > > public function __destruct() { > // release lock > } > } > > $m =3D new Mutex(); > ReflectionLazyObject::makeLazyGhost($m, function ($m) { > }); > unset($m); // will not release the lock. Good point. The Mutex constructor is called during "new Mutex()", but the object is made lazy after that, and the destructor is never called. We have made the following changes to the RFC: - makeLazyGhost / makeLazyProxy will call the object destructor - A new option flag is added, `ReflectionLazyObject::SKIP_DESTRUCTOR`, that disables this behavior This is not ideal since the intended use of these methods is to call them on objects created with newInstanceWithoutConstructor(), or directly in a constructor, and both of these will need this flag, but at least it's safe by default. Thanks again for the feedback. Best Regards, Arnaud