Completed
Branch FET-7992-CoffeePot-DI-containe... (b9d927)
by
unknown
1087:41 queued 1065:23
created

CoffeeShop::addService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
rs 9.4285
c 1
b 0
f 0
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 EventEspresso\core\services\collections\Collection;
8
use EventEspresso\core\services\collections\CollectionInterface;
9
use EventEspresso\core\services\collections\LooseCollection;
10
use EventEspresso\core\services\container\exceptions\InvalidServiceException;
11
use EventEspresso\core\services\container\exceptions\ServiceExistsException;
12
use EventEspresso\core\services\container\exceptions\ServiceNotFoundException;
13
use OutOfBoundsException;
14
15
if ( ! defined('EVENT_ESPRESSO_VERSION')) {
16
    exit('No direct script access allowed');
17
}
18
19
20
21
/**
22
 * Class CoffeeShop
23
 * A Dependency Injection container
24
 *
25
 * @see     /docs/N--Core-Functionality/dependency-injection-coffeepot.md
26
 *          for extensive documentation and examples
27
 * @package Event Espresso
28
 * @author  Brent Christensen
29
 * @since   4.9.1
30
 */
31
class CoffeeShop implements CoffeePotInterface
32
{
33
34
35
    /**
36
     * This was the best coffee related name I could think of to represent class name "aliases"
37
     * So classes can be found via an alias identifier,
38
     * that is revealed when it is run through... the filters... eh? get it?
39
     *
40
     * @var array $filters
41
     */
42
    private $filters = array();
43
44
    /**
45
     * These are the classes that will actually build the objects (to order of course)
46
     *
47
     * @var array $coffee_makers
48
     */
49
    private $coffee_makers = array();
50
51
    /**
52
     * where the instantiated "singleton" objects are stored
53
     *
54
     * @var CollectionInterface $carafe
55
     */
56
    private $carafe;
57
58
    /**
59
     * collection of Recipes that instruct us how to brew objects
60
     *
61
     * @var CollectionInterface $recipes
62
     */
63
    private $recipes;
64
65
    /**
66
     * collection of closures for brewing objects
67
     *
68
     * @var CollectionInterface $reservoir
69
     */
70
    private $reservoir;
71
72
73
74
    /**
75
     * CoffeeShop constructor
76
     */
77
    public function __construct()
78
    {
79
        // array for storing class aliases
80
        $this->filters = array();
81
        // create collection for storing shared services
82
        $this->carafe = new LooseCollection( '' );
83
        // create collection for storing recipes that tell how to build services and entities
84
        $this->recipes = new Collection('EventEspresso\core\services\container\RecipeInterface');
85
        // create collection for storing closures for constructing new entities
86
        $this->reservoir = new Collection('Closure');
87
        // create collection for storing the generators that build our services and entity closures
88
        $this->coffee_makers = new Collection('EventEspresso\core\services\container\CoffeeMakerInterface');
0 ignored issues
show
Documentation Bug introduced by
It seems like new \EventEspresso\core\...\CoffeeMakerInterface') of type object<EventEspresso\cor...collections\Collection> is incompatible with the declared type array of property $coffee_makers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
89
    }
90
91
92
93
    /**
94
     * Returns true if the container can return an entry for the given identifier.
95
     * Returns false otherwise.
96
     * `has($identifier)` returning true does not mean that `get($identifier)` will not throw an exception.
97
     * It does however mean that `get($identifier)` will not throw a `ServiceNotFoundException`.
98
     *
99
     * @param string $identifier  Identifier of the entry to look for.
100
     *                            Typically a Fully Qualified Class Name
101
     * @return boolean
102
     */
103
    public function has($identifier)
104
    {
105
        $identifier = $this->filterIdentifier($identifier);
106
        return $this->carafe->has($identifier);
107
    }
108
109
110
111
    /**
112
     * finds a previously brewed (SHARED) service and returns it
113
     *
114
     * @param  string $identifier Identifier for the entity class to be constructed.
115
     *                            Typically a Fully Qualified Class Name
116
     * @return mixed
117
     * @throws ServiceNotFoundException No service was found for this identifier.
118
     */
119
    public function get($identifier)
120
    {
121
        $identifier = $this->filterIdentifier($identifier);
122
        if ($this->carafe->has($identifier)) {
123
            return $this->carafe->get($identifier);
124
        }
125
        throw new ServiceNotFoundException($identifier);
126
    }
127
128
129
130
    /**
131
     * returns an instance of the requested entity type using the supplied arguments.
132
     * If a shared service is requested and an instance is already in the carafe, then it will be returned.
133
     * If it is not already in the carafe, then the service will be constructed, added to the carafe, and returned
134
     * If the request is for a new entity and a closure exists in the reservoir for creating it,
135
     * then a new entity will be instantiated from the closure and returned.
136
     * If a closure does not exist, then one will be built and added to the reservoir
137
     * before instantiating the requested entity.
138
     *
139
     * @param  string $identifier Identifier for the entity class to be constructed.
140
     *                            Typically a Fully Qualified Class Name
141
     * @param array   $arguments  an array of arguments to be passed to the entity constructor
142
     * @param string  $type
143
     * @return mixed
144
     * @throws ServiceNotFoundException No service was found for this identifier.
145
     */
146
    public function brew($identifier, $arguments = array(), $type = '')
147
    {
148
        // resolve any class aliases that may exist
149
        $identifier = $this->filterIdentifier($identifier);
150
        try {
151
            // is a shared service being requested?
152
            if (empty($type) || $type === CoffeeMaker::BREW_SHARED) {
153
                // if a shared service was requested and an instance is in the carafe, then return it
154
                return $this->get($identifier);
155
            }
156
        } catch (ServiceNotFoundException $e) {
157
            // if not then we'll just catch the ServiceNotFoundException but not do anything just yet,
158
            // and instead, attempt to build whatever was requested
159
        }
160
        $brewed = false;
161
        // if the reservoir doesn't have a closure already for the requested identifier,
162
        // then neither a shared service nor a closure for making entities has been built yet
163
        if ( ! $this->reservoir->has($identifier)) {
164
            // so let's brew something up and add it to the proper collection
165
            $brewed = $this->makeCoffee($identifier, $arguments, $type);
166
        }
167
        // was the brewed item a callable factory function ?
168
        if (is_callable($brewed)) {
169
            // then instantiate a new entity from the cached closure
170
            $entity = $brewed($arguments);
171
        } else if ($brewed) {
172
            // requested object was a shared entity, so attempt to get it from the carafe again
173
            // because if it wasn't there before, then it should have just been brewed and added,
174
            // but if it still isn't there, then this time
175
            // the thrown ServiceNotFoundException will not be caught
176
            $entity = $this->get($identifier);
177
        } else {
178
            // if identifier is for a non-shared entity,
179
            // then either a cached closure already existed, or was just brewed
180
            $closure = $this->reservoir->get($identifier);
181
            $entity = $closure($arguments);
182
        }
183
        return $entity;
184
    }
185
186
187
188
    /**
189
     * @param CoffeeMakerInterface $coffee_maker
190
     * @param string               $type
191
     * @return bool
192
     */
193
    public function addCoffeeMaker(CoffeeMakerInterface $coffee_maker, $type)
194
    {
195
        $type = CoffeeMaker::validateType($type);
196
        return $this->coffee_makers->add($coffee_maker, $type);
0 ignored issues
show
Bug introduced by
The method add cannot be called on $this->coffee_makers (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
197
    }
198
199
200
201
    /**
202
     * @param string   $identifier
203
     * @param callable $closure
204
     * @return callable|null
205
     */
206
    public function addClosure($identifier, $closure)
207
    {
208
        if ( ! is_callable($closure)) {
209
            throw new InvalidDataTypeException('$closure', $closure, 'Closure');
210
        }
211
        $identifier = $this->processIdentifier($identifier);
212
        if ($this->reservoir->add($closure, $identifier)) {
213
            return $closure;
214
        }
215
        return null;
216
    }
217
218
219
220
    /**
221
     * @param  string $identifier Identifier for the entity class that the service applies to
222
     *                            Typically a Fully Qualified Class Name
223
     * @param mixed  $service
224
     * @return bool
225
     */
226
    public function addService($identifier, $service)
227
    {
228
        $identifier = $this->processIdentifier($identifier);
229
        $service = $this->validateService($identifier, $service);
230
        return $this->carafe->add($service, $identifier);
231
    }
232
233
234
235
    /**
236
     * Adds instructions on how to brew objects
237
     *
238
     * @param RecipeInterface $recipe
239
     * @return mixed
240
     */
241
    public function addRecipe(RecipeInterface $recipe)
242
    {
243
        $this->addAliases($recipe->identifier(), $recipe->filters());
244
        $identifier = $this->processIdentifier($recipe->identifier());
245
        return $this->recipes->add($recipe, $identifier);
246
    }
247
248
249
250
    /**
251
     * Get instructions on how to brew objects
252
     *
253
     * @param  string $identifier Identifier for the entity class that the recipe applies to
254
     *                            Typically a Fully Qualified Class Name
255
     * @param string $type
256
     * @return RecipeInterface
257
     */
258
    public function getRecipe($identifier, $type = '')
259
    {
260
        $identifier = $this->processIdentifier($identifier);
261
        if ($this->recipes->has($identifier)) {
262
            return $this->recipes->get($identifier);
263
        }
264
        $default_recipes = $this->getDefaultRecipes();
265
        $matches = array();
266
        foreach ($default_recipes as $wildcard => $default_recipe) {
267
            // is the wildcard recipe prefix in the identifier ?
268
            if (strpos($identifier, $wildcard) !== false) {
269
                // track matches and use the number of wildcard characters matched for the key
270
                $matches[strlen($wildcard)] = $default_recipe;
271
            }
272
        }
273
        if (count($matches) > 0) {
274
            // sort our recipes by the number of wildcard characters matched
275
            ksort($matches);
276
            // then grab the last recipe form the list, since it had the most matching characters
277
            $match = array_pop($matches);
278
            // since we are using a default recipe, we need to set it's identifier and fqcn
279
            return $this->copyDefaultRecipe($match, $identifier, $type);
280
        }
281
        if ($this->recipes->has(Recipe::DEFAULT_ID)) {
282
            // since we are using a default recipe, we need to set it's identifier and fqcn
283
            return $this->copyDefaultRecipe($this->recipes->get(Recipe::DEFAULT_ID), $identifier, $type);
284
        }
285
        throw new OutOfBoundsException(__('Could not brew coffee because no recipes were found.', 'event_espresso'));
286
    }
287
288
289
290
    /**
291
     * adds class name aliases to list of filters
292
     *
293
     * @param  string $identifier Identifier for the entity class that the alias applies to
294
     *                            Typically a Fully Qualified Class Name
295
     * @param  array  $aliases
296
     * @return void
297
     * @throws InvalidIdentifierException
298
     */
299
    public function addAliases($identifier, $aliases)
300
    {
301
        if (empty($aliases)) {
302
            return;
303
        }
304
        $identifier = $this->processIdentifier($identifier);
305
        foreach ((array)$aliases as $alias) {
306
            $this->filters[$this->processIdentifier($alias)] = $identifier;
307
        }
308
    }
309
310
311
312
    /**
313
     * Adds a service to one of the internal collections
314
     *
315
     * @param        $identifier
316
     * @param array  $arguments
317
     * @param string $type
318
     * @return mixed
319
     * @throws ServiceExistsException
320
     */
321
    private function makeCoffee($identifier, $arguments = array(), $type = '')
322
    {
323
        if ((empty($type) || $type === CoffeeMaker::BREW_SHARED) && $this->has($identifier)) {
324
            throw new ServiceExistsException($identifier);
325
        }
326
        $identifier = $this->filterIdentifier($identifier);
327
        $recipe = $this->getRecipe($identifier, $type);
328
        $type = ! empty($type) ? $type : $recipe->type();
329
        $coffee_maker = $this->getCoffeeMaker($type);
330
        return $coffee_maker->brew($recipe, $arguments);
331
    }
332
333
334
335
    /**
336
     * filters alias identifiers to find the real class name
337
     *
338
     * @param  string $identifier Identifier for the entity class that the filter applies to
339
     *                            Typically a Fully Qualified Class Name
340
     * @return string
341
     * @throws InvalidIdentifierException
342
     */
343
    private function filterIdentifier($identifier)
344
    {
345
        $identifier = $this->processIdentifier($identifier);
346
        return isset($this->filters[$identifier]) && ! empty($this->filters[$identifier])
347
            ? $this->filters[$identifier]
348
            : $identifier;
349
    }
350
351
352
353
    /**
354
     * verifies and standardizes identifiers
355
     *
356
     * @param  string $identifier Identifier for the entity class
357
     *                            Typically a Fully Qualified Class Name
358
     * @return string
359
     * @throws InvalidIdentifierException
360
     */
361 View Code Duplication
    private function processIdentifier($identifier)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
362
    {
363
        if ( ! is_string($identifier)) {
364
            throw new InvalidIdentifierException(
365
                is_object($identifier) ? get_class($identifier) : gettype($identifier),
366
                '\Fully\Qualified\ClassName'
367
            );
368
        }
369
        return ltrim($identifier, '\\');
370
    }
371
372
373
374
    /**
375
     * @param string $type
376
     * @return CoffeeMakerInterface
377
     * @throws InvalidDataTypeException
378
     * @throws InvalidClassException
379
     */
380
    private function getCoffeeMaker($type)
381
    {
382
        if ( ! $this->coffee_makers->has($type)) {
0 ignored issues
show
Bug introduced by
The method has cannot be called on $this->coffee_makers (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
383
            throw new OutOfBoundsException(
384
                __('The requested coffee maker is either missing or invalid.', 'event_espresso')
385
            );
386
        }
387
        return $this->coffee_makers->get($type);
0 ignored issues
show
Bug introduced by
The method get cannot be called on $this->coffee_makers (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
388
    }
389
390
391
392
    /**
393
     * Retrieves all recipes that use a wildcard "*" in their identifier
394
     * This allows recipes to be set up for handling
395
     * legacy classes that do not support PSR-4 autoloading.
396
     * for example:
397
     * using "EEM_*" for a recipe identifier would target all legacy models like EEM_Attendee
398
     *
399
     * @return array
400
     */
401
    private function getDefaultRecipes()
402
    {
403
        $default_recipes = array();
404
        $this->recipes->rewind();
405
        while ($this->recipes->valid()) {
406
            $identifier = $this->recipes->getInfo();
407
            // does this recipe use a wildcard ? (but is NOT the global default)
408
            if ($identifier !== Recipe::DEFAULT_ID && strpos($identifier, '*') !== false) {
409
                // strip the wildcard and use identifier as key
410
                $default_recipes[str_replace('*', '', $identifier)] = $this->recipes->current();
0 ignored issues
show
Bug introduced by
The method current() does not exist on EventEspresso\core\servi...ons\CollectionInterface. Did you maybe mean setCurrent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
411
            }
412
            $this->recipes->next();
413
        }
414
        return $default_recipes;
415
    }
416
417
418
419
    /**
420
     * clones a default recipe and then copies details
421
     * from the incoming request to it so that it can be used
422
     *
423
     * @param RecipeInterface $default_recipe
424
     * @param string          $identifier
425
     * @param string          $type
426
     * @return RecipeInterface
427
     */
428
    private function copyDefaultRecipe(RecipeInterface $default_recipe, $identifier, $type = '')
429
    {
430
        $recipe = clone($default_recipe);
431
        if ( ! empty($type)) {
432
            $recipe->setType($type);
433
        }
434
        // is this the base default recipe ?
435
        if ($default_recipe->identifier() === Recipe::DEFAULT_ID) {
436
            $recipe->setIdentifier($identifier);
437
            $recipe->setFqcn($identifier);
438
            return $recipe;
439
        }
440
        $recipe->setIdentifier($identifier);
441
        foreach ($default_recipe->paths() as $path) {
442
            $path = str_replace('*', $identifier, $path);
443
            if (is_readable($path)) {
444
                $recipe->setPaths($path);
445
            }
446
        }
447
        $recipe->setFqcn($identifier);
448
        return $recipe;
449
    }
450
451
452
453
    /**
454
     * @param  string $identifier Identifier for the entity class that the service applies to
455
     *                            Typically a Fully Qualified Class Name
456
     * @param mixed  $service
457
     * @return object
458
     * @throws InvalidServiceException
459
     */
460
    private function validateService($identifier, $service)
461
    {
462
        if ( ! is_object($service)) {
463
            throw new InvalidServiceException(
464
                $identifier,
465
                $service
466
            );
467
        }
468
        return $service;
469
    }
470
471
}
472
// End of file CoffeeShop.php
473
// Location: /CoffeeShop.php