Completed
Pull Request — master (#266)
by Jáchym
03:21
created

OrmExtension::isKdybyEventsPresent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
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
		$entityManagerArguments = [
355
			$connectionService = $this->processConnection($name, $defaults, $isDefault),
356
			$this->prefix('@' . $name . '.ormConfiguration'),
357
		];
358
359
		if ($this->isKdybyEventsPresent()) {
360
			$builder->addDefinition($this->prefix($name . '.evm'))
361
				->setClass('Kdyby\Events\NamespacedEventManager', [Kdyby\Doctrine\Events::NS . '::'])
362
				->addSetup('$dispatchGlobalEvents', [TRUE]) // for BC
363
				->setAutowired(FALSE);
364
365
			$entityManagerArguments[] = $this->prefix('@' . $name . '.evm');
366
		}
367
368
		// entity manager
369
		$entityManager = $builder->addDefinition($managerServiceId = $this->prefix($name . '.entityManager'))
370
			->setClass('Kdyby\Doctrine\EntityManager')
371
			->setFactory('Kdyby\Doctrine\EntityManager::create', $entityManagerArguments)
372
			->addTag(self::TAG_ENTITY_MANAGER)
373
			->addTag('kdyby.doctrine.entityManager')
374
			->setAutowired($isDefault)
375
			->setInject(FALSE);
376
377
		if ($this->isTracyPresent()) {
378
			$entityManager->addSetup('?->bindEntityManager(?)', [$this->prefix('@' . $name . '.diagnosticsPanel'), '@self']);
379
		}
380
381
		$builder->addDefinition($this->prefix('repositoryFactory.' . $name . '.defaultRepositoryFactory'))
382
				->setClass($config['defaultRepositoryClassName'])
383
				->setImplement('Kdyby\Doctrine\DI\IRepositoryFactory')
384
				->setArguments([new Code\PhpLiteral('$entityManager'), new Code\PhpLiteral('$classMetadata')])
385
				->setParameters(['Doctrine\ORM\EntityManagerInterface entityManager', 'Doctrine\ORM\Mapping\ClassMetadata classMetadata'])
386
				->setAutowired(FALSE);
387
388
		$builder->addDefinition($this->prefix($name . '.schemaValidator'))
389
			->setClass('Doctrine\ORM\Tools\SchemaValidator', ['@' . $managerServiceId])
390
			->setAutowired($isDefault);
391
392
		$builder->addDefinition($this->prefix($name . '.schemaTool'))
393
			->setClass('Doctrine\ORM\Tools\SchemaTool', ['@' . $managerServiceId])
394
			->setAutowired($isDefault);
395
396
		$cacheCleaner = $builder->addDefinition($this->prefix($name . '.cacheCleaner'))
397
			->setClass('Kdyby\Doctrine\Tools\CacheCleaner', ['@' . $managerServiceId])
398
			->setAutowired($isDefault);
399
400
		$builder->addDefinition($this->prefix($name . '.schemaManager'))
401
			->setClass('Doctrine\DBAL\Schema\AbstractSchemaManager')
402
			->setFactory('@Kdyby\Doctrine\Connection::getSchemaManager')
403
			->setAutowired($isDefault);
404
405
		foreach ($this->compiler->getExtensions('Kdyby\Annotations\DI\AnnotationsExtension') as $extension) {
406
			/** @var Kdyby\Annotations\DI\AnnotationsExtension $extension */
407
			$cacheCleaner->addSetup('addCacheStorage', [$extension->prefix('@cache.annotations')]);
408
		}
409
410
		if ($isDefault) {
411
			$builder->addDefinition($this->prefix('helper.entityManager'))
412
				->setClass('Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper', ['@' . $managerServiceId])
413
				->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...
414
415
			$builder->addDefinition($this->prefix('helper.connection'))
416
				->setClass('Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper', [$connectionService])
417
				->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...
418
419
			$builder->addAlias($this->prefix('schemaValidator'), $this->prefix($name . '.schemaValidator'));
420
			$builder->addAlias($this->prefix('schemaTool'), $this->prefix($name . '.schemaTool'));
421
			$builder->addAlias($this->prefix('cacheCleaner'), $this->prefix($name . '.cacheCleaner'));
422
			$builder->addAlias($this->prefix('schemaManager'), $this->prefix($name . '.schemaManager'));
423
		}
424
425
		$this->configuredManagers[$name] = $managerServiceId;
426
		$this->managerConfigs[$name] = $config;
427
	}
