Completed
Push — master ( daed8c...cf758d )
by Damian
08:03
created

InjectorTest::testNamedServices()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 14
nc 1
nop 0
dl 0
loc 24
rs 8.9713
c 1
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Tests\Injector;
4
5
use InvalidArgumentException;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Injector\Factory;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Core\Injector\InjectorNotFoundException;
10
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
11
use SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService;
12
use SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService;
13
use SilverStripe\Core\Tests\Injector\InjectorTest\CircularOne;
14
use SilverStripe\Core\Tests\Injector\InjectorTest\CircularTwo;
15
use SilverStripe\Core\Tests\Injector\InjectorTest\ConstructableObject;
16
use SilverStripe\Core\Tests\Injector\InjectorTest\DummyRequirements;
17
use SilverStripe\Core\Tests\Injector\InjectorTest\MyChildClass;
18
use SilverStripe\Core\Tests\Injector\InjectorTest\MyParentClass;
19
use SilverStripe\Core\Tests\Injector\InjectorTest\NeedsBothCirculars;
20
use SilverStripe\Core\Tests\Injector\InjectorTest\NewRequirementsBackend;
21
use SilverStripe\Core\Tests\Injector\InjectorTest\OriginalRequirementsBackend;
22
use SilverStripe\Core\Tests\Injector\InjectorTest\OtherTestObject;
23
use SilverStripe\Core\Tests\Injector\InjectorTest\TestObject;
24
use SilverStripe\Core\Tests\Injector\InjectorTest\TestSetterInjections;
25
use SilverStripe\Core\Tests\Injector\InjectorTest\TestStaticInjections;
26
use SilverStripe\Dev\SapphireTest;
27
use SilverStripe\Dev\TestOnly;
28
use stdClass;
29
30
define('TEST_SERVICES', __DIR__ . '/AopProxyServiceTest');
31
32
/**
33
 * Tests for the dependency injector
34
 *
35
 * Note that these are SS conversions of the existing Simpletest unit tests
36
 *
37
 * @author      [email protected]
38
 * @license     BSD License http://silverstripe.org/bsd-license/
39
 * @skipUpgrade
40
 */
