Completed
Branch BUG-10738-inconsistency-in-ses... (a1eed8)
by
unknown
24:27 queued 12:29
created

CoffeeShop::getShared()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 2
dl 0
loc 13
rs 9.2
c 0
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\InvalidEntityException;
7
use EventEspresso\core\exceptions\InvalidIdentifierException;
8
use EventEspresso\core\exceptions\InvalidInterfaceException;
9
use EventEspresso\core\services\collections\Collection;
10
use EventEspresso\core\services\collections\CollectionInterface;
11
use EventEspresso\core\services\collections\LooseCollection;
12
use EventEspresso\core\services\container\exceptions\InstantiationException;
13
use EventEspresso\core\services\container\exceptions\InvalidServiceException;
14
use EventEspresso\core\services\container\exceptions\ServiceExistsException;
15
use EventEspresso\core\services\container\exceptions\ServiceNotFoundException;
16
use OutOfBoundsException;
17
18
defined('EVENT_ESPRESSO_VERSION') || exit;
19
20
21
22
/**
23
 * Class CoffeeShop
24
 * A Dependency Injection container
25
 *
26
 * @see     /docs/N--Core-Functionality/dependency-injection-coffeepot.md
27
 *          for extensive documentation and examples
28
 * @package Event Espresso
29
 * @author  Brent Christensen
30
 * @since   4.9.1
31
 */