428
429
430
431
	protected function processSecondLevelCache($name, array $config, $isDefault)
432
	{
433
		if (!$config['enabled']) {
434
			return;
435
		}
436
437
		$builder = $this->getContainerBuilder();
438
439
		$cacheFactory = $builder->addDefinition($this->prefix($name . '.cacheFactory'))
440
			->setClass('Doctrine\ORM\Cache\CacheFactory')
441
			->setFactory($config['factoryClass'], [
442
				$this->prefix('@' . $name . '.cacheRegionsConfiguration'),
443
				$this->processCache($config['driver'], $name . '.secondLevel'),
444
			])
445
			->setAutowired($isDefault);
446
447
		if ($config['factoryClass'] === $this->managerDefaults['secondLevelCache']['factoryClass']
448
			|| is_subclass_of($config['factoryClass'], $this->managerDefaults['secondLevelCache']['factoryClass'])
449
		) {
450
			$cacheFactory->addSetup('setFileLockRegionDirectory', [$config['fileLockRegionDirectory']]);
451
		}
452
453
		$builder->addDefinition($this->prefix($name . '.cacheRegionsConfiguration'))
454
			->setClass('Doctrine\ORM\Cache\RegionsConfiguration', [
455
				$config['regions']['defaultLifetime'],
456
				$config['regions']['defaultLockLifetime'],
457
			])
458
			->setAutowired($isDefault);
459
460
		$logger = $builder->addDefinition($this->prefix($name . '.cacheLogger'))
461
			->setClass('Doctrine\ORM\Cache\Logging\CacheLogger')
462
			->setFactory('Doctrine\ORM\Cache\Logging\CacheLoggerChain')
463
			->setAutowired(FALSE);
464
465
		if ($config['logging']) {
466
			$logger->addSetup('setLogger', [
467
				'statistics',
468
				new Statement('Doctrine\ORM\Cache\Logging\StatisticsCacheLogger')
469
			]);
470
		}
471
472
		$builder->addDefinition($cacheConfigName = $this->prefix($name . '.ormCacheConfiguration'))
473
			->setClass('Doctrine\ORM\Cache\CacheConfiguration')
474
			->addSetup('setCacheFactory', [$this->prefix('@' . $name . '.cacheFactory')])
475
			->addSetup('setCacheLogger', [$this->prefix('@' . $name . '.cacheLogger')])
476
			->setAutowired($isDefault);
477
478
		$configuration = $builder->getDefinition($this->prefix($name . '.ormConfiguration'));
479
		$configuration->addSetup('setSecondLevelCacheEnabled');
480
		$configuration->addSetup('setSecondLevelCacheConfiguration', ['@' . $cacheConfigName]);
481
	}
482
483
484
485
	protected function processConnection($name, array $defaults, $isDefault = FALSE)
