Completed
Push — drivers ( 5e0c25...ba8318 )
by Joe
02:08
created

Config::getResolveStack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace PhpWinTools\WmiScripting\Configuration;
4
5
use Closure;
6
use Illuminate\Support\Arr;
7
use PhpWinTools\Support\COM\ComWrapper;
8
use PhpWinTools\Support\COM\VariantWrapper;
9
use PhpWinTools\Support\COM\ComVariantWrapper;
10
use PhpWinTools\WmiScripting\Containers\Container;
11
use PhpWinTools\WmiScripting\Containers\Connections;
12
use PhpWinTools\WmiScripting\Support\Bus\CommandBus;
13
use PhpWinTools\WmiScripting\Connections\ComConnection;
14
use function PhpWinTools\WmiScripting\Support\is_closure;
15
use function PhpWinTools\WmiScripting\Support\if_not_null;
16
use PhpWinTools\WmiScripting\Support\Events\EventProvider;
17
use function PhpWinTools\WmiScripting\Support\is_not_closure;
18
use PhpWinTools\WmiScripting\Exceptions\InvalidArgumentException;
19
use PhpWinTools\WmiScripting\Support\Events\EventHistoryProvider;
20
use PhpWinTools\WmiScripting\Exceptions\InvalidConnectionException;
21
use PhpWinTools\WmiScripting\Exceptions\UnresolvableClassException;
22
23
class Config extends Container
24
{
25
    /** @var Config|null */
26
    protected static $instance = null;
27
28
    /** @var bool */
29
    protected static $test_mode = false;
30
31
    /** @var Resolver */
32
    protected $resolver;
33
34
    /** @var array */
35
    protected $resolve_stack = [];
36
37
    public function __construct(array $config = [], Resolver $resolver = null)
38
    {
39
        static::$instance = $this;
40
41
        $this->resolver = $resolver ?? new Resolver($this);
42
        $this->boot($config);
43
    }
44
45
    /**
46
     * Returns current instance if available and merges any available configuration.
47
     * This is the method used throughout the library to retrieve configuration.
48
     *
49
     * @param array         $items
50
     * @param Resolver|null $resolver
51
     *
52
     * @return Config
53
     */
54
    public static function instance(array $items = [], Resolver $resolver = null)
55
    {
56
        if (static::$instance && !empty($items)) {
57
            return static::newInstance(array_merge(static::$instance->container, $items), $resolver);
58
        }
59
60
        if (static::$instance && $resolver) {
61
            static::$instance->resolver = $resolver;
62
        }
63
64
        return static::$instance ?? static::newInstance($items, $resolver);
65
    }
66
67
    /**
68
     * Always returns a new instance. Used primary for testing. If used be aware that any previous changes
69
     * to the instance will be lost. If it's in test mode it will remain until you explicitly remove it.
70
     * You should never need to reference this directly except inside of tests.
71
     *
72
     * @param array         $items
73
     * @param Resolver|null $resolver
74
     *
75
     * @return Config
76
     */
77
    public static function newInstance(array $items = [], Resolver $resolver = null)
78
    {
79
        return new static($items, $resolver);
80
    }
81
82
    /**
83
     * This merges in a testing configuration. Any instance from this point will use that configuration.
84
     *
85
     * @param array         $items
86
     * @param Resolver|null $resolver
87
     *
88
     * @return Config
89
     */
90
    public static function testInstance(array $items = [], Resolver $resolver = null)
91
    {
92
        if (static::$test_mode === false && !is_null(static::$instance)) {
93
            static::$instance = null;
94
        }
95
96
        static::$test_mode = true;
97
98
        return static::instance($items, $resolver);
99
    }
100
101
    /**
102
     * Same as endTest, but also returns a fresh instance.
103
     *
104
     * @param array         $items
105
     * @param Resolver|null $resolver
106
     *
107
     * @return Config
108
     */
109
    public static function killTestInstance(array $items = [], Resolver $resolver = null)
110
    {
111
        (new static())->endTest();
112
113
        return static::instance($items, $resolver);
114
    }
115
116
    /**
117
     * This removes the testing flag and allow the normal configuration to return. This must be called to have
118
     * the library behave normally when testing.
119
     *
120
     * @return Config
121
     */
122
    public function endTest()
123
    {
124
        static::$test_mode = false;
125
        static::$instance = null;
126
127
        return $this;
128
    }
129
130
    /**
131
     * Returns the Resolver if no class is specified otherwise it attempts to resolve the given class.
132
     *
133
     * @param string|null $class
134
     * @param mixed       ...$parameters
135
     *
136
     * @return Resolver|mixed|null
137
     */
138
    public function __invoke(string $class = null, ...$parameters)
139
    {
140
        if (is_null($class)) {
141
            return $this->resolve();
142
        }
143
144
        if ($this->hasResolvable($class)) {
145
            return $this->resolveFromStack($class);
146
        }
147
148
        if ($this->isProviderClass($class)) {
149
            return $this->getBoundProvider($class);
150
        }
151
152
        if (($bound = $this->bindings($class, false)) !== false) {
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type array expected by parameter $default of PhpWinTools\WmiScripting...tion\Config::bindings(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
        if (($bound = $this->bindings($class, /** @scrutinizer ignore-type */ false)) !== false) {
Loading history...
153
            return $bound;
154
        }
155
156
        if (array_key_exists($class, $this->apiObjects())) {
157
            return $this->resolve()->make($this->getApiObject($class), ...$parameters);
158
        }
159
160
        if (array_key_exists($class, $this->com())) {
161
            return $this->resolve()->make($this->com()[$class], ...$parameters);
162
        }
163
164
        throw UnresolvableClassException::default($class);
165
    }
166
167
    /**
168
     * @return Resolver
169
     */
170
    public function resolve()
171
    {
172
        return $this->resolver;
173
    }
174
175
    /**
176
     * Returns the current resolve stack.
177
     *
178
     * @return array
179
     */
180
    public function getResolveStack()
181
    {
182
        return $this->resolve_stack;
183
    }
184
185
    /**
186
     * This attempts to get a resolvable item from the stack. Items on the stack are FIFO (First In First Out).
187
     * This is only ever utilized if using the Config classes' __invoke capability.
188
     *
189
     * @param $abstract
190
     *
191
     * @return mixed|null
192
     */
193
    public function resolveFromStack($abstract)
194
    {
195
        foreach ($this->resolve_stack as $key => $resolvable) {
196
            if (array_key_exists($abstract, $resolvable)) {
197
                $result = $resolvable[$abstract];
198
                unset($this->resolve_stack[$key]);
199
200
                return $result;
201
            }
202
        }
203
204
        return null;
205
    }
206
207
    /**
208
     * Add new resolvable to the end of the stack.
209
     *
210
     * @param string $abstract
211
     * @param        $concrete
212
     *
213
     * @return Config
214
     */
215
    public function addResolvable(string $abstract, $concrete)
216
    {
217
        $this->resolve_stack[] = [$abstract => $concrete];
218
219
        return $this;
220
    }
221
222
    /**
223
     * Check stack for resolvable. There may be a chance for caching pointers for resolvable abstracts.
224
     *
225
     * @param string $abstract
226
     *
227
     * @return bool
228
     */
229
    public function hasResolvable(string $abstract): bool
230
    {
231
        foreach ($this->resolve_stack as $key => $resolvable) {
232
            if (array_key_exists($abstract, $resolvable)) {
233
                return true;
234
            }
235
        }
236
237
        return false;
238
    }
239
240
    /**
241
     * @return array
242
     */
243
    public function apiObjects()
244
    {
245
        return $this->get('wmi.api_objects', []);
246
    }
247
248
    /**
249
     * @param string                 $abstract_class
250
     * @param string|callable|object $concrete_class
251
     *
252
     * @return Config
253
     */
254
    public function addApiObject(string $abstract_class, $concrete_class)
255
    {
256
        $this->set("wmi.api_objects.{$abstract_class}", $concrete_class);
257
258
        return $this;
259
    }
260
261
    /**
262
     * @param string $class
263
     *
264
     * @return string
265
     */
266
    public function getApiObject(string $class)
267
    {
268
        return $this->get("wmi.api_objects.{$class}");
269
    }
270
271
    /**
272
     * @return array
273
     */
274
    public function com()
275
    {
276
        return $this->get('com', []);
277
    }
278
279
    /**
280
     * @param string                 $abstract_class
281
     * @param string|callable|object $concrete_class
282
     *
283
     * @return Config
284
     */
285
    public function addComObject(string $abstract_class, $concrete_class)
286
    {
287
        $this->set("com.{$abstract_class}", $concrete_class);
288
289
        return $this;
290
    }
291
292
    /**
293
     * @return EventProvider
294
     */
295
    public function eventProvider()
296
    {
297
        return $this->getBoundProvider('event');
298
    }
299
300
    /**
301
     * @return EventHistoryProvider
302
     */
303
    public function eventHistoryProvider()
304
    {
305
        return $this->getBoundProvider('event_history');
306
    }
307
308
    /**
309
     * @return CommandBus
310
     */
311
    public function commandBus()
312
    {
313
        return $this->getBoundProvider('bus');
314
    }
315
316
    /**
317
     * Returns the given binding if exists and provided, or all bindings if not given. If the value cannot be found
318
     * it will return $default which can be a Closure that can be used to resolve the binding.
319
     *
320
     * @param string|null $abstract
321
     * @param array       $default
322
     *
323
     * @return mixed
324
     */
325
    public function bindings(string $abstract = null, $default = [])
326
    {
327
        return $this->get('bindings' . if_not_null($abstract, ".{$abstract}"), $default);
328
    }
329
330
    /**
331
     * Stores the given abstract with a reference to the given instance.
332
     *
333
     * @param string $abstract
334
     * @param        $concrete
335
     *
336
     * @return Config
337
     */
338
    public function bind(string $abstract, $concrete)
339
    {
340
        if (is_object($concrete) && class_exists($abstract) === false && is_not_closure($concrete)) {
341
            $this->set('bindings.' . get_class($concrete), $concrete);
342
        }
343
344
        if ($this->isProviderClass($abstract)) {
345
            $this->set('bindings.' . $this->getProviderAlias($abstract), $concrete);
0 ignored issues
show
Bug introduced by
Are you sure $this->getProviderAlias($abstract) of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

345
            $this->set('bindings.' . /** @scrutinizer ignore-type */ $this->getProviderAlias($abstract), $concrete);
Loading history...
346
        }
347
348
        return $this->set("bindings.{$abstract}", $concrete);
349
    }
350
351
    /**
352
     * Resolves the given concrete with either the given constructor arguments. If the first argument is a Closure
353
     * it will be used to resolve the given concrete.
354
     *
355
     * @param       $class
356
     * @param mixed ...$constructor
357
     *
358
     * @return mixed
359
     */
360
    public function make(string $class, ...$constructor)
361
    {
362
        if (isset($constructor[0]) && is_closure($constructor[0])) {
363
            $make = Arr::pull($constructor, 0);
364
        }
365
366
        $make = $make ?? function ($class) use ($constructor) {
367
            return new $class($this, ...$constructor);
368
        };
369
370
        return $make($class, $this);
371
    }
372
373
    /**
374
     * @param string|null $abstract
375
     * @param array       $default
376
     *
377
     * @return mixed
378
     */
379
    public function concreteProviders(string $abstract = null, $default = [])
380
    {
381
        if (is_null($abstract)) {
382
            return $this->get('providers.concrete', $default);
383
        }
384
385
        if (class_exists($abstract) && array_key_exists($abstract, $this->get('providers.aliases'))) {
386
            return $abstract;
387
        }
388
389
        return $this->get("providers.concrete.{$abstract}", $default);
390
    }
391
392
    /**
393
     * Returns an already registered provider or instantiates it from configuration when $default is null.
394
     *
395
     * @param string     $alias
396
     * @param null|mixed $default
397
     *
398
     * @return mixed
399
     */
400
    public function getBoundProvider($alias, $default = null)
401
    {
402
        $default = $default ?? function () use ($alias) {
403
            return $this->registerProvider($alias);
404
        };
405
406
        return $this->bindings($alias, $default);
0 ignored issues
show
Bug introduced by
It seems like $default can also be of type callable; however, parameter $default of PhpWinTools\WmiScripting...tion\Config::bindings() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

406
        return $this->bindings($alias, /** @scrutinizer ignore-type */ $default);
Loading history...
407
    }
408
409
    /**
410
     * @param string              $abstract
411
     * @param array|mixed|Closure ...$constructor
412
     *
413
     * @return mixed
414
     */
415
    public function makeProvider(string $abstract, ...$constructor)
416
    {
417
        if (($provider = $this->concreteProviders($abstract, false)) === false) {
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type array expected by parameter $default of PhpWinTools\WmiScripting...ig::concreteProviders(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

417
        if (($provider = $this->concreteProviders($abstract, /** @scrutinizer ignore-type */ false)) === false) {
Loading history...
418
            throw new InvalidArgumentException("{$abstract} is not a valid provider.");
419
        }
420
421
        return $this->make($provider, ...$constructor);
422
    }
423
424
    public function getCacheDriver($provider = null)
425
    {
426
        $driver = $this->get(if_not_null($provider, "{$provider}.") . 'cache.driver');
427
428
        return new $driver($this);
429
    }
430
431
    public function shouldTrackEvents()
432
    {
433
        return $this->get('event.track', false);
434
    }
435
436
    public function trackEvents()
437
    {
438
        $this->set('event.track', true);
439
440
        return $this;
441
    }
442
443
    public function doNotTrackEvents()
444
    {
445
        $this->set('event.track', false);
446
447
        return $this;
448
    }
449
450
    /**
451
     * @return string
452
     */
453
    public function getComClass()
454
    {
455
        return $this->get('com.com_class');
456
    }
457
458
    /**
459
     * @return string
460
     */
461
    public function getVariantClass()
462
    {
463
        return $this->get('com.variant_class');
464
    }
465
466
    /**
467
     * @return string
468
     */
469
    public function getComVariantWrapper()
470
    {
471
        return $this->get('com.' . ComVariantWrapper::class);
472
    }
473
474
    /**
475
     * @return string
476
     */
477
    public function getComWrapper()
478
    {
479
        return $this->get('com.' . ComWrapper::class);
480
    }
481
482
    /**
483
     * @return string
484
     */
485
    public function getVariantWrapper()
486
    {
487
        return $this->get('com.' . VariantWrapper::class);
488
    }
489
490
    /**
491
     * @return Connections
492
     */
493
    public function connections()
494
    {
495
        return $this->get('wmi.connections.servers');
496
    }
497
498
    /**
499
     * @param string $name
500
     *
501
     * @return ComConnection|null
502
     */
503
    public function getConnection(string $name = null)
504
    {
505
        if ($name === 'default' || is_null($name)) {
506
            $name = $this->get('wmi.connections.default');
507
        }
508
509
        return $this->connections()->get($name);
510
    }
511
512
    /**
513
     * @param string        $name
514
     * @param ComConnection $connection
515
     *
516
     * @return Config
517
     */
518
    public function addConnection(string $name, ComConnection $connection): self
519
    {
520
        $this->connections()->set($name, $connection);
521
522
        return $this;
523
    }
524
525
    public function getDefaultConnection()
526
    {
527
        return $this->getConnection($this->getDefaultConnectionName());
528
    }
529
530
    public function getDefaultConnectionName()
531
    {
532
        return $this->get('wmi.connections.default');
533
    }
534
535
    public function setDefaultConnection(string $name)
536
    {
537
        if (!$this->getConnection($name)) {
538
            throw InvalidConnectionException::new($name);
539
        }
540
541
        return $this->set('wmi.connections.default', $name);
542
    }
543
544
    public function registerProvider($alias, $instance = null)
545
    {
546
        if (is_null($instance) && ($bound = $this->getBoundProvider($alias, false)) !== false) {
547
            return $bound;
548
        }
549
550
        if (!is_object($instance)) {
551
            $instance = $this->makeProvider($alias);
552
        }
553
554
        $this->bind("{$alias}", $instance);
555
556
        return $instance;
557
    }
558
559
    /**
560
     * @param string $abstract
561
     *
562
     * @return bool
563
     */
564
    protected function isProvider(string $abstract): bool
565
    {
566
        return array_key_exists($abstract, $this->concreteProviders())
567
            || $this->concreteProviders($abstract, false)
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type array expected by parameter $default of PhpWinTools\WmiScripting...ig::concreteProviders(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

567
            || $this->concreteProviders($abstract, /** @scrutinizer ignore-type */ false)
Loading history...
568
            || $this->getProviderAlias($abstract) !== false;
569
    }
570
571
    /**
572
     * @param string $abstract
573
     *
574
     * @return bool
575
     */
576
    protected function isProviderClass(string $abstract): bool
577
    {
578
        return class_exists($abstract) && $this->isProvider($abstract);
579
    }
580
581
    /**
582
     * @param string $abstract
583
     *
584
     * @return bool|false|int|string
585
     */
586
    protected function getProviderAlias(string $abstract)
587
    {
588
        return class_exists($abstract) ? array_search($abstract, $this->concreteProviders(), true) : false;
589
    }
590
591
    protected function boot(array $config)
592
    {
593
        if (static::$test_mode) {
594
            $this->merge(include(__DIR__ . '/../config/testing.php'));
595
        }
596
597
        if (!empty($config)) {
598
            $this->merge($config);
599
        }
600
601
        $this->merge(include(__DIR__ . '/../config/bootstrap.php'))
602
            ->registerProviders()
603
            ->bootConnections();
604
    }
605
606
    protected function registerProviders()
607
    {
608
        array_map(function ($alias) {
609
            $this->registerProvider($alias);
610
        }, array_keys($this->concreteProviders()));
611
612
        return $this;
613
    }
614
615
    protected function bootConnections()
616
    {
617
        Arr::set($this->container, 'wmi.connections.servers', new Connections($this));
618
619
        return $this;
620
    }
621
}
622