Completed
Branch BUG-10626-dst-unit-test (8d5d80)
by
unknown
14:30
created

CoffeeShop::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
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');
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...
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);
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...
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)
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...
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)
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...
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)
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...
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  $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)
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...
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)) {
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...
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);
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...
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();
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...
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