41
class InjectorTest extends SapphireTest
42
{
43
44
    protected $nestingLevel = 0;
45
46
    protected function setUp()
47
    {
48
        parent::setUp();
49
50
        $this->nestingLevel = 0;
51
    }
52
53
    protected function tearDown()
54
    {
55
56
        while ($this->nestingLevel > 0) {
57
            $this->nestingLevel--;
58
            Config::unnest();
59
        }
60
61
        parent::tearDown();
62
    }
63
64
    public function testCorrectlyInitialised()
65
    {
66
        $injector = Injector::inst();
67
        $this->assertTrue(
68
            $injector->getConfigLocator() instanceof SilverStripeServiceConfigurationLocator,
69
            'Failure most likely because the injector has been referenced BEFORE being initialised in Core.php'
70
        );
71
    }
72
73
    public function testBasicInjector()
74
    {
75
        $injector = new Injector();
76
        $injector->setAutoScanProperties(true);
77
        $config = array(
78
            'SampleService' => array(
79
                'src' => TEST_SERVICES . '/SampleService.php',
80
                'class' => SampleService::class,
81
            )
82
        );
83
84
        $injector->load($config);
85
86
87
        $this->assertFalse($injector->has('UnknownService'));
88
        $this->assertNull($injector->getServiceName('UnknownService'));
89
90
        $this->assertTrue($injector->has('SampleService'));
91
        $this->assertEquals(
92
            'SampleService',
93
            $injector->getServiceName('SampleService')
94
        );
95
96
        $myObject = new TestObject();
97
        $injector->inject($myObject);
98
99
        $this->assertInstanceOf(
100
            SampleService::class,
101
            $myObject->sampleService
102
        );
103
    }
104
105
    public function testConfiguredInjector()
106
    {
107
        $injector = new Injector();
108
        $services = array(
109
            'AnotherService' => array(
110
                'class' => AnotherService::class,
111
                'src' => TEST_SERVICES . '/AnotherService.php',
112
                'properties' => array('config_property' => 'Value'),
113
            ),
114
            'SampleService' => array(
115
                'class' => SampleService::class,
116
                'src' => TEST_SERVICES . '/SampleService.php',
117
            )
118
        );
119
120
        $injector->load($services);
121
        $this->assertTrue($injector->has('SampleService'));
122
        $this->assertEquals(
123
            'SampleService',
124
            $injector->getServiceName('SampleService')
125
        );
126
        // We expect a false because the AnotherService::class is actually
127
        // just a replacement of the SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService
128
        $this->assertTrue($injector->has('SampleService'));
129
        $this->assertEquals(
130
            'AnotherService',
131
            $injector->getServiceName('AnotherService')
132
        );
133
134
        $item = $injector->get('AnotherService');
135
136
        $this->assertEquals('Value', $item->config_property);
137
    }
138
139
    public function testIdToNameMap()
140
    {
141
        $injector = new Injector();
142
        $services = array(
143
            'FirstId' => AnotherService::class,
144
            'SecondId' => SampleService::class,
145
        );
146
147
        $injector->load($services);
148
149
        $this->assertTrue($injector->has('FirstId'));
150
        $this->assertEquals($injector->getServiceName('FirstId'), 'FirstId');
151
152
        $this->assertTrue($injector->has('SecondId'));
153
        $this->assertEquals($injector->getServiceName('SecondId'), 'SecondId');
154
155
        $this->assertTrue($injector->get('FirstId') instanceof AnotherService);
156
        $this->assertTrue($injector->get('SecondId') instanceof SampleService);
157
    }
158
159
    public function testReplaceService()
160
    {
161
        $injector = new Injector();
162
        $injector->setAutoScanProperties(true);
163
164
        $config = array(
165
            'SampleService' => array(
166
                'src' => TEST_SERVICES . '/SampleService.php',
167
                'class' => SampleService::class,
168
            )
169
        );
170
171
        // load
172
        $injector->load($config);
173
174
        // inject
175
        $myObject = new TestObject();
176
        $injector->inject($myObject);
177
178
        $this->assertInstanceOf(
179
            SampleService::class,
180
            $myObject->sampleService
181
        );
182
183
        // also tests that ID can be the key in the array
184
        $config = array(
185
            'SampleService' => array(
186
                'src' => TEST_SERVICES . '/AnotherService.php',
187
                'class' => AnotherService::class,
188
            )
189
        );
190
        // , 'id' => SampleService::class));
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
191
        // load
192
        $injector->load($config);
193
194
        $injector->inject($myObject);
195
        $this->assertInstanceOf(
196
            AnotherService::class,
197
            $myObject->sampleService
198
        );
199
    }
200
201
    public function testUpdateSpec()
202
    {
203
        $injector = new Injector();
204
        $services = array(
205
            AnotherService::class => array(
206
                'src' => TEST_SERVICES . '/AnotherService.php',
207
                'properties' => array(
208
                    'filters' => array(
209
                        'One',
210
                        'Two',
211
                    )
212
                ),
213
            )
214
        );
215
216
        $injector->load($services);
217
218
        $injector->updateSpec(AnotherService::class, 'filters', 'Three');
219
        $another = $injector->get(AnotherService::class);
220
221
        $this->assertEquals(3, count($another->filters));
222
        $this->assertEquals('Three', $another->filters[2]);
223
    }
224
225
    public function testConstantUsage()
226
    {
227
        $injector = new Injector();
228
        $services = array(
229
            AnotherService::class => array(
230
                'properties' => array(
231
                    'filters' => array(
232
                        '`BASE_PATH`',
233
                        '`TEMP_FOLDER`',
234
                        '`NOT_DEFINED`',
235
                        'THIRDPARTY_DIR' // Not back-tick escaped
236
                    )
237
                ),
238
            )
239
        );
240
241
        $injector->load($services);
242
        $another = $injector->get(AnotherService::class);
243
        $this->assertEquals(
244
            [
245
                BASE_PATH,
246
                TEMP_FOLDER,
247
                null,
248
                'THIRDPARTY_DIR',
249
            ],
250
            $another->filters
251
        );
252
    }
253
254
    public function testAutoSetInjector()
255
    {
256
        $injector = new Injector();
257
        $injector->setAutoScanProperties(true);
258
        $injector->addAutoProperty('auto', 'somevalue');
259
        $config = array(
260
            'SampleService' => array(
261
                'src' => TEST_SERVICES . '/SampleService.php',
262
                'class' => SampleService::class
263
            )
264
        );
265
        $injector->load($config);
266
267
        $this->assertTrue($injector->has('SampleService'));
268
        $this->assertEquals(
269
            'SampleService',
270
            $injector->getServiceName('SampleService')
271
        );
272
        // We expect a false because the AnotherService::class is actually
273
        // just a replacement of the SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService
274
275
        $myObject = new InjectorTest\TestObject();
276
277
        $injector->inject($myObject);
278
279
        $this->assertInstanceOf(
280
            SampleService::class,
281
            $myObject->sampleService
282
        );
283
        $this->assertEquals($myObject->auto, 'somevalue');
0 ignored issues
show
Bug introduced by
The property auto does not seem to exist in SilverStripe\Core\Tests\...InjectorTest\TestObject.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
284
    }
285
286
    public function testSettingSpecificProperty()
287
    {
288
        $injector = new Injector();
289
        $config = array(AnotherService::class);
290
        $injector->load($config);
291
        $injector->setInjectMapping(TestObject::class, 'sampleService', AnotherService::class);
292
        $testObject = $injector->get(TestObject::class);
293
294
        $this->assertInstanceOf(
295
            AnotherService::class,
296
            $testObject->sampleService
297
        );
298
    }
299
300
    public function testSettingSpecificMethod()
301
    {
302
        $injector = new Injector();
303
        $config = array(AnotherService::class);
304
        $injector->load($config);
305
        $injector->setInjectMapping(TestObject::class, 'setSomething', AnotherService::class, 'method');
306
307
        $testObject = $injector->get(TestObject::class);
308
309
        $this->assertInstanceOf(
310
            AnotherService::class,
311
            $testObject->sampleService
312
        );
313
    }
314
315
    public function testInjectingScopedService()
316
    {
317
        $injector = new Injector();
318
319
        $config = array(
320
            AnotherService::class,
321
            'SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService.DottedChild'   => SampleService::class,
322
        );
323
324
        $injector->load($config);
325
326
        $service = $injector->get('SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService.DottedChild');
327
        $this->assertInstanceOf(SampleService::class, $service);
328
329
        $service = $injector->get('SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService.Subset');
330
        $this->assertInstanceOf(AnotherService::class, $service);
331
332
        $injector->setInjectMapping(TestObject::class, 'sampleService', 'SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService.Geronimo');
333
        $testObject = $injector->create(TestObject::class);
334
        $this->assertEquals(get_class($testObject->sampleService), AnotherService::class);
335
336
        $injector->setInjectMapping(TestObject::class, 'sampleService', 'SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService.DottedChild.AnotherDown');
337
        $testObject = $injector->create(TestObject::class);
338
        $this->assertEquals(get_class($testObject->sampleService), SampleService::class);
339
    }
340
341
    public function testInjectUsingConstructor()
342
    {
343
        $injector = new Injector();
344
        $config = array(
345
            'SampleService' => array(
346
                'src' => TEST_SERVICES . '/SampleService.php',
347
                'class' => SampleService::class,
348
                'constructor' => array(
349
                    'val1',
350
                    'val2',
351
                )
352
            )
353
        );
354
355
        $injector->load($config);
356
        $sample = $injector->get('SampleService');
357
        $this->assertEquals($sample->constructorVarOne, 'val1');
358
        $this->assertEquals($sample->constructorVarTwo, 'val2');
359
360
        $injector = new Injector();
361
        $config = array(
362
            'AnotherService' => AnotherService::class,
363
            'SampleService' => array(
364
                'src' => TEST_SERVICES . '/SampleService.php',
365
                'class' => SampleService::class,
366
                'constructor' => array(
367
                    'val1',
368
                    '%$AnotherService',
369
                )
370
            )
371
        );
372
373
        $injector->load($config);
374
        $sample = $injector->get('SampleService');
375
        $this->assertEquals($sample->constructorVarOne, 'val1');
376
        $this->assertInstanceOf(
377
            AnotherService::class,
378
            $sample->constructorVarTwo
379
        );
380
381
        $injector = new Injector();
382
        $config = array(
383
            'SampleService' => array(
384
                'src' => TEST_SERVICES . '/SampleService.php',
385
                'class' => SampleService::class,
386
                'constructor' => array(
387
                    'val1',
388
                    'val2',
389
                )
390
            )
391
        );
392
393
        $injector->load($config);
394
        $sample = $injector->get('SampleService');
395
        $this->assertEquals($sample->constructorVarOne, 'val1');
396
        $this->assertEquals($sample->constructorVarTwo, 'val2');
397
398
        // test constructors on prototype
399
        $injector = new Injector();
400
        $config = array(
401
            'SampleService' => array(
402
                'type'  => 'prototype',
403
                'src' => TEST_SERVICES . '/SampleService.php',
404
                'class' => SampleService::class,
405
                'constructor' => array(
406
                    'val1',
407
                    'val2',
408
                )
409
            )
410
        );
411
412
        $injector->load($config);
413
        $sample = $injector->get('SampleService');
414
        $this->assertEquals($sample->constructorVarOne, 'val1');
415
        $this->assertEquals($sample->constructorVarTwo, 'val2');
416
417
        $again = $injector->get('SampleService');
418
        $this->assertFalse($sample === $again);
419
420
        $this->assertEquals($sample->constructorVarOne, 'val1');
421
        $this->assertEquals($sample->constructorVarTwo, 'val2');
422
    }
423
424
    public function testInjectUsingSetter()
425
    {
426
        $injector = new Injector();
427
        $injector->setAutoScanProperties(true);
428
        $config = array(
429
            'SampleService' => array(
430
                'src' => TEST_SERVICES . '/SampleService.php',
431
                'class' => SampleService::class,
432
            )
433
        );
434
435
        $injector->load($config);
436
        $this->assertTrue($injector->has('SampleService'));
437
        $this->assertEquals('SampleService', $injector->getServiceName('SampleService'));
438
439
        $myObject = new InjectorTest\OtherTestObject();
440
        $injector->inject($myObject);
441
442
        $this->assertInstanceOf(
443
            SampleService::class,
444
            $myObject->s()
445
        );
446
447
        // and again because it goes down a different code path when setting things
448
        // based on the inject map
449
        $myObject = new InjectorTest\OtherTestObject();
450
        $injector->inject($myObject);
451
452
        $this->assertInstanceOf(
453
            SampleService::class,
454
            $myObject->s()
455
        );
456
    }
457
458
    // make sure we can just get any arbitrary object - it should be created for us
459
    public function testInstantiateAnObjectViaGet()
460
    {
461
        $injector = new Injector();
462
        $injector->setAutoScanProperties(true);
463
        $config = array(
464
            'SampleService' => array(
465
                'src' => TEST_SERVICES . '/SampleService.php',
466
                'class' => SampleService::class,
467
            )
468
        );
469
470
        $injector->load($config);
471
        $this->assertTrue($injector->has('SampleService'));
472
        $this->assertEquals('SampleService', $injector->getServiceName('SampleService'));
473
474
        $myObject = $injector->get(OtherTestObject::class);
475
        $this->assertInstanceOf(
476
            SampleService::class,
477
            $myObject->s()
478
        );
479
480
        // and again because it goes down a different code path when setting things
481
        // based on the inject map
482
        $myObject = $injector->get(OtherTestObject::class);
483
        $this->assertInstanceOf(SampleService::class, $myObject->s());
484
    }
485
486
    public function testCircularReference()
487
    {
488
        $services = array(
489
            'CircularOne' => CircularOne::class,
490
            'CircularTwo' => CircularTwo::class
491
        );
492
        $injector = new Injector($services);
493
        $injector->setAutoScanProperties(true);
494
495
        $obj = $injector->get(NeedsBothCirculars::class);
496
497
        $this->assertTrue($obj->circularOne instanceof InjectorTest\CircularOne);
498
        $this->assertTrue($obj->circularTwo instanceof InjectorTest\CircularTwo);
499
    }
500
501
    public function testPrototypeObjects()
502
    {
503
        $services = array(
504
            'CircularOne' => CircularOne::class,
505
            'CircularTwo' => CircularTwo::class,
506
            'NeedsBothCirculars' => array(
507
                'class' => NeedsBothCirculars::class,
508
                'type' => 'prototype'
509
            )
510
        );
511
        $injector = new Injector($services);
512
        $injector->setAutoScanProperties(true);
513
        $obj1 = $injector->get('NeedsBothCirculars');
514
        $obj2 = $injector->get('NeedsBothCirculars');
515
516
        // if this was the same object, then $obj1->var would now be two
517
        $obj1->var = 'one';
518
        $obj2->var = 'two';
519
520
        $this->assertTrue($obj1->circularOne instanceof InjectorTest\CircularOne);
521
        $this->assertTrue($obj1->circularTwo instanceof InjectorTest\CircularTwo);
522
523
        $this->assertEquals($obj1->circularOne, $obj2->circularOne);
524
        $this->assertNotEquals($obj1, $obj2);
525
    }
526
527
    public function testSimpleInstantiation()
528
    {
529
        $services = array(
530
            'CircularOne' => CircularOne::class,
531
            'CircularTwo' => CircularTwo::class
532
        );
533
        $injector = new Injector($services);
534
535
        // similar to the above, but explicitly instantiating this object here
536
        $obj1 = $injector->create(NeedsBothCirculars::class);
537
        $obj2 = $injector->create(NeedsBothCirculars::class);
538
539
        // if this was the same object, then $obj1->var would now be two
540
        $obj1->var = 'one';
541
        $obj2->var = 'two';
542
543
        $this->assertEquals($obj1->circularOne, $obj2->circularOne);
544
        $this->assertNotEquals($obj1, $obj2);
545
    }
546
547
    public function testCreateWithConstructor()
548
    {
549
        $injector = new Injector();
550
        $obj = $injector->create(CircularTwo::class, 'param');
551
        $this->assertEquals($obj->otherVar, 'param');
552
    }
553
554
    public function testSimpleSingleton()
555
    {
556
        $injector = new Injector();
557
558
        $one = $injector->create(CircularOne::class);
559
        $two = $injector->create(CircularOne::class);
560
561
        $this->assertFalse($one === $two);
562
563
        $one = $injector->get(CircularTwo::class);
564
        $two = $injector->get(CircularTwo::class);
565
566
        $this->assertTrue($one === $two);
567
    }
568
569
    public function testOverridePriority()
570
    {
571
        $injector = new Injector();
572
        $injector->setAutoScanProperties(true);
573
        $config = array(
574
            'SampleService' => array(
575
                'src' => TEST_SERVICES . '/SampleService.php',
576
                'class' => SampleService::class,
577
                'priority' => 10,
578
            )
579
        );
580
581
        // load
582
        $injector->load($config);
583
584
        // inject
585
        $myObject = new InjectorTest\TestObject();
586
        $injector->inject($myObject);
587
588
        $this->assertInstanceOf(SampleService::class, $myObject->sampleService);
589
590
        $config = array(
591
            array(
592
                'src' => TEST_SERVICES . '/AnotherService.php',
593
                'class' => AnotherService::class,
594
                'id' => 'SampleService',
595
                'priority' => 1,
596
            )
597
        );
598
        // load
599
        $injector->load($config);
600
601
        $injector->inject($myObject);
602
        $this->assertInstanceOf(
603
            SampleService::class,
604
            $myObject->sampleService
605
        );
606
    }
607
608
    /**
609
     * Specific test method to illustrate various ways of setting a requirements backend
610
     */
611
    public function testRequirementsSettingOptions()
612
    {
613
        $injector = new Injector();
614
        $config = array(
615
            OriginalRequirementsBackend::class,
616
            NewRequirementsBackend::class,
617
            DummyRequirements::class => array(
618
                'constructor' => array(
619
                    '%$'.OriginalRequirementsBackend::class
620
                )
621
            )
622
        );
623
624
        $injector->load($config);
625
626
        $requirements = $injector->get(DummyRequirements::class);
627
        $this->assertInstanceOf(
628
            OriginalRequirementsBackend::class,
629
            $requirements->backend
630
        );
631
632
        // just overriding the definition here
633
        $injector->load(
634
            array(
635
            DummyRequirements::class => array(
636
                'constructor' => array(
637
                    '%$'.NewRequirementsBackend::class
638
                )
639
            )
640
            )
641
        );
642
643
        // requirements should have been reinstantiated with the new bean setting
644
        $requirements = $injector->get(DummyRequirements::class);
645
        $this->assertInstanceOf(
646
            NewRequirementsBackend::class,
647
            $requirements->backend
648
        );
649
    }
650
651
    /**
652
     * disabled for now
653
     */
654
    public function testStaticInjections()
655
    {
656
        $injector = new Injector();
657
        $config = array(
658
            NewRequirementsBackend::class,
659
        );
660
661
        $injector->load($config);
662
663
        $si = $injector->get(TestStaticInjections::class);
664
        $this->assertInstanceOf(
665
            NewRequirementsBackend::class,
666
            $si->backend
667
        );
668
    }
669
670
    public function testSetterInjections()
671
    {
672
        $injector = new Injector();
673
        $config = array(
674
            NewRequirementsBackend::class,
675
        );
676
677
        $injector->load($config);
678
679
        $si = $injector->get(TestSetterInjections::class);
680
        $this->assertInstanceOf(
681
            NewRequirementsBackend::class,
682
            $si->getBackend()
683
        );
684
    }
685
686
    public function testCustomObjectCreator()
687
    {
688
        $injector = new Injector();
689
        $injector->setObjectCreator(new InjectorTest\SSObjectCreator($injector));
690
        $config = array(
691
            OriginalRequirementsBackend::class,
692
            DummyRequirements::class => array(
693
                'class' => DummyRequirements::class.'(\'%$'.OriginalRequirementsBackend::class.'\')'
694
            )
695
        );
696
        $injector->load($config);
697
698
        $requirements = $injector->get(DummyRequirements::class);
699
        $this->assertEquals(OriginalRequirementsBackend::class, get_class($requirements->backend));
700
    }
701
702
    public function testInheritedConfig()
703
    {
704
705
        // Test that child class does not automatically inherit config
706
        $injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class));
