Skip to content

Instantly share code, notes, and snippets.

@aeneasr
Created January 18, 2014 13:58
Show Gist options
  • Save aeneasr/8490922 to your computer and use it in GitHub Desktop.
Save aeneasr/8490922 to your computer and use it in GitHub Desktop.
<?php
/**
* ZendDiCompiler
*
* This source file is part of the ZendDiCompiler package
*
* @package ZendDiCompiler
* @license New BSD License
* @copyright Copyright (c) 2013, aimfeld
*/
namespace ZendDiCompiler;
use Zend\Di\Di;
use Zend\Di\Definition\ArrayDefinition;
use Zend\Di\Definition\CompilerDefinition;
use Zend\Di\Definition\IntrospectionStrategy;
use Zend\Di\DefinitionList;
use Zend\Di\InstanceManager;
use Zend\Code\Scanner\DirectoryScanner;
use Zend\Config\Config;
use Zend\Di\Config as DiConfig;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\GlobalEventManager;
use Zend\ServiceManager\AbstractFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use DateTime;
use ZendDiCompiler\Exception\RuntimeException;
use ZendDiCompiler\Exception\RecoverException;
/**
* ZendDiCompiler
*
* @package ZendDiCompiler
*/
class ZendDiCompiler implements AbstractFactoryInterface
{
// Class names of generated PHP files
const GENERATED_SERVICE_LOCATOR = 'GeneratedServiceLocator';
const TEMP_SERVICE_LOCATOR = 'TempServiceLocator';
// Generated after code scanning
const COMPONENT_DEPENDENCY_INFO_FILE = 'component-dependency-info.txt';
/**
* @var Config
*/
protected $config;
/**
* @var object[]
*/
protected $sharedInstances = array();
/**
* @var string[]
*/
protected $typePreferences = array();
/**
* @var bool
*/
protected $isInitialized = false;
/**
* @var bool
*/
protected $hasBeenReset = false;
/**
* @var GeneratedServiceLocator|TempServiceLocator
*/
protected $generatedServiceLocator = null;
/**
* @var IntrospectionStrategy
*/
protected $introspectionStrategy = null;
/**
* Add shared instances to be used by ZendDiCompiler.
*
* Typical things you want to add are e.g. a db adapter, the config, a session. These instances are
* then constructor-injected by ZendDiCompiler. Call this e.g. in the onBootstrap() method of your module class.
*
* @param array $sharedInstances ('MyModule\MyClass' => $instance)
*
* @throws RuntimeException
*/
public function addSharedInstances(array $sharedInstances)
{
// Instances added after initialization (e.g. in onBootstrap() calls) have to be passed to the service locator
if ($this->isInitialized) {
$this->setSharedInstances($this->generatedServiceLocator, $sharedInstances);
}
$this->sharedInstances = array_merge($this->sharedInstances, $sharedInstances);
}
/**
* Use this to replace the standard introspection strategy of ZendDiCompiler
*
* Call this the onBootstrap() method of your module class.
*
* @param IntrospectionStrategy $introspectionStrategy
*/
public function setIntrospectionStrategy(IntrospectionStrategy $introspectionStrategy)
{
$this->introspectionStrategy = $introspectionStrategy;
}
/**
* @param string $name The full class name (including namespace)
* @param array $params A parameter array passed to the class constructor, if it has a array $params argument
* @param bool $newInstance If true, create a new instance every time (use as factory)
*
* @throws \Exception
* @return null|object
*/
public function get($name, array $params = array(), $newInstance = false)
{
echo "$name <br />";
$this->checkInit();
$name = $this->getTypePreference($name);
$instance = null;
try {
// Convert PHP errors to exception to catch errors due to changed constructors, etc.
set_error_handler(array($this, 'exceptionErrorHandler'));
// Outdated definition may cause exceptions (e.g. constructor changes) or a null instance (e.g. new class added).
$instance = $this->generatedServiceLocator->get($name, $params, $newInstance);
// Handle null instance
if (!$instance && !$this->hasBeenReset) {
$this->generatedServiceLocator = $this->reset(true);
$instance = $this->generatedServiceLocator->get($name, $params, $newInstance);
}
restore_error_handler();
} catch (RecoverException $e) {
restore_error_handler();
// Oops, maybe the class constructor has changed during development? Try with rescanned DI definitions.
if (!$this->hasBeenReset) {
$this->generatedServiceLocator = $this->reset(true);
$instance = $this->generatedServiceLocator->get($name, $params, $newInstance);
}
} catch (\Exception $e) {
restore_error_handler();
throw $e;
}
return $instance;
}
/**
* Is called by the ZendDiCompiler module itself.
*
* @param Config $config
*/
public function setConfig(Config $config)
{
$this->config = $config;
// Provide easy access to type preferences
/** @var Config $typePreferences */
$typePreferences = $this->config->di->instance->preference;
$this->typePreferences = $typePreferences->toArray();
}
/**
* Set up DI definitions and create instances.
*
* Is called by the ZendDiCompiler module itself.
*/
public function init()
{
$this->addDefaultSharedInstances();
$this->isInitialized = true;
$fileName = realpath(
sprintf(
'%s/%s.php',
$this->config->zendDiCompiler->writePath, self::GENERATED_SERVICE_LOCATOR
)
);
if (file_exists($fileName)) {
require_once $fileName;
$serviceLocatorClass = __NAMESPACE__ . '\\' . self::GENERATED_SERVICE_LOCATOR;
$this->generatedServiceLocator = new $serviceLocatorClass;
$this->setSharedInstances($this->generatedServiceLocator, $this->sharedInstances);
} else {
$this->generatedServiceLocator = $this->reset(false);
}
}
/**
* Determine if we can create a service with name.
*
* This function is called by the ServiceManager.
*
* @param ServiceLocatorInterface $serviceLocator
* @param string $name
* @param string $requestedName
*
* @return bool
*/
public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
try{
return $this->get($name) !== null;
} catch (RuntimeException $e){
return false;
}
//return true; // Yes, we can!
}
/**
* Create service with name.
*
* This function is called by the ServiceManager.
*
* @param ServiceLocatorInterface $serviceLocator
* @param string $name
* @param string $requestedName
*
* @return mixed
*/
public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
return $this->get($requestedName);
}
/**
* Returns class name or its substitute.
*
* @param string $className
*
* @return string
*/
public function getTypePreference($className)
{
return array_key_exists($className, $this->typePreferences) ?
$this->typePreferences[$className] : $className;
}
/**
* Shared instances which can be injected if Zend\Mvc is used.
*
* @param MvcEvent $mvcEvent
*/
public function addMvcSharedInstances(MvcEvent $mvcEvent)
{
$application = $mvcEvent->getApplication();
$serviceManager = $application->getServiceManager();
$mvcSharedInstances = array(
'Zend\Mvc\MvcEvent' => $mvcEvent,
'Zend\Mvc\Application' => $application,
'Zend\ServiceManager\ServiceManager' => $serviceManager,
'Zend\EventManager\EventManager' => GlobalEventManager::getEventCollection(),
'Zend\Mvc\Router\Http\TreeRouteStack' => $mvcEvent->getRouter(),
'Zend\View\Renderer\PhpRenderer' => $serviceManager->get('Zend\View\Renderer\PhpRenderer'),
);
$this->addSharedInstances($mvcSharedInstances);
}
/**
* Default shared instances which can be injected
*/
protected function addDefaultSharedInstances()
{
// Provide merged config as shared instance
$defaultSharedInstances = array(
'Zend\Config\Config' => $this->config, // The merged global configuration
'ZendDiCompiler\DiFactory' => new DiFactory($this), // Provide DiFactory
get_class($this) => $this, // Provide ZendDiCompiler itself
);
$this->addSharedInstances($defaultSharedInstances);
}
/**
* @return DefinitionList
*/
protected function getDefinitionList()
{
// Compiling definitions can take a while.
set_time_limit(1000);
// Set up the directory scanner.
$directoryScanner = new DirectoryScanner;
foreach ($this->config->zendDiCompiler->scanDirectories as $directory) {
$directoryScanner->addDirectory($directory);
}
// Compile definitions and convert them to an array.
$introspectionStrategy = $this->getIntrospectionStrategy();
$compilerDefinition = new CompilerDefinition($introspectionStrategy);
$compilerDefinition->setAllowReflectionExceptions(true);
$compilerDefinition->addDirectoryScanner($directoryScanner);
$compilerDefinition->compile();
$definitionArray = $compilerDefinition->toArrayDefinition()->toArray();
// Set up the DIC with compiled definitions.
$definitionList = new DefinitionList(array());
$compiledDefinition = new ArrayDefinition($definitionArray);
$definitionList->addDefinition($compiledDefinition);
return $definitionList;
}
/**
* Get introspection strategy (use only constructor injection by default)
*
* @return IntrospectionStrategy
*/
protected function getIntrospectionStrategy()
{
if (!$this->introspectionStrategy) {
$this->introspectionStrategy = new IntrospectionStrategy();
$this->introspectionStrategy->setInterfaceInjectionInclusionPatterns(array());
$this->introspectionStrategy->setMethodNameInclusionPatterns(array());
}
return $this->introspectionStrategy;
}
/**
* @param InstanceManager|GeneratedServiceLocator|TempServiceLocator $object
* @param array $sharedInstances
*/
protected function setSharedInstances($object, array $sharedInstances)
{
/** @noinspection PhpUndefinedClassInspection */
assert(
$object instanceof InstanceManager ||
$object instanceof GeneratedServiceLocator ||
$object instanceof TempServiceLocator
);
if ($object instanceof InstanceManager) {
foreach ($sharedInstances as $classOrAlias => $instance) {
$object->addSharedInstance($instance, $classOrAlias);
}
} else {
foreach ($sharedInstances as $classOrAlias => $instance) {
/** @noinspection PhpUndefinedMethodInspection */
$object->set($classOrAlias, $instance);
}
}
}
/**
* @param bool $recoverFromOutdatedDefinitions
*
* @throws Exception\RuntimeException
* @return GeneratedServiceLocator|TempServiceLocator
*/
protected function generateServiceLocator($recoverFromOutdatedDefinitions)
{
// Check if write directory exists and create it if not
$this->prepareWritePath();
// Setup Di
$di = new Di;
$diConfig = new DiConfig($this->config->di);
$di->configure($diConfig);
$definitionList = $this->getDefinitionList();
$this->writeComponentDependencyInfo($definitionList);
$di->setDefinitionList($definitionList);
$this->setSharedInstances($di->instanceManager(), $this->sharedInstances);
$generator = new Generator($di, $this->config);
list($fileName, $generatedClass) = $this->writeServiceLocator($generator, self::GENERATED_SERVICE_LOCATOR);
if ($recoverFromOutdatedDefinitions) {
// Within a php request, a class can only be loaded once. Therefore, we need a
// temporary service locator class with updated definitions. The class
// is used to create the service locator and then immediately deleted afterwards.
// The next request uses the updated service locator class.
list($fileName, $generatedClass) = $this->writeServiceLocator($generator, self::TEMP_SERVICE_LOCATOR);
require_once $fileName;
/** @var TempServiceLocator $serviceLocator */
$serviceLocator = new $generatedClass;
// Reuse previous shared instances.
$serviceLocator->services = $this->generatedServiceLocator->services;
unlink($fileName);
} else {
require_once $fileName;
$serviceLocator = new $generatedClass;
}
return $serviceLocator;
}
/**
* @param Generator $generator
* @param string $className Without namespace
*
* @return string[] Filename and full class name of the generated service locator class
*/
protected function writeServiceLocator(Generator $generator, $className)
{
$generator->setNamespace(__NAMESPACE__);
$generator->setContainerClass($className);
$file = $generator->getCodeGenerator();
$path = $this->config->zendDiCompiler->writePath;
$fileName = $path . "/$className.php";
$file->setFilename($fileName);
$file->write();
return array($fileName, __NAMESPACE__ . '\\' . $className);
}
/**
* Write component dependencies
*
* Scanned classes are grouped into components. The second part of the class name
* is the component name (Zend\Db\Adapter\Adapter => Db component). The output
* shows for every component on which other components it depends by analysing
* class names of injected classes.
*
* @param DefinitionList $definitions
*/
protected function writeComponentDependencyInfo(DefinitionList $definitions)
{
try {
$classes = $definitions->getClasses();
// Group classes into components
$components = array();
foreach ($classes as $className) {
$componentName = $this->getComponentName($className);
if ($componentName === false) {
continue;
}
$constructorParams = $definitions->getMethodParameters($className, '__construct');
foreach ($constructorParams as $constructorParam) {
// Key 1 contains the full class name for typed parameters.
$paramClassName = isset($constructorParam[1]) ? $constructorParam[1] : null;
if (isset($paramClassName) && // array params excluded
(!array_key_exists($componentName, $components) ||
!in_array($paramClassName, $components[$componentName]))
) {
$components[$componentName][] = $paramClassName;
}
}
}
// Sort components alphabetically
ksort($components);
// Generate output
$now = (new DateTime('now'))->format('Y-m-d H:i:s');
$info = sprintf('Component dependency info - generated by %s (%s)', __NAMESPACE__, $now) . PHP_EOL;
$info .= PHP_EOL;
$info .= 'Scanned classes are grouped into components (e.g. the Zend\Mvc\MvcEvent class belongs to the Zend\Mvc component).' . PHP_EOL .
'For every component, all constructor-injected classes are listed. This helps you analyze which components' . PHP_EOL .
'depend on which classes of other components. Consider organizing your components into layers.' . PHP_EOL .
'Each layer should depend on classes of the same or lower layers only.' . PHP_EOL .
'Note that only constructor-injection is considered for this analysis, so the picture might be incomplete.' . PHP_EOL;
$info .= PHP_EOL;
foreach ($components as $componentName => $dependencies) {
if (count($dependencies) == 0) {
continue;
}
sort($dependencies);
$info .= "$componentName classes inject:" . PHP_EOL;
foreach ($dependencies as $dependency) {
$info .= "- $dependency" . PHP_EOL;
}
$info .= PHP_EOL;
}
} catch (\Exception $e) {
$info = $e->getMessage() . PHP_EOL . $e->getTraceAsString();
}
$path = $this->config->zendDiCompiler->writePath;
$fileName = $path . '/' . self::COMPONENT_DEPENDENCY_INFO_FILE;
file_put_contents($fileName, $info);
}
/**
* @param string $className
*
* @return bool|string Return false if component name could not be determined.
*/
protected function getComponentName($className)
{
$separator = '\\';
$pos1 = strpos($className, $separator);
if (!$pos1) {
$separator = '_'; // Try classnames without namespace
$pos1 = strpos($className, $separator);
if (!$pos1) {
return false;
}
}
$pos2 = strpos($className, $separator, $pos1 + 1);
$componentName = $pos2 ? substr($className, 0, $pos2) : substr($className, 0, $pos1);
return $componentName;
}
/**
* Scan code and update the GeneratedServiceLocator class.
*
* @param bool $recoverFromOutdatedDefinitions
*
* @return GeneratedServiceLocator|TempServiceLocator
*/
protected function reset($recoverFromOutdatedDefinitions)
{
$generatedServiceLocator = $this->generateServiceLocator($recoverFromOutdatedDefinitions);
$this->setSharedInstances($generatedServiceLocator, $this->sharedInstances);
$this->hasBeenReset = true;
return $generatedServiceLocator;
}
/**
* @throws RuntimeException
*/
protected function checkInit()
{
if (!$this->isInitialized) {
throw new RuntimeException(sprintf(
'%s:init() must be called before instances can be retrieved.', get_class($this)
));
}
}
/**
* Convert PHP errors to exceptions.
*
* Instantiating classes with wrong constructor arguments results in an
* E_RECOVERABLE_ERROR which is converted in a RecoverException.
*
* @see http://php.net/manual/en/class.errorexception.php
*
* @param $errno
* @param $errstr
* @param $errfile
* @param $errline
*
* @return bool
* @throws RecoverException
*/
public function exceptionErrorHandler($errno, $errstr, $errfile, $errline)
{
if ($errno == E_RECOVERABLE_ERROR) {
throw new RecoverException($errstr, 0, $errno, $errfile, $errline);
}
return;
}
/**
* @throws RuntimeException
*/
protected function prepareWritePath()
{
$path = $this->config->zendDiCompiler->writePath;
if (!file_exists($path) && !is_dir($path) && !mkdir($path)) {
throw new RuntimeException(sprintf('The directory %s could not be created, check write permissions.', $path));
} elseif (!is_writable($path)) {
throw new RuntimeException(sprintf('The directory %s is not writable.', $path));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment