1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Charcoal\Factory; |
4
|
|
|
|
5
|
|
|
// Dependencies from `PHP` |
6
|
|
|
use \Exception; |
7
|
|
|
use \InvalidArgumentException; |
8
|
|
|
|
9
|
|
|
// Local namespace dependencies |
10
|
|
|
use \Charcoal\Factory\FactoryInterface; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Full implementation, as Abstract class, of the FactoryInterface. |
14
|
|
|
*/ |
15
|
|
|
abstract class AbstractFactory implements FactoryInterface |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* If a base class is set, then it must be ensured that the |
19
|
|
|
* @var string $baseClass |
20
|
|
|
*/ |
21
|
|
|
private $baseClass = ''; |
22
|
|
|
/** |
23
|
|
|
* |
24
|
|
|
* @var string $defaultClass |
25
|
|
|
*/ |
26
|
|
|
private $defaultClass = ''; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Keeps loaded instances in memory, in `[$type => $instance]` format. |
30
|
|
|
* Used with the `get()` method only. |
31
|
|
|
* @var array $instances |
32
|
|
|
*/ |
33
|
|
|
private $instances = []; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Create a new instance of a class, by type. |
37
|
|
|
* |
38
|
|
|
* Unlike `get()`, this method *always* return a new instance of the requested class. |
39
|
|
|
* |
40
|
|
|
* ## Object callback |
41
|
|
|
* It is possible to pass a callback method that will be executed upon object instanciation. |
42
|
|
|
* The callable should have a signature: `function($obj);` where $obj is the newly created object. |
43
|
|
|
* |
44
|
|
|
* |
45
|
|
|
* @param string $type The type (class ident). |
46
|
|
|
* @param array $args The constructor arguments (optional). |
47
|
|
|
* @param callable $cb Object callback. |
48
|
|
|
* @throws Exception If the base class is set and the resulting instance is not of the base class. |
49
|
|
|
* @throws InvalidArgumentException If type argument is not a string or is not an available type. |
50
|
|
|
* @return mixed The instance / object |
51
|
|
|
*/ |
52
|
|
|
final public function create($type, array $args = null, callable $cb = null) |
53
|
|
|
{ |
54
|
|
|
if (!is_string($type)) { |
55
|
|
|
throw new InvalidArgumentException( |
56
|
|
|
sprintf( |
57
|
|
|
'%s: Type must be a string.', |
58
|
|
|
get_called_class() |
59
|
|
|
) |
60
|
|
|
); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
if ($this->isResolvable($type) === false) { |
64
|
|
|
$defaultClass = $this->defaultClass(); |
65
|
|
|
if ($defaultClass !== '') { |
66
|
|
|
return new $defaultClass($args); |
67
|
|
|
} else { |
68
|
|
|
throw new InvalidArgumentException( |
69
|
|
|
sprintf( |
70
|
|
|
'%1$s: Type "%2$s" is not a valid type. (Using default class "%3$s")', |
71
|
|
|
get_called_class(), |
72
|
|
|
$type, |
73
|
|
|
$defaultClass |
74
|
|
|
) |
75
|
|
|
); |
76
|
|
|
} |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
// Create the object from the type's class name. |
80
|
|
|
$classname = $this->resolve($type); |
81
|
|
|
$obj = new $classname($args); |
82
|
|
|
|
83
|
|
|
|
84
|
|
|
// Ensure base class is respected, if set. |
85
|
|
|
$baseClass = $this->baseClass(); |
86
|
|
|
if ($baseClass !== '' && !($obj instanceof $baseClass)) { |
87
|
|
|
throw new Exception( |
88
|
|
|
sprintf( |
89
|
|
|
'%1$s: Object is not a valid "%2$s" class', |
90
|
|
|
get_called_class(), |
91
|
|
|
$baseClass |
92
|
|
|
) |
93
|
|
|
); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
if (isset($cb)) { |
97
|
|
|
$cb($obj); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
return $obj; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Get (load or create) an instance of a class, by type. |
105
|
|
|
* |
106
|
|
|
* Unlike `create()` (which always call a `new` instance), this function first tries to load / reuse |
107
|
|
|
* an already created object of this type, from memory. |
108
|
|
|
* |
109
|
|
|
* @param string $type The type (class ident). |
110
|
|
|
* @param array $args The constructor arguments (optional). |
111
|
|
|
* @throws InvalidArgumentException If type argument is not a string. |
112
|
|
|
* @return mixed The instance / object |
113
|
|
|
*/ |
114
|
|
|
final public function get($type, array $args = null) |
115
|
|
|
{ |
116
|
|
|
if (!is_string($type)) { |
117
|
|
|
throw new InvalidArgumentException( |
118
|
|
|
'Type must be a string.' |
119
|
|
|
); |
120
|
|
|
} |
121
|
|
|
if (!isset($this->instances[$type]) || $this->instances[$type] === null) { |
122
|
|
|
$this->instances[$type] = $this->create($type, $args); |
123
|
|
|
} |
124
|
|
|
return $this->instances[$type]; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* If a base class is set, then it must be ensured that the created objects |
129
|
|
|
* are `instanceof` this base class. |
130
|
|
|
* |
131
|
|
|
* @param string $type The FQN of the class, or "type" of object, to set as base class. |
132
|
|
|
* @throws InvalidArgumentException If the class is not a string or is not an existing class / interface. |
133
|
|
|
* @return FactoryInterface |
134
|
|
|
*/ |
135
|
|
|
public function setBaseClass($type) |
136
|
|
|
{ |
137
|
|
|
if (!is_string($type) || empty($type)) { |
138
|
|
|
throw new InvalidArgumentException( |
139
|
|
|
'Class name or type must be a non-empty string.' |
140
|
|
|
); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
$exists = (class_exists($type) || interface_exists($type)); |
144
|
|
|
if ($exists) { |
145
|
|
|
$classname = $type; |
146
|
|
View Code Duplication |
} else { |
|
|
|
|
147
|
|
|
$classname = $this->resolve($type); |
148
|
|
|
|
149
|
|
|
$exists = (class_exists($classname) || interface_exists($classname)); |
150
|
|
|
if (!$exists) { |
151
|
|
|
throw new InvalidArgumentException( |
152
|
|
|
sprintf('Can not set "%s" as base class: Invalid class or interface name.', $classname) |
153
|
|
|
); |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
$this->baseClass = $classname; |
158
|
|
|
|
159
|
|
|
return $this; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* @return string The FQN of the base class |
164
|
|
|
*/ |
165
|
|
|
public function baseClass() |
166
|
|
|
{ |
167
|
|
|
return $this->baseClass; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* If a default class is set, then calling `get()` or `create()` an invalid type |
172
|
|
|
* should return an object of this class instead of throwing an error. |
173
|
|
|
* |
174
|
|
|
* @param string $type The FQN of the class, or "type" of object, to set as default class. |
175
|
|
|
* @throws InvalidArgumentException If the class name is not a string or not a valid class. |
176
|
|
|
* @return FactoryInterface |
177
|
|
|
*/ |
178
|
|
|
public function setDefaultClass($type) |
179
|
|
|
{ |
180
|
|
|
if (!is_string($type) || empty($type)) { |
181
|
|
|
throw new InvalidArgumentException( |
182
|
|
|
'Class name or type must be a non-empty string.' |
183
|
|
|
); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
View Code Duplication |
if (class_exists($type)) { |
|
|
|
|
187
|
|
|
$classname = $type; |
188
|
|
|
} else { |
189
|
|
|
$classname = $this->resolve($type); |
190
|
|
|
|
191
|
|
|
if (!class_exists($classname)) { |
192
|
|
|
throw new InvalidArgumentException( |
193
|
|
|
sprintf('Can not set "%s" as defaut class: Invalid class name.', $classname) |
194
|
|
|
); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$this->defaultClass = $classname; |
199
|
|
|
|
200
|
|
|
return $this; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* @return string The FQN of the default class |
205
|
|
|
*/ |
206
|
|
|
public function defaultClass() |
207
|
|
|
{ |
208
|
|
|
return $this->defaultClass; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
|
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Resolve the class name from "type". |
215
|
|
|
* |
216
|
|
|
* @param string $type The "type" of object to resolve (the object ident). |
217
|
|
|
* @return string |
218
|
|
|
*/ |
219
|
|
|
abstract public function resolve($type); |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Returns wether a type is resolvable (is valid) |
223
|
|
|
* |
224
|
|
|
* @param string $type The "type" of object to resolve (the object ident). |
225
|
|
|
* @return boolean True if the type is available, false if not |
226
|
|
|
*/ |
227
|
|
|
abstract public function isResolvable($type); |
228
|
|
|
} |
229
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.