At this point, I'm not even sure if I'm doing the right thing, or am I even overcomplicating things?
That depends.
In your original design, both the array of class constants and their app-factory calls were hard-encoded inside, so an implementation detail with locality of behaviour and lazy object creation.
The node pattern of the invokable action class AttachMembersToRoom was well aligned with the node pattern of the Validator, which then by PHP's runtime guarantees could benefit from always exiting early by never returning, assumable via throws.
Given the name, AttachMembersToRoom, this appears to me as a good, well balanced, and smooth mixture.
What has changed then up to this point is that the array of class constants is injected into the apps' container state and their app() invocations moved before the AttachMembersToRoom::__construct() invocation. The ctor's parameter type is too wide thought and the parameter is not read-only.
Additionally the tagging requires to match the Tag attribute parameter that is a string operand which degrades the parameter as stringly-typed. That may sound ugly, but the only downside of that detail is, that at the tag point the use of the array of class-strings/objects is not visible. As you only have a single use-point and IIUC that will stay that way, this can likely be ignored.
The consequences of those changes are more prominent insofar that now all memberValidators are created before the AttachMembersToRoom is invoked.
I suggest you to trust your nose. You smell some over-doing here, and then my educated guess would be that you sensed the loss of the symmetry and balance of/between (all) invokable action classes and the node pattern. Perhaps we can even say: on two dimensions (invocable actions, invocable validators).
So this could have been the discovery then: The sequence of validators should be better represented as one validator function, so that a sequence of validators can be easily replaced with one (private) validator function so that such an validator could become an easy to inject invokable action class. Much neater design, smaller steps forward, less growing pain:
class AttachMembersToRoom
{
public function __construct(
#[InvokableValidatorActionClassSet()]
private readonly array $validators = [
CompareMembersValues::class,
IdentifyOverlaps::class,
],
)
{
// intentionally left blank //
}
public function __invoke(MyData $data)
{
// some logic here... //
$this->invokeValidators($data);
}
private function invokeValidators(MyData $data)
{
foreach ($this->validators as $invokableValidatorActionClass)
{
$invokableValidator = app($invokableValidatorActionClass);
$invokableValidator($data);
}
}
}
This defers any tagging by just using an attribute which suffices for static analysis, the default value is the configuration value, place of attribution is place of configuration (defaults), the parameter itself is configurable (inversion of control, e.g. unit testing), the class remains easy to create (important for using incl. testing).
We further begin to see the node pattern both for invokable actions and (invokable) validators.
The example also does not hide its incompleteness, app() is still hard encoded inside the foreach and not yet configurable. This is by intention, because when the validator is made an injectable invokable action class it remains the factory function, and with an injectable invokable action object, it would automatically move out of AttachMembersToRoom.
Until then, it is a single call point, functions are static and bindings can be changed by unfolding via use function statements.
So we can safely defer that until settlement.