Zend Framework: Zend\Stdlib\Configurator Component Proposal
| Proposed Component Name | Zend\Stdlib\Configurator |
|---|---|
| Developer Notes | http://framework.zend.com/wiki/display/ZFDEV/Zend\Stdlib\Configurator |
| Proposers | Renan de Lima Vincent de Lau (original) |
| Zend Liaison | TBD |
| Revision | 1.0 - 5 July 2010: Initial Draft. (wiki revision: 6) |
Table of Contents
1. Overview
Zend\Stdlib\Configurator is intended to provide classes with an easy way to process options. It will accept a Zend\Config object or array and call the appropriate setters with the provided value. The class will accept names as both underscored lowercase and camelcaps.
This component fills the gap that comes with lack of multiple inheritance or mixins in the PHP language. Without this component, each class that accepts a Zend\Config object or array has to process the options manually, which is a repetitive task. Extracting the code should lead to less code duplication and more stable code.
2. References
3. Component Requirements, Constraints, and Acceptance Criteria
- This component will call the setter for each provided option.
- The component will align with coding standards and practices.
- The component will not have any knowledge of the target object.
- The component will allow option names to be passed as underscored words.
- The component will treat option names case insensitive.
- The component will require the target object to have setters for each configurable option.
- The component will not convert or test any configuration data passed.
4. Dependencies on Other Framework Components
- Zend_Config (optional)
- Zend_Exception
5. Theory of Operation
Zend\Stdlib\Configurator will accept a target object and an object implementing Traversable. This includes Zend\Config objects and plain arrays.
For each option passed, the Configurator will call the appropriate setter with the value provided. Option names are stripped of underscores before resolving the setter. Since call_user_method(), is_callback() and method_exists() are case sensitive, option names are practically case insensitive.
Since no state has to be maintained, the Configurator will be implemented as a class with only static methods.
6. Milestones / Tasks
- Milestone 1: Wait for input based on coding standards and the need for configurable objects
- Milestone 2: Finish the proposal
- Milestone 3: Unit tests exist, work, and are checked into SVN.
- Milestone 4: Initial documentation exists.
- Milestone 5: The component is applied to all appropriate framework components
7. Class Index
- Zend\Stdlib\Configurator
- Zend\Stdlib\ConfiguratorException
8. Use Cases
| UC-01 |
|---|
32 Comments
comments.show.hideJul 05, 2010
Marc Bennewitz (private)
Thanks for creating this proposal!
My points are the following:
The configurator simply iterates over options and uses the key to find the right setter and the value as option value.
I can't see arguments to restrict to array and Zend\Config
Method names are case-insensitive! In my eyes we don't need to convert underscored options names to CamelCase because the same method will be called.
I understand matthew to convert to CamelCase because it's the real defined method name but we could save 2 of 3 string conversation calls and the user doesn't note anything of it. (Sure the defined method names should be [set|get]CamelCase)
The exception name should extends SPL exceptions like InvalidArgumentException and therefore should be named as same as the SPL exception or something to show the user better what was wrong like UnknownOptionException extends InvalidArgumentException
I have a case where some options names could be handled by __call and the configurator must call these setter anyway.
A small explain:
An adapter has some options and an adapter overlay implements the adapter interface and adds additional functionalities and options to it but can be used with different adapters. In this case the overlay object using the magic __call to forward method calls (incl. option setters & getters) to the inner adapter.
-> http://framework.zend.com/wiki/display/ZFPROP/Zend+Cache+2.0+-+Marc+Bennewitz
Greetings
Jul 05, 2010
Vincent de Lau
Thanks for the feedback!
I was actually planning on that but hadn't really investigated yet which interface or SPL class I needed. When expanding the class prototype, I'll add traversable to the documentation.
This is exactly what I was planning to do. I will word this a bit more explicit in the Theory of Operation and the documentation.
To be honest I have no idea what the best practice for exceptions is. I was under the impressions that exceptions should be in the namespace, but I have no objection against any other exception type. Besides the name of a class, messages can convey enough helpful information, don't they?
I've thought about this, together with trying public properties and __set, but I think the new way is to be explicit. When looking at API documentation or source code it becomes immediately apparent which options are available.
However, you may have a valid use case here. I'm not sure however that we should add this to the default behavior. How about an optional third parameter $checkSetters = false, which will disable function name checking and thus try __call() if a setter is not available?
Feb 08, 2011
Matthew Weier O'Phinney
One rationale for converting to actual camelCase arises when you want to reduce the number of calls to method_exists(), which can be potentially expensive when called repeatedly. One technique I've used is to call get_class_methods() to cache the available methods, and then do in_array() calls (or an array_flip() and isset()) in order to determine if the method exists.
Regarding the assertion of checking for __call(), I'd argue against that. Typically, we shouldn't be defining __call(), and specifically not for setters – the whole point of doing configuration like this is to batch a bunch of existing, exposed setters. Otherwise, we're hiding implementation from the users, which makes it harder for the code to be self-documenting, and thus harder to learn. While I understand the idea of using __call() to forward to an adapter, I'd argue that such configuration should be represented by a specific key, which is handled by an specific method, which then passes on that adapter configuration to the adapter itself.
Jul 06, 2010
Keith Pope
This looks good a couple of points.
How would you handle required options, in the main class or inside the Configurator?
How about adding a configurable interface? Just a blank one for typing.
Jul 06, 2010
Vincent de Lau
The Configurator has no knowledge about the target class, so I would leave that responsibility with the target class. I would check the required properties after the Configurator has done its work using isset() and empty(). Checking for type constraints should be done in the individual setters.
There was some debate about a Configurable interface. The interface would potentially define a setOptions() function or something like that, for use in DI containers and factories. I intentionally left it out of Configurator but if the dust around a Configurable interface settles, it might be appropriate to add.
Jul 06, 2010
Wil Moore III (wilmoore)
I can't see arguments to restrict to array and Zend\Config
if ( !($options instanceof Traversable) && !is_array($options)) {
agreed
$setter = 'set' . str_replace('_', '', $name);
// instead of
$setter = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $name)));
Though I don't have a problem with either code fragment, I wonder if taking the less readable approach is the right thing to do in a cases where the performance cost is negligible. Potential food for thought.
The exception name should extends SPL exceptions
Absolutely agree.
I would suggest something like:
Zend\Stdlib\UnknownOptionException
or
Zend\Stdlib\Configurator\UnknownOptionException
An additional check of the magic method "__call" should be done
I think an optional flag would be reasonable here (less magic). Maybe $ignoreMissingSetter = FALSE (the default). The side-effect (advantage?) of this option is that it is an optional way to gain a slight (albeit negligible) performance boost (not having to check for method names).
As for required options. I'm thinking yet another "optional" parameter would be $requiredOptions = array(). Obviously this means that we have to be able to explicitly check for the corresponding setter method so this would override a TRUE === $ignoreMissingSetter setting.
Jul 06, 2010
Vincent de Lau
Traversable vs Zend\Config
It is already updated in the proposal. It adds the benefit of being completely independent of Zend_Config.
option to setter name conversion
If readability is an issue (error messages?), I'd certainly convert by convention. I'll leave that as an implementation detail.
The exception name should extends SPL exceptions
I'll change it.
__call and other options
I think 'ignore missing setters' is something different from 'check setters'. The first one could try to detect is a setter or __call is available and ignore options for which no setter is available. The other option would just try setting without checking, implicitly falling back on __call if available. Ignoring would not throw an exception on a missing setter, not checking would.
Providing a list of required options is another setting. This could be done as a simple checklist which is used during the setting of the options. I don't see why this would affect the other two options.
However, such a list just covers a use case where a fixed set of options is required. More complex rules still would have to be checked by the target. For instance, a database configuration could require a 'socket' option when the 'protocol' is 'unix', or a 'hostname' if the 'protocol' is 'inet'
If all these options should be made available, I'd seriously consider moving away from a purely static class. However, I think it is best to reduce the number of options. Especially the handling of missing setters should be the same across all framework components (throw an exception), although a fall-back to __call could be something to configure. Basically it would be the target telling the Configurator 'my __call function will handle setters'.
Just thinking along while typing... A new signature could be
Where $required and $checkSetters are both optional and either can be omitted.
Jul 06, 2010
Wil Moore III (wilmoore)
The updated signature looks good in my opinion.
The other option would just try setting without checking, implicitly falling back on __call if available. Ignoring would not throw an exception on a missing setter, not checking would.
Makes sense; however, while I believe the name $checkSetters does convey the end result, it could be improved to better clarify how it is actually used in implementation. Given that this ($checkSetters === FALSE) will "fall-back" on __call, may I suggest something like $allowOverloadFallback (or something similar).
On the other hand, keeping it as $checkSetters (more vague) could be a good thing if there will later be additions to the language that allow for a different method of implementation (in addition to or instead of __call).
Providing a list of required options is another setting. This could be done as a simple checklist which is used during the setting of the options. I don't see why this would affect the other two options.
After thinking about it further, I see you point here. The given options have no effect on whether the corresponding setter method exists or not. The setter method existing is a detail of the target object. The options given are the responsibility of the caller. I also agree that the target object should handle further validations on its own.
Jul 06, 2010
Marc Bennewitz (private)
I already replied to the mailing list - sry for duplicates
Not checking setters could change the behavior of option setting on different components.
I would prefer an option argument like $allowMagicCall to only fall-back if the setter doesn't exists but the magic method "__call" exists and the magic method have to throw a defined exception if it's unknown which can be re-throw by the setter to leave behavior of failed option setting equal to other components. (I send a prototype on mailing list)
This should be handled within the component its self because options could be required or not dependent on others.
Jul 07, 2010
Vincent de Lau
I've updated the proposal with a third option $tryCall and a basic implementation. Maybe $tryCall should be named $tryMagicCall for more clarity.
Matthew suggested on the ML that non existent options should be ignored to facilitate quick object swapping without having to rework the configuration.
I'm not in favor of making ignore an option. Although I think that component creators might find that useful, it doesn't help to enforce a common way to handle options. The $tryMagicCall option is different, since it is a declaration of the component creator that the __call method knows how to handle setter calls.
Besides bad parameters types, I see no need to throw or re-throw exceptions. IMHO the Exceptions thrown for bad method calls, wrong parameter types and such are sufficient. Wrapping these exceptions and re-throwing them might make it harder to debug.
Jul 07, 2010
Marc Bennewitz (private)
If the configurator ignores unknown options the component get note of it. Why it should be helpful for component writers ?
I'm not a favor of it, too. But it should be a point to ark ZF users (not contributors) to get your votes. It doesn't help to see that 5 users are criticize to not ignoring unknown options but for 100 users exceptions are fine and don't tell it.
This would only be interesting if unknown exceptions have to throw.
My point for it was that for the user only one exceptions should be thrown on unknown option. But if a component allows option setters by __call it should throw directly this exception because:
1. It doesn't know if this was an failed option setter of another (direct) call to it's object.
2. On component shouldn't throw exceptions of others
Therefore for the component it's simply an BadMethodCallException and can be catched by the configurator which knows that this BadMethodCallException is followed of an unknown option name and re-throw it to be for the users its the same type of exception as on other components not using "__call"-setters
Feb 08, 2011
Matthew Weier O'Phinney
The reason I'm against raising exceptions on unknown options is quite simple: configuration inheritance. Consider the following:
Now consider that Some\Other\Adapter\Class doesn't know about option "foo", but does know and care about option "bar" – the problem we have now is that the options array now looks like this:
If we were to throw exceptions on unknown options, we simply cannot do configuration inheritance any more – and this is an incredibly powerful and useful feature of our configuration approach.
Feb 08, 2011
Wil Moore III (wilmoore)
I'd be in favor of having a strict setting which does throw an exception but have it default to non-strict.
Jul 07, 2010
Vincent de Lau
After some more feedback and discussion, I have the following questions for you all:
Jan 15, 2011
Wil Moore III (wilmoore)
Should the configurator function be static. Given the lack of state, I'd say it is fine. I also don't see any issue with testability, but I'm new to unit testing and TDD.
If there is no state, there is no reason for an instance. For cleanliness and clarity you might want to consider blocking __construct and __clone explicitly.
What type of exception should be thrown? Should they be a SPL InvalidArgumentException or must exceptions derive from a zend\stdlib\Exception?
Probably best if a lead answers this one.
Are we going to have a Configurable interface? Should this component only allow target objects that implement this interface? I see no requirement for this, but would not object if that is community consensus.
In this case, I think it makes sense to utilize the interface.
Jul 07, 2010
Marc Bennewitz (private)
There is no need to create an instance and would only be overflow
It should use the standard of ZF 2.0 -> extends SPL-Exception with a component marker.
Like this:
-> extends the base InvalidArgumentException
-> implements Zend\Stdlib\Exception
This should be part of another proposal of Zend\Config
-> But it shouldn't only allow target object's of this interface.
Jul 15, 2010
Marc Bennewitz (private)
I get an idea for an addition to solve the following issue:
Within Zend_Cache_Frontend_Page there there are the options "default_options" and "regexps" which hold a list of the options which are only affected on specific circumstances.
This means these options are part of an option to set but can't directly set and should be validated on set the option group.
(I hope it's clear what I mean)
-> http://framework.zend.com/manual/en/zend.cache.frontends.html#zend.cache.frontends.page
How to validate options without set the option ?
1. get the current option -> set the new option to check -> re-set the old option
or
2. add a method to validate an option on component side and to validate a list of options on configurator side.
-> For every option a method "validate{OptionName}(mixed $value) : boolean" would be required.
-> The configurator would need a method to only validate a list of options.
Jan 08, 2011
Ben Scholzen
What's up with this proposal? I'd still like to see it in ZF2, because it resolves a lot of duplicate code. Oh, by the way, I'd like to see the option to speciy "must have" options, which, if not existent, throw an exception.
Jan 14, 2011
Renan de Lima
This is a good proposal for zf2
-1 for specify "must have", it should be implemented after configurator calling (ie: in configurable object constructor). API shouldn't require all required options every time.
Configurator behavior ("try call", "must have" and future others) are tricks. We should think another way to set those configurations, methods looks dirty when it has a lot of arguments. I don't know how, but adding arguments doesn't look good for me. Those options/behavior should be defined by configurable class, not by the user.
I think options could also work for static methods like \Zend\Mail::setDefaultFrom()
This also could support "set" and "add" methods calling. Bellow a basic implementation.
Jan 14, 2011
Vincent de Lau
Well, to be honest I'm currently not working with Zend Framework anymore, since I changed jobs. Still trying to get this shop over to ZF though...
I do think this proposal has it's benefits, but I think the community has to decide it they want it. I think there is enough stuff in here to start large flame wars...
Having said that, I think the component itself is quite trivial to implement. My implementation is actually in the class skeleton above.
Jan 14, 2011
Renan de Lima
I really do think this proposal is good and necessary, options in zf2 can be improved so much
I dind't want to start a flame. It's just my opnion, here (and lists) is the place to talk about zf, obviously my opnion is not the best one, it's just yet another one
don't worry
I also think implementation is trivial, but this is an important component, somethings looks basic, but our choose today can generate problems in the future
Jan 15, 2011
Ramon Henrique Ornelas
+1
Great proposal to zf2, I think that with this proposal should try fix some problems that already know.
How order of the keys (ZF-10836, ZF-10543 between another) or skip of some keys (see ZF-8175, Zend_Form_Element between another).
A code simple
With this should be done a treatment in Zend\Stdlib\Configurator if the $target implements Zend\Stdlib\Configurable to make called $target->getOptionNames() in Zend\Stdlib\Configurator::configure().
Greetings,
Ramon Henrique Ornelas
Jan 15, 2011
Marc Bennewitz (private)
+1 for creating an Configurable interface which implements basic functions to get available option names and required options. This also solves test to magic methods because its clear which options methods are available ignoring of implementation detail.
This is something I really don't like because it makes debugging more difficult and generates more problems as it solves.
But if this behavior is required we should create an option to disable it for debugging like "ignore_missing_options" but with that it would mean the configuration becomes a state and should instantiable.
In my opinion there is no need to implement this but if it's needed the interface should differ between class options and object options.
Feb 08, 2011
Matthew Weier O'Phinney
Please see my previous comments – the reason why I don't support raising exceptions/errors on unknown options is because it invalidates configuration inheritance.
Jan 31, 2011
Renan de Lima
I'm starting work in this proposal. Next days i'm gonna log every changes in this wiki page.
----- from: Renan de Lima
----- to: Vicent de Lau
Hi Vicent,
I'm Renan, php developer and zf contributor.
I got your message: http://framework.zend.com/wiki/display/ZFPROP/Zend_Stdlib_Configurator+-+Vincent+de+Lau?focusedCommentId=31129771#comment-31129771
This proposal is very important, I wanna start working on this
component, once you think this is possible, I would like coding it.
----- from: Vicent de Lau
----- to: Renan de Lima
Hi Renan,
As I said, I'm currently not actively working with Zend Framework at the moment. If you want, feel free to take up this proposal and finish it.
Although it seems like a simple component, I think the community will have a lot of feedback.
Feb 03, 2011
Renan de Lima
Soon i'm going to update this proposal regarding some subjects. Please, let your option.
We should avoid them. Magic methods are slow and not clear for users. Anyway __set() makes more sense than __call() in this case.
In zf2, we are working to make class APIs clearer. Class definitons have getters and setters explicited defined. Every component should assist that.
Magic methods may give us some problems. For example, user should implement magic methods case insensitive, otherwise the API of this component is wrong. So this component has a dependency of others.
I think we should not use __call() and/or __set()
It's a nice feature for constructors, but it's not for other cases. stdlib\configurator must work everywhere. Required options checking would be optional, maybe a third argument for configure() method.
I agree when Marc says "This should be handled within the component its self because options could be required or not dependent on others.". Anyway an interface, as Ramon suggested, works fine.
I think that optional argument and Interface are better for this issue.
IMO class configuration is needed, specially because class options values handling of zf classes could be easer.
Currently we have some classes and their default values. Every time users want to change them they have to send all options to the object factory. Application resources could handle user options and make configuration.ini file easer to understand and smallest. Anyway providing ways to change default values (configurable by static methods) of anything is good for the framework.
Feb 03, 2011
Marc Bennewitz (private)
Good to hear you are working on it
This isn't true in all cases. For example if a component is working with such properties you can't differ these kind of properties and options.
But if we have an Interface incl. a method to get all available options/names this automatic defines which getters/setters are available for options. If a component implements the Interface it also indicate that every available option has a getter and setter method. -> There is no need test it before call.
Required options
As already noted I see no need of this argument as part of configurator.
Feb 07, 2011
Renan de Lima
sure, properties != options
agreed, no magic calling for objects that implements this Interface, but the configurator have to work for every object, even for those that don't implement configurable interface
thinking better about this, we should have some way to find "getters" or merge current object option values with new options arguments... so, this is an object business
Feb 08, 2011
Renan de Lima
There are a lot of posts, I tried gather topics about this component. The main topics and its "decisions":
This isn't a bigger component, so you'll see something implemented in my zf2 fork soon.
May 04, 2011
Renan de Lima
I've implemented days and days ago. See some files from my fork.
Configurator class: https://github.com/renanbr/zf2/blob/master/library/Zend/Stdlib/Configurator.php
Configurable interface: https://github.com/renanbr/zf2/blob/master/library/Zend/Stdlib/Configurable.php
May 06, 2011
Marc Bennewitz (private)
Interface:
Does it make sense to have a getOptions method returning an array (or object implements ArrayAccess)
Configurator:
1. It makes only sense to check if a setter exists if the target object doesn't implement Configurable interface
2. Normalizing should return a "setCamelCase" format and this method should be called
3. array_keys only works on Arrays
-> It should be enough to iterates once over options
(if Configurable check if option is available within option names (case-insensitive)
or check if method exists)
and if valid normalize and call setter within this iteration (e.g. setCamelCase)
Or do I have an error in reasoning ?
May 24, 2011
Renan de Lima
I don't think so. This interface provides only options information for configurator class. It doesn't mean children classes must provide a kind of option access.
In this implementation, configurable classes can intercept setter calls in setSomething() (or __call() when method does not exist). When configurator checks if a setter exists, this means configurable->getOptionNames() returned as a key option name in array map a specific setter to be called (in despite of default behavior). This is implemented to avoid runtime exception, this way non well-formed option configuration is just ignored.
Makes sense, there is no way to know __call() implementation. I think user expect camel case method name. I think we should change this as you propose.
Sure, i didn't tested it. This situation must be added to test cases.