1
|
|
|
<?php |
2
|
|
|
namespace EventEspresso\core\services\container; |
3
|
|
|
|
4
|
|
|
use EventEspresso\core\exceptions\InvalidClassException; |
5
|
|
|
use EventEspresso\core\exceptions\InvalidDataTypeException; |
6
|
|
|
use EventEspresso\core\exceptions\InvalidIdentifierException; |
7
|
|
|
use RuntimeException; |
8
|
|
|
|
9
|
|
|
if ( ! defined('EVENT_ESPRESSO_VERSION')) { |
10
|
|
|
exit('No direct script access allowed'); |
11
|
|
|
} |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Class Recipe |
17
|
|
|
* Fairly simple DTO (Data Transfer Object) for relaying information about how to construct an object |
18
|
|
|
* |
19
|
|
|
* @package Event Espresso |
20
|
|
|
* @author Brent Christensen |
21
|
|
|
* @since 4.9.1 |
22
|
|
|
*/ |
23
|
|
|
class Recipe implements RecipeInterface |
24
|
|
|
{ |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* A default Recipe to use if none is specified for a class |
28
|
|
|
*/ |
29
|
|
|
const DEFAULT_ID = '*'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Identifier for the entity class to be constructed. |
33
|
|
|
* Typically a Fully Qualified Class Name |
34
|
|
|
* |
35
|
|
|
* @var string $identifier |
36
|
|
|
*/ |
37
|
|
|
private $identifier; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Fully Qualified Class Name |
41
|
|
|
* |
42
|
|
|
* @var string $fqcn |
43
|
|
|
*/ |
44
|
|
|
private $fqcn; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* a dependency class map array |
48
|
|
|
* If a Recipe is for a single class (or group of classes that shares the EXACT SAME constructor arguments), |
49
|
|
|
* and that class type hints for an interface, then this property allows you to configure what dependencies |
50
|
|
|
* get used when instantiating the class. |
51
|
|
|
* For example: |
52
|
|
|
* There's a class called Coffee, and one of its constructor arguments is BeanInterface |
53
|
|
|
* There are two implementations of BeanInterface: HonduranBean, and KenyanBean |
54
|
|
|
* We want one Coffee object to use HonduranBean for its BeanInterface, |
55
|
|
|
* and the 2nd Coffee object to use KenyanBean for its BeanInterface. |
56
|
|
|
* To do this, we need to create two Recipes: |
57
|
|
|
* one with an identifier of 'HonduranCoffee' using the following ingredients : |
58
|
|
|
* array('BeanInterface' => 'HonduranBean') |
59
|
|
|
* and the other with an identifier of 'KenyanCoffee' using the following ingredients : |
60
|
|
|
* array('BeanInterface' => 'KenyanBean') |
61
|
|
|
* Then, whenever the CoffeeShop brews an instance of HonduranCoffee, |
62
|
|
|
* an instance of HonduranBean will get injected for the BeanInterface dependency, |
63
|
|
|
* and whenever the CoffeeShop brews an instance of KenyanCoffee, |
64
|
|
|
* an instance of KenyanBean will get injected for the BeanInterface dependency |
65
|
|
|
* |
66
|
|
|
* @var array $ingredients |
67
|
|
|
*/ |
68
|
|
|
private $ingredients = array(); |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* one of the class constants from CoffeeShop: |
72
|
|
|
* CoffeeMaker::BREW_NEW - creates a new instance |
73
|
|
|
* CoffeeMaker::BREW_SHARED - creates a shared instance |
74
|
|
|
* CoffeeMaker::BREW_LOAD_ONLY - loads but does not instantiate |
75
|
|
|
* |
76
|
|
|
* @var string $type |
77
|
|
|
*/ |
78
|
|
|
private $type; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* class name aliases - typically a Fully Qualified Interface that the class implements |
82
|
|
|
* identifiers passed to the CoffeeShop will be run through the filters to find the correct class name |
83
|
|
|
* |
84
|
|
|
* @var array $filters |
85
|
|
|
*/ |
86
|
|
|
private $filters = array(); |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* array of full server filepaths to files that may contain the class |
90
|
|
|
* |
91
|
|
|
* @var array $paths |
92
|
|
|
*/ |
93
|
|
|
private $paths = array(); |
94
|
|
|
|
95
|
|
|
|
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Recipe constructor. |
99
|
|
|
* |
100
|
|
|
* @param string $identifier class identifier, can be an alias, or FQCN, or whatever |
101
|
|
|
* @param string $fqcn \Fully\Qualified\ClassName, optional if $identifier is FQCN |
102
|
|
|
* @param array $ingredients array of dependencies that can not be resolved automatically, |
103
|
|
|
* used for resolving concrete classes for type hinted interfaces |
104
|
|
|
* for the dependencies of THIS class |
105
|
|
|
* @param string $type recipe type: one of the class constants on |
106
|
|
|
* \EventEspresso\core\services\container\CoffeeMaker |
107
|
|
|
* @param array $filters array of class aliases, or class interfaces |
108
|
|
|
* this works somewhat opposite to the $ingredients array above, |
109
|
|
|
* in that this array specifies interfaces or aliases |
110
|
|
|
* that this Recipe can be used for when resolving OTHER class's dependencies |
111
|
|
|
* @param array $paths if class can not be loaded via PSR-4 autoloading, |
112
|
|
|
* then supply a filepath, or array of filepaths, so that it can be included |
113
|
|
|
*/ |
114
|
|
|
public function __construct( |
115
|
|
|
$identifier, |
116
|
|
|
$fqcn = '', |
117
|
|
|
$filters = array(), |
118
|
|
|
$ingredients = array(), |
119
|
|
|
$type = CoffeeMaker::BREW_NEW, |
120
|
|
|
$paths = array() |
121
|
|
|
) |
122
|
|
|
{ |
123
|
|
|
$this->setIdentifier($identifier); |
124
|
|
|
$this->setFilters((array)$filters); |
125
|
|
|
$this->setIngredients((array)$ingredients); |
126
|
|
|
$this->setType($type); |
127
|
|
|
$this->setPaths($paths); |
128
|
|
|
$this->setFqcn($fqcn); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
|
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @return string |
135
|
|
|
*/ |
136
|
|
|
public function identifier() |
137
|
|
|
{ |
138
|
|
|
return $this->identifier; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
|
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* @return string |
145
|
|
|
*/ |
146
|
|
|
public function fqcn() |
147
|
|
|
{ |
148
|
|
|
return $this->fqcn; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
|
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @return array |
155
|
|
|
*/ |
156
|
|
|
public function filters() |
157
|
|
|
{ |
158
|
|
|
return (array)$this->filters; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
|
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @return array |
165
|
|
|
*/ |
166
|
|
|
public function ingredients() |
167
|
|
|
{ |
168
|
|
|
return $this->ingredients; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
|
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* @return string |
175
|
|
|
*/ |
176
|
|
|
public function type() |
177
|
|
|
{ |
178
|
|
|
return $this->type; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
|
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* @return array |
185
|
|
|
*/ |
186
|
|
|
public function paths() |
187
|
|
|
{ |
188
|
|
|
return (array)$this->paths; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
|
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @param string $identifier Identifier for the entity class that the Recipe applies to |
195
|
|
|
* Typically a Fully Qualified Class Name |
196
|
|
|
*/ |
197
|
|
View Code Duplication |
public function setIdentifier($identifier) |
|
|
|
|
198
|
|
|
{ |
199
|
|
|
if ( ! is_string($identifier) || empty($identifier)) { |
200
|
|
|
throw new InvalidIdentifierException( |
201
|
|
|
is_object($identifier) ? get_class($identifier) : gettype($identifier), |
202
|
|
|
__('class identifier (typically a \Fully\Qualified\ClassName)', 'event_espresso') |
203
|
|
|
); |
204
|
|
|
} |
205
|
|
|
$this->identifier = $identifier; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
|
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Ensures incoming string is a valid Fully Qualified Class Name, |
212
|
|
|
* except if this is the default wildcard Recipe ( * ), |
213
|
|
|
* or it's NOT an actual FQCN because the Recipe is using filepaths |
214
|
|
|
* for classes that are not PSR-4 compatible |
215
|
|
|
* PLZ NOTE: |
216
|
|
|
* Recipe::setFqcn() has a check to see if Recipe::$paths is empty or not, |
217
|
|
|
* therefore you should always call Recipe::setPaths() before Recipe::setFqcn() |
218
|
|
|
* |
219
|
|
|
* @param string $fqcn |
220
|
|
|
*/ |
221
|
|
|
public function setFqcn($fqcn) |
222
|
|
|
{ |
223
|
|
|
$fqcn = ! empty($fqcn) ? $fqcn : $this->identifier; |
224
|
|
|
if ( ! is_string($fqcn)) { |
225
|
|
|
throw new InvalidDataTypeException( |
226
|
|
|
'$fqcn', |
227
|
|
|
is_object($fqcn) ? get_class($fqcn) : gettype($fqcn), |
228
|
|
|
__('string (Fully\Qualified\ClassName)', 'event_espresso') |
229
|
|
|
); |
230
|
|
|
} |
231
|
|
|
$fqcn = ltrim($fqcn, '\\'); |
232
|
|
|
if ( |
233
|
|
|
$fqcn !== Recipe::DEFAULT_ID |
234
|
|
|
&& ! empty($fqcn) |
235
|
|
|
&& empty($this->paths) |
236
|
|
|
&& ! class_exists($fqcn) |
237
|
|
|
) { |
238
|
|
|
throw new InvalidClassException($fqcn); |
239
|
|
|
} |
240
|
|
|
$this->fqcn = $fqcn; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
|
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* @param array $ingredients an array of dependencies where keys are the aliases and values are the FQCNs |
247
|
|
|
* example: |
248
|
|
|
* array( 'ClassInterface' => 'Fully\Qualified\ClassName' ) |
249
|
|
|
*/ |
250
|
|
View Code Duplication |
public function setIngredients(array $ingredients) |
|
|
|
|
251
|
|
|
{ |
252
|
|
|
if (empty($ingredients)) { |
253
|
|
|
return; |
254
|
|
|
} |
255
|
|
|
if ( ! is_array($ingredients)) { |
256
|
|
|
throw new InvalidDataTypeException( |
257
|
|
|
'$ingredients', |
258
|
|
|
is_object($ingredients) ? get_class($ingredients) : gettype($ingredients), |
259
|
|
|
__('array of class dependencies', 'event_espresso') |
260
|
|
|
); |
261
|
|
|
} |
262
|
|
|
$this->ingredients = array_merge($this->ingredients, $ingredients); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param string $type one of the class constants returned from CoffeeMaker::getTypes() |
268
|
|
|
*/ |
269
|
|
|
public function setType($type = CoffeeMaker::BREW_NEW) |
270
|
|
|
{ |
271
|
|
|
$this->type = CoffeeMaker::validateType($type); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
|
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* @param array $filters an array of filters where keys are the aliases and values are the FQCNs |
278
|
|
|
* example: |
279
|
|
|
* array( 'ClassInterface' => 'Fully\Qualified\ClassName' ) |
280
|
|
|
*/ |
281
|
|
View Code Duplication |
public function setFilters(array $filters) |
|
|
|
|
282
|
|
|
{ |
283
|
|
|
if (empty($filters)) { |
284
|
|
|
return; |
285
|
|
|
} |
286
|
|
|
if ( ! is_array($filters)) { |
287
|
|
|
throw new InvalidDataTypeException( |
288
|
|
|
'$filters', |
289
|
|
|
is_object($filters) ? get_class($filters) : gettype($filters), |
290
|
|
|
__('array of class aliases', 'event_espresso') |
291
|
|
|
); |
292
|
|
|
} |
293
|
|
|
$this->filters = array_merge($this->filters, $filters); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
|
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* Ensures incoming paths is a valid filepath, or array of valid filepaths, |
300
|
|
|
* and merges them in with any existing filepaths |
301
|
|
|
* |
302
|
|
|
* PLZ NOTE: |
303
|
|
|
* Recipe::setFqcn() has a check to see if Recipe::$paths is empty or not, |
304
|
|
|
* therefore you should always call Recipe::setPaths() before Recipe::setFqcn() |
305
|
|
|
* |
306
|
|
|
* @param string|array $paths |
307
|
|
|
*/ |
308
|
|
|
public function setPaths($paths = array()) |
309
|
|
|
{ |
310
|
|
|
if (empty($paths)) { |
311
|
|
|
return; |
312
|
|
|
} |
313
|
|
|
if ( ! (is_string($paths) || is_array($paths))) { |
314
|
|
|
throw new InvalidDataTypeException( |
315
|
|
|
'$path', |
316
|
|
|
is_object($paths) ? get_class($paths) : gettype($paths), |
317
|
|
|
__('string or array of strings (full server filepath(s))', 'event_espresso') |
318
|
|
|
); |
319
|
|
|
} |
320
|
|
|
$paths = (array)$paths; |
321
|
|
|
foreach ($paths as $path) { |
322
|
|
View Code Duplication |
if (strpos($path, '*') === false && ! is_readable($path)) { |
323
|
|
|
throw new RuntimeException( |
324
|
|
|
sprintf( |
325
|
|
|
__('The following filepath is not readable: "%1$s"', 'event_espresso'), |
326
|
|
|
$path |
327
|
|
|
) |
328
|
|
|
); |
329
|
|
|
} |
330
|
|
|
} |
331
|
|
|
$this->paths = array_merge($this->paths, $paths); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
|
335
|
|
|
|
336
|
|
|
} |
337
|
|
|
// End of file Recipe.php |
338
|
|
|
// Location: /Recipe.php |
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.