707
        Config::inst()->update(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
708
            Injector::class,
709
            MyParentClass::class,
710
            [
711
            'properties' => ['one' => 'the one'],
712
            'class' => MyParentClass::class,
713
            ]
714
        );
715
        $obj = $injector->get(MyParentClass::class);
716
        $this->assertInstanceOf(MyParentClass::class, $obj);
717
        $this->assertEquals($obj->one, 'the one');
718
719
        // Class isn't inherited and parent properties are ignored
720
        $obj = $injector->get(MyChildClass::class);
721
        $this->assertInstanceOf(MyChildClass::class, $obj);
722
        $this->assertNotEquals($obj->one, 'the one');
723
724
        // Set child class as alias
725
        $injector = new Injector(
726
            array(
727
            'locator' => SilverStripeServiceConfigurationLocator::class
728
            )
729
        );
730
        Config::inst()->update(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
731
            Injector::class,
732
            MyChildClass::class,
733
            '%$'.MyParentClass::class
734
        );
735
736
        // Class isn't inherited and parent properties are ignored
737
        $obj = $injector->get(MyChildClass::class);
738
        $this->assertInstanceOf(MyParentClass::class, $obj);
739
        $this->assertEquals($obj->one, 'the one');
740
    }
741
742
    public function testSameNamedSingeltonPrototype()
743
    {
744
        $injector = new Injector();
745
746
        // get a singleton object
747
        $object = $injector->get(NeedsBothCirculars::class);
748
        $object->var = 'One';
749
750
        $again = $injector->get(NeedsBothCirculars::class);
751
        $this->assertEquals($again->var, 'One');
752
753
        // create a NEW instance object
754
        $new = $injector->create(NeedsBothCirculars::class);
755
        $this->assertNull($new->var);
756
757
        // this will trigger a problem below
758
        $new->var = 'Two';
759
760
        $again = $injector->get(NeedsBothCirculars::class);
761
        $this->assertEquals($again->var, 'One');
762
    }
763
764
    public function testConvertServicePropertyOnCreate()
765
    {
766
        // make sure convert service property is not called on direct calls to create, only on configured
767
        // declarations to avoid un-needed function calls
768
        $injector = new Injector();
769
        $item = $injector->create(ConstructableObject::class, '%$'.TestObject::class);
770
        $this->assertEquals('%$'.TestObject::class, $item->property);
771
772
        // do it again but have test object configured as a constructor dependency
773
        $injector = new Injector();
774
        $config = array(
775
            ConstructableObject::class => array(
776
                'constructor' => array(
777
                    '%$'.TestObject::class
778
                )
779
            )
780
        );
781
782
        $injector->load($config);
783
        $item = $injector->get(ConstructableObject::class);
784
        $this->assertTrue($item->property instanceof InjectorTest\TestObject);
785
786
        // and with a configured object defining TestObject to be something else!
787
        $injector = new Injector(array('locator' => InjectorTest\InjectorTestConfigLocator::class));
788
        $config = array(
789
            ConstructableObject::class => array(
790
                'constructor' => array(
791
                    '%$'.TestObject::class
792
                )
793
            ),
794
        );
795
796
        $injector->load($config);
797
        $item = $injector->get(ConstructableObject::class);
798
        $this->assertTrue($item->property instanceof InjectorTest\ConstructableObject);
799
800
        $this->assertInstanceOf(OtherTestObject::class, $item->property->property);
801
    }
