Completed
Pull Request — master (#266)
by
unknown
02:25
created

OrmExtension::addCollapsePathsToTracy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
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
	 * @var bool
144
	 */
145
	private $kdybyEvents;
146
147
148
149
	public function loadConfiguration()
150
	{
151
		$this->proxyAutoloaders =
152
		$this->targetEntityMappings =
153
		$this->configuredConnections =
154
		$this->managerConfigs =
155
		$this->configuredManagers =
156
		$this->postCompileRepositoriesQueue = [];
157
158
		$this->kdybyEvents = (bool) $this->compiler->getExtensions('Kdyby\Events\DI\EventsExtension');
159
160
		if (!$this->compiler->getExtensions('Kdyby\Annotations\DI\AnnotationsExtension')) {
161
			throw new Nette\Utils\AssertionException('You should register \'Kdyby\Annotations\DI\AnnotationsExtension\' before \'' . get_class($this) . '\'.', E_USER_NOTICE);
162
		}
163
164
		$builder = $this->getContainerBuilder();
165
		$config = $this->getConfig();
166
167
		$builder->parameters[$this->prefix('debug')] = !empty($config['debug']);
168
		if (isset($config['dbname']) || isset($config['driver']) || isset($config['connection'])) {
169
			$config = ['default' => $config];
170
			$defaults = ['debug' => $builder->parameters['debugMode']];
171
172
		} else {
173
			$defaults = array_intersect_key($config, $this->managerDefaults)
174
				+ array_intersect_key($config, $this->connectionDefaults)
175
				+ ['debug' => $builder->parameters['debugMode']];
176
177
			$config = array_diff_key($config, $defaults);
178
		}
179
180
		if (empty($config)) {
181
			throw new Kdyby\Doctrine\UnexpectedValueException("Please configure the Doctrine extensions using the section '{$this->name}:' in your config file.");
182
		}
183
184
		foreach ($config as $name => $emConfig) {
185
			if (!is_array($emConfig) || (empty($emConfig['dbname']) && empty($emConfig['driver']))) {
186
				throw new Kdyby\Doctrine\UnexpectedValueException("Please configure the Doctrine extensions using the section '{$this->name}:' in your config file.");
187
			}
188
189
			$emConfig = Nette\DI\Config\Helpers::merge($emConfig, $defaults);
190
			$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 189 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...
191
		}
192
193
		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...
194
			if (!$this->kdybyEvents) {
195
				throw new Nette\Utils\AssertionException('The option \'targetEntityMappings\' requires \'Kdyby\Events\DI\EventsExtension\'.', E_USER_NOTICE);
196
			}
197
198
			$listener = $builder->addDefinition($this->prefix('resolveTargetEntityListener'))
199
				->setClass('Kdyby\Doctrine\Tools\ResolveTargetEntityListener')
200
				->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...
201
				->setInject(FALSE);
202
203
			foreach ($this->targetEntityMappings as $originalEntity => $mapping) {
204
				$listener->addSetup('addResolveTargetEntity', [$originalEntity, $mapping['targetEntity'], $mapping]);
205
			}
206
		}
207
208
		$this->loadConsole();
209
210
		$builder->addDefinition($this->prefix('registry'))
211
			->setClass('Kdyby\Doctrine\Registry', [
212
				$this->configuredConnections,
213
				$this->configuredManagers,
214
				$builder->parameters[$this->name]['dbal']['defaultConnection'],
215
				$builder->parameters[$this->name]['orm']['defaultEntityManager'],
216
			]);
217
	}
218
219
220
221
	protected function loadConsole()
222
	{
223
		$builder = $this->getContainerBuilder();
224
225
		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...
226
			$cli = $builder->addDefinition($this->prefix('cli.' . $i))
227
				->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...
228
				->setInject(FALSE); // lazy injects
229
230
			if (is_string($command)) {
231
				$cli->setClass($command);
232
233
			} else {
234
				throw new Kdyby\Doctrine\NotSupportedException;
235
			}
236
		}
237
	}
238
239
240
241
	protected function processEntityManager($name, array $defaults)
