diff --git a/src/DI/Compiler.php b/src/DI/Compiler.php index 8cb6fe1d2..c67d9277a 100644 --- a/src/DI/Compiler.php +++ b/src/DI/Compiler.php @@ -123,12 +123,13 @@ public function getConfig() /** - * Adds a files to the list of dependencies. + * Adds dependencies to the list. + * @param array of ReflectionClass|\ReflectionFunctionAbstract|string * @return self */ - public function addDependencies(array $files) + public function addDependencies(array $deps) { - $this->dependencies->add($files); + $this->dependencies->add(array_filter($deps)); return $this; } diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index a4e6cb5f3..48dc15a26 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -45,7 +45,7 @@ class ContainerBuilder /** @var string[] of classes excluded from auto-wiring */ private $excludedClasses = []; - /** @var array of file names */ + /** @var array */ private $dependencies = []; /** @var Nette\PhpGenerator\ClassType[] */ @@ -296,7 +296,7 @@ public function autowireArguments($class, $method, array $arguments) if (!$rm->isPublic()) { throw new ServiceCreationException("$class::$method() is not callable."); } - $this->addDependency((string) $rm->getFileName()); + $this->addDependency($rm); return Helpers::autowireArguments($rm, $arguments, $this); } @@ -384,10 +384,6 @@ public function prepareClassList() } } } - - foreach ($this->classList as $class => $foo) { - $this->addDependency((string) (new ReflectionClass($class))->getFileName()); - } } @@ -399,6 +395,7 @@ private function resolveImplement(ServiceDefinition $def, $name) } self::checkCase($interface); $rc = new ReflectionClass($interface); + $this->addDependency($rc); $method = $rc->hasMethod('create') ? $rc->getMethod('create') : ($rc->hasMethod('get') ? $rc->getMethod('get') : NULL); @@ -474,13 +471,16 @@ private function resolveServiceClass($name, $recursive = []) $recursive[$name] = TRUE; $def = $this->definitions[$name]; - $class = $def->getFactory() ? $this->resolveEntityClass($def->getFactory()->getEntity(), $recursive) : NULL; // call always to check entities - if ($class = $def->getClass() ?: $class) { + $factoryClass = $def->getFactory() ? $this->resolveEntityClass($def->getFactory()->getEntity(), $recursive) : NULL; // call always to check entities + if ($class = $def->getClass() ?: $factoryClass) { $def->setClass($class); if (!class_exists($class) && !interface_exists($class)) { throw new ServiceCreationException("Type $class used in service '$name' not found or is not class or interface."); } self::checkCase($class); + if (count($recursive) === 1) { + $this->addDependency(new ReflectionClass($factoryClass ?: $class)); + } } elseif ($def->getAutowired()) { trigger_error("Type of service '$name' is unknown.", E_USER_NOTICE); @@ -517,6 +517,7 @@ private function resolveEntityClass($entity, $recursive = []) throw new ServiceCreationException(sprintf("Factory '%s' used in service '%s' is not callable.", Nette\Utils\Callback::toString($entity), $name[0])); } + $this->addDependency($reflection); return PhpReflection::getReturnType($reflection); } elseif ($service = $this->getServiceName($entity)) { // alias or factory @@ -610,7 +611,7 @@ public function completeStatement(Statement $statement) $visibility = $rm->isProtected() ? 'protected' : 'private'; throw new ServiceCreationException("Class $entity has $visibility constructor."); } elseif ($constructor = (new ReflectionClass($entity))->getConstructor()) { - $this->addDependency((string) $constructor->getFileName()); + $this->addDependency($constructor); $arguments = Helpers::autowireArguments($constructor, $arguments, $this); } elseif ($arguments) { throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor."); @@ -630,7 +631,7 @@ public function completeStatement(Statement $statement) } $rf = new \ReflectionFunction($entity[1]); - $this->addDependency((string) $rf->getFileName()); + $this->addDependency($rf); $arguments = Helpers::autowireArguments($rf, $arguments, $this); } else { @@ -704,25 +705,25 @@ public function addExcludedClasses(array $classes) /** - * Adds a file to the list of dependencies. + * Adds item to the list of dependencies. + * @param ReflectionClass|\ReflectionFunctionAbstract|string * @return self * @internal */ - public function addDependency($file) + public function addDependency($dep) { - $this->dependencies[$file] = TRUE; + $this->dependencies[] = $dep; return $this; } /** - * Returns the list of dependent files. + * Returns the list of dependencies. * @return array */ public function getDependencies() { - unset($this->dependencies[FALSE]); - return array_keys($this->dependencies); + return $this->dependencies; } diff --git a/src/DI/ContainerLoader.php b/src/DI/ContainerLoader.php index 329a652b3..56931e377 100644 --- a/src/DI/ContainerLoader.php +++ b/src/DI/ContainerLoader.php @@ -100,7 +100,7 @@ private function isExpired($file) { if ($this->autoRebuild) { $meta = @unserialize(file_get_contents("$file.meta")); // @ - file may not exist - return empty($meta[0]) || DependencyChecker::isExpired($meta); + return empty($meta[0]) || DependencyChecker::isExpired(...$meta); } return FALSE; } diff --git a/src/DI/DependencyChecker.php b/src/DI/DependencyChecker.php index 43136b90d..7ed674bdd 100644 --- a/src/DI/DependencyChecker.php +++ b/src/DI/DependencyChecker.php @@ -9,6 +9,7 @@ use Nette; use ReflectionClass; +use ReflectionMethod; /** @@ -16,9 +17,11 @@ */ class DependencyChecker { + const VERSION = 1; + use Nette\SmartObject; - /** @var array */ + /** @var array of ReflectionClass|\ReflectionFunctionAbstract|string */ private $dependencies = []; @@ -39,9 +42,33 @@ public function add(array $deps) */ public function export() { - $files = array_filter($this->dependencies); + $deps = array_unique($this->dependencies, SORT_REGULAR); + $files = $phpFiles = $classes = $functions = []; + foreach ($deps as $dep) { + if (is_string($dep)) { + $files[] = $dep; + + } elseif ($dep instanceof ReflectionClass) { + foreach (PhpReflection::getClassTree($dep) as $item) { + $phpFiles[] = (new ReflectionClass($item))->getFileName(); + $classes[] = $item; + } + + } elseif ($dep instanceof \ReflectionFunctionAbstract) { + $phpFiles[] = $dep->getFileName(); + $functions[] = $dep instanceof ReflectionMethod ? $dep->getDeclaringClass()->getName() . '::' . $dep->getName() : $dep->getName(); + + } else { + throw new Nette\InvalidStateException('Unexpected dependency ' . gettype($dep)); + } + } + + $classes = array_unique($classes); + $functions = array_unique($functions, SORT_REGULAR); + $hash = self::calculateHash($classes, $functions); $files = @array_map('filemtime', array_combine($files, $files)); // @ - file may not exist - return $files; + $phpFiles = @array_map('filemtime', array_combine($phpFiles, $phpFiles)); // @ - file may not exist + return [self::VERSION, $files, $phpFiles, $classes, $functions, $hash]; } @@ -49,10 +76,65 @@ public function export() * Are dependencies expired? * @return bool */ - public static function isExpired($files) + public static function isExpired($version, $files, $phpFiles, $classes, $functions, $hash) { $current = @array_map('filemtime', array_combine($tmp = array_keys($files), $tmp)); // @ - files may not exist - return $files !== $current; + $currentClass = @array_map('filemtime', array_combine($tmp = array_keys($phpFiles), $tmp)); // @ - files may not exist + return $version !== self::VERSION + || $files !== $current + || ($phpFiles !== $currentClass && $hash !== self::calculateHash($classes, $functions)); + } + + + private static function calculateHash($classes, $functions) + { + $hash = []; + foreach ($classes as $name) { + try { + $class = new ReflectionClass($name); + } catch (\ReflectionException $e) { + return; + } + $hash[] = [$name, PhpReflection::getUseStatements($class)]; + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { + if ($prop->getDeclaringClass() == $class) { // intentionally == + $hash[] = [$name, $prop->getName(), $prop->getDocComment()]; + } + } + foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getDeclaringClass() == $class) { // intentionally == + $hash[] = [ + $name, + $method->getName(), + $method->getDocComment(), + implode('', $method->getParameters()), + PHP_VERSION >= 70000 ? $method->getReturnType() : NULL + ]; + } + } + } + + $flip = array_flip($classes); + foreach ($functions as $name) { + try { + $method = strpos($name, '::') ? new ReflectionMethod($name) : new \ReflectionFunction($name); + } catch (\ReflectionException $e) { + return; + } + $class = $method instanceof ReflectionMethod ? $method->getDeclaringClass() : NULL; + if ($class && isset($flip[$class->getName()])) { + continue; + } + $hash[] = [ + $name, + $class ? PhpReflection::getUseStatements($method->getDeclaringClass()) : NULL, + $method->getDocComment(), + implode('', $method->getParameters()), + PHP_VERSION >= 70000 ? $method->getReturnType() : NULL + ]; + } + + return md5(serialize($hash)); } } diff --git a/tests/DI/Compiler.dependencies.phpt b/tests/DI/Compiler.dependencies.phpt index 8835d0e86..80420d245 100644 --- a/tests/DI/Compiler.dependencies.phpt +++ b/tests/DI/Compiler.dependencies.phpt @@ -14,26 +14,26 @@ require __DIR__ . '/../bootstrap.php'; $compiler = new DI\Compiler; Assert::same( - [], + [DependencyChecker::VERSION, [], [], [], [], '40cd750bba9870f18aada2478b24840a'], $compiler->exportDependencies() ); -Assert::false(DependencyChecker::isExpired($compiler->exportDependencies())); +Assert::false(DependencyChecker::isExpired(...$compiler->exportDependencies())); $compiler->addDependencies(['file1', __FILE__]); Assert::same( - ['file1' => FALSE, __FILE__ => filemtime(__FILE__)], + [DependencyChecker::VERSION, ['file1' => FALSE, __FILE__ => filemtime(__FILE__)], [], [], [], '40cd750bba9870f18aada2478b24840a'], $compiler->exportDependencies() ); -Assert::false(DependencyChecker::isExpired($compiler->exportDependencies())); +Assert::false(DependencyChecker::isExpired(...$compiler->exportDependencies())); $compiler->addDependencies(['file1', NULL, 'file3']); Assert::same( - ['file1' => FALSE, __FILE__ => filemtime(__FILE__), 'file3' => FALSE], + [DependencyChecker::VERSION, ['file1' => FALSE, __FILE__ => filemtime(__FILE__), 'file3' => FALSE], [], [], [], '40cd750bba9870f18aada2478b24840a'], $compiler->exportDependencies() ); $res = $compiler->exportDependencies(); -$res['file4'] = 123; -Assert::true(DependencyChecker::isExpired($res)); +$res[1]['file4'] = 123; +Assert::true(DependencyChecker::isExpired(...$res));