802
803
    public function testNamedServices()
804
    {
805
        $injector = new Injector();
806
        $service  = new TestObject();
807
        $service->setSomething('injected');
808
809
        // Test registering with non-class name
810
        $injector->registerService($service, 'NamedService');
811
        $this->assertTrue($injector->has('NamedService'));
812
        $this->assertEquals($service, $injector->get('NamedService'));
813
814
        // Unregister service by name
815
        $injector->unregisterNamedObject('NamedService');
816
        $this->assertFalse($injector->has('NamedService'));
817
818
        // Test registered with class name
819
        $injector->registerService($service);
820
        $this->assertTrue($injector->has(TestObject::class));
821
        $this->assertEquals($service, $injector->get(TestObject::class));
822
823
        // Unregister service by class
824
        $injector->unregisterNamedObject(TestObject::class);
825
        $this->assertFalse($injector->has(TestObject::class));
826
    }
827
828
    public function testCreateConfiggedObjectWithCustomConstructorArgs()
829
    {
830
        // need to make sure that even if the config defines some constructor params,
831
        // that we take our passed in constructor args instead
832
        $injector = new Injector(array('locator' => InjectorTest\InjectorTestConfigLocator::class));
833
834
        $item = $injector->create('ConfigConstructor', 'othervalue');
835
        $this->assertEquals($item->property, 'othervalue');
836
    }