242
	{
243
		$builder = $this->getContainerBuilder();
244
		$config = $this->resolveConfig($defaults, $this->managerDefaults, $this->connectionDefaults);
245
246
		if ($isDefault = !isset($builder->parameters[$this->name]['orm']['defaultEntityManager'])) {
247
			$builder->parameters[$this->name]['orm']['defaultEntityManager'] = $name;
248
		}
249
250
		$metadataDriver = $builder->addDefinition($this->prefix($name . '.metadataDriver'))
251
			->setClass('Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain')
252
			->setAutowired(FALSE)
253
			->setInject(FALSE);
254
		/** @var Nette\DI\ServiceDefinition $metadataDriver */
255
256
		Validators::assertField($config, 'metadata', 'array');
257
		Validators::assertField($config, 'targetEntityMappings', 'array');
258
		$config['targetEntityMappings'] = $this->normalizeTargetEntityMappings($config['targetEntityMappings']);
259
		foreach ($this->compiler->getExtensions() as $extension) {
260
			if ($extension instanceof IEntityProvider) {
261
				$metadata = $extension->getEntityMappings();
262
				Validators::assert($metadata, 'array');
263
				$config['metadata'] = array_merge($config['metadata'], $metadata);
264
			}
265
266
			if ($extension instanceof ITargetEntityProvider) {
267
				$targetEntities = $extension->getTargetEntityMappings();
268
				Validators::assert($targetEntities, 'array');
269
				$config['targetEntityMappings'] = Nette\Utils\Arrays::mergeTree($config['targetEntityMappings'], $this->normalizeTargetEntityMappings($targetEntities));
270
			}
271
272
			if ($extension instanceof IDatabaseTypeProvider) {
273
				$providedTypes = $extension->getDatabaseTypes();
274
				Validators::assert($providedTypes, 'array');
275
276
				if (!isset($defaults['types'])) {
277
					$defaults['types'] = [];
278
				}
279
280
				$defaults['types'] = array_merge($defaults['types'], $providedTypes);
281
			}
282
		}
283
284
		foreach (self::natSortKeys($config['metadata']) as $namespace => $driver) {
285
			$this->processMetadataDriver($metadataDriver, $namespace, $driver, $name);
286
		}
287
288
		$this->processMetadataDriver($metadataDriver, 'Kdyby\\Doctrine', __DIR__ . '/../Entities', $name);
289
290
		if (empty($config['metadata'])) {
291
			$metadataDriver->addSetup('setDefaultDriver', [
292
				new Statement($this->metadataDriverClasses[self::ANNOTATION_DRIVER], [
293
					[$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...
294
					2 => $this->prefix('@cache.' . $name . '.metadata')
295
				])
296
			]);
297
		}
298
299
		if ($config['repositoryFactoryClassName'] === 'default') {
300
			$config['repositoryFactoryClassName'] = 'Doctrine\ORM\Repository\DefaultRepositoryFactory';
301
		}
302
		$builder->addDefinition($this->prefix($name . '.repositoryFactory'))
303
			->setClass($config['repositoryFactoryClassName'])
304
			->setAutowired(FALSE);
305
306
		Validators::assertField($config, 'namespaceAlias', 'array');
307
		Validators::assertField($config, 'hydrators', 'array');
308
		Validators::assertField($config, 'dql', 'array');
309
		Validators::assertField($config['dql'], 'string', 'array');
310
		Validators::assertField($config['dql'], 'numeric', 'array');
311
		Validators::assertField($config['dql'], 'datetime', 'array');
312
		Validators::assertField($config['dql'], 'hints', 'array');
313
314
		$autoGenerateProxyClasses = is_bool($config['autoGenerateProxyClasses'])
315
			? ($config['autoGenerateProxyClasses'] ? AbstractProxyFactory::AUTOGENERATE_ALWAYS : AbstractProxyFactory::AUTOGENERATE_NEVER)
316
			: $config['autoGenerateProxyClasses'];
317
318
		$configuration = $builder->addDefinition($this->prefix($name . '.ormConfiguration'))
319
			->setClass('Kdyby\Doctrine\Configuration')
320
			->addSetup('setMetadataCacheImpl', [$this->processCache($config['metadataCache'], $name . '.metadata')])
321
			->addSetup('setQueryCacheImpl', [$this->processCache($config['queryCache'], $name . '.query')])
322
			->addSetup('setResultCacheImpl', [$this->processCache($config['resultCache'], $name . '.ormResult')])
323
			->addSetup('setHydrationCacheImpl', [$this->processCache($config['hydrationCache'], $name . '.hydration')])
324
			->addSetup('setMetadataDriverImpl', [$this->prefix('@' . $name . '.metadataDriver')])
325
			->addSetup('setClassMetadataFactoryName', [$config['classMetadataFactory']])
326
			->addSetup('setDefaultRepositoryClassName', [$config['defaultRepositoryClassName']])
327
			->addSetup('setQueryBuilderClassName', [$config['queryBuilderClassName']])
328
			->addSetup('setRepositoryFactory', [$this->prefix('@' . $name . '.repositoryFactory')])
329
			->addSetup('setProxyDir', [$config['proxyDir']])
330
			->addSetup('setProxyNamespace', [$config['proxyNamespace']])
331
			->addSetup('setAutoGenerateProxyClasses', [$autoGenerateProxyClasses])
332
			->addSetup('setEntityNamespaces', [$config['namespaceAlias']])
333
			->addSetup('setCustomHydrationModes', [$config['hydrators']])
334
			->addSetup('setCustomStringFunctions', [$config['dql']['string']])
335
			->addSetup('setCustomNumericFunctions', [$config['dql']['numeric']])
336
			->addSetup('setCustomDatetimeFunctions', [$config['dql']['datetime']])
337
			->addSetup('setDefaultQueryHints', [$config['dql']['hints']])
338
			->addSetup('setNamingStrategy', CacheHelpers::filterArgs($config['namingStrategy']))
339
			->addSetup('setQuoteStrategy', CacheHelpers::filterArgs($config['quoteStrategy']))
340
			->addSetup('setEntityListenerResolver', CacheHelpers::filterArgs($config['entityListenerResolver']))
341
			->setAutowired(FALSE)
342
			->setInject(FALSE);
343
		/** @var Nette\DI\ServiceDefinition $configuration */
344
345
		$this->proxyAutoloaders[$config['proxyNamespace']] = $config['proxyDir'];
346
347
		$this->processSecondLevelCache($name, $config['secondLevelCache'], $isDefault);
348
349
		Validators::assertField($config, 'filters', 'array');
350
		foreach ($config['filters'] as $filterName => $filterClass) {
351
			$configuration->addSetup('addFilter', [$filterName, $filterClass]);
352
		}
353
354
		if ($config['targetEntityMappings']) {
355
			$configuration->addSetup('setTargetEntityMap', [array_map(function ($mapping) {
356
				return $mapping['targetEntity'];
357
			}, $config['targetEntityMappings'])]);
358
			$this->targetEntityMappings = Nette\Utils\Arrays::mergeTree($this->targetEntityMappings, $config['targetEntityMappings']);
359
		}
360
361
		$entityManagerArguments = [
362
			$connectionService = $this->processConnection($name, $defaults, $isDefault),
363
			$this->prefix('@' . $name . '.ormConfiguration'),
364
		];
365
366
		if ($this->kdybyEvents) {
367
			$builder->addDefinition($this->prefix($name . '.evm'))
368
				->setClass('Kdyby\Events\NamespacedEventManager', [Kdyby\Doctrine\Events::NS . '::'])
369
				->addSetup('$dispatchGlobalEvents', [TRUE]) // for BC
370
				->setAutowired(FALSE);
371
372
			$entityManagerArguments[] = $this->prefix('@' . $name . '.evm');
373
		}
374
375
		// entity manager
376
		$entityManager = $builder->addDefinition($managerServiceId = $this->prefix($name . '.entityManager'))
377
			->setClass('Kdyby\Doctrine\EntityManager')
378
			->setFactory('Kdyby\Doctrine\EntityManager::create', $entityManagerArguments)
379
			->addTag(self::TAG_ENTITY_MANAGER)
380
			->addTag('kdyby.doctrine.entityManager')
381
			->setAutowired($isDefault)
382
			->setInject(FALSE);
383
384
		if ($this->isTracyPresent()) {
385
			$entityManager->addSetup('?->bindEntityManager(?)', [$this->prefix('@' . $name . '.diagnosticsPanel'), '@self']);
386
		}
387
388
		$builder->addDefinition($this->prefix('repositoryFactory.' . $name . '.defaultRepositoryFactory'))
389
				->setClass($config['defaultRepositoryClassName'])
390
				->setImplement('Kdyby\Doctrine\DI\IRepositoryFactory')
391
				->setArguments([new Code\PhpLiteral('$entityManager'), new Code\PhpLiteral('$classMetadata')])
392
				->setParameters(['Doctrine\ORM\EntityManagerInterface entityManager', 'Doctrine\ORM\Mapping\ClassMetadata classMetadata'])
393
				->setAutowired(FALSE);
394
395
		$builder->addDefinition($this->prefix($name . '.schemaValidator'))
396
			->setClass('Doctrine\ORM\Tools\SchemaValidator', ['@' . $managerServiceId])
397
			->setAutowired($isDefault);
398
399
		$builder->addDefinition($this->prefix($name . '.schemaTool'))
400
			->setClass('Doctrine\ORM\Tools\SchemaTool', ['@' . $managerServiceId])
401
			->setAutowired($isDefault);
402
403
		$cacheCleaner = $builder->addDefinition($this->prefix($name . '.cacheCleaner'))
404
			->setClass('Kdyby\Doctrine\Tools\CacheCleaner', ['@' . $managerServiceId])
405
			->setAutowired($isDefault);
406
407
		$builder->addDefinition($this->prefix($name . '.schemaManager'))
408
			->setClass('Doctrine\DBAL\Schema\AbstractSchemaManager')
409
			->setFactory('@Kdyby\Doctrine\Connection::getSchemaManager')
410
			->setAutowired($isDefault);
411
412
		foreach ($this->compiler->getExtensions('Kdyby\Annotations\DI\AnnotationsExtension') as $extension) {
413
			/** @var Kdyby\Annotations\DI\AnnotationsExtension $extension */
414
			$cacheCleaner->addSetup('addCacheStorage', [$extension->prefix('@cache.annotations')]);
415
		}
416
417
		if ($isDefault) {
418
			$builder->addDefinition($this->prefix('helper.entityManager'))
419
				->setClass('Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper', ['@' . $managerServiceId])
420
				->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...
421
422
			$builder->addDefinition($this->prefix('helper.connection'))
423
				->setClass('Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper', [$connectionService])
424
				->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...
425
426
			$builder->addAlias($this->prefix('schemaValidator'), $this->prefix($name . '.schemaValidator'));
427
			$builder->addAlias($this->prefix('schemaTool'), $this->prefix($name . '.schemaTool'));
428
			$builder->addAlias($this->prefix('cacheCleaner'), $this->prefix($name . '.cacheCleaner'));
429
			$builder->addAlias($this->prefix('schemaManager'), $this->prefix($name . '.schemaManager'));
430
		}
431
432
		$this->configuredManagers[$name] = $managerServiceId;
433
		$this->managerConfigs[$name] = $config;
434
	}
435
436
437
438
	protected function processSecondLevelCache($name, array $config, $isDefault)
439
	{
440
		if (!$config['enabled']) {
441
			return;
442
		}
443
444
		$builder = $this->getContainerBuilder();
445
446
		$cacheFactory = $builder->addDefinition($this->prefix($name . '.cacheFactory'))
447
			->setClass('Doctrine\ORM\Cache\CacheFactory')
448
			->setFactory($config['factoryClass'], [
449
				$this->prefix('@' . $name . '.cacheRegionsConfiguration'),
450
				$this->processCache($config['driver'], $name . '.secondLevel'),
451
			])
452
			->setAutowired($isDefault);
453
454
		if ($config['factoryClass'] === $this->managerDefaults['secondLevelCache']['factoryClass']
455
			|| is_subclass_of($config['factoryClass'], $this->managerDefaults['secondLevelCache']['factoryClass'])
456
		) {
457
			$cacheFactory->addSetup('setFileLockRegionDirectory', [$config['fileLockRegionDirectory']]);
458
		}
459
460
		$builder->addDefinition($this->prefix($name . '.cacheRegionsConfiguration'))
461
			->setClass('Doctrine\ORM\Cache\RegionsConfiguration', [
462
				$config['regions']['defaultLifetime'],
463
				$config['regions']['defaultLockLifetime'],
464
			])
465
			->setAutowired($isDefault);
466
467
		$logger = $builder->addDefinition($this->prefix($name . '.cacheLogger'))
468
			->setClass('Doctrine\ORM\Cache\Logging\CacheLogger')
469
			->setFactory('Doctrine\ORM\Cache\Logging\CacheLoggerChain')
470
			->setAutowired(FALSE);
471
472
		if ($config['logging']) {
473
			$logger->addSetup('setLogger', [
474
				'statistics',
475
				new Statement('Doctrine\ORM\Cache\Logging\StatisticsCacheLogger')
476
			]);
477
		}
478
479
		$builder->addDefinition($cacheConfigName = $this->prefix($name . '.ormCacheConfiguration'))
480
			->setClass('Doctrine\ORM\Cache\CacheConfiguration')
481
			->addSetup('setCacheFactory', [$this->prefix('@' . $name . '.cacheFactory')])
482
			->addSetup('setCacheLogger', [$this->prefix('@' . $name . '.cacheLogger')])
483
			->setAutowired($isDefault);
484
485
		$configuration = $builder->getDefinition($this->prefix($name . '.ormConfiguration'));
486
		$configuration->addSetup('setSecondLevelCacheEnabled');
487
		$configuration->addSetup('setSecondLevelCacheConfiguration', ['@' . $cacheConfigName]);
488
	}
489
490
491
492
	protected function processConnection($name, array $defaults, $isDefault = FALSE)
493
	{
494
		$builder = $this->getContainerBuilder();
495
		$config = $this->resolveConfig($defaults, $this->connectionDefaults, $this->managerDefaults);
496
497
		if ($isDefault) {
498
			$builder->parameters[$this->name]['dbal']['defaultConnection'] = $name;
499
		}
500
501
		if (isset($defaults['connection'])) {
502
			return $this->prefix('@' . $defaults['connection'] . '.connection');
503
		}
504
505
		// config
506
		$configuration = $builder->addDefinition($this->prefix($name . '.dbalConfiguration'))
507
			->setClass('Doctrine\DBAL\Configuration')
508
			->addSetup('setResultCacheImpl', [$this->processCache($config['resultCache'], $name . '.dbalResult')])
509
			->addSetup('setSQLLogger', [new Statement('Doctrine\DBAL\Logging\LoggerChain')])
510
			->addSetup('setFilterSchemaAssetsExpression', [$config['schemaFilter']])
511
			->setAutowired(FALSE)
512
			->setInject(FALSE);
513
514
		// types
515
		Validators::assertField($config, 'types', 'array');
516
		$schemaTypes = $dbalTypes = [];
517
		foreach ($config['types'] as $dbType => $className) {
518
			$typeInst = Code\Helpers::createObject($className, []);
519
			/** @var Doctrine\DBAL\Types\Type $typeInst */
520
			$dbalTypes[$typeInst->getName()] = $className;
521
			$schemaTypes[$dbType] = $typeInst->getName();
522
		}
523
524
		// tracy panel
525
		if ($this->isTracyPresent()) {
526
			$builder->addDefinition($this->prefix($name . '.diagnosticsPanel'))
527
				->setClass('Kdyby\Doctrine\Diagnostics\Panel')
528
				->setAutowired(FALSE);
529
		}
530
531
		// connection
532
		$options = array_diff_key($config, array_flip(['types', 'resultCache', 'connection', 'logging']));
533
534
		$connectionArguments = [
535
			$options,
536
			$this->prefix('@' . $name . '.dbalConfiguration'),
537
		];
538
539
		if ($this->kdybyEvents) {
540
			$connectionArguments[] = $this->prefix('@' . $name . '.evm');
541
		}
542
543
		$connection = $builder->addDefinition($connectionServiceId = $this->prefix($name . '.connection'))
544
			->setClass('Kdyby\Doctrine\Connection')
545
			->setFactory('Kdyby\Doctrine\Connection::create', $connectionArguments)
546
			->addSetup('setSchemaTypes', [$schemaTypes])
547
			->addSetup('setDbalTypes', [$dbalTypes])
548
			->addTag(self::TAG_CONNECTION)
549
			->addTag('kdyby.doctrine.connection')
550
			->setAutowired($isDefault)
551
			->setInject(FALSE);
552
553
		if ($this->isTracyPresent()) {
554
			$connection->addSetup('$panel = ?->bindConnection(?)', [$this->prefix('@' . $name . '.diagnosticsPanel'), '@self']);
555
		}
556
557
		/** @var Nette\DI\ServiceDefinition $connection */
558
559
		$this->configuredConnections[$name] = $connectionServiceId;
560
561
		if (!is_bool($config['logging'])) {
562
			$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...
563
			$configuration->addSetup('$service->getSQLLogger()->addLogger(?)', [$fileLogger]);
564
565
		} elseif ($config['logging']) {
566
			$connection->addSetup('?->enableLogging()', [new Code\PhpLiteral('$panel')]);
567
		}
568
569
		return $this->prefix('@' . $name . '.connection');
570
	}
571
572
573
574
	/**
575
	 * @param \Nette\DI\ServiceDefinition $metadataDriver
576
	 * @param string $namespace
577
	 * @param string|object $driver
578
	 * @param string $prefix
579
	 * @throws \Nette\Utils\AssertionException
580
	 * @return string
581
	 */
582
	protected function processMetadataDriver(Nette\DI\ServiceDefinition $metadataDriver, $namespace, $driver, $prefix)
583
	{
584
		if (!is_string($namespace) || !Strings::match($namespace, '#^' . self::PHP_NAMESPACE . '\z#')) {
585
			throw new Nette\Utils\AssertionException("The metadata namespace expects to be valid namespace, $namespace given.");
586
		}
587
		$namespace = ltrim($namespace, '\\');
588
589
		if (is_string($driver) || is_array($driver)) {
590
			$paths = is_array($driver) ? $driver : [$driver];
591
			foreach ($paths as $path) {
592
				if (($pos = strrpos($path, '*')) !== FALSE) {
593
					$path = substr($path, 0, $pos);
594
				}
595
596
				if (!file_exists($path)) {
597
					throw new Nette\Utils\AssertionException("The metadata path expects to be an existing directory, $path given.");
598
				}
599
			}
600
601
			$driver = new Statement(self::ANNOTATION_DRIVER, is_array($paths) ? $paths : [$paths]);
602
		}
603
604
		$impl = $driver instanceof \stdClass ? $driver->value : ($driver instanceof Statement ? $driver->getEntity() : (string) $driver);
605
		list($driver) = CacheHelpers::filterArgs($driver);
606
		/** @var Statement $driver */
607
608
		if (isset($this->metadataDriverClasses[$impl])) {
609
			$driver->setEntity($this->metadataDriverClasses[$impl]);
610
		}
611
612
		if (is_string($driver->getEntity()) && substr($driver->getEntity(), 0, 1) === '@') {
613
			$metadataDriver->addSetup('addDriver', [$driver->getEntity(), $namespace]);
614
			return $driver->getEntity();
615
		}
616
617
		if ($impl === self::ANNOTATION_DRIVER) {
618
			$driver->arguments = [
619
				Nette\Utils\Arrays::flatten($driver->arguments),
620
				2 => $this->prefix('@cache.' . $prefix . '.metadata')
621
			];
622
		}
623
624
		$serviceName = $this->prefix($prefix . '.driver.' . str_replace('\\', '_', $namespace) . '.' . str_replace('\\', '_', $impl) . 'Impl');
625
626
		$this->getContainerBuilder()->addDefinition($serviceName)
627
			->setClass('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver')
628
			->setFactory($driver->getEntity(), $driver->arguments)
629
			->setAutowired(FALSE)
630
			->setInject(FALSE);
631
632
		$metadataDriver->addSetup('addDriver', ['@' . $serviceName, $namespace]);
633
		return '@' . $serviceName;
634
	}
635
636
637
638
	/**
639
	 * @param string|\stdClass $cache
640
	 * @param string $suffix
641
	 * @return string
642
	 */
643
	protected function processCache($cache, $suffix)
644
	{
645
		return CacheHelpers::processCache($this, $cache, $suffix, $this->getContainerBuilder()->parameters[$this->prefix('debug')]);
646
	}
647
648
649
650
	public function beforeCompile()
651
	{
652
		$builder = $this->getContainerBuilder();
653
654
		$disabled = TRUE;
655
		foreach ($this->configuredManagers as $managerName => $_) {
656
			$factoryClassName = $builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))->class;
657
			if ($factoryClassName === 'Kdyby\Doctrine\RepositoryFactory' || in_array('Kdyby\Doctrine\RepositoryFactory', class_parents($factoryClassName), TRUE)) {
658
				$disabled = FALSE;
659
			}
660
		}
