*/
class Compiler
{
private string $containerClass;
private string $containerParentClass;
/**
* Definitions indexed by the entry name. The value can be null if the definition needs to be fetched.
*
* Keys are strings, values are `Definition` objects or null.
*/
private \ArrayIterator $entriesToCompile;
/**
* Progressive counter for definitions.
*
* Each key in $entriesToCompile is defined as 'SubEntry' + counter
* and each definition has always the same key in the CompiledContainer
* if PHP-DI configuration does not change.
*/
private int $subEntryCounter = 0;
/**
* Progressive counter for CompiledContainer get methods.
*
* Each CompiledContainer method name is defined as 'get' + counter
* and remains the same after each recompilation
* if PHP-DI configuration does not change.
*/
private int $methodMappingCounter = 0;
/**
* Map of entry names to method names.
*
* @var string[]
*/
private array $entryToMethodMapping = [];
/**
* @var string[]
*/
private array $methods = [];
private bool $autowiringEnabled;
public function __construct(
private ProxyFactoryInterface $proxyFactory,
) {
}
public function getProxyFactory() : ProxyFactoryInterface
{
return $this->proxyFactory;
}
/**
* Compile the container.
*
* @return string The compiled container file name.
*/
public function compile(
DefinitionSource $definitionSource,
string $directory,
string $className,
string $parentClassName,
bool $autowiringEnabled,
) : string {
$fileName = rtrim($directory, '/') . '/' . $className . '.php';
if (file_exists($fileName)) {
// The container is already compiled
return $fileName;
}
$this->autowiringEnabled = $autowiringEnabled;
// Validate that a valid class name was provided
$validClassName = preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $className);
if (!$validClassName) {
throw new InvalidArgumentException("The container cannot be compiled: `$className` is not a valid PHP class name");
}
$this->entriesToCompile = new \ArrayIterator($definitionSource->getDefinitions());
// We use an ArrayIterator so that we can keep adding new items to the list while we compile entries
foreach ($this->entriesToCompile as $entryName => $definition) {
$silenceErrors = false;
// This is an entry found by reference during autowiring
if (!$definition) {
$definition = $definitionSource->getDefinition($entryName);
// We silence errors for those entries because type-hints may reference interfaces/abstract classes
// which could later be defined, or even not used (we don't want to block the compilation for those)
$silenceErrors = true;
}
if (!$definition) {
// We do not throw a `NotFound` exception here because the dependency
// could be defined at runtime
continue;
}
// Check that the definition can be compiled
$errorMessage = $this->isCompilable($definition);
if ($errorMessage !== true) {
continue;
}
try {
$this->compileDefinition($entryName, $definition);
} catch (InvalidDefinition $e) {
if ($silenceErrors) {
// forget the entry
unset($this->entryToMethodMapping[$entryName]);
} else {
throw $e;
}
}
}
$this->containerClass = $className;
$this->containerParentClass = $parentClassName;
ob_start();
require __DIR__ . '/Template.php';
$fileContent = ob_get_clean();
$fileContent = "createCompilationDirectory(dirname($fileName));
$this->writeFileAtomic($fileName, $fileContent);
return $fileName;
}
private function writeFileAtomic(string $fileName, string $content) : void
{
$tmpFile = @tempnam(dirname($fileName), 'swap-compile');
if ($tmpFile === false) {
throw new InvalidArgumentException(
sprintf('Error while creating temporary file in %s', dirname($fileName))
);
}
@chmod($tmpFile, 0666);
$written = file_put_contents($tmpFile, $content);
if ($written === false) {
@unlink($tmpFile);
throw new InvalidArgumentException(sprintf('Error while writing to %s', $tmpFile));
}
@chmod($tmpFile, 0666);
$renamed = @rename($tmpFile, $fileName);
if (!$renamed) {
@unlink($tmpFile);
throw new InvalidArgumentException(sprintf('Error while renaming %s to %s', $tmpFile, $fileName));
}
}
/**
* @return string The method name
* @throws DependencyException
* @throws InvalidDefinition
*/
private function compileDefinition(string $entryName, Definition $definition) : string
{
// Generate a unique method name
$methodName = 'get' . (++$this->methodMappingCounter);
$this->entryToMethodMapping[$entryName] = $methodName;
switch (true) {
case $definition instanceof ValueDefinition:
$value = $definition->getValue();
$code = 'return ' . $this->compileValue($value) . ';';
break;
case $definition instanceof Reference:
$targetEntryName = $definition->getTargetEntryName();
$code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');';
// If this method is not yet compiled we store it for compilation
if (!isset($this->entriesToCompile[$targetEntryName])) {
$this->entriesToCompile[$targetEntryName] = null;
}
break;
case $definition instanceof StringDefinition:
$entryName = $this->compileValue($definition->getName());
$expression = $this->compileValue($definition->getExpression());
$code = 'return \DI\Definition\StringDefinition::resolveExpression(' . $entryName . ', ' . $expression . ', $this->delegateContainer);';
break;
case $definition instanceof EnvironmentVariableDefinition:
$variableName = $this->compileValue($definition->getVariableName());
$isOptional = $this->compileValue($definition->isOptional());
$defaultValue = $this->compileValue($definition->getDefaultValue());
$code = <<