Completed
Pull Request — master (#278)
by Jáchym
03:53
created

OrmExtension::processEventManagers()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
rs 9.2
cc 4
eloc 7
nc 3
nop 0
1
<?php
2
3
/**
4
 * This file is part of the Kdyby (http://www.kdyby.org)
5
 *
6
 * Copyright (c) 2008 Filip Procházka ([email protected])
7
 *
8
 * For the full copyright and license information, please view the file license.txt that was distributed with this source code.
9
 */
10
11
namespace Kdyby\Doctrine\DI;
12
13
use Doctrine;
14
use Doctrine\Common\Proxy\AbstractProxyFactory;
15
use Kdyby;
16
use Kdyby\DoctrineCache\DI\Helpers as CacheHelpers;
17
use Nette;
18
use Nette\DI\Statement;
19
use Nette\PhpGenerator as Code;
20
use Nette\PhpGenerator\Method;
21
use Nette\Utils\Strings;
22
use Nette\Utils\Validators;
23
24
25
26
/**
27
 * @author Filip Procházka <[email protected]>
28
 */
29
class OrmExtension extends Nette\DI\CompilerExtension
30
{
31
32
	const ANNOTATION_DRIVER = 'annotations';
33
	const PHP_NAMESPACE = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*';
34
	const TAG_CONNECTION = 'doctrine.connection';
35
	const TAG_ENTITY_MANAGER = 'doctrine.entityManager';
36
	const TAG_BIND_TO_MANAGER = 'doctrine.bindToManager';
37
	const TAG_REPOSITORY_ENTITY = 'doctrine.repositoryEntity';
38
39
	/**
40
	 * @var array
41
	 */
42
	public $managerDefaults = [
43
		'metadataCache' => 'default',
44
		'queryCache' => 'default',
45
		'resultCache' => 'default',
46
		'hydrationCache' => 'default',
47
		'secondLevelCache' => [
48
			'enabled' => FALSE,
49
			'factoryClass' => 'Doctrine\ORM\Cache\DefaultCacheFactory',
50
			'driver' => 'default',
51
			'regions' => [
52
				'defaultLifetime' => 3600,
53
				'defaultLockLifetime' => 60,
54
			],
55
			'fileLockRegionDirectory' => '%tempDir%/cache/Doctrine.Cache.Locks', // todo fix
56
			'logging' => '%debugMode%',
57
		],
58
		'classMetadataFactory' => 'Kdyby\Doctrine\Mapping\ClassMetadataFactory',
59
		'defaultRepositoryClassName' => 'Kdyby\Doctrine\EntityRepository',
60
		'repositoryFactoryClassName' => 'Kdyby\Doctrine\RepositoryFactory',
61
		'queryBuilderClassName' => 'Kdyby\Doctrine\QueryBuilder',
62
		'autoGenerateProxyClasses' => '%debugMode%',
63
		'namingStrategy' => 'Doctrine\ORM\Mapping\UnderscoreNamingStrategy',
64
		'quoteStrategy' => 'Doctrine\ORM\Mapping\DefaultQuoteStrategy',
65
		'entityListenerResolver' => 'Kdyby\Doctrine\Mapping\EntityListenerResolver',
66
		'proxyDir' => '%tempDir%/proxies',
67
		'proxyNamespace' => 'Kdyby\GeneratedProxy',
68
		'dql' => ['string' => [], 'numeric' => [], 'datetime' => [], 'hints' => []],
69
		'hydrators' => [],
70
		'metadata' => [],
71
		'filters' => [],
72
		'namespaceAlias' => [],
73
		'targetEntityMappings' => [],
74
	];
75
76
	/**
77
	 * @var array
78
	 */
79
	public $connectionDefaults = [
80
		'dbname' => NULL,
81
		'host' => '127.0.0.1',
82
		'port' => NULL,
83
		'user' => NULL,
84
		'password' => NULL,
85
		'charset' => 'UTF8',
86
		'driver' => 'pdo_mysql',
87
		'driverClass' => NULL,
88
		'options' => NULL,
89
		'path' => NULL,
90
		'memory' => NULL,
91
		'unix_socket' => NULL,
92
		'logging' => '%debugMode%',
93
		'platformService' => NULL,
94
		'defaultTableOptions' => [],
95
		'resultCache' => 'default',
96
		'types' => [],
97
		'schemaFilter' => NULL,
98
	];
99
100
	/**
101
	 * @var array
102
	 */
103
	public $metadataDriverClasses = [
104
		self::ANNOTATION_DRIVER => 'Kdyby\Doctrine\Mapping\AnnotationDriver',
105
		'static' => 'Doctrine\Common\Persistence\Mapping\Driver\StaticPHPDriver',
106
		'yml' => 'Doctrine\ORM\Mapping\Driver\YamlDriver',
107
		'yaml' => 'Doctrine\ORM\Mapping\Driver\YamlDriver',
108
		'xml' => 'Doctrine\ORM\Mapping\Driver\XmlDriver',
109
		'db' => 'Doctrine\ORM\Mapping\Driver\DatabaseDriver',
110
	];
111
112
	/**
113
	 * @var array
114
	 */
115
	private $proxyAutoloaders = [];
116
117
	/**
118
	 * @var array
119
	 */
120
	private $targetEntityMappings = [];
121
122
	/**
123
	 * @var array
124
	 */
125
	private $configuredManagers = [];
126
127
	/**
128
	 * @var array
129
	 */
130
	private $managerConfigs = [];
131
132
	/**
133
	 * @var array
134
	 */
135
	private $configuredConnections = [];
136
137
	/**
138
	 * @var array
139
	 */
140
	private $postCompileRepositoriesQueue = [];
141
142
143
144
	public function loadConfiguration()
145
	{
146
		$this->proxyAutoloaders =
147
		$this->targetEntityMappings =
148
		$this->configuredConnections =
149
		$this->managerConfigs =
150
		$this->configuredManagers =
151
		$this->postCompileRepositoriesQueue = [];
152
153
		if (!$this->compiler->getExtensions('Kdyby\Annotations\DI\AnnotationsExtension')) {
154
			throw new Nette\Utils\AssertionException('You should register \'Kdyby\Annotations\DI\AnnotationsExtension\' before \'' . get_class($this) . '\'.', E_USER_NOTICE);
155
		}
156
157
		$builder = $this->getContainerBuilder();
158
		$config = $this->getConfig();
159
160
		$builder->parameters[$this->prefix('debug')] = !empty($config['debug']);
161
		if (isset($config['dbname']) || isset($config['driver']) || isset($config['connection'])) {
162
			$config = ['default' => $config];
163
			$defaults = ['debug' => $builder->parameters['debugMode']];
164
165
		} else {
166
			$defaults = array_intersect_key($config, $this->managerDefaults)
167
				+ array_intersect_key($config, $this->connectionDefaults)
168
				+ ['debug' => $builder->parameters['debugMode']];
169
170
			$config = array_diff_key($config, $defaults);
171
		}
172
173
		if (empty($config)) {
174
			throw new Kdyby\Doctrine\UnexpectedValueException("Please configure the Doctrine extensions using the section '{$this->name}:' in your config file.");
175
		}
176
177
		foreach ($config as $name => $emConfig) {
178
			if (!is_array($emConfig) || (empty($emConfig['dbname']) && empty($emConfig['driver']))) {
179
				throw new Kdyby\Doctrine\UnexpectedValueException("Please configure the Doctrine extensions using the section '{$this->name}:' in your config file.");
180
			}
181
182
			$emConfig = Nette\DI\Config\Helpers::merge($emConfig, $defaults);
183
			$this->processEntityManager($name, $emConfig);
0 ignored issues
show
Bug introduced by
It seems like $emConfig defined by \Nette\DI\Config\Helpers...e($emConfig, $defaults) on line 182 can also be of type string; however, Kdyby\Doctrine\DI\OrmExt...:processEntityManager() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
184
		}
185
186
		if ($this->targetEntityMappings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->targetEntityMappings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
187
			if (!$this->isKdybyEventsPresent()) {
188
				throw new Nette\Utils\AssertionException('The option \'targetEntityMappings\' requires \'Kdyby\Events\DI\EventsExtension\'.', E_USER_NOTICE);
189
			}
190
191
			$listener = $builder->addDefinition($this->prefix('resolveTargetEntityListener'))
192
				->setClass('Kdyby\Doctrine\Tools\ResolveTargetEntityListener')
193
				->addTag(Kdyby\Events\DI\EventsExtension::SUBSCRIBER_TAG)
0 ignored issues
show
Deprecated Code introduced by
The constant Kdyby\Events\DI\EventsExtension::SUBSCRIBER_TAG has been deprecated.

This class constant has been deprecated.

Loading history...
194
				->setInject(FALSE);
195
196
			foreach ($this->targetEntityMappings as $originalEntity => $mapping) {
197
				$listener->addSetup('addResolveTargetEntity', [$originalEntity, $mapping['targetEntity'], $mapping]);
198
			}
199
		}
200
201
		$this->loadConsole();
202
203
		$builder->addDefinition($this->prefix('registry'))
204
			->setClass('Kdyby\Doctrine\Registry', [
205
				$this->configuredConnections,
206
				$this->configuredManagers,
207
				$builder->parameters[$this->name]['dbal']['defaultConnection'],
208
				$builder->parameters[$this->name]['orm']['defaultEntityManager'],
209
			]);
210
	}
211
212
213
214
	protected function loadConsole()
215
	{
216
		$builder = $this->getContainerBuilder();
217
218
		foreach ($this->loadFromFile(__DIR__ . '/console.neon') as $i => $command) {
0 ignored issues
show
Bug introduced by
The expression $this->loadFromFile(__DIR__ . '/console.neon') of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
219
			$cli = $builder->addDefinition($this->prefix('cli.' . $i))
220
				->addTag(Kdyby\Console\DI\ConsoleExtension::COMMAND_TAG)
0 ignored issues
show
Deprecated Code introduced by
The constant Kdyby\Console\DI\ConsoleExtension::COMMAND_TAG has been deprecated.

This class constant has been deprecated.

Loading history...
221
				->setInject(FALSE); // lazy injects
222
223
			if (is_string($command)) {
224
				$cli->setClass($command);
225
226
			} else {
227
				throw new Kdyby\Doctrine\NotSupportedException;
228
			}
229
		}
230
	}
231
232
233
234
	protected function processEntityManager($name, array $defaults)
235
	{
236
		$builder = $this->getContainerBuilder();
237
		$config = $this->resolveConfig($defaults, $this->managerDefaults, $this->connectionDefaults);
238
239
		if ($isDefault = !isset($builder->parameters[$this->name]['orm']['defaultEntityManager'])) {
240
			$builder->parameters[$this->name]['orm']['defaultEntityManager'] = $name;
241
		}
242
243
		$metadataDriver = $builder->addDefinition($this->prefix($name . '.metadataDriver'))
244
			->setClass('Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain')
245
			->setAutowired(FALSE)
246
			->setInject(FALSE);
247
		/** @var Nette\DI\ServiceDefinition $metadataDriver */
248
249
		Validators::assertField($config, 'metadata', 'array');
250
		Validators::assertField($config, 'targetEntityMappings', 'array');
251
		$config['targetEntityMappings'] = $this->normalizeTargetEntityMappings($config['targetEntityMappings']);
252
		foreach ($this->compiler->getExtensions() as $extension) {
253
			if ($extension instanceof IEntityProvider) {
254
				$metadata = $extension->getEntityMappings();
255
				Validators::assert($metadata, 'array');
256
				$config['metadata'] = array_merge($config['metadata'], $metadata);
257
			}
258
259
			if ($extension instanceof ITargetEntityProvider) {
260
				$targetEntities = $extension->getTargetEntityMappings();
261
				Validators::assert($targetEntities, 'array');
262
				$config['targetEntityMappings'] = Nette\Utils\Arrays::mergeTree($config['targetEntityMappings'], $this->normalizeTargetEntityMappings($targetEntities));
263
			}
264
265
			if ($extension instanceof IDatabaseTypeProvider) {
266
				$providedTypes = $extension->getDatabaseTypes();
267
				Validators::assert($providedTypes, 'array');
268
269
				if (!isset($defaults['types'])) {
270
					$defaults['types'] = [];
271
				}
272
273
				$defaults['types'] = array_merge($defaults['types'], $providedTypes);
274
			}
275
		}
276
277
		foreach (self::natSortKeys($config['metadata']) as $namespace => $driver) {
278
			$this->processMetadataDriver($metadataDriver, $namespace, $driver, $name);
279
		}
280
281
		$this->processMetadataDriver($metadataDriver, 'Kdyby\\Doctrine', __DIR__ . '/../Entities', $name);
282
283
		if (empty($config['metadata'])) {
284
			$metadataDriver->addSetup('setDefaultDriver', [
285
				new Statement($this->metadataDriverClasses[self::ANNOTATION_DRIVER], [
286
					[$builder->expand('%appDir%')],
0 ignored issues
show
Deprecated Code introduced by
The method Nette\DI\ContainerBuilder::expand() has been deprecated.

This method has been deprecated.

Loading history...
287
					2 => $this->prefix('@cache.' . $name . '.metadata')
288
				])
289
			]);
290
		}
291
292
		if ($config['repositoryFactoryClassName'] === 'default') {
293
			$config['repositoryFactoryClassName'] = 'Doctrine\ORM\Repository\DefaultRepositoryFactory';
294
		}
295
		$builder->addDefinition($this->prefix($name . '.repositoryFactory'))
296
			->setClass($config['repositoryFactoryClassName'])
297
			->setAutowired(FALSE);
298
299
		Validators::assertField($config, 'namespaceAlias', 'array');
300
		Validators::assertField($config, 'hydrators', 'array');
301
		Validators::assertField($config, 'dql', 'array');
302
		Validators::assertField($config['dql'], 'string', 'array');
303
		Validators::assertField($config['dql'], 'numeric', 'array');
304
		Validators::assertField($config['dql'], 'datetime', 'array');
305
		Validators::assertField($config['dql'], 'hints', 'array');
306
307
		$autoGenerateProxyClasses = is_bool($config['autoGenerateProxyClasses'])
308
			? ($config['autoGenerateProxyClasses'] ? AbstractProxyFactory::AUTOGENERATE_ALWAYS : AbstractProxyFactory::AUTOGENERATE_NEVER)
309
			: $config['autoGenerateProxyClasses'];
310
311
		$configuration = $builder->addDefinition($this->prefix($name . '.ormConfiguration'))
312
			->setClass('Kdyby\Doctrine\Configuration')
313
			->addSetup('setMetadataCacheImpl', [$this->processCache($config['metadataCache'], $name . '.metadata')])
314
			->addSetup('setQueryCacheImpl', [$this->processCache($config['queryCache'], $name . '.query')])
315
			->addSetup('setResultCacheImpl', [$this->processCache($config['resultCache'], $name . '.ormResult')])
316
			->addSetup('setHydrationCacheImpl', [$this->processCache($config['hydrationCache'], $name . '.hydration')])
317
			->addSetup('setMetadataDriverImpl', [$this->prefix('@' . $name . '.metadataDriver')])
318
			->addSetup('setClassMetadataFactoryName', [$config['classMetadataFactory']])
319
			->addSetup('setDefaultRepositoryClassName', [$config['defaultRepositoryClassName']])
320
			->addSetup('setQueryBuilderClassName', [$config['queryBuilderClassName']])
321
			->addSetup('setRepositoryFactory', [$this->prefix('@' . $name . '.repositoryFactory')])
322
			->addSetup('setProxyDir', [$config['proxyDir']])
323
			->addSetup('setProxyNamespace', [$config['proxyNamespace']])
324
			->addSetup('setAutoGenerateProxyClasses', [$autoGenerateProxyClasses])
325
			->addSetup('setEntityNamespaces', [$config['namespaceAlias']])
326
			->addSetup('setCustomHydrationModes', [$config['hydrators']])
327
			->addSetup('setCustomStringFunctions', [$config['dql']['string']])
328
			->addSetup('setCustomNumericFunctions', [$config['dql']['numeric']])
329
			->addSetup('setCustomDatetimeFunctions', [$config['dql']['datetime']])
330
			->addSetup('setDefaultQueryHints', [$config['dql']['hints']])
331
			->addSetup('setNamingStrategy', CacheHelpers::filterArgs($config['namingStrategy']))
332
			->addSetup('setQuoteStrategy', CacheHelpers::filterArgs($config['quoteStrategy']))
333
			->addSetup('setEntityListenerResolver', CacheHelpers::filterArgs($config['entityListenerResolver']))
334
			->setAutowired(FALSE)
335
			->setInject(FALSE);
336
		/** @var Nette\DI\ServiceDefinition $configuration */
337
338
		$this->proxyAutoloaders[$config['proxyNamespace']] = $config['proxyDir'];
339
340
		$this->processSecondLevelCache($name, $config['secondLevelCache'], $isDefault);
341
342
		Validators::assertField($config, 'filters', 'array');
343
		foreach ($config['filters'] as $filterName => $filterClass) {
344
			$configuration->addSetup('addFilter', [$filterName, $filterClass]);
345
		}
346
347
		if ($config['targetEntityMappings']) {
348
			$configuration->addSetup('setTargetEntityMap', [array_map(function ($mapping) {
349
				return $mapping['targetEntity'];
350
			}, $config['targetEntityMappings'])]);
351
			$this->targetEntityMappings = Nette\Utils\Arrays::mergeTree($this->targetEntityMappings, $config['targetEntityMappings']);
352
		}
353
354
		if ($this->isKdybyEventsPresent()) {
355
			$builder->addDefinition($this->prefix($name . '.evm'))
356
				->setClass('Kdyby\Events\NamespacedEventManager', [Kdyby\Doctrine\Events::NS . '::'])
357
				->addSetup('$dispatchGlobalEvents', [TRUE]) // for BC
358
				->setAutowired(FALSE);
359
360
		} else {
361
			$builder->addDefinition($this->prefix($name . '.evm'))
362
				->setClass('Doctrine\Common\EventManager')
363
				->setAutowired(FALSE);
364
		}
365
366
		// entity manager
367
		$entityManager = $builder->addDefinition($managerServiceId = $this->prefix($name . '.entityManager'))
368
			->setClass('Kdyby\Doctrine\EntityManager')
369
			->setFactory('Kdyby\Doctrine\EntityManager::create', [
370
				$connectionService = $this->processConnection($name, $defaults, $isDefault),
371
				$this->prefix('@' . $name . '.ormConfiguration'),
372
				$this->prefix('@' . $name . '.evm'),
373
			])
374
			->addTag(self::TAG_ENTITY_MANAGER)
375
			->addTag('kdyby.doctrine.entityManager')
376
			->setAutowired($isDefault)
377
			->setInject(FALSE);
378
379
		if ($this->isTracyPresent()) {
380
			$entityManager->addSetup('?->bindEntityManager(?)', [$this->prefix('@' . $name . '.diagnosticsPanel'), '@self']);
381
		}
382
383
		$builder->addDefinition($this->prefix('repositoryFactory.' . $name . '.defaultRepositoryFactory'))
384
				->setClass($config['defaultRepositoryClassName'])
385
				->setImplement('Kdyby\Doctrine\DI\IRepositoryFactory')
386
				->setArguments([new Code\PhpLiteral('$entityManager'), new Code\PhpLiteral('$classMetadata')])
387
				->setParameters(['Doctrine\ORM\EntityManagerInterface entityManager', 'Doctrine\ORM\Mapping\ClassMetadata classMetadata'])
388
				->setAutowired(FALSE);
389
390
		$builder->addDefinition($this->prefix($name . '.schemaValidator'))
391
			->setClass('Doctrine\ORM\Tools\SchemaValidator', ['@' . $managerServiceId])
392
			->setAutowired($isDefault);
393
394
		$builder->addDefinition($this->prefix($name . '.schemaTool'))
395
			->setClass('Doctrine\ORM\Tools\SchemaTool', ['@' . $managerServiceId])
396
			->setAutowired($isDefault);
397
398
		$cacheCleaner = $builder->addDefinition($this->prefix($name . '.cacheCleaner'))
399
			->setClass('Kdyby\Doctrine\Tools\CacheCleaner', ['@' . $managerServiceId])
400
			->setAutowired($isDefault);
401
402
		$builder->addDefinition($this->prefix($name . '.schemaManager'))
403
			->setClass('Doctrine\DBAL\Schema\AbstractSchemaManager')
404
			->setFactory('@Kdyby\Doctrine\Connection::getSchemaManager')
405
			->setAutowired($isDefault);
406
407
		foreach ($this->compiler->getExtensions('Kdyby\Annotations\DI\AnnotationsExtension') as $extension) {
408
			/** @var Kdyby\Annotations\DI\AnnotationsExtension $extension */
409
			$cacheCleaner->addSetup('addCacheStorage', [$extension->prefix('@cache.annotations')]);
410
		}
411
412
		if ($isDefault) {
413
			$builder->addDefinition($this->prefix('helper.entityManager'))
414
				->setClass('Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper', ['@' . $managerServiceId])
415
				->addTag(Kdyby\Console\DI\ConsoleExtension::HELPER_TAG, 'em');
0 ignored issues
show
Documentation introduced by
'em' is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by
The constant Kdyby\Console\DI\ConsoleExtension::HELPER_TAG has been deprecated.

This class constant has been deprecated.

Loading history...
416
417
			$builder->addDefinition($this->prefix('helper.connection'))
418
				->setClass('Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper', [$connectionService])
419
				->addTag(Kdyby\Console\DI\ConsoleExtension::HELPER_TAG, 'db');
0 ignored issues
show
Documentation introduced by
'db' is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by
The constant Kdyby\Console\DI\ConsoleExtension::HELPER_TAG has been deprecated.

This class constant has been deprecated.

Loading history...
420
421
			$builder->addAlias($this->prefix('schemaValidator'), $this->prefix($name . '.schemaValidator'));
422
			$builder->addAlias($this->prefix('schemaTool'), $this->prefix($name . '.schemaTool'));
423
			$builder->addAlias($this->prefix('cacheCleaner'), $this->prefix($name . '.cacheCleaner'));
424
			$builder->addAlias($this->prefix('schemaManager'), $this->prefix($name . '.schemaManager'));
425
		}
426
427
		$this->configuredManagers[$name] = $managerServiceId;
428
		$this->managerConfigs[$name] = $config;
429
	}
430
431
432
433
	protected function processSecondLevelCache($name, array $config, $isDefault)
434
	{
435
		if (!$config['enabled']) {
436
			return;
437
		}
438
439
		$builder = $this->getContainerBuilder();
440
441
		$cacheFactory = $builder->addDefinition($this->prefix($name . '.cacheFactory'))
442
			->setClass('Doctrine\ORM\Cache\CacheFactory')
443
			->setFactory($config['factoryClass'], [
444
				$this->prefix('@' . $name . '.cacheRegionsConfiguration'),
445
				$this->processCache($config['driver'], $name . '.secondLevel'),
446
			])
447
			->setAutowired($isDefault);
448
449
		if ($config['factoryClass'] === $this->managerDefaults['secondLevelCache']['factoryClass']
450
			|| is_subclass_of($config['factoryClass'], $this->managerDefaults['secondLevelCache']['factoryClass'])
451
		) {
452
			$cacheFactory->addSetup('setFileLockRegionDirectory', [$config['fileLockRegionDirectory']]);
453
		}
454
455
		$builder->addDefinition($this->prefix($name . '.cacheRegionsConfiguration'))
456
			->setClass('Doctrine\ORM\Cache\RegionsConfiguration', [
457
				$config['regions']['defaultLifetime'],
458
				$config['regions']['defaultLockLifetime'],
459
			])
460
			->setAutowired($isDefault);
461
462
		$logger = $builder->addDefinition($this->prefix($name . '.cacheLogger'))
463
			->setClass('Doctrine\ORM\Cache\Logging\CacheLogger')
464
			->setFactory('Doctrine\ORM\Cache\Logging\CacheLoggerChain')
465
			->setAutowired(FALSE);
466
467
		if ($config['logging']) {
468
			$logger->addSetup('setLogger', [
469
				'statistics',
470
				new Statement('Doctrine\ORM\Cache\Logging\StatisticsCacheLogger')
471
			]);
472
		}
473
474
		$builder->addDefinition($cacheConfigName = $this->prefix($name . '.ormCacheConfiguration'))
475
			->setClass('Doctrine\ORM\Cache\CacheConfiguration')
476
			->addSetup('setCacheFactory', [$this->prefix('@' . $name . '.cacheFactory')])
477
			->addSetup('setCacheLogger', [$this->prefix('@' . $name . '.cacheLogger')])
478
			->setAutowired($isDefault);
479
480
		$configuration = $builder->getDefinition($this->prefix($name . '.ormConfiguration'));
481
		$configuration->addSetup('setSecondLevelCacheEnabled');
482
		$configuration->addSetup('setSecondLevelCacheConfiguration', ['@' . $cacheConfigName]);
483
	}
484
485
486
487
	protected function processConnection($name, array $defaults, $isDefault = FALSE)
488
	{
489
		$builder = $this->getContainerBuilder();
490
		$config = $this->resolveConfig($defaults, $this->connectionDefaults, $this->managerDefaults);
491
492
		if ($isDefault) {
493
			$builder->parameters[$this->name]['dbal']['defaultConnection'] = $name;
494
		}
495
496
		if (isset($defaults['connection'])) {
497
			return $this->prefix('@' . $defaults['connection'] . '.connection');
498
		}
499
500
		// config
501
		$configuration = $builder->addDefinition($this->prefix($name . '.dbalConfiguration'))
502
			->setClass('Doctrine\DBAL\Configuration')
503
			->addSetup('setResultCacheImpl', [$this->processCache($config['resultCache'], $name . '.dbalResult')])
504
			->addSetup('setSQLLogger', [new Statement('Doctrine\DBAL\Logging\LoggerChain')])
505
			->addSetup('setFilterSchemaAssetsExpression', [$config['schemaFilter']])
506
			->setAutowired(FALSE)
507
			->setInject(FALSE);
508
509
		// types
510
		Validators::assertField($config, 'types', 'array');
511
		$schemaTypes = $dbalTypes = [];
512
		foreach ($config['types'] as $dbType => $className) {
513
			$typeInst = Code\Helpers::createObject($className, []);
514
			/** @var Doctrine\DBAL\Types\Type $typeInst */
515
			$dbalTypes[$typeInst->getName()] = $className;
516
			$schemaTypes[$dbType] = $typeInst->getName();
517
		}
518
519
		// tracy panel
520
		if ($this->isTracyPresent()) {
521
			$builder->addDefinition($this->prefix($name . '.diagnosticsPanel'))
522
				->setClass('Kdyby\Doctrine\Diagnostics\Panel')
523
				->setAutowired(FALSE);
524
		}
525
526
		// connection
527
		$options = array_diff_key($config, array_flip(['types', 'resultCache', 'connection', 'logging']));
528
		$connection = $builder->addDefinition($connectionServiceId = $this->prefix($name . '.connection'))
529
			->setClass('Kdyby\Doctrine\Connection')
530
			->setFactory('Kdyby\Doctrine\Connection::create', [
531
				$options,
532
				$this->prefix('@' . $name . '.dbalConfiguration'),
533
				$this->prefix('@' . $name . '.evm'),
534
			])
535
			->addSetup('setSchemaTypes', [$schemaTypes])
536
			->addSetup('setDbalTypes', [$dbalTypes])
537
			->addTag(self::TAG_CONNECTION)
538
			->addTag('kdyby.doctrine.connection')
539
			->setAutowired($isDefault)
540
			->setInject(FALSE);
541
542
		if ($this->isTracyPresent()) {
543
			$connection->addSetup('$panel = ?->bindConnection(?)', [$this->prefix('@' . $name . '.diagnosticsPanel'), '@self']);
544
		}
545
546
		/** @var Nette\DI\ServiceDefinition $connection */
547
548
		$this->configuredConnections[$name] = $connectionServiceId;
549
550
		if (!is_bool($config['logging'])) {
551
			$fileLogger = new Statement('Kdyby\Doctrine\Diagnostics\FileLogger', [$builder->expand($config['logging'])]);
0 ignored issues
show
Deprecated Code introduced by
The method Nette\DI\ContainerBuilder::expand() has been deprecated.

This method has been deprecated.

Loading history...
552
			$configuration->addSetup('$service->getSQLLogger()->addLogger(?)', [$fileLogger]);
553
554
		} elseif ($config['logging']) {
555
			$connection->addSetup('?->enableLogging()', [new Code\PhpLiteral('$panel')]);
556
		}
557
558
		return $this->prefix('@' . $name . '.connection');
559
	}
560
561
562
563
	/**
564
	 * @param \Nette\DI\ServiceDefinition $metadataDriver
565
	 * @param string $namespace
566
	 * @param string|object $driver
567
	 * @param string $prefix
568
	 * @throws \Nette\Utils\AssertionException
569
	 * @return string
570
	 */
571
	protected function processMetadataDriver(Nette\DI\ServiceDefinition $metadataDriver, $namespace, $driver, $prefix)
572
	{
573
		if (!is_string($namespace) || !Strings::match($namespace, '#^' . self::PHP_NAMESPACE . '\z#')) {
574
			throw new Nette\Utils\AssertionException("The metadata namespace expects to be valid namespace, $namespace given.");
575
		}
576
		$namespace = ltrim($namespace, '\\');
577
578
		if (is_string($driver) || is_array($driver)) {
579
			$paths = is_array($driver) ? $driver : [$driver];
580
			foreach ($paths as $path) {
581
				if (($pos = strrpos($path, '*')) !== FALSE) {
582
					$path = substr($path, 0, $pos);
583
				}
584
585
				if (!file_exists($path)) {
586
					throw new Nette\Utils\AssertionException("The metadata path expects to be an existing directory, $path given.");
587
				}
588
			}
589
590
			$driver = new Statement(self::ANNOTATION_DRIVER, is_array($paths) ? $paths : [$paths]);
591
		}
592
593
		$impl = $driver instanceof \stdClass ? $driver->value : ($driver instanceof Statement ? $driver->getEntity() : (string) $driver);
594
		list($driver) = CacheHelpers::filterArgs($driver);
595
		/** @var Statement $driver */
596
597
		/** @var string $impl */
598
		if (isset($this->metadataDriverClasses[$impl])) {
599
			$driver = new Statement($this->metadataDriverClasses[$impl], $driver->arguments);
600
		}
601
602
		if (is_string($driver->getEntity()) && substr($driver->getEntity(), 0, 1) === '@') {
603
			$metadataDriver->addSetup('addDriver', [$driver->getEntity(), $namespace]);
604
			return $driver->getEntity();
605
		}
606
607
		if ($impl === self::ANNOTATION_DRIVER) {
608
			$driver->arguments = [
609
				Nette\Utils\Arrays::flatten($driver->arguments),
610
				2 => $this->prefix('@cache.' . $prefix . '.metadata')
611
			];
612
		}
613
614
		$serviceName = $this->prefix($prefix . '.driver.' . str_replace('\\', '_', $namespace) . '.' . str_replace('\\', '_', $impl) . 'Impl');
615
616
		$this->getContainerBuilder()->addDefinition($serviceName)
617
			->setClass('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver')
618
			->setFactory($driver->getEntity(), $driver->arguments)
619
			->setAutowired(FALSE)
620
			->setInject(FALSE);
621
622
		$metadataDriver->addSetup('addDriver', ['@' . $serviceName, $namespace]);
623
		return '@' . $serviceName;
624
	}
625
626
627
628
	/**
629
	 * @param string|\stdClass $cache
630
	 * @param string $suffix
631
	 * @return string
632
	 */
633
	protected function processCache($cache, $suffix)
634
	{
635
		return CacheHelpers::processCache($this, $cache, $suffix, $this->getContainerBuilder()->parameters[$this->prefix('debug')]);
636
	}
637
638
639
640
	public function beforeCompile()
641
	{
642
		$this->processRepositories();
643
		$this->processEventManagers();
644
	}
645
646
647
648
	protected function processRepositories()
649
	{
650
		$builder = $this->getContainerBuilder();
651
652
		$disabled = TRUE;
653
		foreach ($this->configuredManagers as $managerName => $_) {
654
			$factoryClassName = $builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))->getClass();
655
			if ($factoryClassName === 'Kdyby\Doctrine\RepositoryFactory' || in_array('Kdyby\Doctrine\RepositoryFactory', class_parents($factoryClassName), TRUE)) {
656
				$disabled = FALSE;
657
			}
658
		}
659
660
		if ($disabled) {
661
			return;
662
		}
663
664
		if (!method_exists($builder, 'findByType')) {
665
			foreach ($this->configuredManagers as $managerName => $_) {
666
				$builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))
667
					->addSetup('setServiceIdsMap', [[], $this->prefix('repositoryFactory.' . $managerName . '.defaultRepositoryFactory')]);
668
			}