661
662
		if ($disabled) {
663
			return;
664
		}
665
666
		if (!method_exists($builder, 'findByType')) {
667
			foreach ($this->configuredManagers as $managerName => $_) {
668
				$builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))
669
					->addSetup('setServiceIdsMap', [[], $this->prefix('repositoryFactory.' . $managerName . '.defaultRepositoryFactory')]);
670
			}
671
672
			return;
673
		}
674
675
		$serviceMap = array_fill_keys(array_keys($this->configuredManagers), []);
676
		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...
677
			if (is_string($originalDef)) {
678
				$originalServiceName = $originalDef;
679
				$originalDef = $builder->getDefinition($originalServiceName);
680
			}
681
682
			if (strpos($originalServiceName, $this->name . '.') === 0) {
683
				continue; // ignore
684
			}
685
686
			$factory = $originalDef->getFactory() ? $originalDef->getFactory()->getEntity() : $originalDef->getClass();
687
			if (stripos($factory, '::getRepository') !== FALSE) {
688
				continue; // ignore
689
			}
690
691
			$factoryServiceName = $this->prefix('repositoryFactory.' . $originalServiceName);
692
			$factoryDef = $builder->addDefinition($factoryServiceName, $originalDef)
693
				->setImplement('Kdyby\Doctrine\DI\IRepositoryFactory')
