1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Colada\X; |
4
|
|
|
|
5
|
|
|
use Closure; |
6
|
|
|
use ArrayAccess; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Immutable functor, that collects behaviour and resolves it to concrete value |
10
|
|
|
* |
11
|
|
|
* Will NOT be implemented: __isset(). Useless, because call to isset() or empty() is external and can not be wrapped. |
12
|
|
|
* Will NOT be implemented: __unset(). Useless, because call to unset() is external and can not be wrapped. |
13
|
|
|
* Will NOT be implemented: __set(). Useless, because assigning a value is an external action and can not be wrapped. |
14
|
|
|
* |
15
|
|
|
* Will NOT be implemented: \Traversable support. Useless, because work with traversable value is external and can not be wrapped. |
16
|
|
|
* Will NOT be implemented: \Countable support. Useless, because call to count() is external and can not be wrapped. |
17
|
|
|
* |
18
|
|
|
* Will NOT be implemented: __toString(). This method can't be collected, it should always return a string. |
19
|
|
|
* |
20
|
|
|
* @author Alexey Shokov <[email protected]> |
21
|
|
|
*/ |
22
|
|
|
class LazyObjectProxy implements ArrayAccess |
23
|
|
|
{ |
24
|
|
|
/** |
25
|
|
|
* @var Closure |
26
|
|
|
*/ |
27
|
|
|
private $mapper; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @param callable $constructor Not required. \Colada\X\id function will be used by default. |
31
|
|
|
*/ |
32
|
|
|
public function __construct(callable $constructor = null) |
33
|
|
|
{ |
34
|
|
|
$constructor = $constructor ?: 'Colada\\X\\id'; |
35
|
|
|
|
36
|
|
|
$this->mapper = as_closure($constructor); |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
public function offsetGet($key) |
40
|
|
|
{ |
41
|
|
|
return $this->__call(__FUNCTION__, [$key]); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Call to this method should be available, because it can be called explicitly |
46
|
|
|
* |
47
|
|
|
* @param mixed $key |
48
|
|
|
* |
49
|
|
|
* @return static |
50
|
|
|
*/ |
51
|
|
|
public function offsetUnset($key) |
52
|
|
|
{ |
53
|
|
|
return $this->__call(__FUNCTION__, [$key]); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
public function offsetExists($key) |
57
|
|
|
{ |
58
|
|
|
return $this->__call(__FUNCTION__, [$key]); |
|
|
|
|
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Call to this method should be available, because it can be called explicitly |
63
|
|
|
* |
64
|
|
|
* @param mixed $key |
65
|
|
|
* @param mixed $value |
66
|
|
|
* |
67
|
|
|
* @return static |
68
|
|
|
*/ |
69
|
|
|
public function offsetSet($key, $value) |
70
|
|
|
{ |
71
|
|
|
return $this->__call(__FUNCTION__, [$key, $value]); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
public function __get($property) |
75
|
|
|
{ |
76
|
|
|
return new static(function (...$arguments) use ($property) { |
77
|
|
|
return $this->mapper->__invoke(...$arguments)->$property; |
78
|
|
|
}); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
public function __call($method, $methodArguments) |
82
|
|
|
{ |
83
|
|
|
return new static(function (...$arguments) use ($method, $methodArguments) { |
84
|
|
|
return call_user_func_array([$this->mapper->__invoke(...$arguments), $method], $methodArguments); |
85
|
|
|
}); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Play recorded actions on concrete value |
90
|
|
|
* |
91
|
|
|
* @param mixed ...$arguments |
92
|
|
|
* |
93
|
|
|
* @return mixed |
94
|
|
|
*/ |
95
|
|
|
public function __invoke(...$arguments) |
96
|
|
|
{ |
97
|
|
|
return $this->__asClosure()->__invoke(...$arguments); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Recorder as \Closure object |
102
|
|
|
* |
103
|
|
|
* Useful for working in places, that don't allow anything except \Closure instances. Like Doctrine's ArrayCollection. |
104
|
|
|
* |
105
|
|
|
* @return Closure |
106
|
|
|
*/ |
107
|
|
|
public function __asClosure() |
108
|
|
|
{ |
109
|
|
|
return function (...$arguments) { |
110
|
|
|
// In the end return original value. |
111
|
|
|
$result = $this->mapper->__invoke(...$arguments); |
112
|
|
|
if (is_object($result) && ($result instanceof ValueWrapper)) { |
113
|
|
|
$result = $result->__getWrappedValue(); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
return $result; |
117
|
|
|
}; |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.