669
670
			return;
671
		}
672
673
		$serviceMap = array_fill_keys(array_keys($this->configuredManagers), []);
674
		foreach ($builder->findByType('Doctrine\ORM\EntityRepository', FALSE) as $originalServiceName => $originalDef) {
0 ignored issues
show
Unused Code introduced by
The call to ContainerBuilder::findByType() has too many arguments starting with FALSE.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
675
			if (is_string($originalDef)) {
676
				$originalServiceName = $originalDef;
677
				$originalDef = $builder->getDefinition($originalServiceName);
678
			}
679
680
			if (strpos($originalServiceName, $this->name . '.') === 0) {
681
				continue; // ignore
682
			}
683
684
			$factory = $originalDef->getFactory() ? $originalDef->getFactory()->getEntity() : $originalDef->getClass();
685
			if (stripos($factory, '::getRepository') !== FALSE) {
686
				continue; // ignore
687
			}
688
689
			$factoryServiceName = $this->prefix('repositoryFactory.' . $originalServiceName);
690
			$factoryDef = $builder->addDefinition($factoryServiceName, $originalDef)
691
				->setImplement('Kdyby\Doctrine\DI\IRepositoryFactory')
692
				->setParameters(['Doctrine\ORM\EntityManagerInterface entityManager', 'Doctrine\ORM\Mapping\ClassMetadata classMetadata'])
693
				->setAutowired(FALSE);
694
			$factoryStatement = $factoryDef->getFactory() ?: new Statement($factoryDef->getClass());
695
			$factoryStatement->arguments[0] = new Code\PhpLiteral('$entityManager');
696
			$factoryStatement->arguments[1] = new Code\PhpLiteral('$classMetadata');
697
			$factoryDef->setArguments($factoryStatement->arguments);
698
699
			$boundManagers = $this->getServiceBoundManagers($originalDef);
700
			Validators::assert($boundManagers, 'list:1', 'bound manager');
701
702
			if ($boundEntity = $originalDef->getTag(self::TAG_REPOSITORY_ENTITY)) {
703
				if (!is_string($boundEntity) || !class_exists($boundEntity)) {
704
					throw new Nette\Utils\AssertionException(sprintf('The entity "%s" for repository "%s" cannot be autoloaded.', $boundEntity, $originalDef->getClass()));
705
				}
706
				$entityArgument = $boundEntity;
707
708
			} else {
709
				$entityArgument = new Code\PhpLiteral('"%entityName%"');
710
				$this->postCompileRepositoriesQueue[$boundManagers[0]][] = [ltrim($originalDef->getClass(), '\\'), $originalServiceName];
711
			}
712
713
			$builder->removeDefinition($originalServiceName);
714
			$builder->addDefinition($originalServiceName)
715
				->setClass($originalDef->getClass())
716
				->setFactory(sprintf('@%s::getRepository', $this->configuredManagers[$boundManagers[0]]), [$entityArgument]);
717
718
			$serviceMap[$boundManagers[0]][$originalDef->getClass()] = $factoryServiceName;
719
		}