694
				->setParameters(['Doctrine\ORM\EntityManagerInterface entityManager', 'Doctrine\ORM\Mapping\ClassMetadata classMetadata'])
695
				->setAutowired(FALSE);
696
			$factoryStatement = $factoryDef->getFactory() ?: new Statement($factoryDef->getClass());
697
			$factoryStatement->arguments[0] = new Code\PhpLiteral('$entityManager');
698
			$factoryStatement->arguments[1] = new Code\PhpLiteral('$classMetadata');
699
			$factoryDef->setArguments($factoryStatement->arguments);
700
701
			$boundManagers = $this->getServiceBoundManagers($originalDef);
702
			Validators::assert($boundManagers, 'list:1', 'bound manager');
703
704
			if ($boundEntity = $originalDef->getTag(self::TAG_REPOSITORY_ENTITY)) {
705
				if (!is_string($boundEntity) || !class_exists($boundEntity)) {
706
					throw new Nette\Utils\AssertionException(sprintf('The entity "%s" for repository "%s" cannot be autoloaded.', $boundEntity, $originalDef->class));
707
				}
708
				$entityArgument = $boundEntity;
709
710
			} else {
711
				$entityArgument = new Code\PhpLiteral('"%entityName%"');
712
				$this->postCompileRepositoriesQueue[$boundManagers[0]][] = [ltrim($originalDef->class, '\\'), $originalServiceName];
713
			}
