-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Workflow] Remove final from Definition class #23310
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
Conversation
What kind of extensions do you need for this object? |
Any domain specific values associated to states/transitions. Descriptions, labels, email addresses associated to transitions, trigger type (system, user, admin), roles etc. See referenced ticket. |
Note that adding any custom methods of properties in a child of one of our classes is forbidden by our BC promises anyway. So even if we remove the final keyword, adding new properties and methods is not something you should do. |
It's not forbidden by your BC promises, you just don't guarantee BC in such cases, which is allright, since it's impossible |
Meaning you still should not do it, and so I don't consider it as a valid use case for opening the class to inheritance. |
Look, what's the alternative? I know you don't want inheritance because it's fragile, but that is precisely advantage for you in this case. Let me explain: Workflow component is still new and I don't believe its API is stabilized. Workflow class currently depends on instance of Definition class
Either allow me to inherit the class, or you won't be allowed to extend it (introduce new public methods) in future. |
I don't see how removing the final keyword (i.e. letting you inheriting the class) is necessary to be able to add new methods in the future in Symfony. The current codebase is the one where we can add whatever we want in Definition in Symfony without BC break (for instance implementing #23257). |
I was comparing inheritance vs composition problem. If you don't allow users to do any changes then of course that's safest for you, but it's also middle finger for users of this component and I will rather stop using it in such case. There is plenty domain specific use cases which would never be accepted to core, because - they are domain specific!
I don't see how removing final keyword prevents you doing the same thing. Your only argument is that I should not subclass for my own safety but did not offer any alternative. I will gladly take the ridiculously low chance of introducing conflicting method name in my child though, so thank you for your concerns. I just want the ability to extend this class with domain specific features, so please explain your reluctance to this change and suggest better alternative. Thanks. |
Well, if you have a need to attach additional info, you should rather put a comment on #23257 describing your use case, to give more input to the thinking about the proper way to solve the issue. My main concern is that you want to advocate that the solution to your issue is using Symfony in an unsupported way (i.e. extending classes in ways not covered by our policy). This is not something solving the use case. |
Only thing #23257 covers is to attach custom array with scalar values. That's good enough for general purpose, but it's not enough for my needs. I also want getter methods with type safety features for retrieving such data and validation of such data. That's not possible to specify via configuration. I'm not advocating that this PR is solution for #23257 (notice empty fixed tickets column). That issue still needs to be done and I'm inclined to do it myself. But first stop trying to convince me that you know better than me what I need in domain of my application and let's merge this PR. |
Thank you for the ping. I do understand you use case @ostrolucky. Many people has asked this before. We have answered that the Workflow is no place for arbitrary data. But I do understand it is super convenient to get your arbitrary data being sent all around the event system. The supported way to do this is to store your arbitrary data some place else (any place) and then fetch it in the event listener. It may seams weird to split data that really belongs together (transition names and description) into two places... And yes, it is weird. It's correct, but weird. Forgive me @stof, but "adding any custom methods of properties in a child of one of our classes is forbidden by our BC promises" means that we say: "Do it at your own risk". It is a risk no bundle SHOULD take, but @ostrolucky is obviously will to do so in his app. I suggest we accept this PR. Let @ostrolucky and others be experimental, implement things in their app and find out what's good, what's working and make suggestions what is needed to be brought back into the component. Maybe they find that a transition should have a The "risk" we (Symfony/the Workflow component) take is minimal. We may get a complaint from some user that did not read our BC promise. |
Wouldn't #23257 be a better approach? You don't mess with this "final" thing, but allow storing arbitrary data, so users can implement any of their needs. @lyrixx seems to be slightly in favor of that too: #23257 (comment) |
👍 |
Okey. I stand corrected then. =) |
I've already explained why is #23257 not a solution here #23310 (comment). In summary, that ticket is for attaching only simple array with scalar values, not for attaching any custom methods. |
You can fetch that scalar values into your own value object with validation and own business logic. I don't see why you need to inherit the |
That's what I did. It creates fragile and messy API, duplications, complicates the whole thing and I need to introduce even more children (way to inject this into Workflow class, way to retrieve it, possibly override DefinitionBuilder). Now, please don't comment here @unkind, you don't have any association to this component, unlike core symfony members and contributors to this component. There is already too much comments here. I won't continue discussing with you. |
How you did it if
Symfony community is not closed one, so I feel that it's OK if I share my thoughts regarding changes in the project which I actively use. However, it's your right to ignore me, I mostly share my thoughts for other people, so they can take into accounts different opinions. |
@ostrolucky Even if @unkind is not part of the "Core Team", we do appreciate comments from the community. People like @unkind help a lot in reviewing/triaging and moving forward issues and pull requests. Without people like him, we would not be able to cope with Symfony's crazy activity. |
I think you took it wrong way. I do appreciate comments from community and am aware of your stance regarding this. I am myself such a person. I just don't appreciate it in this specific case, because there is too much comments here and people started to talk about things I already answered and am forced to repeat myself, which just produces more comments and more readers who wouldn't read them all. Also I'm very well aware of @unkind's hard stance regarding final classes and if I was supposed to discuss about it with him, discussion would never end. So what needs to be done here to come to resolution? I have answered all objections. |
Hi @ostrolucky How do you use this component? In a stand alone way or with the full-stack framework? Then could you describe us what you will add to the Definition class ? Some users already asked to be able to give extra information to the workflow. But the targeted class were So could you describe us how you will use this "extended" Definition class ? Thanks. |
Hi @lyrixx I use full stack framework. Change in this PR can be leveraged via Definition::setClass in compiler pass.
Not sure why you need to know that, I don't want you guys to focus on concrete problems. My core problem is that whatever it is that I need to add to Definition, I can't. Problems it focuses to solve are dynamic and will be changed with time. But anyway currently it is:
I can just inject workflow or workflow registry and pull definition from there. That said, currently I don't need this in event listeners. |
I like to know what people do with the composant in order to improve it and also to take decisions on issues / PRs. I can not merge every single PR on the workflow because one person need it. I hope you understand why I'm acting like that. So for now I have the same opinion on this PR as this comment in #23257. But If we implement what you asked in #23257 this PR will not be needed except for the 3/. And I guess we will implement it... |
Exactly. So allow for more generic way of extension (such as this PR) so user can build his own solution, according to his own needs in userspace, instead of forcing him to submit new feature requests each time (where some of them will be unavoidably denied) and waiting 6 months to have it in stable release. Community could even create bundle for this.
Right, as I thought you are looking for reasons to deny this based on my very present needs and ignored my warning that these kind of things are dynamic in nature. Please don't be shortsighted. How probable do you think it is that there doesn't exist proper domain specific use case for extending this class? Okay, currently #23257 will be enough for me (even though I doubt approved version will be the one I asked for). So you will close this PR right? And what will you do when after few months I or someone else will come back with another feature request which you won't accept, because this time it's too domain specific? Not even sure why am I put in defensive position here for the whole time. It's like inquisition. You guys want from me all kind of explanations, but nobody could explain what's so bad about this change (argument about unsupport is ridiculous, 98.3% classes in Symfony are non-final and nobody is even asking you to officialy support this - quite contrary, I've expressed that I want to be able to do this at my own risk), or even why was this made final in the first place. Except If you are so uneasy about this, mark it |
I assume that we don't understand why it should be done via inheritance: /** @var $definition MyDefinition */ // Assumption that we have MyDefinition object
$definition = $workflow->getDefition();
$definition->customMethod();
// vs
$foo = Foo::fromArray($workflow->getDefiniton()->getData()); // #23257
$foo->customMethod();
// or
$foo = new Foo($definition);
$foo->customMethod();
// or
$foo = new Foo($definition->getData('foo'), $definition->getData('bar'));
$foo->customMethod(); Isn't it the very same behavior, but decoupled from Symfony details? |
I've already responded to you here, do you really need to force same opinion of yours 3x? You seem to be oblivious why I don't want to do this. I don't have time explaining everything wrong with this though, so hopefully just the most obvious thing will suffice: This "extension" approach forces consumer of application API to wrap the returned object into separate call every time it receives such object. It's totally inferior in comparison with other approaches. Now let's hear the need for this class to be final. As I explained, it's very unique in Symfony codebase. |
Sure, but then I asked you how you did it if
Can you provide real-life example when it becomes a problem? I mean, creating objects is not that rare operation in OOP code. It happens quite often I'd say. OK, let's assume that you need to inherit the I mean, I saw the example like Moreover, I was in doubt since release of the |
Responding just because I don't see this moving. I don't like this discussion at all. I expect answer why does this change warrant 22+ comments when this class has no reason to be final in the first place.
I don't think you understand. By consumer, I meant programmer. He is forced to transform that object by instantiating different one and supplying old one to it, by hand. Performance is not important here. It's painful violation of DRY. You always receive object you don't want, so you always have to transform it into different object, where you copy functionality of old one and add your own. Instead of always receiving object you want (old one, extended with functionality).
I did answer all of those. You would notice if you didn't spam this thread. Rest of your answer is pure off topic. This isn't place to discuss usefulness of component or to satisfy your curiosity how did I implement something. Feel free to ask me that on slack. |
@ostrolucky I'm not going to dive into the actual change you request because I don't know that part of the code. Yet if you allow me to try helping you, your tone is really not encouraging anyone to pursue the discussion. It is very common for reviewers to ask for example use cases and alternatives, so they can get the pov of the submitter and help make the best move. Here it's like you're saying loudly "trust me you idiots". I'm not sure that's the best plan for getting anything merged... The arguments you posted have to be carefully understood. You may have understood faster than anyone else why this change is best. But saying "nothing is final anywhere" (which is not accurate, see eg So, I'm kindly asking any reviewers to ignore your past tone and focus on the actual technical arguments. And kindly asking you to be more tactical in your communication style if you want people to care about this PR at all (that's everyone's free time, so better be friendly to each others...) Just my 2cts :) |
Thank you for you wise words. I didn't want to come off as arrogant. Try to understand my frustration though. For a while, I did my best to respond to everybody asking for more information related to this change. Yet, people don't notice that and ask me same thing over and over. I have answered queries regarding use cases in these comments: #23310 (comment), #23310 (comment), #23310 (comment), #23310 (comment). Meanwhile, nobody answered my queries. It's exhausting to defend myself over and over without seeing a reason. Care to help dispute at least some of these conclusions of mine?
|
One more attempt to understand the use case: is it possible to create DI-factory which gets the return function (ContainerConfigurator $c) {
$s->set(Foo::class)
->factory(function (Definition $definition) {
return new Foo(
$definition,
$definition->getData('bar'),
$definition->getData('baz')
);
})
->argument(ref('definition')); // or DefinitionBuilder, for example, not sure
}; (I'm trying to mimic proposed format for DI configuration: #23834 (comment)) Doesn't it fix your issue with "transformation by hand" "every time"? Since |
Care to explain this one? The open/closed principle states that an object should be open to extension (in any form, including composition or decorating) but closed for modification. The only way this would break that principle is when this object is to open, relies to much on instable functionality. Or doesn't allow to change the functionality of something using eg. DI. The "dependency inversion principle" doesn't apply here as this object has no direct dependencies. I consider making a class final a very good thing actually. It makes the code easier to change (by maintainers) (and be modernized) and prevents developers from making "bad" decisions. Having a class be "open" allows the developer do to whatever they want, which is not exactly what the designer of the class/system had in mind, and can complicate changes in the future. And the "I will take that risk" is very short sighted, this class isn't just open to you, but everyone else who assumes it's safe to extend this and add any functionality they want. Then the maintainer improves the class, and people start to explain the maintainer just broke there code... (which is why Symfony has a BC promise in place). Removing the final on a class without a good proper, well thought out reason, feels more like a "there I fixed your problem" solution. The proposed solutions work just as good (as you indicated), you can add any form of additional information (including a values container of your own to store structured strict-typed data). It's not so much there is no solution, it's more you're not happy with "the" solution, but they can in fact work. Or as Paul M. Jones states There are no solutions, only tradeoffs. Systems like these work best with a generic metadata-attribute where you can store additional information and rules. And allows for enough flexibility, without having to open the class for everyone and the risk that introduces.
Which can be perfectly solved using the proposed solutions (in a another issue), secondly. You can also add your metadata system and invoke this when transitioning, it will require a bit more work to implement but does solve your problem.
To quote from the article:
So you may want to explain this in a bit more detail. The Definition can be considered a value-object (though still mutable). |
Of course there ARE solutions to solve my problem. For instance, I can just override Workflow#getDefinition(). I can replace class via composer configuration. I can create fork of this component. I can replace it with another library. Or I can write factory as you suggested (and yeah, call it every goddamn time I need my custom object). Each of this solution just circumvents the issue. I wanted to solve root of the problem. But I see everybody think it's ok in symfony components to have dependencies on final classes with no reason. I feel 0 support for this change, so I give up. |
Please don't make this class final. As a value holder object it's very restrictive to disallow users to do any extension of such object, since you don't even provide any interface for it. Will at least partially ease up current limitations highlighted by #23257