32
class CoffeeShop implements CoffeePotInterface
33
{
34
35
36
    /**
37
     * This was the best coffee related name I could think of to represent class name "aliases"
38
     * So classes can be found via an alias identifier,
39
     * that is revealed when it is run through... the filters... eh? get it?
40
     *
41
     * @var array $filters
42
     */
43
    private $filters;
44
45
    /**
46
     * These are the classes that will actually build the objects (to order of course)
47
     *
48
     * @var array $coffee_makers
49
     */
50
    private $coffee_makers;
51
52
    /**
53
     * where the instantiated "singleton" objects are stored
54
     *
55
     * @var CollectionInterface $carafe
56
     */
57
    private $carafe;
58
59
    /**
60
     * collection of Recipes that instruct us how to brew objects
61
     *
62
     * @var CollectionInterface $recipes
63
     */
64
    private $recipes;
65
66
    /**
67
     * collection of closures for brewing objects
68
     *
69
     * @var CollectionInterface $reservoir
70
     */
71
    private $reservoir;
72
73
74
75
    /**
76
     * CoffeeShop constructor
77
     *
78
     * @throws InvalidInterfaceException
79
     */
80
    public function __construct()
81
    {
82
        // array for storing class aliases
83
        $this->filters = array();
84
        // create collection for storing shared services
85
        $this->carafe = new LooseCollection( '' );
86
        // create collection for storing recipes that tell us how to build services and entities
87
        $this->recipes = new Collection('EventEspresso\core\services\container\RecipeInterface');
88
        // create collection for storing closures for constructing new entities
89
        $this->reservoir = new Collection('Closure');
90
        // create collection for storing the generators that build our services and entity closures
91
        $this->coffee_makers = new Collection('EventEspresso\core\services\container\CoffeeMakerInterface');
92
    }
93
94
95
96
    /**
97
     * Returns true if the container can return an entry for the given identifier.
98
     * Returns false otherwise.
99
     * `has($identifier)` returning true does not mean that `get($identifier)` will not throw an exception.
100
     * It does however mean that `get($identifier)` will not throw a `ServiceNotFoundException`.
101
     *
102
     * @param string $identifier  Identifier of the entry to look for.
103
     *                            Typically a Fully Qualified Class Name
104
     * @return boolean
105
     * @throws InvalidIdentifierException
106
     */
107
    public function has($identifier)
108
    {
109
        $identifier = $this->filterIdentifier($identifier);
110
        return $this->carafe->has($identifier);
111
    }
112
113
114
115
    /**
116
     * finds a previously brewed (SHARED) service and returns it
117
     *
118
     * @param  string $identifier Identifier for the entity class to be constructed.
119
     *                            Typically a Fully Qualified Class Name
120
     * @return mixed
121
     * @throws InvalidIdentifierException
122
     * @throws ServiceNotFoundException No service was found for this identifier.
123
     */
124
    public function get($identifier)
125
    {
126
        $identifier = $this->filterIdentifier($identifier);
127
        if ($this->carafe->has($identifier)) {
128
            return $this->carafe->get($identifier);
129
        }
130
        throw new ServiceNotFoundException($identifier);
131
    }
132
133
134
135
    /**
136
     * returns an instance of the requested entity type using the supplied arguments.
137
     * If a shared service is requested and an instance is already in the carafe, then it will be returned.
138
     * If it is not already in the carafe, then the service will be constructed, added to the carafe, and returned
139
     * If the request is for a new entity and a closure exists in the reservoir for creating it,
140
     * then a new entity will be instantiated from the closure and returned.
141
     * If a closure does not exist, then one will be built and added to the reservoir
142
     * before instantiating the requested entity.
143
     *
144
     * @param  string $identifier Identifier for the entity class to be constructed.
145
     *                            Typically a Fully Qualified Class Name
146
     * @param array   $arguments  an array of arguments to be passed to the entity constructor
147
     * @param string  $type
148
     * @return mixed
149
     * @throws OutOfBoundsException
150
     * @throws InstantiationException
151
     * @throws InvalidDataTypeException
152
     * @throws InvalidClassException
153
     * @throws InvalidIdentifierException
154
     * @throws ServiceExistsException
155
     * @throws ServiceNotFoundException No service was found for this identifier.
156
     */
157
    public function brew($identifier, $arguments = array(), $type = '')
158
    {
159
        // resolve any class aliases that may exist
160
        $identifier = $this->filterIdentifier($identifier);
161
        // is a shared service being requested and already exists in the carafe?
162
        $brewed = $this->getShared($identifier, $type);
163
        // then return whatever was found
164
        if($brewed !== false) {
165
            return $brewed;
166
        }
167
        // if the reservoir doesn't have a closure already for the requested identifier,
168
        // then neither a shared service nor a closure for making entities has been built yet
169
        if (! $this->reservoir->has($identifier)) {
170
            // so let's brew something up and add it to the proper collection
171
            $brewed = $this->makeCoffee($identifier, $arguments, $type);
172
        }
173
        // did the requested class only require loading, and if so, was that successful?
174
        if($this->brewedLoadOnly($brewed, $identifier, $type) === true) {
175
            return true;
176
        }
177
        // was the brewed item a callable factory function ?
178
        if (is_callable($brewed)) {
179
            // then instantiate a new entity from the cached closure
180
            return $brewed($arguments);
181
        }
182
        if ($brewed) {
183
            // requested object was a shared entity, so attempt to get it from the carafe again
184
            // because if it wasn't there before, then it should have just been brewed and added,
185
            // but if it still isn't there, then this time the thrown ServiceNotFoundException will not be caught
186
            return $this->get($identifier);
187
        }
188
        // if identifier is for a non-shared entity,
189
        // then either a cached closure already existed, or was just brewed
190
        return $this->brewedClosure($identifier, $arguments);
191
    }
192
193
194
195
    /**
196
     * @param string $identifier
197
     * @param string $type
198
     * @return bool|mixed
199
     * @throws InvalidIdentifierException
200
     */
201
    protected function getShared($identifier, $type)
202
    {
203
        try {
204
            if (empty($type) || $type === CoffeeMaker::BREW_SHARED) {
205
                // if a shared service was requested and an instance is in the carafe, then return it
206
                return $this->get($identifier);
207
            }
208
        } catch (ServiceNotFoundException $e) {
209
            // if not then we'll just catch the ServiceNotFoundException but not do anything just yet,
210
            // and instead, attempt to build whatever was requested
211
        }
212
        return false;
213
    }
214
215
216
217
    /**
218
     * @param mixed  $brewed
219
     * @param string $identifier
220
     * @param string $type
221
     * @return bool
222
     * @throws InvalidClassException
223
     * @throws InvalidDataTypeException
224
     * @throws InvalidIdentifierException
225
     * @throws OutOfBoundsException
226
     * @throws ServiceExistsException
227
     * @throws ServiceNotFoundException
228
     */
229
    protected function brewedLoadOnly($brewed, $identifier, $type)
230
    {
231
        if ($type === CoffeeMaker::BREW_LOAD_ONLY) {
232
            if ($brewed !== true) {
233
                throw new ServiceNotFoundException(
234
                    sprintf(
235
                        esc_html__(
236
                            'The "%1$s" class could not be loaded.',
237
                            'event_espresso'
238
                        ),
239
                        $identifier
240
                    )
241
                );
242
            }
243
            return true;
244
        }
245
        return false;
246
    }
247
248
249
250
    /**
251
     * @param string $identifier
252
     * @param array  $arguments
253
     * @return mixed
254
     * @throws InstantiationException
255
     */
256
    protected function brewedClosure($identifier, array $arguments)
257
    {
258
        $closure = $this->reservoir->get($identifier);
259
        if (empty($closure)) {
260
            throw new InstantiationException(
261
                sprintf(
262
                    esc_html__(
263
                        'Could not brew an instance of "%1$s".',
264
                        'event_espresso'
265
                    ),
266
                    $identifier
267
                )
268
            );
269
        }
270
        return $closure($arguments);
271
    }
272
273
274
275
    /**
276
     * @param CoffeeMakerInterface $coffee_maker
277
     * @param string               $type
278
     * @return bool
279
     * @throws InvalidIdentifierException
280
     * @throws InvalidEntityException
281
     */
282
    public function addCoffeeMaker(CoffeeMakerInterface $coffee_maker, $type)
283
    {
284
        $type = CoffeeMaker::validateType($type);
285
        return $this->coffee_makers->add($coffee_maker, $type);
286
    }
287
288
289
290
    /**
291
     * @param string   $identifier
292
     * @param callable $closure
293
     * @return callable|null
294
     * @throws InvalidIdentifierException
295
     * @throws InvalidDataTypeException
296
     */
297
    public function addClosure($identifier, $closure)
298
    {
299
        if ( ! is_callable($closure)) {
300
            throw new InvalidDataTypeException('$closure', $closure, 'Closure');
301
        }
302
        $identifier = $this->processIdentifier($identifier);
303
        if ($this->reservoir->add($closure, $identifier)) {
304
            return $closure;
305
        }
306
        return null;
307
    }
308
309
310
311
    /**
312
     * @param string $identifier
313
     * @return boolean
314
     * @throws InvalidIdentifierException
315
     */
316 View Code Duplication
    public function removeClosure($identifier)
317
    {
318
        $identifier = $this->processIdentifier($identifier);
319
        if ($this->reservoir->has($identifier)) {
320
            return $this->reservoir->remove($this->reservoir->get($identifier));
321
        }
322
        return false;
323
    }
324
325
326
327
    /**
328
     * @param  string $identifier Identifier for the entity class that the service applies to
329
     *                            Typically a Fully Qualified Class Name
330
     * @param mixed   $service
331
     * @return bool
332
     * @throws \EventEspresso\core\services\container\exceptions\InvalidServiceException
333
     * @throws InvalidIdentifierException
334
     */
335
    public function addService($identifier, $service)
336
    {
337
        $identifier = $this->processIdentifier($identifier);
338
        $service = $this->validateService($identifier, $service);
339
        return $this->carafe->add($service, $identifier);
340
    }
341
342
343
344
    /**
345
     * @param string $identifier
346
     * @return boolean
347
     * @throws InvalidIdentifierException
348
     */
349 View Code Duplication
    public function removeService($identifier)
350
    {
351
        $identifier = $this->processIdentifier($identifier);
352
        if ($this->carafe->has($identifier)) {
353
            return $this->carafe->remove($this->carafe->get($identifier));
354
        }
355
        return false;
356
    }
357
358
359
360
    /**
361
     * Adds instructions on how to brew objects
362
     *
363
     * @param RecipeInterface $recipe
364
     * @return mixed
365
     * @throws InvalidIdentifierException
366
     */
367
    public function addRecipe(RecipeInterface $recipe)
368
    {
369
        $this->addAliases($recipe->identifier(), $recipe->filters());
370
        $identifier = $this->processIdentifier($recipe->identifier());
371
        return $this->recipes->add($recipe, $identifier);
372
    }
373
374
375
376
    /**
377
     * @param string $identifier The Recipe's identifier
378
     * @return boolean
379
     * @throws InvalidIdentifierException
380
     */
381 View Code Duplication
    public function removeRecipe($identifier)
382
    {
383
        $identifier = $this->processIdentifier($identifier);
384
        if ($this->recipes->has($identifier)) {
385
            return $this->recipes->remove($this->recipes->get($identifier));
386
        }
387
        return false;
388
    }
389
390
391
392
    /**
393
     * Get instructions on how to brew objects
394
     *
395
     * @param  string $identifier Identifier for the entity class that the recipe applies to
396
     *                            Typically a Fully Qualified Class Name
397
     * @param string  $type
398
     * @return RecipeInterface
399
     * @throws OutOfBoundsException
400
     * @throws InvalidIdentifierException
401
     */
402
    public function getRecipe($identifier, $type = '')
403
    {
404
        $identifier = $this->processIdentifier($identifier);
405
        if ($this->recipes->has($identifier)) {
406
            return $this->recipes->get($identifier);
407
        }
408
        $default_recipes = $this->getDefaultRecipes();
409
        $matches = array();
410
        foreach ($default_recipes as $wildcard => $default_recipe) {
411
            // is the wildcard recipe prefix in the identifier ?
412
            if (strpos($identifier, $wildcard) !== false) {
413
                // track matches and use the number of wildcard characters matched for the key
414
                $matches[strlen($wildcard)] = $default_recipe;
415
            }
416
        }
417
        if (count($matches) > 0) {
418
            // sort our recipes by the number of wildcard characters matched
419
            ksort($matches);
420
            // then grab the last recipe form the list, since it had the most matching characters
421
            $match = array_pop($matches);
422
            // since we are using a default recipe, we need to set it's identifier and fqcn
423
            return $this->copyDefaultRecipe($match, $identifier, $type);
424
        }
425
        if ($this->recipes->has(Recipe::DEFAULT_ID)) {
426
            // since we are using a default recipe, we need to set it's identifier and fqcn
427
            return $this->copyDefaultRecipe($this->recipes->get(Recipe::DEFAULT_ID), $identifier, $type);
428
        }
429
        throw new OutOfBoundsException(
430
            sprintf(
431
                __('Could not brew coffee because no recipes were found for class "%1$s".', 'event_espresso'),
432
                $identifier
433
            )
434
        );
435
    }
436
437
438
439
    /**
440
     * adds class name aliases to list of filters
441
     *
442
     * @param  string       $identifier Identifier for the entity class that the alias applies to
443
     *                                  Typically a Fully Qualified Class Name
444
     * @param  array|string $aliases
445
     * @return void
446
     * @throws InvalidIdentifierException
447
     */
448
    public function addAliases($identifier, $aliases)
449
    {
450
        if (empty($aliases)) {
451
            return;
452
        }
453
        $identifier = $this->processIdentifier($identifier);
454
        foreach ((array)$aliases as $alias) {
455
            $this->filters[$this->processIdentifier($alias)] = $identifier;
456
        }
457
    }
458
459
460
461
    /**
462
     * Adds a service to one of the internal collections
463
     *
464
     * @param        $identifier
465
     * @param array  $arguments
466
     * @param string $type
467
     * @return mixed
468
     * @throws InvalidDataTypeException
469
     * @throws InvalidClassException
470
     * @throws OutOfBoundsException
471
     * @throws InvalidIdentifierException
472
     * @throws ServiceExistsException
473
     */
474
    private function makeCoffee($identifier, $arguments = array(), $type = '')
475
    {
476
        if ((empty($type) || $type === CoffeeMaker::BREW_SHARED) && $this->has($identifier)) {
477
            throw new ServiceExistsException($identifier);
478
        }
479
        $identifier = $this->filterIdentifier($identifier);
480
        $recipe = $this->getRecipe($identifier, $type);
481
        $type = ! empty($type) ? $type : $recipe->type();
482
        $coffee_maker = $this->getCoffeeMaker($type);
483
        return $coffee_maker->brew($recipe, $arguments);
484
    }
485
486
487
488
    /**
489
     * filters alias identifiers to find the real class name
490
     *
491
     * @param  string $identifier Identifier for the entity class that the filter applies to
492
     *                            Typically a Fully Qualified Class Name
493
     * @return string
494
     * @throws InvalidIdentifierException
495
     */
496
    private function filterIdentifier($identifier)
497
    {
498
        $identifier = $this->processIdentifier($identifier);
499
        return isset($this->filters[$identifier]) && ! empty($this->filters[$identifier])
500
            ? $this->filters[$identifier]
501
            : $identifier;
502
    }
503
504
505
506
    /**
507
     * verifies and standardizes identifiers
508
     *
509
     * @param  string $identifier Identifier for the entity class
510
     *                            Typically a Fully Qualified Class Name
511
     * @return string
512
     * @throws InvalidIdentifierException
513
     */
514 View Code Duplication
    private function processIdentifier($identifier)
515
    {
516
        if ( ! is_string($identifier)) {
517
            throw new InvalidIdentifierException(
518
                is_object($identifier) ? get_class($identifier) : gettype($identifier),
519
                '\Fully\Qualified\ClassName'
520
            );
521
        }
522
        return ltrim($identifier, '\\');
523
    }
524
525
526
527
    /**
528
     * @param string $type
529
     * @return CoffeeMakerInterface
530
     * @throws OutOfBoundsException
531
     * @throws InvalidDataTypeException
532
     * @throws InvalidClassException
533
     */
534
    private function getCoffeeMaker($type)
535
    {
536
        if ( ! $this->coffee_makers->has($type)) {
537
            throw new OutOfBoundsException(
538
                __('The requested coffee maker is either missing or invalid.', 'event_espresso')
539
            );
540
        }
541
        return $this->coffee_makers->get($type);
542
    }
543
544
545
546
    /**
547
     * Retrieves all recipes that use a wildcard "*" in their identifier
548
     * This allows recipes to be set up for handling
549
     * legacy classes that do not support PSR-4 autoloading.
550
     * for example:
551
     * using "EEM_*" for a recipe identifier would target all legacy models like EEM_Attendee
552
     *
553
     * @return array
554
     */
555
    private function getDefaultRecipes()
556
    {
557
        $default_recipes = array();
558
        $this->recipes->rewind();
559
        while ($this->recipes->valid()) {
560
            $identifier = $this->recipes->getInfo();
561
            // does this recipe use a wildcard ? (but is NOT the global default)
562
            if ($identifier !== Recipe::DEFAULT_ID && strpos($identifier, '*') !== false) {
563
                // strip the wildcard and use identifier as key
564
                $default_recipes[str_replace('*', '', $identifier)] = $this->recipes->current();
565
            }
566
            $this->recipes->next();
567
        }
568
        return $default_recipes;
569
    }
570
571
572
573
    /**
574
     * clones a default recipe and then copies details
575
     * from the incoming request to it so that it can be used
576
     *
577
     * @param RecipeInterface $default_recipe
578
     * @param string          $identifier
579
     * @param string          $type
580
     * @return RecipeInterface
581
     */
582
    private function copyDefaultRecipe(RecipeInterface $default_recipe, $identifier, $type = '')
583
    {
584
        $recipe = clone $default_recipe;
585
        if ( ! empty($type)) {
586
            $recipe->setType($type);
587
        }
588
        // is this the base default recipe ?
589
        if ($default_recipe->identifier() === Recipe::DEFAULT_ID) {
590
            $recipe->setIdentifier($identifier);
591
            $recipe->setFqcn($identifier);
592
            return $recipe;
593
        }
594
        $recipe->setIdentifier($identifier);
595
        foreach ($default_recipe->paths() as $path) {
596
            $path = str_replace('*', $identifier, $path);
597
            if (is_readable($path)) {
598
                $recipe->setPaths($path);
599
            }
600
        }
601
        $recipe->setFqcn($identifier);
602
        return $recipe;
603
    }
604
605
606
607
    /**
608
     * @param  string $identifier Identifier for the entity class that the service applies to
609
     *                            Typically a Fully Qualified Class Name
610
     * @param mixed  $service
611
     * @return mixed
612
     * @throws InvalidServiceException
613
     */
614
    private function validateService($identifier, $service)
615
    {
616
        if ( ! is_object($service)) {
617
            throw new InvalidServiceException(
618
                $identifier,
619
                $service
620
            );
621
        }
622
        return $service;
623
    }
624
625
}
626
// End of file CoffeeShop.php
627
// Location: /CoffeeShop.php