714
715
			$builder->removeDefinition($originalServiceName);
716
			$builder->addDefinition($originalServiceName)
717
				->setClass($originalDef->class)
718
				->setFactory(sprintf('@%s::getRepository', $this->configuredManagers[$boundManagers[0]]), [$entityArgument]);
719
720
			$serviceMap[$boundManagers[0]][$originalDef->class] = $factoryServiceName;
721
		}
722
723
		foreach ($this->configuredManagers as $managerName => $_) {
724
			$builder->getDefinition($this->prefix($managerName . '.repositoryFactory'))
725
				->addSetup('setServiceIdsMap', [
726
					$serviceMap[$managerName],
727
					$this->prefix('repositoryFactory.' . $managerName . '.defaultRepositoryFactory')
728
				]);
729
		}
730
	}
731
732
733
734
	/**
735
	 * @param Nette\DI\ServiceDefinition $def
736
	 * @return string[]
737
	 */
738
	protected function getServiceBoundManagers(Nette\DI\ServiceDefinition $def)
739
	{
740
		$builder = $this->getContainerBuilder();
741
		$boundManagers = $def->getTag(self::TAG_BIND_TO_MANAGER);
742
743
		return is_array($boundManagers) ? $boundManagers : [$builder->parameters[$this->name]['orm']['defaultEntityManager']];
744
	}