720
721
		foreach ($this->configuredManagers as $managerName => $_) {
722
			$builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))
723
				->addSetup('setServiceIdsMap', [
724
					$serviceMap[$managerName],
725
					$this->prefix('repositoryFactory.' . $managerName . '.defaultRepositoryFactory')
726
				]);
727
		}
728
	}
729
730
731
732
	protected function processEventManagers()
733
	{
734
		$builder = $this->getContainerBuilder();
735
		$customEvmService = $builder->getByType('Doctrine\Common\EventManager');
736
		if ($this->isKdybyEventsPresent() || !$customEvmService) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $customEvmService of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
737
			return;
738
		}
739
740
		foreach ($this->configuredManagers as $managerName => $_) {
741
			$builder->getDefinition($this->prefix($managerName . '.evm'))->setFactory('@' . $customEvmService);
742
		}
743
	}
744
745
746
747
	/**
748
	 * @param Nette\DI\ServiceDefinition $def
749
	 * @return string[]
750
	 */
751
	protected function getServiceBoundManagers(Nette\DI\ServiceDefinition $def)
752
	{
753
		$builder = $this->getContainerBuilder();
754
		$boundManagers = $def->getTag(self::TAG_BIND_TO_MANAGER);
755
756
		return is_array($boundManagers) ? $boundManagers : [$builder->parameters[$this->name]['orm']['defaultEntityManager']];
757
	}
