Skip to content

Commit

Permalink
implemented Fine Grained Dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed May 23, 2016
1 parent 4fc765d commit fb70434
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 32 deletions.
7 changes: 4 additions & 3 deletions src/DI/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
33 changes: 17 additions & 16 deletions src/DI/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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[] */
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -384,10 +384,6 @@ public function prepareClassList()
}
}
}

foreach ($this->classList as $class => $foo) {
$this->addDependency((string) (new ReflectionClass($class))->getFileName());
}
}


Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.");
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}


Expand Down
2 changes: 1 addition & 1 deletion src/DI/ContainerLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]) || DependenciesChecker::isExpired($meta);
return empty($meta[0]) || DependenciesChecker::isExpired(...$meta);
}
return FALSE;
}
Expand Down
92 changes: 87 additions & 5 deletions src/DI/DependenciesChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@

use Nette;
use ReflectionClass;
use ReflectionMethod;


/**
* Cache dependencies checker.
*/
class DependenciesChecker
{
const VERSION = 1;

use Nette\SmartObject;

/** @var array */
/** @var array of ReflectionClass|\ReflectionFunctionAbstract|string */
private $dependencies = [];


Expand All @@ -39,20 +42,99 @@ 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];
}


/**
* 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));
}

}
14 changes: 7 additions & 7 deletions tests/DI/Compiler.dependencies.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,26 @@ require __DIR__ . '/../bootstrap.php';
$compiler = new DI\Compiler;

Assert::same(
[],
[DependenciesChecker::VERSION, [], [], [], [], '40cd750bba9870f18aada2478b24840a'],
$compiler->exportDependencies()
);
Assert::false(DependenciesChecker::isExpired($compiler->exportDependencies()));
Assert::false(DependenciesChecker::isExpired(...$compiler->exportDependencies()));


$compiler->addDependencies(['file1', __FILE__]);
Assert::same(
['file1' => FALSE, __FILE__ => filemtime(__FILE__)],
[DependenciesChecker::VERSION, ['file1' => FALSE, __FILE__ => filemtime(__FILE__)], [], [], [], '40cd750bba9870f18aada2478b24840a'],
$compiler->exportDependencies()
);
Assert::false(DependenciesChecker::isExpired($compiler->exportDependencies()));
Assert::false(DependenciesChecker::isExpired(...$compiler->exportDependencies()));


$compiler->addDependencies(['file1', NULL, 'file3']);
Assert::same(
['file1' => FALSE, __FILE__ => filemtime(__FILE__), 'file3' => FALSE],
[DependenciesChecker::VERSION, ['file1' => FALSE, __FILE__ => filemtime(__FILE__), 'file3' => FALSE], [], [], [], '40cd750bba9870f18aada2478b24840a'],
$compiler->exportDependencies()
);

$res = $compiler->exportDependencies();
$res['file4'] = 123;
Assert::true(DependenciesChecker::isExpired($res));
$res[1]['file4'] = 123;
Assert::true(DependenciesChecker::isExpired(...$res));

1 comment on commit fb70434

@matej21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ amazing, thanks!

Please sign in to comment.