837
838
    /**
839
     * Tests creating a service with a custom factory.
840
     */
841
    public function testCustomFactory()
842
    {
843
        $injector = new Injector(
844
            array(
845
            'service' => array('factory' => 'factory', 'constructor' => array(1, 2, 3))
846
            )
847
        );
848
849
        $factory = $this->getMockBuilder(Factory::class)->getMock();
850
        $factory
851
            ->expects($this->once())
852
            ->method('create')
853
            ->with($this->equalTo('service'), $this->equalTo(array(1, 2, 3)))
854
            ->will(
855
                $this->returnCallback(
856
                    function ($args) {
0 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
857
                        return new InjectorTest\TestObject();
858
                    }
859
                )
860
            );
861
862
        $injector->registerService($factory, 'factory');
863
864
        $this->assertInstanceOf(TestObject::class, $injector->get('service'));
865
    }
866
867
    public function testMethods()
868
    {
869
        // do it again but have test object configured as a constructor dependency
870
        $injector = new Injector();
871
        $config = array(
872
            'A' => array(
873
                'class' => TestObject::class,
874
            ),
875
            'B' => array(
876
                'class' => TestObject::class,
877
            ),
878
            'TestService' => array(
879
                'class' => TestObject::class,
880
                'calls' => array(
881
                    array('myMethod', array('%$A')),
882
                    array('myMethod', array('%$B')),
883
                    array('noArgMethod')
884
                )
885
            )
886
        );
887
888
        $injector->load($config);
889
        $item = $injector->get('TestService');
890
        $this->assertTrue($item instanceof InjectorTest\TestObject);
891
        $this->assertEquals(
892
            array($injector->get('A'), $injector->get('B'), 'noArgMethod called'),
893
            $item->methodCalls
894
        );
895
    }
