I'm not a PHP expert but in any programming language using attribute bags as objects, there always exist functions to ensure that a given attribute is defined, and how it is defined. Essentially all the basics you need to enforce Duck Typing.
This does leads to a distribution of such checks throughout the code base. Which is not just complex to manage but often results in code duplication.
This is obviously not a good state to be in and leads to the concept of a Contract.
Imagine a function (or object with some functions on it) which given an object can verify that all relevant attributes are defined, that they are of the correct types, and those types are correctly correlated. It can ensure that every function has the correct number of arguments, order, type, etc...
This is the same as verifying a house by looking at it and saying:
- 1 door... check
- n windows... check
- on a street... check
- it must be a house...
We can go further and purposefully invoke "Readonly" methods to ensure that the object's state has not transitioned, or that other expectations are met. But this is getting awfully close to unit testing at run time.
The issue here is that these no-effect operations might still actually have an effect, the effect is simply not observable by the contract. Imagine for example a function that will throw an exception, this may be logged even though the contract simply discards the exception after verifying its expectations.
Kind of like walking up to the house and kicking a wall. If it isn't rotten or otherwise broken the foot reports that the pain is what was expected. But if it wasn't what you expected it to be then your foot is now stuck in the wall...
Offline Verification - Destructive Contracts
While not strictly about your runtime case - Contracts can be extended explicitly for test purposes.
This version actually needs a way to generate multiple instances of the object. Each being run through a series of scenarios to ensure that the higher level semantics implied by the contract are respected (eg: the item pushed onto the stack is the item popped off the stack).
There is no danger here, if the object misbehaves the test has failed by definition and nothing has happened in production.
It may be useful to supply these sorts of Protocol Contracts along with your framework. When the unexpected happens it is always useful to have a scenario laid out with exactly what should happen, and what didn't happen. The developer need only pass an example in to see what is wrong.
Sometimes you do not wish to dictate the entire object interface/protocol - but you still need to reasonably enforce expectations.
The User Contract to the rescue. This is a contract owned solely by this given function/object to describe how it wishes to interact with a given collaborator.
This approach is bottom up as it is not the collaborator dictating usage, but the using function/object itself.
A Protocol Lawyer in the Middle
- Yes, they slow things down.
- Yes, they get in the way.
- Yes, they are more code.
They can enforce a protocol between a User and one or more collaborators.
In essence The Protocol Lawyer holds an understanding of the protocol that is to be followed. A simple example:
- When you received one or more collaborators you would register them with the lawyer, and use whatever object the lawyer passes back.
- When the user code makes a call, say
['name'], the lawyer checks its own idea of what is allowed, and verifies that this call is legal.
- After verifying that the call is legal it dispatches the call to the desired collaborator.
- The collaborator does its thing and ships back its response, which the lawyer dutifully verifies against its own ledger of acceptable responses.
- Once the response has been validated the lawyer hands it back to the user.
If at any stage the lawyer detects anyone in the protocol having acted inappropriately they can decide what to do. An obvious strategy is to stop the interactions and notify some authority by throwing an exception, logging a message, or killing the process.
Other interventions are possible.
- Correcting the output/request
- Letting the transgression slide, but making a note of it.
- Requesting some form of outside intervention.
It is even possible that there are several lawyers at play here, one introduced by each interested party, each verifying the interactions by their definition of the protocol going on.
This is perhaps not what you need, but it will ensure that the object behaves appropriately (or you know what happened and who to blame).
In this case you cannot possible ensure that every object complies with your expectations at runtime. However you can:
- provide a Destructive/Protocol Contract as a testing/diagnostic tool for other developers
- provide a lawyer to oversee that the protocol is executed correctly, or some sensible action is taken instead.
- enforce a Destructive/Protocol Contract on known collaborators
- Enforce either a full Interface Contract, or a User Contract on collaborators as they are passed in, or about to be used.