745
746
747
748
	public function afterCompile(Code\ClassType $class)
749
	{
750
		$init = $class->getMethod('initialize');
751
752
		if ($this->isTracyPresent()) {
753
			$init->addBody('Kdyby\Doctrine\Diagnostics\Panel::registerBluescreen($this);');
754
			$this->addCollapsePathsToTracy($init);
755
		}
756
757
		foreach ($this->proxyAutoloaders as $namespace => $dir) {
758
			$originalInitialize = $init->getBody();
759
			$init->setBody('Kdyby\Doctrine\Proxy\ProxyAutoloader::create(?, ?)->register();', [$dir, $namespace]);
760
			$init->addBody($originalInitialize);
761
		}
762
763
		$this->processRepositoryFactoryEntities($class);
764
	}
765
766
767
768
	protected function processRepositoryFactoryEntities(Code\ClassType $class)
769
	{
770
		if (empty($this->postCompileRepositoriesQueue)) {
771
			return;
772
		}
773
774
		$dic = self::evalAndInstantiateContainer($class);
775
776
		foreach ($this->postCompileRepositoriesQueue as $manager => $items) {
777
			$config = $this->managerConfigs[$manager];
778
			/** @var Kdyby\Doctrine\EntityManager $entityManager */
779
			$entityManager = $dic->getService($this->configuredManagers[$manager]);
780
			/** @var Doctrine\ORM\Mapping\ClassMetadata $entityMetadata */
781
			$metadataFactory = $entityManager->getMetadataFactory();
782
783
			$allMetadata = [];
784
			foreach ($metadataFactory->getAllMetadata() as $entityMetadata) {
785
				if ($config['defaultRepositoryClassName'] === $entityMetadata->customRepositoryClassName || empty($entityMetadata->customRepositoryClassName)) {
786
					continue;
787
				}
788
789
				$allMetadata[ltrim($entityMetadata->customRepositoryClassName, '\\')] = $entityMetadata;
790
			}
791
792
			foreach ($items as $item) {
793
				if (!isset($allMetadata[$item[0]])) {
794
					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]));
795
				}