486
	{
487
		$builder = $this->getContainerBuilder();
488
		$config = $this->resolveConfig($defaults, $this->connectionDefaults, $this->managerDefaults);
489
490
		if ($isDefault) {
491
			$builder->parameters[$this->name]['dbal']['defaultConnection'] = $name;
492
		}
493
494
		if (isset($defaults['connection'])) {
495
			return $this->prefix('@' . $defaults['connection'] . '.connection');
496
		}
497
498
		// config
499
		$configuration = $builder->addDefinition($this->prefix($name . '.dbalConfiguration'))
500
			->setClass('Doctrine\DBAL\Configuration')
501
			->addSetup('setResultCacheImpl', [$this->processCache($config['resultCache'], $name . '.dbalResult')])
502
			->addSetup('setSQLLogger', [new Statement('Doctrine\DBAL\Logging\LoggerChain')])
503
			->addSetup('setFilterSchemaAssetsExpression', [$config['schemaFilter']])
504
			->setAutowired(FALSE)
505
			->setInject(FALSE);
506
507
		// types
508
		Validators::assertField($config, 'types', 'array');
509
		$schemaTypes = $dbalTypes = [];
510
		foreach ($config['types'] as $dbType => $className) {
511
			$typeInst = Code\Helpers::createObject($className, []);
512
			/** @var Doctrine\DBAL\Types\Type $typeInst */
513
			$dbalTypes[$typeInst->getName()] = $className;
514
			$schemaTypes[$dbType] = $typeInst->getName();
515
		}
516
517
		// tracy panel
518
		if ($this->isTracyPresent()) {
519
			$builder->addDefinition($this->prefix($name . '.diagnosticsPanel'))
520
				->setClass('Kdyby\Doctrine\Diagnostics\Panel')
521
				->setAutowired(FALSE);
522
		}
523
524
		// connection
525
		$options = array_diff_key($config, array_flip(['types', 'resultCache', 'connection', 'logging']));
526
527
		$connectionArguments = [
528
			$options,
529
			$this->prefix('@' . $name . '.dbalConfiguration'),
530
		];
531
532
		if ($this->isKdybyEventsPresent()) {
533
			$connectionArguments[] = $this->prefix('@' . $name . '.evm');
534
		}
535
536
		$connection = $builder->addDefinition($connectionServiceId = $this->prefix($name . '.connection'))
537
			->setClass('Kdyby\Doctrine\Connection')
538
			->setFactory('Kdyby\Doctrine\Connection::create', $connectionArguments)
539
			->addSetup('setSchemaTypes', [$schemaTypes])
540
			->addSetup('setDbalTypes', [$dbalTypes])
541
			->addTag(self::TAG_CONNECTION)
542
			->addTag('kdyby.doctrine.connection')
543
			->setAutowired($isDefault)
544
			->setInject(FALSE);
545
546
		if ($this->isTracyPresent()) {
547
			$connection->addSetup('$panel = ?->bindConnection(?)', [$this->prefix('@' . $name . '.diagnosticsPanel'), '@self']);
548
		}
549
550
		/** @var Nette\DI\ServiceDefinition $connection */
551
552
		$this->configuredConnections[$name] = $connectionServiceId;
553
554
		if (!is_bool($config['logging'])) {
555
			$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...
556
			$configuration->addSetup('$service->getSQLLogger()->addLogger(?)', [$fileLogger]);
557
558
		} elseif ($config['logging']) {
559
			$connection->addSetup('?->enableLogging()', [new Code\PhpLiteral('$panel')]);
560
		}
561
562
		return $this->prefix('@' . $name . '.connection');
563
	}
564
565
566
567
	/**
568
	 * @param \Nette\DI\ServiceDefinition $metadataDriver
569
	 * @param string $namespace
570
	 * @param string|object $driver
571
	 * @param string $prefix
572
	 * @throws \Nette\Utils\AssertionException
573
	 * @return string
574
	 */
575
	protected function processMetadataDriver(Nette\DI\ServiceDefinition $metadataDriver, $namespace, $driver, $prefix)
576
	{
577
		if (!is_string($namespace) || !Strings::match($namespace, '#^' . self::PHP_NAMESPACE . '\z#')) {
578
			throw new Nette\Utils\AssertionException("The metadata namespace expects to be valid namespace, $namespace given.");
579
		}
580
		$namespace = ltrim($namespace, '\\');
581
582
		if (is_string($driver) || is_array($driver)) {
583
			$paths = is_array($driver) ? $driver : [$driver];
584
			foreach ($paths as $path) {
585
				if (($pos = strrpos($path, '*')) !== FALSE) {
586
					$path = substr($path, 0, $pos);
587
				}
588
589
				if (!file_exists($path)) {
590
					throw new Nette\Utils\AssertionException("The metadata path expects to be an existing directory, $path given.");
591
				}
592
			}
593
594
			$driver = new Statement(self::ANNOTATION_DRIVER, is_array($paths) ? $paths : [$paths]);
595
		}
596
597
		$impl = $driver instanceof \stdClass ? $driver->value : ($driver instanceof Statement ? $driver->getEntity() : (string) $driver);
598
		list($driver) = CacheHelpers::filterArgs($driver);
599
		/** @var Statement $driver */
600
601
		/** @var string $impl */
602
		if (isset($this->metadataDriverClasses[$impl])) {
603
			$driver->setEntity($this->metadataDriverClasses[$impl]);
604
		}
605
606
		if (is_string($driver->getEntity()) && substr($driver->getEntity(), 0, 1) === '@') {
607
			$metadataDriver->addSetup('addDriver', [$driver->getEntity(), $namespace]);
608
			return $driver->getEntity();
609
		}
610
611
		if ($impl === self::ANNOTATION_DRIVER) {
612
			$driver->arguments = [
613
				Nette\Utils\Arrays::flatten($driver->arguments),
614
				2 => $this->prefix('@cache.' . $prefix . '.metadata')
615
			];
616
		}
617
618
		$serviceName = $this->prefix($prefix . '.driver.' . str_replace('\\', '_', $namespace) . '.' . str_replace('\\', '_', $impl) . 'Impl');
619
620
		$this->getContainerBuilder()->addDefinition($serviceName)
621
			->setClass('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver')
622
			->setFactory($driver->getEntity(), $driver->arguments)
623
			->setAutowired(FALSE)
624
			->setInject(FALSE);
625
626
		$metadataDriver->addSetup('addDriver', ['@' . $serviceName, $namespace]);
627
		return '@' . $serviceName;
628
	}
629
630
631
632
	/**
633
	 * @param string|\stdClass $cache
634
	 * @param string $suffix
635
	 * @return string
636
	 */
637
	protected function processCache($cache, $suffix)
638
	{
639
		return CacheHelpers::processCache($this, $cache, $suffix, $this->getContainerBuilder()->parameters[$this->prefix('debug')]);
640
	}
641
642
643
644
	public function beforeCompile()
645
	{
646
		$this->processRepositories();
647
	}
648
649
650
651
	protected function processRepositories()
652
	{
653
		$builder = $this->getContainerBuilder();
654
655
		$disabled = TRUE;
656
		foreach ($this->configuredManagers as $managerName => $_) {
657
			$factoryClassName = $builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))->getClass();
658
			if ($factoryClassName === 'Kdyby\Doctrine\RepositoryFactory' || in_array('Kdyby\Doctrine\RepositoryFactory', class_parents($factoryClassName), TRUE)) {
659
				$disabled = FALSE;
660
			}
661
		}
662
663
		if ($disabled) {
664
			return;
665
		}
666
667
		if (!method_exists($builder, 'findByType')) {
668
			foreach ($this->configuredManagers as $managerName => $_) {
669
				$builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))
670
					->addSetup('setServiceIdsMap', [[], $this->prefix('repositoryFactory.' . $managerName . '.defaultRepositoryFactory')]);
671
			}
672
673
			return;
674
		}
675
676
		$serviceMap = array_fill_keys(array_keys($this->configuredManagers), []);
677
		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...
678
			if (is_string($originalDef)) {
679
				$originalServiceName = $originalDef;
680
				$originalDef = $builder->getDefinition($originalServiceName);
681
			}
682
683
			if (strpos($originalServiceName, $this->name . '.') === 0) {
684
				continue; // ignore
685
			}
686
687
			$factory = $originalDef->getFactory() ? $originalDef->getFactory()->getEntity() : $originalDef->getClass();
688
			if (stripos($factory, '::getRepository') !== FALSE) {
689
				continue; // ignore
690
			}
691
692
			$factoryServiceName = $this->prefix('repositoryFactory.' . $originalServiceName);
693
			$factoryDef = $builder->addDefinition($factoryServiceName, $originalDef)
694
				->setImplement('Kdyby\Doctrine\DI\IRepositoryFactory')
695
				->setParameters(['Doctrine\ORM\EntityManagerInterface entityManager', 'Doctrine\ORM\Mapping\ClassMetadata classMetadata'])
696
				->setAutowired(FALSE);
697
			$factoryStatement = $factoryDef->getFactory() ?: new Statement($factoryDef->getClass());
698
			$factoryStatement->arguments[0] = new Code\PhpLiteral('$entityManager');
699
			$factoryStatement->arguments[1] = new Code\PhpLiteral('$classMetadata');
700
			$factoryDef->setArguments($factoryStatement->arguments);
701
702
			$boundManagers = $this->getServiceBoundManagers($originalDef);
703
			Validators::assert($boundManagers, 'list:1', 'bound manager');