758
759
760
761
	public function afterCompile(Code\ClassType $class)
762
	{
763
		$init = $class->getMethod('initialize');
764
765
		if ($this->isTracyPresent()) {
766
			$init->addBody('Kdyby\Doctrine\Diagnostics\Panel::registerBluescreen($this);');
767
			$this->addCollapsePathsToTracy($init);
768
		}
769
770
		foreach ($this->proxyAutoloaders as $namespace => $dir) {
771
			$originalInitialize = $init->getBody();
772
			$init->setBody('Kdyby\Doctrine\Proxy\ProxyAutoloader::create(?, ?)->register();', [$dir, $namespace]);
773
			$init->addBody($originalInitialize);
774
		}
775
776
		$this->processRepositoryFactoryEntities($class);
777
	}
778
779
780
781
	protected function processRepositoryFactoryEntities(Code\ClassType $class)
782
	{
783
		if (empty($this->postCompileRepositoriesQueue)) {
784
			return;
785
		}
786
787
		$dic = self::evalAndInstantiateContainer($class);
788
789
		foreach ($this->postCompileRepositoriesQueue as $manager => $items) {
790
			$config = $this->managerConfigs[$manager];
791
			/** @var Kdyby\Doctrine\EntityManager $entityManager */
792
			$entityManager = $dic->getService($this->configuredManagers[$manager]);
793
			/** @var Doctrine\ORM\Mapping\ClassMetadata $entityMetadata */
794
			$metadataFactory = $entityManager->getMetadataFactory();
795
796
			$allMetadata = [];
797
			foreach ($metadataFactory->getAllMetadata() as $entityMetadata) {
798
				if ($config['defaultRepositoryClassName'] === $entityMetadata->customRepositoryClassName || empty($entityMetadata->customRepositoryClassName)) {
799
					continue;
800
				}
801
802
				$allMetadata[ltrim($entityMetadata->customRepositoryClassName, '\\')] = $entityMetadata;
803
			}
804
805
			foreach ($items as $item) {
806
				if (!isset($allMetadata[$item[0]])) {
807
					throw new Nette\Utils\AssertionException(sprintf('Repository class %s have been found in DIC, but no entity has it assigned and it has no entity configured', $item[0]));
808
				}
809
810
				$entityMetadata = $allMetadata[$item[0]];
811
				$serviceMethod = Nette\DI\Container::getMethodName($item[1]);
812
813
				$method = $class->getMethod($serviceMethod);
814
				$methodBody = $method->getBody();
815
				$method->setBody(str_replace('"%entityName%"', Code\Helpers::format('?', $entityMetadata->getName()), $methodBody));
816
			}
817
		}
818
	}