796
797
				$entityMetadata = $allMetadata[$item[0]];
798
				$serviceMethod = Nette\DI\Container::getMethodName($item[1]);
799
800
				$method = $class->getMethod($serviceMethod);
801
				$methodBody = $method->getBody();
802
				$method->setBody(str_replace('"%entityName%"', Code\Helpers::format('?', $entityMetadata->getName()), $methodBody));
803
			}
804
		}
805
	}
806
807
808
809
	/**
810
	 * @param Code\ClassType $class
811
	 * @return Nette\DI\Container
812
	 */
813
	private static function evalAndInstantiateContainer(Code\ClassType $class)
814
	{
815
		$classCopy = clone $class;
816
		$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...
817
818
		$containerCode = "$classCopy";
819
820
		return call_user_func(function () use ($className, $containerCode) {
821
			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...
822
			return new $className();
823
		});
824
	}
825
826
827
828
	/**
829
	 * @param $provided
830
	 * @param $defaults
831
	 * @param $diff
832
	 * @return array
833
	 */
834
	private function resolveConfig(array $provided, array $defaults, array $diff = [])
835
	{
836
		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...
837
			array_diff_key($provided, array_diff_key($diff, $defaults)),
838
			$defaults
839
		));
840
	}
841
842
843
	/**
844
	 * @param array $targetEntityMappings
845
	 * @return array
846
	 */
