From 405eeea171908fd3fa5d373bc0df2341489d9d95 Mon Sep 17 00:00:00 2001 From: Jonathan Oddy Date: Wed, 2 Dec 2015 11:56:40 +0000 Subject: [PATCH 1/4] Refactor bounce to split config parsing away from how beans are instantiated. --- .../ApcCachedXmlApplicationContext.php | 140 +------- .../Context/ApcCachedXmlContextParser.php | 165 +++++++++ .../Bounce/Context/ApplicationContext.php | 8 +- lib/MooDev/Bounce/Context/BeanFactory.php | 2 +- lib/MooDev/Bounce/Context/IBeanFactory.php | 19 ++ .../Bounce/Context/IContextProvider.php | 23 ++ .../Bounce/Context/XmlApplicationContext.php | 285 +--------------- .../Bounce/Context/XmlContextParser.php | 314 ++++++++++++++++++ 8 files changed, 538 insertions(+), 418 deletions(-) create mode 100644 lib/MooDev/Bounce/Context/ApcCachedXmlContextParser.php create mode 100644 lib/MooDev/Bounce/Context/IBeanFactory.php create mode 100644 lib/MooDev/Bounce/Context/IContextProvider.php create mode 100644 lib/MooDev/Bounce/Context/XmlContextParser.php diff --git a/lib/MooDev/Bounce/Context/ApcCachedXmlApplicationContext.php b/lib/MooDev/Bounce/Context/ApcCachedXmlApplicationContext.php index 58e39a1..8af5133 100644 --- a/lib/MooDev/Bounce/Context/ApcCachedXmlApplicationContext.php +++ b/lib/MooDev/Bounce/Context/ApcCachedXmlApplicationContext.php @@ -21,146 +21,20 @@ * @author steve * */ -class ApcCachedXmlApplicationContext extends XmlApplicationContext +class ApcCachedXmlApplicationContext extends ApplicationContext { - /** - * @var Logger the logging instance - */ - private $_log; - /** - * @var string => array map of stat info returned by stat calls keyed on - * the filename - */ - protected $_statInfoCache = array(); /** - * Creates the caching instance, setting up logging for delegating upwards - * @param string $xmlFilePath - * @param bool $shareBeanCache + * @param $xmlFilePath + * @param bool $shareBeanCache whether to share the bean cache when the context's uniqueID (file path) is the same. * @param ValueTagProvider[] $customNamespaces - * @param array $logFactory Callback array to call to obtain a logger instance. Will be called with a single param (class name.) + * @param array $logFactory */ public function __construct($xmlFilePath, $shareBeanCache = true, $customNamespaces = array(), $logFactory = array('\MooDev\Bounce\Logger\NullLogFactory', 'getLog')) { - $this->_log = call_user_func($logFactory, get_class($this)); - parent::__construct($xmlFilePath, $shareBeanCache, $customNamespaces); + $contextConfig = (new XmlContextParser($xmlFilePath, $customNamespaces))->getContext(); + //Create the bean factory + parent::__construct(BeanFactory::getInstance($contextConfig, $shareBeanCache)); } - protected function _parseXmlFile($xmlFilePath) - { - //Do we have caching enabled? - if (function_exists("apc_fetch")) { - if ($this->_log->isDebugEnabled()) { - $this->_log->debug("APC caching is ENABLED"); - } - //Do we have a cache entry for this file path? - $cacheKey = "bouncexml~$xmlFilePath"; - $cacheContentSerialized = apc_fetch($cacheKey); - //Cache Content structure is: - // array( - // "xmlFiles" => array( - // "/absolute/path/to/parent.xml" => array( - // "dev" => 5, //Device number from the underlying O/S - // "ino" => 1234567, //Inode number of the file on the device - // "mtime" => 124124681274 //Last modified time - // ), - // "/absolute/path/to/child1.xml" => array( - // "dev" => 5, - // "ino" => 345346364, - // "mtime" => 124124681274 - // ), - // "/absolute/path/to/child2.xml" => array( - // "dev" => 5, - // "ino" => 345346364, - // "mtime" => 124124681274 - // ), - // ), - // "context" => Config_Context - // ) - if ($cacheContentSerialized) { - if ($this->_log->isDebugEnabled()) { - $this->_log->debug("Found cached version on $xmlFilePath"); - } - $cacheContent = unserialize($cacheContentSerialized); - //Look at all the files from the cache content array to see - //if any of them are different - $filesToCheck = $cacheContent["xmlFiles"]; - $cacheUpToDate = true; //Be positive ... - foreach ($filesToCheck as $fileName => $cachedStatInfo) { - //Stat the filename - $onDiskStatInfo = $this->_getStatInfo($fileName); - if ($onDiskStatInfo["mtime"] != $cachedStatInfo["mtime"] || - $onDiskStatInfo["ino"] != $cachedStatInfo["ino"] || - $onDiskStatInfo["dev"] != $cachedStatInfo["dev"] - ) { - $cacheUpToDate = false; - if ($this->_log->isDebugEnabled()) { - $this->_log->debug("Cache out of date: $fileName has stat: " . print_r($onDiskStatInfo, true) . " but cached " . print_r($cachedStatInfo, true)); - } - break; - } - } - if ($cacheUpToDate) { - //Yes, so update the list of processed files for completeness, and - //return the cached context - /*foreach ($filesToCheck as $fileName => $cachedStatInfo) { - $this->_processedFiles[$fileName] = $cacheContent["context"]; - }*/ //TODO We can't update this any more since it's a map of child contexts, which we don't have - if ($this->_log->isDebugEnabled()) { - $this->_log->debug("Cache is up to date. Returning cached context"); - } - return $cacheContent["context"]; - } - } - if ($this->_log->isDebugEnabled()) { - $this->_log->debug("Cache item not found or invalid for $xmlFilePath. Reloading ..."); - } - //If we get here, we're basically not up to date one way or another - //so load up the context, and cache it. - $context = parent::_parseXmlFile($xmlFilePath); - //Now create the cache structure. NOTE! It's possible that for a - //nested content structure we're going to include files from the - //processed list that had been processed *BEFORE* we loaded this - //particular XML file. However, we can't tell which ones might - //also be dependencies of this child file, and using another - //XmlApplicationContext instance is impractical as it means exposing - //way more of the internals as public variables purely for the purposes - //of the caching system. - $cacheContent = array( - "xmlFiles" => array(), - "context" => $context - ); - foreach (array_keys($this->_processedFiles) as $fileName) { - $cacheContent["xmlFiles"][$fileName] = $this->_getStatInfo($fileName); - } - $cacheContentStr = serialize($cacheContent); - apc_store($cacheKey, $cacheContentStr); - if ($this->_log->isDebugEnabled()) { - $this->_log->debug("Cache item added for $xmlFilePath"); - } - //Return it - return $context; - } else { - $this->_log->warn("APC caching is DISABLED"); - //No caching possible, so revert to the normal method of - //reading from the filesystem all the time - return parent::_parseXmlFile($xmlFilePath); - } - } - - /** - * Returns the stat info for a given filename. This will make reading and - * processing for putting into the cache rather more efficient. - * - * @param $fileName string the name of the file we want to get stat information - * for - * @return array result of the stat call for this filename - */ - private function _getStatInfo($fileName) - { - if (!array_key_exists($fileName, $this->_statInfoCache)) { - $this->_statInfoCache[$fileName] = stat($fileName); - } - return $this->_statInfoCache[$fileName]; - } } diff --git a/lib/MooDev/Bounce/Context/ApcCachedXmlContextParser.php b/lib/MooDev/Bounce/Context/ApcCachedXmlContextParser.php new file mode 100644 index 0000000..e3b7e5f --- /dev/null +++ b/lib/MooDev/Bounce/Context/ApcCachedXmlContextParser.php @@ -0,0 +1,165 @@ + + * @copyright Copyright (c) 2012, MOO Print Ltd. + * @license ISC + */ + +namespace MooDev\Bounce\Context; + +use MooDev\Bounce\Config; +use MooDev\Bounce\Logger\Logger; + +/** + * Sub-class of the XmlApplicationContext which caches the parsed configuration + * objects into the APC cache. + * + * It keeps track of both modification times, and the inodes of all the files + * which have been read in to create the configuration. If any files are newer + * or have different inodes, then reload everything and re-cache it. + * + * @author steve + * + */ +class ApcCachedXmlContextProvider extends XmlContextParser +{ + /** + * @var Logger the logging instance + */ + private $_log; + /** + * @var string => array map of stat info returned by stat calls keyed on + * the filename + */ + protected $_statInfoCache = array(); + + /** + * Creates the caching instance, setting up logging for delegating upwards + * @param string $xmlFilePath + * @param ValueTagProvider[] $customNamespaces + * @param array $logFactory Callback array to call to obtain a logger instance. Will be called with a single param (class name.) + */ + public function __construct($xmlFilePath, $customNamespaces = array(), $logFactory = array('\MooDev\Bounce\Logger\NullLogFactory', 'getLog')) + { + $this->_log = call_user_func($logFactory, get_class($this)); + parent::__construct($xmlFilePath, $customNamespaces); + } + + protected function _parseXmlFile($xmlFilePath) + { + //Do we have caching enabled? + if (function_exists("apc_fetch")) { + if ($this->_log->isDebugEnabled()) { + $this->_log->debug("APC caching is ENABLED"); + } + //Do we have a cache entry for this file path? + $cacheKey = "bouncexml~$xmlFilePath"; + $cacheContentSerialized = apc_fetch($cacheKey); + //Cache Content structure is: + // array( + // "xmlFiles" => array( + // "/absolute/path/to/parent.xml" => array( + // "dev" => 5, //Device number from the underlying O/S + // "ino" => 1234567, //Inode number of the file on the device + // "mtime" => 124124681274 //Last modified time + // ), + // "/absolute/path/to/child1.xml" => array( + // "dev" => 5, + // "ino" => 345346364, + // "mtime" => 124124681274 + // ), + // "/absolute/path/to/child2.xml" => array( + // "dev" => 5, + // "ino" => 345346364, + // "mtime" => 124124681274 + // ), + // ), + // "context" => Config_Context + // ) + if ($cacheContentSerialized) { + if ($this->_log->isDebugEnabled()) { + $this->_log->debug("Found cached version on $xmlFilePath"); + } + $cacheContent = unserialize($cacheContentSerialized); + //Look at all the files from the cache content array to see + //if any of them are different + $filesToCheck = $cacheContent["xmlFiles"]; + $cacheUpToDate = true; //Be positive ... + foreach ($filesToCheck as $fileName => $cachedStatInfo) { + //Stat the filename + $onDiskStatInfo = $this->_getStatInfo($fileName); + if ($onDiskStatInfo["mtime"] != $cachedStatInfo["mtime"] || + $onDiskStatInfo["ino"] != $cachedStatInfo["ino"] || + $onDiskStatInfo["dev"] != $cachedStatInfo["dev"] + ) { + $cacheUpToDate = false; + if ($this->_log->isDebugEnabled()) { + $this->_log->debug("Cache out of date: $fileName has stat: " . print_r($onDiskStatInfo, true) . " but cached " . print_r($cachedStatInfo, true)); + } + break; + } + } + if ($cacheUpToDate) { + //Yes, so update the list of processed files for completeness, and + //return the cached context + /*foreach ($filesToCheck as $fileName => $cachedStatInfo) { + $this->_processedFiles[$fileName] = $cacheContent["context"]; + }*/ //TODO We can't update this any more since it's a map of child contexts, which we don't have + if ($this->_log->isDebugEnabled()) { + $this->_log->debug("Cache is up to date. Returning cached context"); + } + return $cacheContent["context"]; + } + } + if ($this->_log->isDebugEnabled()) { + $this->_log->debug("Cache item not found or invalid for $xmlFilePath. Reloading ..."); + } + //If we get here, we're basically not up to date one way or another + //so load up the context, and cache it. + $context = parent::_parseXmlFile($xmlFilePath); + //Now create the cache structure. NOTE! It's possible that for a + //nested content structure we're going to include files from the + //processed list that had been processed *BEFORE* we loaded this + //particular XML file. However, we can't tell which ones might + //also be dependencies of this child file, and using another + //XmlApplicationContext instance is impractical as it means exposing + //way more of the internals as public variables purely for the purposes + //of the caching system. + $cacheContent = array( + "xmlFiles" => array(), + "context" => $context + ); + foreach (array_keys($this->_processedFiles) as $fileName) { + $cacheContent["xmlFiles"][$fileName] = $this->_getStatInfo($fileName); + } + $cacheContentStr = serialize($cacheContent); + apc_store($cacheKey, $cacheContentStr); + if ($this->_log->isDebugEnabled()) { + $this->_log->debug("Cache item added for $xmlFilePath"); + } + //Return it + return $context; + } else { + $this->_log->warn("APC caching is DISABLED"); + //No caching possible, so revert to the normal method of + //reading from the filesystem all the time + return parent::_parseXmlFile($xmlFilePath); + } + } + + /** + * Returns the stat info for a given filename. This will make reading and + * processing for putting into the cache rather more efficient. + * + * @param $fileName string the name of the file we want to get stat information + * for + * @return array result of the stat call for this filename + */ + private function _getStatInfo($fileName) + { + if (!array_key_exists($fileName, $this->_statInfoCache)) { + $this->_statInfoCache[$fileName] = stat($fileName); + } + return $this->_statInfoCache[$fileName]; + } +} diff --git a/lib/MooDev/Bounce/Context/ApplicationContext.php b/lib/MooDev/Bounce/Context/ApplicationContext.php index 5b60c0b..0fa066a 100644 --- a/lib/MooDev/Bounce/Context/ApplicationContext.php +++ b/lib/MooDev/Bounce/Context/ApplicationContext.php @@ -15,8 +15,14 @@ */ class ApplicationContext { + + public function __construct(IBeanFactory $beanFactory) + { + $this->_beanFactory = $beanFactory; + } + /** - * @var $_beanFactory BeanFactory a bean factory instance + * @var $_beanFactory IBeanFactory a bean factory instance * to retrieve objects from. */ protected $_beanFactory; diff --git a/lib/MooDev/Bounce/Context/BeanFactory.php b/lib/MooDev/Bounce/Context/BeanFactory.php index d94cfdc..045acf1 100644 --- a/lib/MooDev/Bounce/Context/BeanFactory.php +++ b/lib/MooDev/Bounce/Context/BeanFactory.php @@ -20,7 +20,7 @@ * * @property Config\Context contextConfig */ -class BeanFactory +class BeanFactory implements IBeanFactory { /** diff --git a/lib/MooDev/Bounce/Context/IBeanFactory.php b/lib/MooDev/Bounce/Context/IBeanFactory.php new file mode 100644 index 0000000..f2d0123 --- /dev/null +++ b/lib/MooDev/Bounce/Context/IBeanFactory.php @@ -0,0 +1,19 @@ +_customNamespaces = $customNamespaces; - $contextConfig = $this->_parseXmlFile(realpath($xmlFilePath)); + $contextConfig = (new XmlContextParser($xmlFilePath, $customNamespaces))->getContext(); //Create the bean factory - $this->_beanFactory = BeanFactory::getInstance($contextConfig, $shareBeanCache); - } - - protected function _parseXmlFile($xmlFilePath) - { - //Check we haven't processed this file already - if (array_key_exists($xmlFilePath, $this->_processedFiles)) { - return $this->_processedFiles[$xmlFilePath]; - } - //Check we haven't started processing this file already - if (in_array($xmlFilePath, $this->_importFilesStack)) { - throw new BounceException("Infinite recursion import detected with file $xmlFilePath"); - } - array_push($this->_importFilesStack, $xmlFilePath); - - //Load the XML file - $xmlContent = file_get_contents($xmlFilePath); - //Parse into Context config - $this->_beansTypeSafeParser = new TypeSafeParser("http://www.moo.com/xsd/bounce-beans-1.0"); - $this->_phpTypeSafeParser = new TypeSafeParser("http://www.moo.com/xsd/bounce-php-1.0"); - $contextConfig = $this->_parseConfigXml($xmlContent); - $contextConfig->uniqueId = $xmlFilePath; - - //Remember that we've processed this, and pop it off the processing stack - $this->_processedFiles[$xmlFilePath] = $contextConfig; - array_pop($this->_importFilesStack); - return $contextConfig; - } - - protected function _parseConfigXml($xmlContent) - { - $beansXml = new SimpleXMLElement($xmlContent); - $beansXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); - $beansXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); - //Create the config object - $contextConfig = new Config\Context(); - //Process all imports first - $importItems = $beansXml->xpath("beans:import"); - foreach ($importItems as $importXml) { - $relativePath = $this->_beansTypeSafeParser->extractAttribute($importXml, "path"); - //Work out the absolute path based on the directory of the file currently being processed - $importXmlPath = dirname($this->_importFilesStack[count($this->_importFilesStack) - 1]) . "/" . $relativePath; - if (!is_file($importXmlPath) || !is_readable($importXmlPath)) { - throw new BounceException("Unable to find/read file: " . $importXmlPath); - } - $importedContextConfig = $this->_parseXmlFile(realpath($importXmlPath)); - if (!is_null($importedContextConfig)) { - //Add on all the beans to our context - $contextConfig->childContexts[] = $importedContextConfig; - } - } - - //Go through all the bean references to create their config - $beanItems = $beansXml->xpath("beans:bean"); - foreach ($beanItems as $beanXml) { - $beanXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); - $beanXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); - $this->_parseAndRegisterBean($beanXml, $contextConfig); - } - return $contextConfig; - } - - /** - * Returns whether the given bean name is already registered within this context (or its children) - * - * @param Config\Context $context the parent context to be checked - * @param string $beanName the name of the bean to search for in this context and all its - * children - * @return bool - */ - protected function _alreadyRegistered(Config\Context $context, $beanName) - { - if (isset($context->beans[$beanName])) { - return true; - } elseif (count($context->childContexts) > 0) { - foreach ($context->childContexts as $childContext) { - if ($this->_alreadyRegistered($childContext, $beanName)) { - return true; - } - } - } - return false; - } - - /** - * Parses a tag and returna a Bean opbject holding all the configuration - * items for the bean. - * - * @param \SimpleXMLElement $beanXml the XML element for the Bean tag - * @param \MooDev\Bounce\Config\Context $contextConfig - * @throws BounceException - * @return Config\Bean - */ - protected function _parseAndRegisterBean(SimpleXMLElement $beanXml, Config\Context $contextConfig) - { - $bean = new Config\Bean(); - //Parse the items out - $bean->class = strval($beanXml["class"]); - $name = $beanXml["name"]; - $id = $beanXml["id"]; - - $factoryBean = $beanXml["factory-bean"]; - $factoryMethod = $beanXml["factory-method"]; - - $bean->factoryBean = !is_null($factoryBean) ? strval($factoryBean) : null; - $bean->factoryMethod = !is_null($factoryMethod) ? strval($factoryMethod) : null; - - $bean->scope = isset($beanXml["scope"]) ? strval($beanXml["scope"]) : null; - - $bean->name = strval(!is_null($id) ? $id : $name); - - $lookupMethodItems = $beanXml->xpath("beans:lookup-method"); - foreach ($lookupMethodItems as $lookupMethodXml) { - $lookupMethodXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); - $lookupMethodXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); - $lookupMethodConfig = $this->_parseLookupMethod($lookupMethodXml, $contextConfig); - if (!is_null($lookupMethodConfig)) { - $bean->lookupMethods[] = $lookupMethodConfig; - } - } - - $propertyItems = $beanXml->xpath("beans:property"); - foreach ($propertyItems as $propertyXml) { - $propertyXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); - $propertyXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); - $propertyName = strval($propertyXml["name"]); - $propertyValueProvider = $this->_parseProperty($propertyXml, $contextConfig); - if (!is_null($propertyValueProvider)) { - $bean->properties[$propertyName] = $propertyValueProvider; - } - } - $constructorArgItems = $beanXml->xpath("beans:constructor-arg"); - foreach ($constructorArgItems as $constructorArgXml) { - $constructorArgXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); - $constructorArgXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); - $propertyValueProvider = $this->_parseProperty($constructorArgXml, $contextConfig); - if (!is_null($propertyValueProvider)) { - $bean->constructorArguments[] = $propertyValueProvider; - } - } - if (isset($bean->name) && trim($bean->name) != "") { - if ($this->_alreadyRegistered($contextConfig, $bean->name)) { - throw new BounceException("Duplicate bean definition: $bean->name"); - } - $contextConfig->beans[$bean->name] = $bean; - } - return $bean; - } - - protected function _createSimpleValueProvider($value) - { - $valueProvider = null; - if (is_numeric($value)) { - if (is_integer($value)) { - $valueProvider = new Config\SimpleValueProvider(intval($value)); - } else { - $valueProvider = new Config\SimpleValueProvider(floatval($value)); - } - } else { - $valueProvider = new Config\SimpleValueProvider($value); - } - return $valueProvider; - } - - protected function _parseValueTag(SimpleXMLElement $element, Config\Context $contextConfig) - { - $valueProvider = null; - $tagName = $element->getName(); - $nsArray = $element->getNamespaces(); - if (count($nsArray) > 0) { - $namespace = array_shift($nsArray); - if (isset($this->_customNamespaces[$namespace])) { - return $this->_customNamespaces[$namespace]->getValueProvider($element, $contextConfig); - } - } - if ($tagName == "value") { - $valueProvider = $this->_createSimpleValueProvider(strval($element)); - } elseif ($tagName == "bean") { - $ref = $this->_beansTypeSafeParser->extractAttribute($element, "ref"); - if (is_null($ref)) { - $element->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); - $element->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); - $valueProvider = new Config\BeanValueProvider($this->_parseAndRegisterBean($element, $contextConfig)); - } else { - $valueProvider = new Config\BeanRefValueProvider(strval($ref)); - } - } elseif ($tagName == "null") { - $valueProvider = new Config\NullValueProvider(); - } elseif ($tagName == "map") { - $mapArray = array(); - $element->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); - $mapEntryItems = $element->xpath("beans:entry"); - foreach ($mapEntryItems as $entryXml) { - $entryName = strval($entryXml["name"]); - $entryValueProvider = $this->_parseProperty($entryXml, $contextConfig); - if (!is_null($entryValueProvider)) { - $mapArray[$entryName] = $entryValueProvider; - } - } - $valueProvider = new Config\MapValueProvider($mapArray); - } elseif ($tagName == "list") { - $listArray = array(); - $listEntryItems = $element->xpath("*"); - foreach ($listEntryItems as $entryXml) { - $entryValueProvider = $this->_parseValueTag($entryXml, $contextConfig); - if (!is_null($entryValueProvider)) { - $listArray[] = $entryValueProvider; - } - } - $valueProvider = new Config\ListValueProvider($listArray); - } elseif ($tagName == "constant") { - $constantName = $this->_phpTypeSafeParser->extractAttribute($element, "name"); - $valueProvider = new Config\ConstantValueProvider($constantName); - } elseif ($tagName == "string") { - $valueProvider = new Config\SimpleValueProvider(strval($element)); - } elseif ($tagName == "int") { - $valueProvider = new Config\SimpleValueProvider(intval($element)); - } elseif ($tagName == "float") { - $valueProvider = new Config\SimpleValueProvider(floatval($element)); - } elseif ($tagName == "bool") { - $valueProvider = new Config\SimpleValueProvider(strval($element) == "true"); - } elseif ($tagName == "file") { - $valueProvider = new Config\FilePathValueProvider(strval($element)); - } - return $valueProvider; - } - - protected function _parseProperty(SimpleXMLElement $propertyXml, Config\Context $contextConfig) - { - //Work out what kind of property this is - $valueProvider = null; - $valueAttr = $this->_beansTypeSafeParser->extractAttribute($propertyXml, "value"); - if (!is_null($valueAttr)) { - //Simple value - $value = strval($valueAttr); - $valueProvider = $this->_createSimpleValueProvider($value); - } else { - //Look to see if there's a ref - $refAttr = $propertyXml["ref"]; - if (!is_null($refAttr)) { - $valueProvider = new Config\BeanRefValueProvider(strval($refAttr)); - } else { - $propertyValueItems = $propertyXml->xpath("*"); - foreach ($propertyValueItems as $propertyValue) { - $valueProvider = $this->_parseValueTag($propertyValue, $contextConfig); - } - } - } - return $valueProvider; - } - - protected function _parseLookupMethod(SimpleXMLElement $lookupMethodXml, Config\Context $contextConfig) { - $nameAttr = $this->_beansTypeSafeParser->extractAttribute($lookupMethodXml, "name"); - $beanAttr = $this->_beansTypeSafeParser->extractAttribute($lookupMethodXml, "bean"); - - return new Config\LookupMethod(strval($nameAttr), strval($beanAttr)); - + parent::__construct(BeanFactory::getInstance($contextConfig, $shareBeanCache)); } } diff --git a/lib/MooDev/Bounce/Context/XmlContextParser.php b/lib/MooDev/Bounce/Context/XmlContextParser.php new file mode 100644 index 0000000..13b3dcb --- /dev/null +++ b/lib/MooDev/Bounce/Context/XmlContextParser.php @@ -0,0 +1,314 @@ + + * @copyright Copyright (c) 2012, MOO Print Ltd. + * @license ISC + */ + +namespace MooDev\Bounce\Context; + +use MooDev\Bounce\Exception\BounceException; +use MooDev\Bounce\Config; +use SimpleXMLElement; +use MooDev\Bounce\Xml\TypeSafeParser; + +class XmlContextParser implements IContextProvider +{ + + /** + * @var TypeSafeParser a parsing helper instance + */ + private $_beansTypeSafeParser; + /** + * @var TypeSafeParser a parsing helper instance + */ + private $_phpTypeSafeParser; + /** + * @var string[] keeps track of all the files we're processing in nested + * order to detect and prevent any infinite loops + */ + protected $_importFilesStack = array(); + protected $_processedFiles = array(); + + /** + * @var ValueTagProvider[] + */ + private $_customNamespaces = array(); + + private $xmlFilePath; + + /** + * @param $xmlFilePath + * @param ValueTagProvider[] $customNamespaces + * @throws \MooDev\Bounce\Exception\BounceException + */ + public function __construct($xmlFilePath, $customNamespaces = array()) + { + if (!file_exists($xmlFilePath)) { + throw new BounceException("XML context not found: " . $xmlFilePath); + } + $this->xmlFilePath = realpath($xmlFilePath); + $this->_customNamespaces = $customNamespaces; + } + + public function getContext() + { + return $this->_parseXmlFile($this->xmlFilePath); + } + + protected function _parseXmlFile($xmlFilePath) { + //Check we haven't processed this file already + if (array_key_exists($xmlFilePath, $this->_processedFiles)) { + return $this->_processedFiles[$xmlFilePath]; + } + //Check we haven't started processing this file already + if (in_array($xmlFilePath, $this->_importFilesStack)) { + throw new BounceException("Infinite recursion import detected with file $xmlFilePath"); + } + array_push($this->_importFilesStack, $xmlFilePath); + + //Load the XML file + $xmlContent = file_get_contents($xmlFilePath); + //Parse into Context config + $this->_beansTypeSafeParser = new TypeSafeParser("http://www.moo.com/xsd/bounce-beans-1.0"); + $this->_phpTypeSafeParser = new TypeSafeParser("http://www.moo.com/xsd/bounce-php-1.0"); + $contextConfig = $this->_parseConfigXml($xmlContent); + $contextConfig->uniqueId = $xmlFilePath; + + //Remember that we've processed this, and pop it off the processing stack + $this->_processedFiles[$xmlFilePath] = $contextConfig; + array_pop($this->_importFilesStack); + return $contextConfig; + } + + protected function _parseConfigXml($xmlContent) + { + $beansXml = new SimpleXMLElement($xmlContent); + $beansXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); + $beansXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); + //Create the config object + $contextConfig = new Config\Context(); + //Process all imports first + $importItems = $beansXml->xpath("beans:import"); + foreach ($importItems as $importXml) { + $relativePath = $this->_beansTypeSafeParser->extractAttribute($importXml, "path"); + //Work out the absolute path based on the directory of the file currently being processed + $importXmlPath = dirname($this->_importFilesStack[count($this->_importFilesStack) - 1]) . "/" . $relativePath; + if (!is_file($importXmlPath) || !is_readable($importXmlPath)) { + throw new BounceException("Unable to find/read file: " . $importXmlPath); + } + $importedContextConfig = $this->_parseXmlFile(realpath($importXmlPath)); + if (!is_null($importedContextConfig)) { + //Add on all the beans to our context + $contextConfig->childContexts[] = $importedContextConfig; + } + } + + //Go through all the bean references to create their config + $beanItems = $beansXml->xpath("beans:bean"); + foreach ($beanItems as $beanXml) { + $beanXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); + $beanXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); + $this->_parseAndRegisterBean($beanXml, $contextConfig); + } + return $contextConfig; + } + + /** + * Returns whether the given bean name is already registered within this context (or its children) + * + * @param Config\Context $context the parent context to be checked + * @param string $beanName the name of the bean to search for in this context and all its + * children + * @return bool + */ + protected function _alreadyRegistered(Config\Context $context, $beanName) + { + if (isset($context->beans[$beanName])) { + return true; + } elseif (count($context->childContexts) > 0) { + foreach ($context->childContexts as $childContext) { + if ($this->_alreadyRegistered($childContext, $beanName)) { + return true; + } + } + } + return false; + } + + /** + * Parses a tag and returna a Bean opbject holding all the configuration + * items for the bean. + * + * @param \SimpleXMLElement $beanXml the XML element for the Bean tag + * @param \MooDev\Bounce\Config\Context $contextConfig + * @throws BounceException + * @return Config\Bean + */ + protected function _parseAndRegisterBean(SimpleXMLElement $beanXml, Config\Context $contextConfig) + { + $bean = new Config\Bean(); + //Parse the items out + $bean->class = strval($beanXml["class"]); + $name = $beanXml["name"]; + $id = $beanXml["id"]; + + $factoryBean = $beanXml["factory-bean"]; + $factoryMethod = $beanXml["factory-method"]; + + $bean->factoryBean = !is_null($factoryBean) ? strval($factoryBean) : null; + $bean->factoryMethod = !is_null($factoryMethod) ? strval($factoryMethod) : null; + + $bean->scope = isset($beanXml["scope"]) ? strval($beanXml["scope"]) : null; + + $bean->name = strval(!is_null($id) ? $id : $name); + + $lookupMethodItems = $beanXml->xpath("beans:lookup-method"); + foreach ($lookupMethodItems as $lookupMethodXml) { + $lookupMethodXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); + $lookupMethodXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); + $lookupMethodConfig = $this->_parseLookupMethod($lookupMethodXml, $contextConfig); + if (!is_null($lookupMethodConfig)) { + $bean->lookupMethods[] = $lookupMethodConfig; + } + } + + $propertyItems = $beanXml->xpath("beans:property"); + foreach ($propertyItems as $propertyXml) { + $propertyXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); + $propertyXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); + $propertyName = strval($propertyXml["name"]); + $propertyValueProvider = $this->_parseProperty($propertyXml, $contextConfig); + if (!is_null($propertyValueProvider)) { + $bean->properties[$propertyName] = $propertyValueProvider; + } + } + $constructorArgItems = $beanXml->xpath("beans:constructor-arg"); + foreach ($constructorArgItems as $constructorArgXml) { + $constructorArgXml->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); + $constructorArgXml->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); + $propertyValueProvider = $this->_parseProperty($constructorArgXml, $contextConfig); + if (!is_null($propertyValueProvider)) { + $bean->constructorArguments[] = $propertyValueProvider; + } + } + if (isset($bean->name) && trim($bean->name) != "") { + if ($this->_alreadyRegistered($contextConfig, $bean->name)) { + throw new BounceException("Duplicate bean definition: $bean->name"); + } + $contextConfig->beans[$bean->name] = $bean; + } + return $bean; + } + + protected function _createSimpleValueProvider($value) + { + $valueProvider = null; + if (is_numeric($value)) { + if (is_integer($value)) { + $valueProvider = new Config\SimpleValueProvider(intval($value)); + } else { + $valueProvider = new Config\SimpleValueProvider(floatval($value)); + } + } else { + $valueProvider = new Config\SimpleValueProvider($value); + } + return $valueProvider; + } + + protected function _parseValueTag(SimpleXMLElement $element, Config\Context $contextConfig) + { + $valueProvider = null; + $tagName = $element->getName(); + $nsArray = $element->getNamespaces(); + if (count($nsArray) > 0) { + $namespace = array_shift($nsArray); + if (isset($this->_customNamespaces[$namespace])) { + return $this->_customNamespaces[$namespace]->getValueProvider($element, $contextConfig); + } + } + if ($tagName == "value") { + $valueProvider = $this->_createSimpleValueProvider(strval($element)); + } elseif ($tagName == "bean") { + $ref = $this->_beansTypeSafeParser->extractAttribute($element, "ref"); + if (is_null($ref)) { + $element->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); + $element->registerXPathNamespace("php", "http://www.moo.com/xsd/bounce-php-1.0"); + $valueProvider = new Config\BeanValueProvider($this->_parseAndRegisterBean($element, $contextConfig)); + } else { + $valueProvider = new Config\BeanRefValueProvider(strval($ref)); + } + } elseif ($tagName == "null") { + $valueProvider = new Config\NullValueProvider(); + } elseif ($tagName == "map") { + $mapArray = array(); + $element->registerXPathNamespace("beans", "http://www.moo.com/xsd/bounce-beans-1.0"); + $mapEntryItems = $element->xpath("beans:entry"); + foreach ($mapEntryItems as $entryXml) { + $entryName = strval($entryXml["name"]); + $entryValueProvider = $this->_parseProperty($entryXml, $contextConfig); + if (!is_null($entryValueProvider)) { + $mapArray[$entryName] = $entryValueProvider; + } + } + $valueProvider = new Config\MapValueProvider($mapArray); + } elseif ($tagName == "list") { + $listArray = array(); + $listEntryItems = $element->xpath("*"); + foreach ($listEntryItems as $entryXml) { + $entryValueProvider = $this->_parseValueTag($entryXml, $contextConfig); + if (!is_null($entryValueProvider)) { + $listArray[] = $entryValueProvider; + } + } + $valueProvider = new Config\ListValueProvider($listArray); + } elseif ($tagName == "constant") { + $constantName = $this->_phpTypeSafeParser->extractAttribute($element, "name"); + $valueProvider = new Config\ConstantValueProvider($constantName); + } elseif ($tagName == "string") { + $valueProvider = new Config\SimpleValueProvider(strval($element)); + } elseif ($tagName == "int") { + $valueProvider = new Config\SimpleValueProvider(intval($element)); + } elseif ($tagName == "float") { + $valueProvider = new Config\SimpleValueProvider(floatval($element)); + } elseif ($tagName == "bool") { + $valueProvider = new Config\SimpleValueProvider(strval($element) == "true"); + } elseif ($tagName == "file") { + $valueProvider = new Config\FilePathValueProvider(strval($element)); + } + return $valueProvider; + } + + protected function _parseProperty(SimpleXMLElement $propertyXml, Config\Context $contextConfig) + { + //Work out what kind of property this is + $valueProvider = null; + $valueAttr = $this->_beansTypeSafeParser->extractAttribute($propertyXml, "value"); + if (!is_null($valueAttr)) { + //Simple value + $value = strval($valueAttr); + $valueProvider = $this->_createSimpleValueProvider($value); + } else { + //Look to see if there's a ref + $refAttr = $propertyXml["ref"]; + if (!is_null($refAttr)) { + $valueProvider = new Config\BeanRefValueProvider(strval($refAttr)); + } else { + $propertyValueItems = $propertyXml->xpath("*"); + foreach ($propertyValueItems as $propertyValue) { + $valueProvider = $this->_parseValueTag($propertyValue, $contextConfig); + } + } + } + return $valueProvider; + } + + protected function _parseLookupMethod(SimpleXMLElement $lookupMethodXml, Config\Context $contextConfig) { + $nameAttr = $this->_beansTypeSafeParser->extractAttribute($lookupMethodXml, "name"); + $beanAttr = $this->_beansTypeSafeParser->extractAttribute($lookupMethodXml, "bean"); + + return new Config\LookupMethod(strval($nameAttr), strval($beanAttr)); + + } + +} From 4d350cdafbc2a6de2e73eb3ec53754fed30cd538 Mon Sep 17 00:00:00 2001 From: Jonathan Oddy Date: Wed, 2 Dec 2015 12:11:11 +0000 Subject: [PATCH 2/4] Refactor bounce to split config parsing away from how beans are instantiated. --- lib/MooDev/Bounce/Context/IBeanFactory.php | 20 +++++++++++++------ .../Bounce/Context/IContextProvider.php | 11 +++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/MooDev/Bounce/Context/IBeanFactory.php b/lib/MooDev/Bounce/Context/IBeanFactory.php index f2d0123..6665de5 100644 --- a/lib/MooDev/Bounce/Context/IBeanFactory.php +++ b/lib/MooDev/Bounce/Context/IBeanFactory.php @@ -1,19 +1,27 @@ + * @copyright Copyright (c) 2015, MOO Print Ltd. + * @license ISC */ - namespace MooDev\Bounce\Context; - +/** + * Thing that creates beans. + */ interface IBeanFactory { + /** + * Create/retrieve an instance of a named bean. + * @param string $name + * @return mixed + */ public function createByName($name); + /** + * @return string[] A map of bean names to class names. + */ public function getAllBeanClasses(); } \ No newline at end of file diff --git a/lib/MooDev/Bounce/Context/IContextProvider.php b/lib/MooDev/Bounce/Context/IContextProvider.php index 165e71f..c6a9b64 100644 --- a/lib/MooDev/Bounce/Context/IContextProvider.php +++ b/lib/MooDev/Bounce/Context/IContextProvider.php @@ -1,16 +1,17 @@ + * @copyright Copyright (c) 2015, MOO Print Ltd. + * @license ISC */ - namespace MooDev\Bounce\Context; use MooDev\Bounce\Config; use MooDev\Bounce\Exception\BounceException; +/** + * Provider of bounce config contexts + */ interface IContextProvider { From 7d0f9338370a235524ce3be7bf0e3ce9dcc8821f Mon Sep 17 00:00:00 2001 From: Jonathan Oddy Date: Wed, 2 Dec 2015 12:13:49 +0000 Subject: [PATCH 3/4] Update travis builds to sane PHP versions. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ee157a2..9c0db11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: php php: + - "5.6" + - "5.5" - "5.4" - - "5.3" before_script: - echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - echo "apc.enable_cli = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini From 7b6feb187160394551c4f91e51cc3759e39bb6a1 Mon Sep 17 00:00:00 2001 From: Jonathan Oddy Date: Wed, 2 Dec 2015 12:24:23 +0000 Subject: [PATCH 4/4] OK, so we can't test against 5.5 or 5.6 because apc... --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c0db11..13719e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: php php: - - "5.6" - - "5.5" - "5.4" before_script: - echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini