| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* This file is part of the Nette Framework (https://nette.org) |
| 5 |
* Copyright (c) 2004 David Grudl (https://davidgrudl.com) |
| 6 |
*/ |
| 7 |
declare (strict_types=1); |
| 8 |
namespace Packetery\Nette\DI; |
| 9 |
|
| 10 |
use Packetery\Nette; |
| 11 |
use Packetery\Nette\Schema; |
| 12 |
/** |
| 13 |
* DI container compiler. |
| 14 |
*/ |
| 15 |
class Compiler |
| 16 |
{ |
| 17 |
use \Packetery\Nette\SmartObject; |
| 18 |
private const Services = 'services', Parameters = 'parameters', DI = 'di'; |
| 19 |
/** @var CompilerExtension[] */ |
| 20 |
private $extensions = []; |
| 21 |
/** @var ContainerBuilder */ |
| 22 |
private $builder; |
| 23 |
/** @var array */ |
| 24 |
private $config = []; |
| 25 |
/** @var array [section => array[]] */ |
| 26 |
private $configs = []; |
| 27 |
/** @var string */ |
| 28 |
private $sources = ''; |
| 29 |
/** @var DependencyChecker */ |
| 30 |
private $dependencies; |
| 31 |
/** @var string */ |
| 32 |
private $className = 'Container'; |
| 33 |
public function __construct(?ContainerBuilder $builder = null) |
| 34 |
{ |
| 35 |
$this->builder = $builder ?: new ContainerBuilder(); |
| 36 |
$this->dependencies = new DependencyChecker(); |
| 37 |
$this->addExtension(self::Services, new Extensions\ServicesExtension()); |
| 38 |
$this->addExtension(self::Parameters, new Extensions\ParametersExtension($this->configs)); |
| 39 |
} |
| 40 |
/** |
| 41 |
* Add custom configurator extension. |
| 42 |
* @return static |
| 43 |
*/ |
| 44 |
public function addExtension(?string $name, CompilerExtension $extension) |
| 45 |
{ |
| 46 |
if ($name === null) { |
| 47 |
$name = '_' . \count($this->extensions); |
| 48 |
} elseif (isset($this->extensions[$name])) { |
| 49 |
throw new \Packetery\Nette\InvalidArgumentException(\sprintf("Name '%s' is already used or reserved.", $name)); |
| 50 |
} |
| 51 |
$lname = \strtolower($name); |
| 52 |
foreach (\array_keys($this->extensions) as $nm) { |
| 53 |
if ($lname === \strtolower((string) $nm)) { |
| 54 |
throw new \Packetery\Nette\InvalidArgumentException(\sprintf("Name of extension '%s' has the same name as '%s' in a case-insensitive manner.", $name, $nm)); |
| 55 |
} |
| 56 |
} |
| 57 |
$this->extensions[$name] = $extension->setCompiler($this, $name); |
| 58 |
return $this; |
| 59 |
} |
| 60 |
public function getExtensions(?string $type = null) : array |
| 61 |
{ |
| 62 |
return $type ? \array_filter($this->extensions, function ($item) use($type) : bool { |
| 63 |
return $item instanceof $type; |
| 64 |
}) : $this->extensions; |
| 65 |
} |
| 66 |
public function getContainerBuilder() : ContainerBuilder |
| 67 |
{ |
| 68 |
return $this->builder; |
| 69 |
} |
| 70 |
/** @return static */ |
| 71 |
public function setClassName(string $className) |
| 72 |
{ |
| 73 |
$this->className = $className; |
| 74 |
return $this; |
| 75 |
} |
| 76 |
/** |
| 77 |
* Adds new configuration. |
| 78 |
* @return static |
| 79 |
*/ |
| 80 |
public function addConfig(array $config) |
| 81 |
{ |
| 82 |
foreach ($config as $section => $data) { |
| 83 |
$this->configs[$section][] = $data; |
| 84 |
} |
| 85 |
$this->sources .= "// source: array\n"; |
| 86 |
return $this; |
| 87 |
} |
| 88 |
/** |
| 89 |
* Adds new configuration from file. |
| 90 |
* @return static |
| 91 |
*/ |
| 92 |
public function loadConfig(string $file, ?Config\Loader $loader = null) |
| 93 |
{ |
| 94 |
$sources = $this->sources . "// source: {$file}\n"; |
| 95 |
$loader = $loader ?: new Config\Loader(); |
| 96 |
foreach ($loader->load($file, \false) as $data) { |
| 97 |
$this->addConfig($data); |
| 98 |
} |
| 99 |
$this->dependencies->add($loader->getDependencies()); |
| 100 |
$this->sources = $sources; |
| 101 |
return $this; |
| 102 |
} |
| 103 |
/** |
| 104 |
* Returns configuration. |
| 105 |
* @deprecated |
| 106 |
*/ |
| 107 |
public function getConfig() : array |
| 108 |
{ |
| 109 |
return $this->config; |
| 110 |
} |
| 111 |
/** |
| 112 |
* Sets the names of dynamic parameters. |
| 113 |
* @return static |
| 114 |
*/ |
| 115 |
public function setDynamicParameterNames(array $names) |
| 116 |
{ |
| 117 |
\assert($this->extensions[self::Parameters] instanceof Extensions\ParametersExtension); |
| 118 |
$this->extensions[self::Parameters]->dynamicParams = $names; |
| 119 |
return $this; |
| 120 |
} |
| 121 |
/** |
| 122 |
* Adds dependencies to the list. |
| 123 |
* @param array $deps of ReflectionClass|\ReflectionFunctionAbstract|string |
| 124 |
* @return static |
| 125 |
*/ |
| 126 |
public function addDependencies(array $deps) |
| 127 |
{ |
| 128 |
$this->dependencies->add(\array_filter($deps)); |
| 129 |
return $this; |
| 130 |
} |
| 131 |
/** |
| 132 |
* Exports dependencies. |
| 133 |
*/ |
| 134 |
public function exportDependencies() : array |
| 135 |
{ |
| 136 |
return $this->dependencies->export(); |
| 137 |
} |
| 138 |
/** @return static */ |
| 139 |
public function addExportedTag(string $tag) |
| 140 |
{ |
| 141 |
if (isset($this->extensions[self::DI])) { |
| 142 |
\assert($this->extensions[self::DI] instanceof Extensions\DIExtension); |
| 143 |
$this->extensions[self::DI]->exportedTags[$tag] = \true; |
| 144 |
} |
| 145 |
return $this; |
| 146 |
} |
| 147 |
/** @return static */ |
| 148 |
public function addExportedType(string $type) |
| 149 |
{ |
| 150 |
if (isset($this->extensions[self::DI])) { |
| 151 |
\assert($this->extensions[self::DI] instanceof Extensions\DIExtension); |
| 152 |
$this->extensions[self::DI]->exportedTypes[$type] = \true; |
| 153 |
} |
| 154 |
return $this; |
| 155 |
} |
| 156 |
public function compile() : string |
| 157 |
{ |
| 158 |
$this->processExtensions(); |
| 159 |
$this->processBeforeCompile(); |
| 160 |
return $this->generateCode(); |
| 161 |
} |
| 162 |
/** @internal */ |
| 163 |
public function processExtensions() : void |
| 164 |
{ |
| 165 |
$first = $this->getExtensions(Extensions\ParametersExtension::class) + $this->getExtensions(Extensions\ExtensionsExtension::class); |
| 166 |
foreach ($first as $name => $extension) { |
| 167 |
$config = $this->processSchema($extension->getConfigSchema(), $this->configs[$name] ?? [], $name); |
| 168 |
$extension->setConfig($this->config[$name] = $config); |
| 169 |
$extension->loadConfiguration(); |
| 170 |
} |
| 171 |
$last = $this->getExtensions(Extensions\InjectExtension::class); |
| 172 |
$this->extensions = \array_merge(\array_diff_key($this->extensions, $last), $last); |
| 173 |
if ($decorator = $this->getExtensions(Extensions\DecoratorExtension::class)) { |
| 174 |
\Packetery\Nette\Utils\Arrays::insertBefore($this->extensions, \key($decorator), $this->getExtensions(Extensions\SearchExtension::class)); |
| 175 |
} |
| 176 |
$extensions = \array_diff_key($this->extensions, $first, [self::Services => 1]); |
| 177 |
foreach ($extensions as $name => $extension) { |
| 178 |
$config = $this->processSchema($extension->getConfigSchema(), $this->configs[$name] ?? [], $name); |
| 179 |
$extension->setConfig($this->config[$name] = $config); |
| 180 |
} |
| 181 |
foreach ($extensions as $extension) { |
| 182 |
$extension->loadConfiguration(); |
| 183 |
} |
| 184 |
foreach ($this->getExtensions(Extensions\ServicesExtension::class) as $name => $extension) { |
| 185 |
$config = $this->processSchema($extension->getConfigSchema(), $this->configs[$name] ?? [], $name); |
| 186 |
$extension->setConfig($this->config[$name] = $config); |
| 187 |
$extension->loadConfiguration(); |
| 188 |
} |
| 189 |
if ($extra = \array_diff_key($this->extensions, $extensions, $first, [self::Services => 1])) { |
| 190 |
throw new \Packetery\Nette\DeprecatedException(\sprintf("Extensions '%s' were added while container was being compiled.", \implode("', '", \array_keys($extra)))); |
| 191 |
} elseif ($extra = \key(\array_diff_key($this->configs, $this->extensions))) { |
| 192 |
$hint = \Packetery\Nette\Utils\Helpers::getSuggestion(\array_keys($this->extensions), $extra); |
| 193 |
throw new InvalidConfigurationException(\sprintf("Found section '%s' in configuration, but corresponding extension is missing", $extra) . ($hint ? ", did you mean '{$hint}'?" : '.')); |
| 194 |
} |
| 195 |
} |
| 196 |
private function processBeforeCompile() : void |
| 197 |
{ |
| 198 |
$this->builder->resolve(); |
| 199 |
foreach ($this->extensions as $extension) { |
| 200 |
$extension->beforeCompile(); |
| 201 |
$this->dependencies->add([(new \ReflectionClass($extension))->getFileName()]); |
| 202 |
} |
| 203 |
$this->builder->complete(); |
| 204 |
} |
| 205 |
/** |
| 206 |
* Merges and validates configurations against scheme. |
| 207 |
* @return array|object |
| 208 |
*/ |
| 209 |
private function processSchema(Schema\Schema $schema, array $configs, $name = null) |
| 210 |
{ |
| 211 |
$processor = new Schema\Processor(); |
| 212 |
$processor->onNewContext[] = function (Schema\Context $context) use($name) { |
| 213 |
$context->path = $name ? [$name] : []; |
| 214 |
$context->dynamics =& $this->extensions[self::Parameters]->dynamicValidators; |
| 215 |
}; |
| 216 |
try { |
| 217 |
$res = $processor->processMultiple($schema, $configs); |
| 218 |
} catch (Schema\ValidationException $e) { |
| 219 |
throw new \Packetery\Nette\DI\InvalidConfigurationException($e->getMessage()); |
| 220 |
} |
| 221 |
foreach ($processor->getWarnings() as $warning) { |
| 222 |
\trigger_error($warning, \E_USER_DEPRECATED); |
| 223 |
} |
| 224 |
return $res; |
| 225 |
} |
| 226 |
/** @internal */ |
| 227 |
public function generateCode() : string |
| 228 |
{ |
| 229 |
$generator = $this->createPhpGenerator(); |
| 230 |
$class = $generator->generate($this->className); |
| 231 |
$this->dependencies->add($this->builder->getDependencies()); |
| 232 |
foreach ($this->extensions as $extension) { |
| 233 |
$extension->afterCompile($class); |
| 234 |
$generator->addInitialization($class, $extension); |
| 235 |
} |
| 236 |
return $this->sources . "\n" . $generator->toString($class); |
| 237 |
} |
| 238 |
/** |
| 239 |
* Loads list of service definitions from configuration. |
| 240 |
*/ |
| 241 |
public function loadDefinitionsFromConfig(array $configList) : void |
| 242 |
{ |
| 243 |
$extension = $this->extensions[self::Services]; |
| 244 |
\assert($extension instanceof Extensions\ServicesExtension); |
| 245 |
$extension->loadDefinitions($this->processSchema($extension->getConfigSchema(), [$configList])); |
| 246 |
} |
| 247 |
protected function createPhpGenerator() : PhpGenerator |
| 248 |
{ |
| 249 |
return new PhpGenerator($this->builder); |
| 250 |
} |
| 251 |
} |
| 252 |
|