704
705
			if ($boundEntity = $originalDef->getTag(self::TAG_REPOSITORY_ENTITY)) {
706
				if (!is_string($boundEntity) || !class_exists($boundEntity)) {
707
					throw new Nette\Utils\AssertionException(sprintf('The entity "%s" for repository "%s" cannot be autoloaded.', $boundEntity, $originalDef->getClass()));
708
				}
709
				$entityArgument = $boundEntity;
710
711
			} else {
712
				$entityArgument = new Code\PhpLiteral('"%entityName%"');
713
				$this->postCompileRepositoriesQueue[$boundManagers[0]][] = [ltrim($originalDef->getClass(), '\\'), $originalServiceName];
714
			}
715
716
			$builder->removeDefinition($originalServiceName);
717
			$builder->addDefinition($originalServiceName)
718
				->setClass($originalDef->getClass())
719
				->setFactory(sprintf('@%s::getRepository', $this->configuredManagers[$boundManagers[0]]), [$entityArgument]);
720
721
			$serviceMap[$boundManagers[0]][$originalDef->getClass()] = $factoryServiceName;
722
		}
723
724
		foreach ($this->configuredManagers as $managerName => $_) {
725
			$builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))
726
				->addSetup('setServiceIdsMap', [
727
					$serviceMap[$managerName],
728
					$this->prefix('repositoryFactory.' . $managerName . '.defaultRepositoryFactory')
729
				]);
730
		}
731
	}
732
733
734
735
	/**
736
	 * @param Nette\DI\ServiceDefinition $def
737
	 * @return string[]
738
	 */
739
	protected function getServiceBoundManagers(Nette\DI\ServiceDefinition $def)
740
	{
741
		$builder = $this->getContainerBuilder();
742
		$boundManagers = $def->getTag(self::TAG_BIND_TO_MANAGER);
743
744
		return is_array($boundManagers) ? $boundManagers : [$builder->parameters[$this->name]['orm']['defaultEntityManager']];
745
	}
746
747
748
749
	public function afterCompile(Code\ClassType $class)
750
	{
751
		$init = $class->getMethod('initialize');
752
753
		if ($this->isTracyPresent()) {
754
			$init->addBody('Kdyby\Doctrine\Diagnostics\Panel::registerBluescreen($this);');
755
			$this->addCollapsePathsToTracy($init);
756
		}
757
758
		foreach ($this->proxyAutoloaders as $namespace => $dir) {
759
			$originalInitialize = $init->getBody();
760
			$init->setBody('Kdyby\Doctrine\Proxy\ProxyAutoloader::create(?, ?)->register();', [$dir, $namespace]);
761
			$init->addBody($originalInitialize);
762
		}
763
764
		$this->processRepositoryFactoryEntities($class);
765
	}
766
767
768
769
	protected function processRepositoryFactoryEntities(Code\ClassType $class)
770
	{
771
		if (empty($this->postCompileRepositoriesQueue)) {
772
			return;
773
		}
774
775
		$dic = self::evalAndInstantiateContainer($class);
776
777
		foreach ($this->postCompileRepositoriesQueue as $manager => $items) {
778
			$config = $this->managerConfigs[$manager];
779
			/** @var Kdyby\Doctrine\EntityManager $entityManager */
780
			$entityManager = $dic->getService($this->configuredManagers[$manager]);
781
			/** @var Doctrine\ORM\Mapping\ClassMetadata $entityMetadata */
782
			$metadataFactory = $entityManager->getMetadataFactory();
783
784
			$allMetadata = [];
785
			foreach ($metadataFactory->getAllMetadata() as $entityMetadata) {
786
				if ($config['defaultRepositoryClassName'] === $entityMetadata->customRepositoryClassName || empty($entityMetadata->customRepositoryClassName)) {
787
					continue;
788
				}
789
790
				$allMetadata[ltrim($entityMetadata->customRepositoryClassName, '\\')] = $entityMetadata;
791
			}
792
793
			foreach ($items as $item) {
794
				if (!isset($allMetadata[$item[0]])) {
795
					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]));
796
				}
797
798
				$entityMetadata = $allMetadata[$item[0]];
799
				$serviceMethod = Nette\DI\Container::getMethodName($item[1]);
800
801
				$method = $class->getMethod($serviceMethod);
