Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:127853 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 lists.php.net (Postfix) with ESMTPS id 79A5C1A00BC for ; Wed, 2 Jul 2025 22:27:02 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1751495109; bh=AvtI5k4AUQmQstqNmaLPVjnnW0tBGb9WhSZ/BKGXATY=; h=From:Date:Subject:To:From; b=YW+SCpcsUFeI27WkpXc3u9LnrBJanmTVI0FupW7Dj+xjhZF5PVBZnIV2f7UinnZcZ xjTGqIsCgi8hSrQW8w06YxXrk9LKkxHZ+0tPPCsxF0yhUTF0F/q4PkEM/D7O+y/XeR HCYmWbUjszEsnWoDnnjE9Vu+49CD2p/PN7gCxrnmpuZHCCz9NxI9ItP4oe/qPC3e9P CRuTRj5uDs1dWnsAybK3d3sgtERP4gV5z2KtkeRfIqXNZChOatT3oTXQAoy5oEPTsW SN8qXU1L4BPcxHAyEI7118WlLZbv20pss95EPrMAeH3zUSI/T0DapiVfBw16sn55CH EGha5vU3w2ZuQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 1E85C180069 for ; Wed, 2 Jul 2025 22:25:08 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-3.1 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H2,SPF_HELO_NONE,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: Error (Cannot connect to unix socket '/var/run/clamav/clamd.ctl': connect: Connection refused) X-Envelope-From: Received: from mail-yb1-f193.google.com (mail-yb1-f193.google.com [209.85.219.193]) (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, 2 Jul 2025 22:25:07 +0000 (UTC) Received: by mail-yb1-f193.google.com with SMTP id 3f1490d57ef6-e82314f9a51so4953262276.0 for ; Wed, 02 Jul 2025 15:27:00 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=dqxtech.net; s=google; t=1751495219; x=1752100019; darn=lists.php.net; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=g+cJVrh+dKnvE70YzKhhX3EVoEUdTZDSAfSSALwKt5I=; b=agF7ExE2mkZkbd1KpvAQVBM25VHV+ZRBkKfFTfnxBWYipzxmei9gfkwpjyWBTC6fTT 7sbvuVPwIBlNbCUE5uZINnefviSO3tY+IS15Lev+ZMnclwSxWbocUFwhdlK6HwuIuBm9 MX6bZVCUZqY/tzA9QPIbGcmOzO4Tsl8rmU8DJpIvDNPv1t/crzG2oWvacPZNqsfqd2Ng 8AeIZ7mHQlmBZZwZlOFes+O0Xrv2jaJuj8bSBHJchVE4z1Jzb0WcwMT4CsrY+oPmlf8B E21xK66R5LS4pLUCxee/5rKEkDs2ex4luufanbCTe4byoLKZkUCvIImEtEMOU1P4B0dl HMdg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1751495219; x=1752100019; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=g+cJVrh+dKnvE70YzKhhX3EVoEUdTZDSAfSSALwKt5I=; b=RL1rYaZC5kEA5WIypTMIIRSLdwFboTjZPLwe/+st84a42tSae3Pv8N58eG0aAUpIQj 5xaWwXvWweu8ocaB66oxLtUjicid26X5tUTy0PtjneVxhY76mUCcgqAMuv1MYatpZSza CqaqHgdKoDJuU3uCLaYv5GkDCcdCM2D8/YutlVVNSlk9g7EE+JToikfIsN8x0JwY8GaV yZnEdaUIsvsvXPEcWE5pmEoaZiwgkLOoJVgDqFujIZN0vsgCqp8YyzPPqgOXQbOWbbN8 jaioko/xIEZZ3so/DwYQOrR8nNw5ixp4zhwv6wzo1nM2w40AQASwl1eDiajh1yl/Md/N xiTg== X-Gm-Message-State: AOJu0YxurjafZ+7cMjUi2/R5h/7i4+fu5lWOPerM5JbXW1ELZ77xq7Kn yYGPoE05ixBgeNIWbf2YOuAUgoGq1YM6kyoa6bBethWHyf8AUiFUCxMbZQRexSR2te8E/9IdC05 UFYISIve022LF0yVGW3Tw1lUofiaYgFXMAlALZvjm3PCu1aqzDsAP522V3MYwYXY= X-Gm-Gg: ASbGncvR+FDDUyUhqlgpFKobBVmn6NoxmD/zr8MRx342eJ5Kcn80GBRa9aCBSqB0Zdd bnO7I0ObcmkGU8NRg+08DGm2Z50c6OeyvUbo0m1PANvcji1vSG8ef56+hPAzLHPJKur9yB+R2u/ 3vRN7xBK/TtQ/pGdYv+NFwkcpdSr2hpy7XbBZf3euzAlG98/H08EceOFElz0EcD7U+iMT6PXwYw Bbu1A== X-Google-Smtp-Source: AGHT+IETBBJtcW84wmlPq1jQHHQ2JBiEETFz02CJpyqFwJ8VQ9mzcbd1ZsnRorat0ihuNdJqQSK8wN7fRsBE4Hvf3Io= X-Received: by 2002:a05:690c:48ca:b0:70e:7706:824e with SMTP id 00721157ae682-7164d313418mr71857387b3.6.1751495219469; Wed, 02 Jul 2025 15:26:59 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 Date: Thu, 3 Jul 2025 00:26:48 +0200 X-Gm-Features: Ac12FXzJWdA4nRJbqGvqelr6j743WXg86ZVTQDZH6_NaQsIfFBWFSwBxuSqY2gk Message-ID: Subject: [PHP-DEV] [RFC idea] Target-aware attributes To: PHP internals Content-Type: text/plain; charset="UTF-8" From: andreas@dqxtech.net (Andreas Hennings) This topic was discussed in the past as "Declaration-aware attributes", and mentioned in the discussion to "Amendments to Attributes". I now want to propose a close-to-RFC iteration of this. (I don't have RFC Karma, my wiki account is "Andreas Hennings (donquixote)") ----- Primary proposal ============= I propose to introduce 3 new methods on ReflectionAttribute. static ReflectionAttribute::getCurrentTargetReflector(): ?Reflector Most of the time, this will return NULL. During the execution of ReflectionAttribute->newInstance(), it will return the reflector of the symbol on which the attribute is found. (in other words, during $reflector->getAttributes()[$i]->newInstance(), it will return $reflector.) During the execution of ReflectionAttribute::invokeWithTargetAttribute($target, $callback), it will return $target. If the call stack contains multiple calls to the above mentioned methods, only the closest/deepest one counts. (This means that php needs to maintain a stack of reflectors.) static ReflectionAttribute::invokeWithTargetReflector(?Reflector $target, callable $callback): void This will invoke $callback, with no arguments. During the invocation, ReflectionAttribute::getCurrentTargetReflector() will return $target. (This allows testing attribute classes without using them as attributes.) ReflectionAttribute->getTargetReflector(): \Reflector This returns the reflector of the symbol on which the attribute is found. This method mostly exists for completeness: The ReflectionAttribute must store the target reflector, so one would expect to be able to obtain it. Example #[Attribute(Attribute::TARGET_PARAMETER)] class MyAutowireAttribute { public readonly string $serviceId; public function __construct() { $reflectionParameter = ReflectionAttribute::getCurrentTargetReflector(); if ($reflectionParameter === null) { throw new \RuntimeException('This class can only be instantiated as an attribute.'); } assert($reflectionParameter instanceof ReflectionParameter); // @todo Some validation. $this->serviceId = (string) $reflectionParameter->getType(); } } class MyService { public function __construct(#[MyAutowireAttribute] private readonly MyOtherService $otherService) {} } // Regular usage. $reflector = (new ReflectionMethod(MyService::class, '__construct'))->getParameters()[0]; $reflection_attribute = $reflector->getAttributes()[0]; assert($reflection_attribute->getTargetReflector() === $reflector); $attribute = $reflection_attribute->newInstance(); assert($attribute instanceof MyAutowireAttribute); assert($attribute->serviceId === MyOtherService::class); // Simulation mode for tests. $reflector = (new ReflectionFunction(fn (MyOtherService $arg) => null))->getParameters()[0]; $attribute = ReflectionAttribute::invokeWithTargetReflector($reflector, fn () => new MyAutowireAttribute()); assert($attribute instanceof MyAutowireAttribute); assert($attribute->serviceId === MyOtherService::class); // Nested calls. function test(\Reflector $a, \Reflector $b) { assert(ReflectionAttribute::getCurrentTargetReflector() === null); ReflectionAttribute::invokeWithTargetReflector($a, function () use ($a, $b) { assert(ReflectionAttribute::getCurrentTargetReflector() === $a); ReflectionAttribute::invokeWithTargetReflector($b, function () use ($b) { assert(ReflectionAttribute::getCurrentTargetReflector() === $b); ReflectionAttribute::invokeWithTargetReflector(null, function () { assert(ReflectionAttribute::getCurrentTargetReflector() === null); }); }); assert(ReflectionAttribute::getCurrentTargetReflector() === $a); }); assert(ReflectionAttribute::getCurrentTargetReflector() === null); } ------------------------------ Alternative proposal ================= For completeness, I am also proposing an alternative version of this. The two are not necessarily mutually exclusive, but having both would introduce some kind of redundancy. Personally, I prefer the first proposal (see below why). I propose to introduce 3 new methods on ReflectionAttribute. static ReflectionAttribute::getCurrent(): ?\ReflectionAttribute Most of the time, this will return NULL. During the execution of ReflectionAttribute->newInstance(), it will return the ReflectionAttribute instance on which ->newInstance() was called. ReflectionAttribute->getTargetReflector(): \Reflector This returns the reflector of the symbol on which the attribute is found. static ReflectionAttribute::create(\Reflector $target, string $name, array $arguments, bool $is_repeated = false): \ReflectionAttribute This returns a ReflectionAttribute object that behaves as if the attribute was found on $target. This is mostly for testing purposes. Example #[Attribute(Attribute::TARGET_PARAMETER)] class MyAutowireAttribute { public readonly string $serviceId; public function __construct() { $reflectionParameter = ReflectionAttribute::getCurrent()->getTargetReflector(); [..] // @todo Some validation. $this->serviceId = (string) $reflectionParameter->getType(); } } class MyService { public function __construct(#[MyAutowireAttribute] private readonly MyOtherService $otherService) {} } // Regular usage. $reflection_parameter = (new ReflectionMethod(MyService::class, '__construct'))->getParameters()[0]; $reflection_attribute = $reflection_parameter->getAttributes()[0]; assert($reflection_attribute->getTargetReflector() === $reflection_parameter); $attribute_instance = $reflectionAttribute->newInstance(); assert($attribute_instance instanceof MyAutowireAttribute); assert($attribute_instance->serviceId === MyOtherService::class); // Simulation mode for tests. $reflection_parameter = (new ReflectionFunction(fn (MyOtherService $arg) => null))->getParameters()[0]; $reflection_attribute = ReflectionAttribute::create($reflection_parameter, MyAutowireAttribute::class, []); assert($reflection_attribute->getTargetReflector() === $reflection_parameter); assert($reflection_attribute->getTargetReflector()->getAttributes() === []); $attribute_instance = $reflection_attribute->newInstance(); assert($attribute_instance instanceof MyAutowireAttribute); assert($attribute_instance->serviceId === MyOtherService::class); Why do I like this version less? For most use cases, the attribute instance does not need access to the ReflectionAttribute object. For the testing scenario, the "fake" ReflectionAttribute object feels strange, because: - ReflectionAttribute::create($reflector, ...)->getTargetReflector()->getAttributes() may be empty, or does not contain the fake attribute. - ReflectionAttribute::create($reflector, ...)->isRepeated() is completely meaningless. - If we add ReflectionAttribute->getPosition() in the future, the result from the "fake" one will be off. Any code that relies on these methods of ReflectionAttribute to look for other attributes on the same symbol may break with a "fake" instance. Details, thoughts ================= The return type for ReflectionAttribute::getCurrentTargetReflector() would not simply be "Reflector", but "\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant", assuming that no dedicated interface is introduced until then. For ReflectionAttribute::getCurrentTargetReflector(), I was wondering if instead we may want a function like current_attribute_target(). This would be inspired by func_get_args(). In the end, the method is still related to reflection, so for now I decided to keep it here. For ReflectionAttribute::invokeWithTargetReflector(), we could instead introduce something with ::push() and ::pop(). This would be more flexible, but it would also lead to people forgetting to remove a reflector that was set temporarily, leaving the system polluted. For ReflectionAttribute::invokeWithTargetReflector() returning NULL, we could instead have it throw an exception. But then people might want an alternative method or mode that _does_ return NULL when called outside ->newInstance(). By having it return NULL, the calling code can decide whether and which exception to throw. Implementation =============== An instance of ReflectionAttribute would need to maintain a reference to the reflector it was created from. The ReflectionAttribute class would need an internal static property with a stack of ReflectionAttribute instances, OR of Reflector instances, depending which version of the proposal is chosen. Other alternatives ====================== In older discussions, it was suggested to provide the target reflector as a special constructor parameter. This is problematic because an attribute expression #[MyAttribute('a', 'b', 'c')] expects to pass values to all the parameters. Another idea was to provide the target reflector through a kind of setter method on the attribute class. This can work, but it makes attribute classes harder to write, because the constructor does not have all the information. It may also prevent attribute classes from being stateless (depending how we define stateless). Userland implementations ========================= One userland implementation that was mentioned in this list in the past is in the 'crell/attributeutils' package. This one uses a kind of setter injection for the target reflector. See https://github.com/Crell/AttributeUtils/blob/master/src/FromReflectionClass.php Another userland implementation is in the 'ock/reflector-aware-attributes' package. https://github.com/ock-php/reflector-aware-attributes (I created that one) This supports both a setter method and getting the target reflector from the attribute constructor. The problem with any userland implementation is that it only works if the attribute is instantiated (or processed) using that userland library. Simply calling $reflector->getAttributes()[0]->newInstance() would either return an instance that is incomplete, or it would break, if the attribute class expects access to its target. -------- I can create an RFC, if I get the Karma :) But, perhaps we want to discuss a bit first. -- Andreas