Completed
Branch BUG-9871-email-validation (e62b1a)
by
unknown
350:41 queued 333:27
created

CoffeeShop::getDefaultRecipes()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 3
nop 0
dl 0
loc 15
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\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
222
     * @return boolean
223
     */
224 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...
225
    {
226
        $identifier = $this->processIdentifier($identifier);
227
        if ($this->reservoir->has($identifier)) {
228
            $this->reservoir->remove($this->reservoir->get($identifier));
229
            if ( ! $this->reservoir->has($identifier)) {
230
                return true;
231
            }
232
        }
233
        return false;
234
    }
235
236
237
238
    /**
239
     * @param  string $identifier Identifier for the entity class that the service applies to
240
     *                            Typically a Fully Qualified Class Name
241
     * @param mixed  $service
242
     * @return bool
243
     */
244
    public function addService($identifier, $service)
245
    {
246
        $identifier = $this->processIdentifier($identifier);
247
        $service = $this->validateService($identifier, $service);
248
        return $this->carafe->add($service, $identifier);
249
    }
250
251
252
253
    /**
254
     * @param string $identifier
255
     * @return boolean
256
     */
257 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...
258
    {
259
        $identifier = $this->processIdentifier($identifier);
260
        if ($this->carafe->has($identifier)) {
261
            $this->carafe->remove($this->carafe->get($identifier));
262
            if ( ! $this->carafe->has($identifier)) {
263
                return true;
264
            }
265
        }
266
        return false;
267
    }
268
269
270
271
    /**
272
     * Adds instructions on how to brew objects
273
     *
274
     * @param RecipeInterface $recipe
275
     * @return mixed
276
     */
277
    public function addRecipe(RecipeInterface $recipe)
278
    {
279
        $this->addAliases($recipe->identifier(), $recipe->filters());
280
        $identifier = $this->processIdentifier($recipe->identifier());
281
        return $this->recipes->add($recipe, $identifier);
282
    }
283
284
285
286
    /**
287
     * @param string $identifier The Recipe's identifier
288
     * @return boolean
289
     */
290 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...
291
    {
292
        $identifier = $this->processIdentifier($identifier);
293
        if ($this->recipes->has($identifier)) {
294
            $this->recipes->remove(
295
                $this->recipes->get($identifier)
296
            );
297
            if ( ! $this->recipes->has($identifier)) {
298
                return true;
299
            }
300
        }
301
        return false;
302
    }
303
304
305
306
    /**
307
     * Get instructions on how to brew objects
308
     *
309
     * @param  string $identifier Identifier for the entity class that the recipe applies to
310
     *                            Typically a Fully Qualified Class Name
311
     * @param string $type
312
     * @return RecipeInterface
313
     */
314
    public function getRecipe($identifier, $type = '')
315
    {
316
        $identifier = $this->processIdentifier($identifier);
317
        if ($this->recipes->has($identifier)) {
318
            return $this->recipes->get($identifier);
319
        }
320
        $default_recipes = $this->getDefaultRecipes();
321
        $matches = array();
322
        foreach ($default_recipes as $wildcard => $default_recipe) {
323
            // is the wildcard recipe prefix in the identifier ?
324
            if (strpos($identifier, $wildcard) !== false) {
325
                // track matches and use the number of wildcard characters matched for the key
326
                $matches[strlen($wildcard)] = $default_recipe;
327
            }
328
        }
329
        if (count($matches) > 0) {
330
            // sort our recipes by the number of wildcard characters matched
331
            ksort($matches);
332
            // then grab the last recipe form the list, since it had the most matching characters
333
            $match = array_pop($matches);
334
            // since we are using a default recipe, we need to set it's identifier and fqcn
335
            return $this->copyDefaultRecipe($match, $identifier, $type);
336
        }
337
        if ($this->recipes->has(Recipe::DEFAULT_ID)) {
338
            // since we are using a default recipe, we need to set it's identifier and fqcn
339
            return $this->copyDefaultRecipe($this->recipes->get(Recipe::DEFAULT_ID), $identifier, $type);
340
        }
341
        throw new OutOfBoundsException(
342
            sprintf(
343
                __('Could not brew coffee because no recipes were found for class "%1$s".', 'event_espresso'),
344
                $identifier
345
            )
346
        );
347
    }
348
349
350
351
    /**
352
     * adds class name aliases to list of filters
353
     *
354
     * @param  string $identifier Identifier for the entity class that the alias applies to
355
     *                            Typically a Fully Qualified Class Name
356
     * @param  array  $aliases
357
     * @return void
358
     * @throws InvalidIdentifierException
359
     */
360
    public function addAliases($identifier, $aliases)
361
    {
362
        if (empty($aliases)) {
363
            return;
364
        }
365
        $identifier = $this->processIdentifier($identifier);
366
        foreach ((array)$aliases as $alias) {
367
            $this->filters[$this->processIdentifier($alias)] = $identifier;
368
        }
369
    }
