-
Notifications
You must be signed in to change notification settings - Fork 1
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
WIP: Fix callback inconsistency #3
base: main
Are you sure you want to change the base?
Conversation
@@ -216,6 +216,13 @@ public function setAttribute(Attribute $attribute) | |||
return $this; | |||
} | |||
|
|||
public function setCallback($name, $getter, $setter = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should settle on a single name, I already renamed this once for you ;-) We could also introduce different Setters for Read/Write Callbacks. Because with this signature, I'm unsure what would happen when running:
->setCallback('value', null, [$this, 'setValue'])->setCallback('value', [$this, 'getValue'])
I would expect my setter to be still in place. Do you have a good proposal for this?
And apart from this, I didn't grasp what inconsistency this is going to fix, this is just a proxy method, isn't it? Is it about naming?
It's not about the method name or signature. But I'd go with the following methods:
The inconsistency is about the following (not complete, just to give you an idea): 1) Example 1: Getter callback for the attribute $attrs = new Attributes();
$attrs->registerGetter('class', function () {
return new Attribute('class', 'state-critical');
});
$attrs->get('class')->getValue(); // returns null Example 2: Setter callback for the attribute $attrs = new Attributes();
$attrs->registerSetter('class', function ($value) {
// Magic happens here
});
$attrs->get('class'); // Must throw an exception because there is no getter for class Example 1: Getter callback return values must be validated: $attrs = new Attributes();
$attrs->registerGetter('class', function () {
return new Attribute('cheat', 'state-critical');
});
$attrs->get('class'); // Must throw an exception because the attribute's name from the callback does not match the registered name 2) Example 1: Getter callback for the attribute $attrs = new Attributes();
$attrs->registerGetter('class', function () {
return new Attribute('class', 'state-critical');
});
$attrs->set('class', 'state-ok'); // Must throw an exception because there is no setter for class Example 2: Ambiguous state when using Attribute instances in $class= 'state-ok';
$attrs->registerGetter('class', function () use ($class) {
return $class;
});
$attrs->registerSetter('class', function ($value) use (&$class) {
$class = $value;
});
$attrs->set(Attribute::create('class', 'state-critical'));
echo $class; // Yields 'state-ok' though it should 'state-critical' 3) The methods for registering callbacks must throw exceptions if the attribute already exists: $attrs = new Attributes(['class' => 'state-ok']);
// Exception must be thrown because the attribute already exists
$attrs->registerGetter('class', function () {
}); 4) Attributes from getter callbacks should have the setter callback proxied? $attrs = new Attributes();
$class = Attribute::create('class', 'state-ok');
$attrs->registerGetter('class', function () use ($class) {
return $class;
});
$attrs->registerSetter('class', function ($value) use ($class) {
$class->setValue($value);
});
$fromCallback = $attrs->get('class');
$fromCallback->setValue('state-critical');
echo $fromCallback->getValue(); // Yields state-critical
echo $class->getValue(); // Yields state-ok though it should state-critical too This is a little bit of a headache because we would need callback handling in the I'd be happy to fix this. |
First of all, Getter and Setter Callbacks are to be considered complete distinct beasts. The fact that there is a Getter should in no way have any influence on what to expect when getting an Attribute. There are different use-cases, sometimes you want to "steal" a value, work with it but never set a related attribute. Therefore I do not like the idea of proxying setters in getted values. I understand the technical explanation, but see no use in it. So, IMO:
Of course I'm open for arguments pointing to the other direction. Is this pull request here to address a just the inconsistency, or do you have related use-cases you want to see addressed? Thanks, |
I want to address the inconsistency. Related use cases are in the failing tests and the examples I gave.
That's the whole problem I'd like to fix. Callbacks must be seen as attributes else we face ambiguous states.
Why should it? Why do I have to expect different results from
I saw that in the form implementation. IMO that's flawed. With the current implementation you may build attributes with completely different states in the very same instance. Why do even think about not fixing this? Back to the points:
TL;DRWe should settle on the following rules for callbacks:
Isn't that exactly your use case of callbacks? You want to manipulate how the |
Real world use-cases would be great. I understand the inconsistency problem, I'm trying to evaluate whether it is worth to increase complexity while making it harder to implement the use-cases this has been built for.
That's where I disagree, those callbacks have initially been introduced to fake state and behavior for various reasons.
That's what this feature is all about. As of now, a SelectElement for example intercepts the "options" attribute. I do not expect to get an Attribute with a dictionary value when running A SubFormElement could be populated with an 'value' property, but that's not an Attribute at all - it just populates other elements. Pick the submit label, it renders a value attribute, but that attributes value is the label(!). Still, This hooks allow us to cheat. Such cheats give a powerful instrument and allow to avoid some pitfalls ZF1 ran into. When you try to make it consistent, you risk to break the possibility to cheat. And by the end of the day, that's what this is all about. When I need an Attribute with special behavior I can extend the Attribute class, no need for a hook - that's a completely different use-case. Do you understand what I mean? |
1ff6fde
to
f0dde5b
Compare
I stumbled over this today while implementing the |
84dd33a
to
9c91ad8
Compare
While working on FormElement: Add RadioElement #68 PR and Enhance select element #88, I noticed that the attributes need to be in a certain order to work properly. (similar problem). Currently I have fixed this as follows:
However, there may be other attributes that should be in a certain order. It would be nice if we could fix this dynamically, without having to sort the attribute in the element's constructor. |
The particular order is only necessary because the code is flawed. There must be no dependencies between setters. Although one could argue that the value should simply always be set last. But even that can easily be bypassed by calling the setters instead of using the construct-time attributes: (new FlawedElement())->setValue('bypassed')->setAttribute('attribute-that-setValue-depends-on', 'value'); Sure, there could be an attribute callback that adjusts a value that's already set, but honestly, ... 🙅 |
Didn't read over this all over again, but I'd like to add this:
|
Attribute callbacks should work with all methods in the Attributes class. Right now they are only evaluated in
render()
. Further, there is no error handling if a callback fails. This PR attempts to fix the following issues:Attributes::get()
must return anAttribute
instance which is produced from the callback's result. If the callback fails, aRuntimeException
is raised. If the callback's result type is invalid, aUnexpectedValueException
is raised. The exception stuff applies torender()
as well.Setter callbacks must be proxied in the
Attribute
instance returned from a callback inAttributes::get()
.Attributes::set()
andAttributes::add()
must use registered callbacks. If the attribute already exists inadd()
an exception must be raised.If there is no setter callback, an Exception must be raised when trying to override the respective attribute.
When trying to add callbacks to an attribute which already exists, an exception is raised.
Attributes::get()
must raise an exception if there is only a setter callback specified.For the moment I just added failing tests for the uses cases above. @Thomas-Gelf Please evaluate whether this makes sense. I'll start fixing this then.