Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:127954 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 949FB1A00BC for ; Mon, 7 Jul 2025 23:42:06 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1751931615; bh=g7sg32K+sY/7lbZiIy6wx52efw2vjzePGzn3a32rIG0=; h=References:In-Reply-To:From:Date:Subject:To:From; b=mC5JkrxAljbNlxyfG3xjQ5PI19shehQLgB0G6u/2AXGAb5PMkZgMIx+2NiGwEihot z+Cigg6b/vt7LB9Kbdt6C1hPD8KWvCmqu1CGKqIzU1cVLT+9s7/RK2GCk877eHTFW4 uzBjCULH6q5zJtz9Rp/rLv7FwNaPE+vQhj81KNmPh75KXfMSPdcuSk9ZuqNAB/4w1T VCkn5vkqBBjm9KLFYWUVJnTTSMYE03ZWgzyYOb3icVg7fl9RGo3pI9/bDmdlhdDlPw WVCxu9stv+tcTvdLkfLlTSFgTNnwP2jH+5RIIPxQTForW79otE6mT/ysSot6bxcULs Ib+Kooz4D7qJg== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 38EF21801E0 for ; Mon, 7 Jul 2025 23:40:14 +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-f174.google.com (mail-yb1-f174.google.com [209.85.219.174]) (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 ; Mon, 7 Jul 2025 23:40:13 +0000 (UTC) Received: by mail-yb1-f174.google.com with SMTP id 3f1490d57ef6-e7311e66a8eso3359847276.2 for ; Mon, 07 Jul 2025 16:42:04 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=dqxtech.net; s=google; t=1751931723; x=1752536523; darn=lists.php.net; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :from:to:cc:subject:date:message-id:reply-to; bh=0HxiXhRNUg/IgA7tyXN6ikOxsFs8e+RVqHH0M84ZHJI=; b=i0iuPvpQXSjGecy0h37xBUvqgUOP7+D3DfdldjC0xvEJlfRLsuDkGyY1i5XpA77/3X OrZO8PGI7kTgWbRSaWlROkhYEKz+pPjIUyw0m1Byo04FeR2mpNZonBGekq73T54ym8tF SptAB6oiAP3ytTIkyTHOkyp2e1cVOEFv7JSi5UG47VsyTGxbGkf8GPlLDWLQyZCb+o3R jK5kMbVre1G9ei1ob9++N/JLw5MDlFljPZfzFfITux1XBCVPlptWgLqKuZB9Os0qL1sC WfRJt1xR7J2LKQ9o2jUshgND1xrhCSrstHI7EO8TRXZ+N3/sPvd1BWvtnmr/jiYRzFcp JMvg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1751931723; x=1752536523; h=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=0HxiXhRNUg/IgA7tyXN6ikOxsFs8e+RVqHH0M84ZHJI=; b=OaXoOrVlQ369gRGksmv53guV9L6bw1pL39EgAFRjTMux3VRpZeshvhJ9HHpJ5vtU02 aMm7tRn8+329g2hrRCfP8KzHC92oh7A/uuRkNnf9qspMrAvdlDXpsX/0an5KOcjwSBX4 HH9IrAl9d7rRiG6iurHvPVHMwUszTVW0vTDTsYXTJOL514eqjqZK9ieOguy34FSA7d82 yqbH2Ok2SX6xx/2hfRGs+wItQnaty+clrjn1DMG2U/+s3Gl24Bq1dpK58qNSN9aMwG/w 1QP3+fYU0ymJiM7w3bWrusCwRdhDnbQ3yOlYlBYIEN9zL8zopwuke9X7G6OUvAicwqtM LQyA== X-Gm-Message-State: AOJu0YzopY61TYOaC4bhMsakBRjhU8DGmJ5waWKmZApsRpouI8vWK0cm 0lnK0iX76s/c7Ofu8wa3mB2JsjXACKRhFXLTi/vChqI/aUnvwXnqVNWFRfR2/xxKM7g/Uzu1gpz /6sGKCM5f+P7LBi3S3wUS8qbiHFIHnN4nXYbzg5dgvxDWVxAwURPWEDNpiUkM X-Gm-Gg: ASbGncs3R3z/pHj4nJ2xvY+SWaOqPp1xLzoqwVjWNQpDtrnDAlfbwv4Ei7kENmKDn94 7X0UkQaXX/ojeOOW00ZVmcIIMFFFkPa+cHCn6Kekmn+CiKWvXxAmt7xZz49LVNprv5+QaZjwBYv OUy5q1rvzgOUoyajT/CEV7gegeCHyUnkAVIU7uDnUvLdLD/DLwxMJw0e4RVisoAC/3vwbVpYyEO 2gB X-Google-Smtp-Source: AGHT+IHy2CPO0CGBjoSVmlXWGAgsOen6mN//VfFtSGHy/I/DpaP7js83jyi9lZ3OxPO9opgxpiYYTJLHYGbMk4r9gts= X-Received: by 2002:a05:690c:6304:b0:70d:ed5d:b4b2 with SMTP id 00721157ae682-7166b5f6b72mr181957997b3.13.1751931723504; Mon, 07 Jul 2025 16:42:03 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 References: In-Reply-To: Date: Tue, 8 Jul 2025 01:41:52 +0200 X-Gm-Features: Ac12FXx-QD3k-h7v0rfkOhx5bDDwV5ATJDwcmxcY9u_1ICrnrI2D8Brwgeh1buU Message-ID: Subject: [PHP-DEV] Re: [RFC idea] Target-aware attributes To: PHP internals Content-Type: text/plain; charset="UTF-8" From: andreas@dqxtech.net (Andreas Hennings) On Thu, 3 Jul 2025 at 00:26, Andreas Hennings wrote: > > 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 I created an RFC and PR with _only_ the ReflectionAttribute->getTargetReflector() I still get memory leaks, says the pipeline. https://wiki.php.net/rfc/attribute-target-reflector https://github.com/php/php-src/pull/19066 For now this shall be considered draft, but whoever is interested may have a look. -- Andreas