370
371
372
373
    /**
374
     * Adds a service to one of the internal collections
375
     *
376
     * @param        $identifier
377
     * @param array  $arguments
378
     * @param string $type
379
     * @return mixed
380
     * @throws ServiceExistsException
381
     */
382
    private function makeCoffee($identifier, $arguments = array(), $type = '')
383
    {
384
        if ((empty($type) || $type === CoffeeMaker::BREW_SHARED) && $this->has($identifier)) {
385
            throw new ServiceExistsException($identifier);
386
        }
387
        $identifier = $this->filterIdentifier($identifier);
388
        $recipe = $this->getRecipe($identifier, $type);
389
        $type = ! empty($type) ? $type : $recipe->type();
390
        $coffee_maker = $this->getCoffeeMaker($type);
391
        return $coffee_maker->brew($recipe, $arguments);
392
    }
393
394
395
396
    /**
397
     * filters alias identifiers to find the real class name
398
     *
399
     * @param  string $identifier Identifier for the entity class that the filter applies to
400
     *                            Typically a Fully Qualified Class Name
401
     * @return string
402
     * @throws InvalidIdentifierException
403
     */
404
    private function filterIdentifier($identifier)
405
    {
406
        $identifier = $this->processIdentifier($identifier);
407
        return isset($this->filters[$identifier]) && ! empty($this->filters[$identifier])
408
            ? $this->filters[$identifier]
409
            : $identifier;
410
    }
411
412
413
414
    /**
415
     * verifies and standardizes identifiers
416
     *
417
     * @param  string $identifier Identifier for the entity class
418
     *                            Typically a Fully Qualified Class Name
419
     * @return string
420
     * @throws InvalidIdentifierException
421
     */
422 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...
423
    {
424
        if ( ! is_string($identifier)) {
425
            throw new InvalidIdentifierException(
426
                is_object($identifier) ? get_class($identifier) : gettype($identifier),
427
                '\Fully\Qualified\ClassName'
428
            );
429
        }
430
        return ltrim($identifier, '\\');
431
    }
432
433
434
435
    /**
436
     * @param string $type
437
     * @return CoffeeMakerInterface
438
     * @throws InvalidDataTypeException
439
     * @throws InvalidClassException
440
     */
441
    private function getCoffeeMaker($type)
442
    {
443
        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...
444
            throw new OutOfBoundsException(
445
                __('The requested coffee maker is either missing or invalid.', 'event_espresso')
446
            );
447
        }
448
        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...
449
    }
450
451
452
453
    /**
454
     * Retrieves all recipes that use a wildcard "*" in their identifier
455
     * This allows recipes to be set up for handling
456
     * legacy classes that do not support PSR-4 autoloading.
457
     * for example:
458
     * using "EEM_*" for a recipe identifier would target all legacy models like EEM_Attendee
459
     *
460
     * @return array
461
     */
462
    private function getDefaultRecipes()
463
    {
464
        $default_recipes = array();
465
        $this->recipes->rewind();
466
        while ($this->recipes->valid()) {
467
            $identifier = $this->recipes->getInfo();
468
            // does this recipe use a wildcard ? (but is NOT the global default)
469
            if ($identifier !== Recipe::DEFAULT_ID && strpos($identifier, '*') !== false) {
470
                // strip the wildcard and use identifier as key
471
                $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...
472
            }
473
            $this->recipes->next();
474
        }
475
        return $default_recipes;
476
    }
477
478
479
480
    /**
481
     * clones a default recipe and then copies details
482
     * from the incoming request to it so that it can be used
483
     *
484
     * @param RecipeInterface $default_recipe
485
     * @param string          $identifier
486
     * @param string          $type
487
     * @return RecipeInterface
488
     */
489
    private function copyDefaultRecipe(RecipeInterface $default_recipe, $identifier, $type = '')
490
    {
491
        $recipe = clone($default_recipe);
492
        if ( ! empty($type)) {
493
            $recipe->setType($type);
494
        }
495
        // is this the base default recipe ?
496
        if ($default_recipe->identifier() === Recipe::DEFAULT_ID) {
497
            $recipe->setIdentifier($identifier);
498
            $recipe->setFqcn($identifier);
499
            return $recipe;
500
        }
501
        $recipe->setIdentifier($identifier);
502
        foreach ($default_recipe->paths() as $path) {
503
            $path = str_replace('*', $identifier, $path);
504
            if (is_readable($path)) {
505
                $recipe->setPaths($path);
506
            }
507
        }
508
        $recipe->setFqcn($identifier);
509
        return $recipe;
510
    }
511
512
513
514
    /**
515
     * @param  string $identifier Identifier for the entity class that the service applies to
516
     *                            Typically a Fully Qualified Class Name
517
     * @param mixed  $service
518
     * @return object
519
     * @throws InvalidServiceException
520
     */
521
    private function validateService($identifier, $service)
522
    {
523
        if ( ! is_object($service)) {
524
            throw new InvalidServiceException(
525
                $identifier,
526
                $service
527
            );
528
        }
529
        return $service;
530
    }
531
532
}
533
// End of file CoffeeShop.php
534
// Location: /CoffeeShop.php