819
820
821
822
	/**
823
	 * @param Code\ClassType $class
824
	 * @return Nette\DI\Container
825
	 */
826
	private static function evalAndInstantiateContainer(Code\ClassType $class)
827
	{
828
		$classCopy = clone $class;
829
		$classCopy->setName($className = 'Kdyby_Doctrine_IamTheKingOfHackingNette_' . $class->getName() . '_' . rand());
830
831
		$containerCode = "$classCopy";
832
833
		return call_user_func(function () use ($className, $containerCode) {
834
			eval($containerCode);
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
835
			return new $className();
836
		});
837
	}
838
839
840
841
	/**
842
	 * @param $provided
843
	 * @param $defaults
844
	 * @param $diff
845
	 * @return array
846
	 */
847
	private function resolveConfig(array $provided, array $defaults, array $diff = [])
848
	{
849
		return $this->getContainerBuilder()->expand(Nette\DI\Config\Helpers::merge(
0 ignored issues
show
Deprecated Code introduced by
The method Nette\DI\ContainerBuilder::expand() has been deprecated.

This method has been deprecated.

Loading history...
850
			array_diff_key($provided, array_diff_key($diff, $defaults)),
851
			$defaults
852
		));
853
	}
854
855
856
	/**
857
	 * @param array $targetEntityMappings
858
	 * @return array
859
	 */