896
897
    /**
898
     * @expectedException InvalidArgumentException
899
     */
900
    public function testNonExistentMethods()
901
    {
902
        $injector = new Injector();
903
        $config = array(
904
            'TestService' => array(
905
                'class' => TestObject::class,
906
                'calls' => array(
907
                    array('thisDoesntExist')
908
                )
909
            )
910
        );
911
912
        $injector->load($config);
913
        $item = $injector->get('TestService');
914
    }
915
916
    /**
917
     * @expectedException InvalidArgumentException
918
     */
919
    public function testProtectedMethods()
920
    {
921
        $injector = new Injector();
922
        $config = array(
923
            'TestService' => array(
924
                'class' => TestObject::class,
925
                'calls' => array(
926
                    array('protectedMethod')
927
                )
928
            )
929
        );
930
931
        $injector->load($config);
932
        $item = $injector->get('TestService');
933
    }
934
935
    /**
936
     * @expectedException InvalidArgumentException
937
     */
938
    public function testTooManyArrayValues()
939
    {
940
        $injector = new Injector();
941
        $config = array(
942
            'TestService' => array(
943
                'class' => TestObject::class,
944
                'calls' => array(
945
                    array('method', array('args'), 'what is this?')
946
                )
947
            )
948
        );
949
950
        $injector->load($config);
951
        $item = $injector->get('TestService');
952
    }
