1<?php
2
3/**
4 * @file
5 * Contains \Drupal\Tests\Core\Entity\EntityTypeManagerTest.
6 */
7
8namespace Drupal\Tests\Core\Entity;
9
10use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
11use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
12use Drupal\Component\Plugin\Exception\PluginNotFoundException;
13use Drupal\Core\Cache\CacheBackendInterface;
14use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
15use Drupal\Core\Entity\EntityHandlerBase;
16use Drupal\Core\Entity\EntityInterface;
17use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
18use Drupal\Core\Entity\EntityManagerInterface;
19use Drupal\Core\Entity\EntityTypeInterface;
20use Drupal\Core\Entity\EntityTypeManager;
21use Drupal\Core\Entity\EntityTypeManagerInterface;
22use Drupal\Core\Entity\Exception\InvalidLinkTemplateException;
23use Drupal\Core\Extension\ModuleHandlerInterface;
24use Drupal\Core\StringTranslation\TranslationInterface;
25use Drupal\Tests\UnitTestCase;
26use Prophecy\Argument;
27use Symfony\Component\DependencyInjection\ContainerInterface;
28
29/**
30 * @coversDefaultClass \Drupal\Core\Entity\EntityTypeManager
31 * @group Entity
32 */
33class EntityTypeManagerTest extends UnitTestCase {
34
35  /**
36   * The entity type manager under test.
37   *
38   * @var \Drupal\Core\Entity\EntityTypeManager
39   */
40  protected $entityTypeManager;
41
42  /**
43   * The translation manager.
44   *
45   * @var \Drupal\Core\StringTranslation\TranslationInterface|\Prophecy\Prophecy\ProphecyInterface
46   */
47  protected $translationManager;
48
49  /**
50   * The plugin discovery.
51   *
52   * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface|\Prophecy\Prophecy\ProphecyInterface
53   */
54  protected $discovery;
55
56  /**
57   * The module handler.
58   *
59   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface
60   */
61  protected $moduleHandler;
62
63  /**
64   * The cache backend.
65   *
66   * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ProphecyInterface
67   */
68  protected $cacheBackend;
69
70  /**
71   * The entity last installed schema repository.
72   *
73   * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface|\Prophecy\Prophecy\ProphecyInterface
74   */
75  protected $entityLastInstalledSchemaRepository;
76
77  /**
78   * {@inheritdoc}
79   */
80  protected function setUp() {
81    parent::setUp();
82
83    $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
84    $this->moduleHandler->getImplementations('entity_type_build')->willReturn([]);
85    $this->moduleHandler->alter('entity_type', Argument::type('array'))->willReturn(NULL);
86
87    $this->cacheBackend = $this->prophesize(CacheBackendInterface::class);
88    $this->translationManager = $this->prophesize(TranslationInterface::class);
89    $this->entityLastInstalledSchemaRepository = $this->prophesize(EntityLastInstalledSchemaRepositoryInterface::class);
90
91    $this->entityTypeManager = new TestEntityTypeManager(new \ArrayObject(), $this->moduleHandler->reveal(), $this->cacheBackend->reveal(), $this->translationManager->reveal(), $this->getClassResolverStub(), $this->entityLastInstalledSchemaRepository->reveal());
92    $this->discovery = $this->prophesize(DiscoveryInterface::class);
93    $this->entityTypeManager->setDiscovery($this->discovery->reveal());
94  }
95
96  /**
97   * Sets up the entity type manager to be tested.
98   *
99   * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions
100   *   (optional) An array of entity type definitions.
101   */
102  protected function setUpEntityTypeDefinitions($definitions = []) {
103    $class = $this->getMockClass(EntityInterface::class);
104    foreach ($definitions as $key => $entity_type) {
105      // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called
106      // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must
107      // always be mocked.
108      $entity_type->getLinkTemplates()->willReturn([]);
109
110      // Give the entity type a legitimate class to return.
111      $entity_type->getClass()->willReturn($class);
112      $entity_type->setClass($class)->willReturn($entity_type->reveal());
113
114      $definitions[$key] = $entity_type->reveal();
115    }
116
117    $this->discovery->getDefinition(Argument::cetera())
118      ->will(function ($args) use ($definitions) {
119        $entity_type_id = $args[0];
120        $exception_on_invalid = $args[1];
121        if (isset($definitions[$entity_type_id])) {
122          return $definitions[$entity_type_id];
123        }
124        elseif (!$exception_on_invalid) {
125          return NULL;
126        }
127        else {
128          throw new PluginNotFoundException($entity_type_id);
129        }
130      });
131    $this->discovery->getDefinitions()->willReturn($definitions);
132
133  }
134
135  /**
136   * Tests the hasHandler() method.
137   *
138   * @covers ::hasHandler
139   *
140   * @dataProvider providerTestHasHandler
141   */
142  public function testHasHandler($entity_type_id, $expected) {
143    $apple = $this->prophesize(EntityTypeInterface::class);
144    $apple->hasHandlerClass('storage')->willReturn(TRUE);
145
146    $banana = $this->prophesize(EntityTypeInterface::class);
147    $banana->hasHandlerClass('storage')->willReturn(FALSE);
148
149    $this->setUpEntityTypeDefinitions([
150      'apple' => $apple,
151      'banana' => $banana,
152    ]);
153
154    $entity_type = $this->entityTypeManager->hasHandler($entity_type_id, 'storage');
155    $this->assertSame($expected, $entity_type);
156  }
157
158  /**
159   * Provides test data for testHasHandler().
160   *
161   * @return array
162   *   Test data.
163   */
164  public function providerTestHasHandler() {
165    return [
166      ['apple', TRUE],
167      ['banana', FALSE],
168      ['pear', FALSE],
169    ];
170  }
171
172  /**
173   * Tests the getStorage() method.
174   *
175   * @covers ::getStorage
176   */
177  public function testGetStorage() {
178    $class = $this->getTestHandlerClass();
179    $entity = $this->prophesize(EntityTypeInterface::class);
180    $entity->getHandlerClass('storage')->willReturn($class);
181    $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]);
182
183    $this->assertInstanceOf($class, $this->entityTypeManager->getStorage('test_entity_type'));
184  }
185
186  /**
187   * Tests the getListBuilder() method.
188   *
189   * @covers ::getListBuilder
190   */
191  public function testGetListBuilder() {
192    $class = $this->getTestHandlerClass();
193    $entity = $this->prophesize(EntityTypeInterface::class);
194    $entity->getHandlerClass('list_builder')->willReturn($class);
195    $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]);
196
197    $this->assertInstanceOf($class, $this->entityTypeManager->getListBuilder('test_entity_type'));
198  }
199
200  /**
201   * Tests the getViewBuilder() method.
202   *
203   * @covers ::getViewBuilder
204   */
205  public function testGetViewBuilder() {
206    $class = $this->getTestHandlerClass();
207    $entity = $this->prophesize(EntityTypeInterface::class);
208    $entity->getHandlerClass('view_builder')->willReturn($class);
209    $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]);
210
211    $this->assertInstanceOf($class, $this->entityTypeManager->getViewBuilder('test_entity_type'));
212  }
213
214  /**
215   * Tests the getAccessControlHandler() method.
216   *
217   * @covers ::getAccessControlHandler
218   */
219  public function testGetAccessControlHandler() {
220    $class = $this->getTestHandlerClass();
221    $entity = $this->prophesize(EntityTypeInterface::class);
222    $entity->getHandlerClass('access')->willReturn($class);
223    $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]);
224
225    $this->assertInstanceOf($class, $this->entityTypeManager->getAccessControlHandler('test_entity_type'));
226  }
227
228  /**
229   * Tests the getFormObject() method.
230   *
231   * @covers ::getFormObject
232   */
233  public function testGetFormObject() {
234    $apple = $this->prophesize(EntityTypeInterface::class);
235    $apple->getFormClass('default')->willReturn(TestEntityForm::class);
236
237    $banana = $this->prophesize(EntityTypeInterface::class);
238    $banana->getFormClass('default')->willReturn(TestEntityFormInjected::class);
239
240    $this->setUpEntityTypeDefinitions([
241      'apple' => $apple,
242      'banana' => $banana,
243    ]);
244
245    $apple_form = $this->entityTypeManager->getFormObject('apple', 'default');
246    $this->assertInstanceOf(TestEntityForm::class, $apple_form);
247    $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_form);
248    $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_form);
249
250    $banana_form = $this->entityTypeManager->getFormObject('banana', 'default');
251    $this->assertInstanceOf(TestEntityFormInjected::class, $banana_form);
252    $this->assertAttributeEquals('yellow', 'color', $banana_form);
253
254  }
255
256  /**
257   * Tests the getFormObject() method with an invalid operation.
258   *
259   * @covers ::getFormObject
260   */
261  public function testGetFormObjectInvalidOperation() {
262    $entity = $this->prophesize(EntityTypeInterface::class);
263    $entity->getFormClass('edit')->willReturn('');
264    $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]);
265
266    $this->expectException(InvalidPluginDefinitionException::class);
267    $this->entityTypeManager->getFormObject('test_entity_type', 'edit');
268  }
269
270  /**
271   * Tests the getHandler() method.
272   *
273   * @covers ::getHandler
274   */
275  public function testGetHandler() {
276    $class = $this->getTestHandlerClass();
277    $apple = $this->prophesize(EntityTypeInterface::class);
278    $apple->getHandlerClass('storage')->willReturn($class);
279
280    $this->setUpEntityTypeDefinitions([
281      'apple' => $apple,
282    ]);
283
284    $apple_controller = $this->entityTypeManager->getHandler('apple', 'storage');
285    $this->assertInstanceOf($class, $apple_controller);
286    $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_controller);
287    $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_controller);
288  }
289
290  /**
291   * Tests the getHandler() method when no controller is defined.
292   *
293   * @covers ::getHandler
294   */
295  public function testGetHandlerMissingHandler() {
296    $entity = $this->prophesize(EntityTypeInterface::class);
297    $entity->getHandlerClass('storage')->willReturn('');
298    $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]);
299    $this->expectException(InvalidPluginDefinitionException::class);
300    $this->entityTypeManager->getHandler('test_entity_type', 'storage');
301  }
302
303  /**
304   * @covers ::getRouteProviders
305   */
306  public function testGetRouteProviders() {
307    $apple = $this->prophesize(EntityTypeInterface::class);
308    $apple->getRouteProviderClasses()->willReturn(['default' => TestRouteProvider::class]);
309
310    $this->setUpEntityTypeDefinitions([
311      'apple' => $apple,
312    ]);
313
314    $apple_route_provider = $this->entityTypeManager->getRouteProviders('apple');
315    $this->assertInstanceOf(TestRouteProvider::class, $apple_route_provider['default']);
316    $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_route_provider['default']);
317    $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_route_provider['default']);
318  }
319
320  /**
321   * Tests the processDefinition() method.
322   *
323   * @covers ::processDefinition
324   */
325  public function testProcessDefinition() {
326    $apple = $this->prophesize(EntityTypeInterface::class);
327    $this->setUpEntityTypeDefinitions(['apple' => $apple]);
328
329    $apple->getLinkTemplates()->willReturn(['canonical' => 'path/to/apple']);
330
331    $definition = $apple->reveal();
332    $this->expectException(InvalidLinkTemplateException::class);
333    $this->expectExceptionMessage("Link template 'canonical' for entity type 'apple' must start with a leading slash, the current link template is 'path/to/apple'");
334    $this->entityTypeManager->processDefinition($definition, 'apple');
335  }
336
337  /**
338   * Tests the getDefinition() method.
339   *
340   * @covers ::getDefinition
341   *
342   * @dataProvider providerTestGetDefinition
343   */
344  public function testGetDefinition($entity_type_id, $expected) {
345    $entity = $this->prophesize(EntityTypeInterface::class);
346
347    $this->setUpEntityTypeDefinitions([
348      'apple' => $entity,
349      'banana' => $entity,
350    ]);
351
352    $entity_type = $this->entityTypeManager->getDefinition($entity_type_id, FALSE);
353    if ($expected) {
354      $this->assertInstanceOf(EntityTypeInterface::class, $entity_type);
355    }
356    else {
357      $this->assertNull($entity_type);
358    }
359  }
360
361  /**
362   * Provides test data for testGetDefinition().
363   *
364   * @return array
365   *   Test data.
366   */
367  public function providerTestGetDefinition() {
368    return [
369      ['apple', TRUE],
370      ['banana', TRUE],
371      ['pear', FALSE],
372    ];
373  }
374
375  /**
376   * Tests the getDefinition() method with an invalid definition.
377   *
378   * @covers ::getDefinition
379   */
380  public function testGetDefinitionInvalidException() {
381    $this->setUpEntityTypeDefinitions();
382
383    $this->expectException(PluginNotFoundException::class);
384    $this->expectExceptionMessage('The "pear" entity type does not exist.');
385    $this->entityTypeManager->getDefinition('pear', TRUE);
386  }
387
388  /**
389   * Gets a mock controller class name.
390   *
391   * @return string
392   *   A mock controller class name.
393   */
394  protected function getTestHandlerClass() {
395    return get_class($this->getMockForAbstractClass(EntityHandlerBase::class));
396  }
397
398}
399
400class TestEntityTypeManager extends EntityTypeManager {
401
402  /**
403   * Sets the discovery for the manager.
404   *
405   * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery
406   *   The discovery object.
407   */
408  public function setDiscovery(DiscoveryInterface $discovery) {
409    $this->discovery = $discovery;
410  }
411
412}
413
414/**
415 * Provides a test entity form.
416 */
417class TestEntityForm extends EntityHandlerBase {
418
419  /**
420   * The entity manager.
421   *
422   * @var \Drupal\Core\Entity\EntityManagerInterface
423   */
424  protected $entityManager;
425
426  /**
427   * The entity type manager.
428   *
429   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
430   */
431  protected $entityTypeManager;
432
433  /**
434   * {@inheritdoc}
435   */
436  public function getBaseFormId() {
437    return 'the_base_form_id';
438  }
439
440  /**
441   * {@inheritdoc}
442   */
443  public function getFormId() {
444    return 'the_form_id';
445  }
446
447  /**
448   * {@inheritdoc}
449   */
450  public function setEntity(EntityInterface $entity) {
451    return $this;
452  }
453
454  /**
455   * {@inheritdoc}
456   */
457  public function setOperation($operation) {
458    return $this;
459  }
460
461  /**
462   * {@inheritdoc}
463   */
464  public function setEntityManager(EntityManagerInterface $entity_manager) {
465    $this->entityManager = $entity_manager;
466    return $this;
467  }
468
469  /**
470   * {@inheritdoc}
471   */
472  public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
473    $this->entityTypeManager = $entity_type_manager;
474    return $this;
475  }
476
477}
478
479/**
480 * Provides a test entity form that uses injection.
481 */
482class TestEntityFormInjected extends TestEntityForm implements ContainerInjectionInterface {
483
484  /**
485   * The color of the entity type.
486   *
487   * @var string
488   */
489  protected $color;
490
491  /**
492   * Constructs a new TestEntityFormInjected.
493   *
494   * @param string $color
495   *   The color of the entity type.
496   */
497  public function __construct($color) {
498    $this->color = $color;
499  }
500
501  /**
502   * {@inheritdoc}
503   */
504  public static function create(ContainerInterface $container) {
505    return new static('yellow');
506  }
507
508}
509
510/**
511 * Provides a test entity route provider.
512 */
513class TestRouteProvider extends EntityHandlerBase {
514
515}
516