860
	private function normalizeTargetEntityMappings(array $targetEntityMappings)
861
	{
862
		$normalized = [];
863
		foreach ($targetEntityMappings as $originalEntity => $targetEntity) {
864
			$originalEntity = ltrim($originalEntity, '\\');
865
			Validators::assert($targetEntity, 'array|string');
866
			if (is_array($targetEntity)) {
867
				Validators::assertField($targetEntity, 'targetEntity', 'string');
868
				$mapping = array_merge($targetEntity, [
869
					'targetEntity' => ltrim($targetEntity['targetEntity'], '\\')
870
				]);
871
872
			} else {
873
				$mapping = [
874
					'targetEntity' => ltrim($targetEntity, '\\'),
875
				];
876
			}
877
			$normalized[$originalEntity] = $mapping;
878
		}
879
		return $normalized;
880
	}
881
882
883
884
	/**
885
	 * @return bool
886
	 */
887
	private function isTracyPresent()
888
	{
889
		return interface_exists('Tracy\IBarPanel');
890
	}
891
892
893
894
	/**
895
	 * @return bool
896
	 */
897
	private function isKdybyEventsPresent()
898
	{
899
		return (bool) $this->compiler->getExtensions('Kdyby\Events\DI\EventsExtension');
900
	}
901
902
903
904
	private function addCollapsePathsToTracy(Method $init)