953
954
    /**
955
     * @expectedException \SilverStripe\Core\Injector\InjectorNotFoundException
956
     */
957
    public function testGetThrowsOnNotFound()
958
    {
959
        $injector = new Injector();
960
        $injector->get('UnknownService');
961
    }
962
963
    /**
964
     * Test nesting of injector
965
     */
966
    public function testNest()
967
    {
968
969
        // Outer nest to avoid interference with other
970
        Injector::nest();
971
        $this->nestingLevel++;
972
973
        // Test services
974
        $config = array(
975
            NewRequirementsBackend::class,
976
        );
977
        Injector::inst()->load($config);
978
        $si = Injector::inst()->get(TestStaticInjections::class);
979
        $this->assertInstanceOf(TestStaticInjections::class, $si);
980
        $this->assertInstanceOf(NewRequirementsBackend::class, $si->backend);
981
        $this->assertInstanceOf(MyParentClass::class, Injector::inst()->get(MyParentClass::class));
982
        $this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class));
983
984
        // Test that nested injector values can be overridden
985
        Injector::nest();
986
        $this->nestingLevel++;
987
        Injector::inst()->unregisterObjects([
988
            TestStaticInjections::class,
989
            MyParentClass::class,
990
        ]);
991
        $newsi = Injector::inst()->get(TestStaticInjections::class);
992
        $newsi->backend = new InjectorTest\OriginalRequirementsBackend();
993
        Injector::inst()->registerService($newsi, TestStaticInjections::class);
994
        Injector::inst()->registerService(new InjectorTest\MyChildClass(), MyParentClass::class);
995
996
        // Check that these overridden values are retrievable
997
        $si = Injector::inst()->get(TestStaticInjections::class);
998
        $this->assertInstanceOf(TestStaticInjections::class, $si);
999
        $this->assertInstanceOf(OriginalRequirementsBackend::class, $si->backend);
1000
        $this->assertInstanceOf(MyParentClass::class, Injector::inst()->get(MyParentClass::class));
1001
        $this->assertInstanceOf(MyParentClass::class, Injector::inst()->get(MyChildClass::class));
1002
1003
        // Test that unnesting restores expected behaviour
1004
        Injector::unnest();
1005
        $this->nestingLevel--;
1006
        $si = Injector::inst()->get(TestStaticInjections::class);
1007
        $this->assertInstanceOf(TestStaticInjections::class, $si);
1008
        $this->assertInstanceOf(NewRequirementsBackend::class, $si->backend);
1009
        $this->assertInstanceOf(MyParentClass::class, Injector::inst()->get(MyParentClass::class));
1010
        $this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class));
1011
1012
        // Test reset of cache
1013
        Injector::inst()->unregisterObjects([
1014
            TestStaticInjections::class,
1015
            MyParentClass::class,
1016
        ]);
1017
        $si = Injector::inst()->get(TestStaticInjections::class);
1018
        $this->assertInstanceOf(TestStaticInjections::class, $si);
1019
        $this->assertInstanceOf(NewRequirementsBackend::class, $si->backend);
1020
        $this->assertInstanceOf(MyParentClass::class, Injector::inst()->get(MyParentClass::class));
1021
        $this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class));
1022
1023
        // Return to nestingLevel 0
1024
        Injector::unnest();
1025
        $this->nestingLevel--;
1026
    }
1027
}
1028