802
				$methodBody = $method->getBody();
803
				$method->setBody(str_replace('"%entityName%"', Code\Helpers::format('?', $entityMetadata->getName()), $methodBody));
804
			}
805
		}
806
	}
807
808
809
810
	/**
811
	 * @param Code\ClassType $class
812
	 * @return Nette\DI\Container
813
	 */
814
	private static function evalAndInstantiateContainer(Code\ClassType $class)
815
	{
816
		$classCopy = clone $class;
817
		$classCopy->setName($className = 'Kdyby_Doctrine_IamTheKingOfHackingNette_' . $class->getName() . '_' . rand());
0 ignored issues
show
Deprecated Code introduced by
The method Nette\PhpGenerator\ClassType::setName() has been deprecated.

This method has been deprecated.

Loading history...
818
819
		$containerCode = "$classCopy";
820
821
		return call_user_func(function () use ($className, $containerCode) {
822
			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...
823
			return new $className();
824
		});
825
	}
826
827
828
829
	/**
830
	 * @param $provided
831
	 * @param $defaults
832
	 * @param $diff
833
	 * @return array
834
	 */
835
	private function resolveConfig(array $provided, array $defaults, array $diff = [])
836
	{
837
		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...
838
			array_diff_key($provided, array_diff_key($diff, $defaults)),
839
			$defaults
840
		));
841
	}
842
843
844
	/**
845
	 * @param array $targetEntityMappings
846
	 * @return array
847
	 */
848
	private function normalizeTargetEntityMappings(array $targetEntityMappings)
849
	{
850
		$normalized = [];
851
		foreach ($targetEntityMappings as $originalEntity => $targetEntity) {
852
			$originalEntity = ltrim($originalEntity, '\\');
853
			Validators::assert($targetEntity, 'array|string');
854
			if (is_array($targetEntity)) {
855
				Validators::assertField($targetEntity, 'targetEntity', 'string');
856
				$mapping = array_merge($targetEntity, [
857
					'targetEntity' => ltrim($targetEntity['targetEntity'], '\\')
858
				]);
859
860
			} else {
861
				$mapping = [
862
					'targetEntity' => ltrim($targetEntity, '\\'),
863
				];
864
			}
865
			$normalized[$originalEntity] = $mapping;
866
		}
867
		return $normalized;
868
	}
869
870
871
872
	/**
873
	 * @return bool
874
	 */
875
	private function isTracyPresent()
876
	{
877
		return interface_exists('Tracy\IBarPanel');
878
	}
879
880
881
882
	/**
883
	 * @return bool
884
	 */
885
	private function isKdybyEventsPresent()
886
	{
887
		return (bool) $this->compiler->getExtensions('Kdyby\Events\DI\EventsExtension');
888
	}
889
890
891
892
	private function addCollapsePathsToTracy(Method $init)
893
	{
894
		$blueScreen = 'Tracy\Debugger::getBlueScreen()';
895
		$commonDirname = dirname(Nette\Reflection\ClassType::from('Doctrine\Common\Version')->getFileName());
896
897
		$init->addBody($blueScreen . '->collapsePaths[] = ?;', [dirname(Nette\Reflection\ClassType::from('Kdyby\Doctrine\Exception')->getFileName())]);
898
		$init->addBody($blueScreen . '->collapsePaths[] = ?;', [dirname(dirname(dirname(dirname($commonDirname))))]); // this should be vendor/doctrine
899
		foreach ($this->proxyAutoloaders as $dir) {
900
			$init->addBody($blueScreen . '->collapsePaths[] = ?;', [$dir]);
901
		}
902
	}
903
904
905
906
	/**
907
	 * @param \Nette\Configurator $configurator
908
	 */
909
	public static function register(Nette\Configurator $configurator)
910
	{
911
		$configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) {
912
			$compiler->addExtension('doctrine', new OrmExtension());
913
		};
914
	}
915
916
917
918
	/**
919
	 * @param array $array
920
	 */
921
	private static function natSortKeys(array &$array)
922
	{
923
		$keys = array_keys($array);
924
		natsort($keys);
925
		$keys = array_flip(array_reverse($keys, TRUE));
926
		$array = array_merge($keys, $array);
927
		return $array;
928
	}
929
930
}
931