905
	{
906
		$blueScreen = 'Tracy\Debugger::getBlueScreen()';
907
		$commonDirname = dirname(Nette\Reflection\ClassType::from('Doctrine\Common\Version')->getFileName());
908
909
		$init->addBody($blueScreen . '->collapsePaths[] = ?;', [dirname(Nette\Reflection\ClassType::from('Kdyby\Doctrine\Exception')->getFileName())]);
910
		$init->addBody($blueScreen . '->collapsePaths[] = ?;', [dirname(dirname(dirname(dirname($commonDirname))))]); // this should be vendor/doctrine
911
		foreach ($this->proxyAutoloaders as $dir) {
912
			$init->addBody($blueScreen . '->collapsePaths[] = ?;', [$dir]);
913
		}
914
	}
915
916
917
918
	/**
919
	 * @param \Nette\Configurator $configurator
920
	 */
921
	public static function register(Nette\Configurator $configurator)
922
	{
923
		$configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) {
924
			$compiler->addExtension('doctrine', new OrmExtension());
925
		};
926
	}
927
928
929
930
	/**
931
	 * @param array $array
932
	 */
933
	private static function natSortKeys(array &$array)
934
	{
935
		$keys = array_keys($array);
936
		natsort($keys);
937
		$keys = array_flip(array_reverse($keys, TRUE));
938
		$array = array_merge($keys, $array);
939
		return $array;
940
	}
941
942
}
943