847
	private function normalizeTargetEntityMappings(array $targetEntityMappings)
848
	{
849
		$normalized = [];
850
		foreach ($targetEntityMappings as $originalEntity => $targetEntity) {
851
			$originalEntity = ltrim($originalEntity, '\\');
852
			Validators::assert($targetEntity, 'array|string');
853
			if (is_array($targetEntity)) {
854
				Validators::assertField($targetEntity, 'targetEntity', 'string');
855
				$mapping = array_merge($targetEntity, [
856
					'targetEntity' => ltrim($targetEntity['targetEntity'], '\\')
857
				]);
858
859
			} else {
860
				$mapping = [
861
					'targetEntity' => ltrim($targetEntity, '\\'),
862
				];
863
			}
864
			$normalized[$originalEntity] = $mapping;
865
		}
866
		return $normalized;
867
	}
868
869
870
871
	/**
872
	 * @return bool
873
	 */
874
	private function isTracyPresent()
875
	{
876
		return interface_exists('Tracy\IBarPanel');
877
	}
878
879
880
881
	private function addCollapsePathsToTracy(Method $init)
882
	{
883
		$blueScreen = 'Tracy\Debugger::getBlueScreen()';
884
		$commonDirname = dirname(Nette\Reflection\ClassType::from('Doctrine\Common\Version')->getFileName());
885
886
		$init->addBody($blueScreen . '->collapsePaths[] = ?;', [dirname(Nette\Reflection\ClassType::from('Kdyby\Doctrine\Exception')->getFileName())]);
887
		$init->addBody($blueScreen . '->collapsePaths[] = ?;', [dirname(dirname(dirname(dirname($commonDirname))))]); // this should be vendor/doctrine
888
		foreach ($this->proxyAutoloaders as $dir) {
889
			$init->addBody($blueScreen . '->collapsePaths[] = ?;', [$dir]);
890
		}
891
	}
892
893
894
895
	/**
896
	 * @param \Nette\Configurator $configurator
897
	 */
898
	public static function register(Nette\Configurator $configurator)
899
	{
900
		$configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) {
901
			$compiler->addExtension('doctrine', new OrmExtension());
902
		};
903
	}
904
905
906
907
	/**
908
	 * @param array $array
909
	 */
910
	private static function natSortKeys(array &$array)
911
	{
912
		$keys = array_keys($array);
913
		natsort($keys);
914
		$keys = array_flip(array_reverse($keys, TRUE));
915
		$array = array_merge($keys, $array);
916
		return $array;
917
	}
918
919
}
920